Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ def get_non_retryable_errors(self) -> dict[str, str | None]:
"401 Client Error": "Your Google Analytics connection is invalid or expired. Please reconnect your account.",
"403 Client Error": "PostHog is not authorized to read this Google Analytics property. Please make sure the connected Google account has access to the property.",
"ACCESS_TOKEN_SCOPE_INSUFFICIENT": "Insufficient permissions. Please reconnect your Google Analytics account with the required scopes.",
# Raised as a bare `RefreshError` from `AuthorizedSession` when the stored refresh token
# has been revoked or expired. `validate_credentials` already maps this to a reconnect
# prompt, but only runs before a sync starts. Mid-sync it reaches `_run_report` via
# `session.post()` before any HTTP status is available to match on, so match Google's
# stable OAuth error code instead.
"invalid_grant": "Your Google Analytics connection has expired or been revoked. Please reconnect your account.",
}

def get_retryable_errors(self) -> set[str]:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

from posthog.models.integration import Integration

from products.warehouse_sources.backend.temporal.data_imports.sources.common.base import error_message_matches
from products.warehouse_sources.backend.temporal.data_imports.sources.generated_configs.googleanalytics import (
GoogleAnalyticsSourceConfig,
)
Expand Down Expand Up @@ -262,6 +263,17 @@ def test_non_retryable_errors_cover_auth_failures():
assert "401 Client Error" in errors
assert "403 Client Error" in errors
assert "ACCESS_TOKEN_SCOPE_INSUFFICIENT" in errors
assert "invalid_grant" in errors


def test_non_retryable_errors_matches_revoked_refresh_token():
# `_run_report` refreshes credentials via `session.post()` before any HTTP status is
# available, so a revoked/expired refresh token surfaces as a bare `RefreshError` whose
# `str()` is the raw (message, response_dict) tuple repr, e.g.:
# ('invalid_grant: Bad Request', {'error': 'invalid_grant', 'error_description': 'Bad Request'})
observed_error = str(RefreshError("invalid_grant: Bad Request", {"error": "invalid_grant"}))
non_retryable_errors = GoogleAnalyticsSource().get_non_retryable_errors()
assert error_message_matches(observed_error, non_retryable_errors)


def test_retryable_errors_cover_exhausted_quota_retries():
Expand Down
Loading