fix(google_analytics): stop retrying revoked/expired OAuth refresh tokens - #76414
fix(google_analytics): stop retrying revoked/expired OAuth refresh tokens#76414Gilbert09 wants to merge 2 commits into
Conversation
…kens Google's OAuth `invalid_grant` error surfaces mid-sync as a bare `RefreshError` from `AuthorizedSession`, before any HTTP status is available to match on. `get_non_retryable_errors` didn't recognize it, so the import kept retrying and re-reporting the same unrecoverable credential error instead of stopping and prompting a reconnect. Generated-By: PostHog Code Task-Id: 9ef29e05-b0ae-4e1c-b3f0-f3316cb772a7
|
Merging to
After your PR is submitted to the merge queue, this comment will be automatically updated with its status. If the PR fails, failure details will also be posted here |
|
Hey @Gilbert09! 👋 It looks like your git author email on this PR isn't your
You can fix it for this repo with: git config user.email "you@posthog.com"Or set it globally with |
There was a problem hiding this comment.
Pull request overview
This PR updates the Google Analytics warehouse source’s error-classification so OAuth refresh-token revocation/expiry (invalid_grant) is treated as non-retryable, preventing Temporal from repeatedly retrying an unrecoverable mid-sync RefreshError and instead prompting the user to reconnect.
Changes:
- Added
"invalid_grant"toGoogleAnalyticsSource.get_non_retryable_errors()with a reconnect message. - Added/extended unit tests to assert the new pattern exists and that it matches the observed
RefreshErrorstring shape.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| products/warehouse_sources/backend/temporal/data_imports/sources/google_analytics/source.py | Marks invalid_grant as non-retryable so revoked/expired refresh tokens stop retry loops and surface a reconnect prompt. |
| products/warehouse_sources/backend/temporal/data_imports/sources/google_analytics/tests/test_google_analytics_source.py | Adds regression coverage ensuring RefreshError(...invalid_grant...) strings are recognized as non-retryable. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Generated-By: PostHog Code Task-Id: 9ef29e05-b0ae-4e1c-b3f0-f3316cb772a7
There was a problem hiding this comment.
Small, well-tested error-classification fix within the author's own team's code; no risky territory, review comments resolved, no blocking signals.
- Author wrote 0% of the modified lines and has 7 merged PRs in these paths (familiarity MODERATE).
- 👍 on the PR from hex-security-app[bot].
Gate mechanics and policy version
| Gate | Result | |
|---|---|---|
| prerequisites | ✓ | all clear |
| deny-list | ✓ | no deny categories matched |
| size | ✓ | 6L, 1F substantive, 18L/2F incl. docs/generated/snapshots — within ceiling |
| tier | ✓ | T1-agent / T1a-trivial (18L, 2F, single-area, fix) |
| stamphog 2.0.0b4 | .stamphog/policy.yml @ f381e58 · reviewed head b2b776a |
Problem
Error tracking surfaced a recurring
RefreshErrorfrom the Google Analytics data warehouse source:invalid_grant: Bad Request. The stack trace shows it originates in_run_reportingoogle_analytics.py, when theAuthorizedSessiontries to refresh the stored OAuth token before making arunReportcall.invalid_grantmeans Google has rejected the stored refresh token: it was revoked, expired, or the connection was disconnected on the Google side. This is a customer/upstream condition, not something PostHog can fix by retrying.The connector's
get_non_retryable_errors()only matched401 Client Error,403 Client Error, andACCESS_TOKEN_SCOPE_INSUFFICIENT— all of which come from an HTTP response. But aRefreshErrorraised whileAuthorizedSessionrefreshes its token happens before any HTTP response exists, so its message (a raw(message, response_dict)tuple repr) never matched any of those patterns. The result: the sync kept retrying and re-reporting the same unrecoverable error on every attempt instead of stopping and prompting a reconnect.Note
validate_credentialsalready catches this sameRefreshErrorand returns a friendly reconnect message, but that only runs before a sync starts — it doesn't cover the mid-sync path through_run_report.Changes
Added
invalid_granttoGoogleAnalyticsSource.get_non_retryable_errors(), matching Google's stable OAuth error code (rather than any part of the response that could vary) and mapping it to a reconnect-account message, consistent with the existing entries for this source and the equivalent pattern inStripeSource.get_non_retryable_errors().How did you test this code?
Added
test_non_retryable_errors_matches_revoked_refresh_token, which reproduces the observedRefreshErrorshape and asserts it's now recognized as non-retryable viaerror_message_matches. Also extendedtest_non_retryable_errors_cover_auth_failuresto assert the new key is present.Ran the full test file locally:
Also ran
ruff check --fixandruff formaton the changed files (no issues), andmypyon the changed source file (no issues).Automatic notifications
Docs update
N/A — internal error-handling classification, no user-facing config or workflow change.
🤖 Agent context
Autonomy: Fully autonomous
I (Claude Code) investigated a
RefreshErrorreported by PostHog error tracking for the Google Analytics data warehouse source, confirmed it originates in this source's_run_reportcode path (not a downstream/serialization artifact), and classified it as a customer/upstream OAuth issue rather than a PostHog bug. I checked for duplicate open PRs (by exception type, message phrase, module path, and the maintainer's own open PRs) and found none addressing this. The fix mirrors the existingRefreshErrorhandling already present invalidate_credentialsand theget_non_retryable_errorspattern used across other sources (e.g. Stripe).Created with PostHog Code