Skip to content

fix(google_analytics): stop retrying revoked/expired OAuth refresh tokens - #76414

Open
Gilbert09 wants to merge 2 commits into
masterfrom
posthog-code/fix-google-analytics-invalid-grant
Open

fix(google_analytics): stop retrying revoked/expired OAuth refresh tokens#76414
Gilbert09 wants to merge 2 commits into
masterfrom
posthog-code/fix-google-analytics-invalid-grant

Conversation

@Gilbert09

Copy link
Copy Markdown
Member

Problem

Error tracking surfaced a recurring RefreshError from the Google Analytics data warehouse source: invalid_grant: Bad Request. The stack trace shows it originates in _run_report in google_analytics.py, when the AuthorizedSession tries to refresh the stored OAuth token before making a runReport call.

invalid_grant means 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 matched 401 Client Error, 403 Client Error, and ACCESS_TOKEN_SCOPE_INSUFFICIENT — all of which come from an HTTP response. But a RefreshError raised while AuthorizedSession refreshes 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_credentials already catches this same RefreshError and 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_grant to GoogleAnalyticsSource.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 in StripeSource.get_non_retryable_errors().

How did you test this code?

Added test_non_retryable_errors_matches_revoked_refresh_token, which reproduces the observed RefreshError shape and asserts it's now recognized as non-retryable via error_message_matches. Also extended test_non_retryable_errors_cover_auth_failures to assert the new key is present.

Ran the full test file locally:

pytest products/warehouse_sources/backend/temporal/data_imports/sources/google_analytics/tests/test_google_analytics_source.py
28 passed

Also ran ruff check --fix and ruff format on the changed files (no issues), and mypy on the changed source file (no issues).

Automatic notifications

  • Publish to changelog?
  • Alert Sales and Marketing teams?

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 RefreshError reported by PostHog error tracking for the Google Analytics data warehouse source, confirmed it originates in this source's _run_report code 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 existing RefreshError handling already present in validate_credentials and the get_non_retryable_errors pattern used across other sources (e.g. Stripe).


Created with PostHog Code

…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
Copilot AI review requested due to automatic review settings August 2, 2026 11:01
@trunk-io

trunk-io Bot commented Aug 2, 2026

Copy link
Copy Markdown

Merging to master in this repository is managed by Trunk.

  • To merge this pull request, check the box to the left or comment /trunk merge below.

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

@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Hey @Gilbert09! 👋

It looks like your git author email on this PR isn't your @posthog.com address (owerstom@gmail.com). Since you're on the PostHog team, it's worth pointing your local git author email at your @posthog.com address. Why it matters:

  • Consistent work identity in git history — internal tooling that attributes commits to team members keys off your @posthog.com address.
  • Keeps team contributions easy to tell apart from external community ones when scanning history.

You can fix it for this repo with:

git config user.email "you@posthog.com"

Or set it globally with git config --global user.email "you@posthog.com". No need to redo this PR — just a nudge for next time. 🙂

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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" to GoogleAnalyticsSource.get_non_retryable_errors() with a reconnect message.
  • Added/extended unit tests to assert the new pattern exists and that it matches the observed RefreshError string 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
@Gilbert09 Gilbert09 added the stamphog Request AI approval (no full review) label Aug 2, 2026 — with PostHog

@stamphog stamphog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stamphog Request AI approval (no full review)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants