diff --git a/products/warehouse_sources/backend/temporal/data_imports/sources/convex/source.py b/products/warehouse_sources/backend/temporal/data_imports/sources/convex/source.py index 33d5c7b7a1ba..825aeb922bcf 100644 --- a/products/warehouse_sources/backend/temporal/data_imports/sources/convex/source.py +++ b/products/warehouse_sources/backend/temporal/data_imports/sources/convex/source.py @@ -116,6 +116,16 @@ def get_non_retryable_errors(self) -> dict[str, str | None]: return { "401 Client Error": "Authentication failed. Check your Convex deploy key.", "403 Client Error": "Access denied. Check your Convex deploy key.", + # A sync only calls list_snapshot / document_deltas, and Convex answers those with a 404 + # when the table schema discovery listed is gone at read time (deleted on the source, or a + # component table that isn't served by streaming export). The next scheduled run reissues + # the identical request, so every retry replays the same 404. Cloudflare surfaces transient + # edge problems as the 52x/530 family instead (retried in `_CONVEX_RETRY`), so a 404 is + # never a transient blip that this could disable a sync over. + "404 Client Error": ( + "PostHog couldn't find this table in your Convex deployment. It was likely deleted, so " + "turn off syncing for this table, then re-enable the sync." + ), "StreamingExportNotEnabled": "Streaming export requires the Convex Professional plan. See https://www.convex.dev/plans to upgrade.", # Match a stable substring of the raised message, not the `InvalidWindowError` class name: # the non-retryable check compares against `str(exception)`, which contains the message diff --git a/products/warehouse_sources/backend/temporal/data_imports/sources/convex/tests/test_convex.py b/products/warehouse_sources/backend/temporal/data_imports/sources/convex/tests/test_convex.py index 564ffeacbc9b..2a8c3f4ffcd8 100644 --- a/products/warehouse_sources/backend/temporal/data_imports/sources/convex/tests/test_convex.py +++ b/products/warehouse_sources/backend/temporal/data_imports/sources/convex/tests/test_convex.py @@ -12,6 +12,7 @@ ReadTimeout, ) +from products.warehouse_sources.backend.temporal.data_imports.sources.common.base import error_message_matches from products.warehouse_sources.backend.temporal.data_imports.sources.common.resumable import ResumableSourceManager from products.warehouse_sources.backend.temporal.data_imports.sources.convex.convex import ( _CONVEX_RETRY, @@ -558,6 +559,11 @@ def test_get_json_schemas_400_with_unparseable_body_falls_through_to_http_error( [ ("401", "401 Client Error: Unauthorized for url: https://x.convex.cloud/api/document_deltas"), ("403", "403 Client Error: Forbidden for url: https://x.convex.cloud/api/document_deltas"), + ( + "missing_table_404", + "404 Client Error: Not Found for url: " + "https://x.convex.cloud/api/list_snapshot?tableName=verification&format=json&component=betterAuth", + ), ( "invalid_window", "Delta cursor for table 'events' is older than Convex's ~30 day retention window. " @@ -569,6 +575,24 @@ def test_known_errors_match(self, _name: str, observed_error: str) -> None: non_retryable_errors = ConvexSource().get_non_retryable_errors() assert any(key in observed_error for key in non_retryable_errors) + def test_missing_table_404_surfaces_actionable_message(self) -> None: + # A deleted table's 404 must stop retrying and tell the customer to turn off syncing for it, + # not store the raw driver text (which carries the deployment host). Mirror the finalizer's + # first-match selection (external_data_job.py), including its case-insensitive matching via + # `error_message_matches`, so a reorder that shadowed it with an earlier None key would be caught. + error_msg = ( + "404 Client Error: Not Found for url: " + "https://x.convex.cloud/api/list_snapshot?tableName=verification&format=json&component=betterAuth" + ) + matches = [ + friendly + for key, friendly in ConvexSource().get_non_retryable_errors().items() + if error_message_matches(error_msg, [key]) + ] + assert matches, "a missing-table 404 must be classified non-retryable" + assert matches[0] is not None, "a missing-table 404 must surface an actionable message, not raw driver text" + assert "turn off syncing" in matches[0].lower() + @parameterized.expand( [ ("server_error", "500 Server Error for url: https://x.convex.cloud/api/document_deltas"),