Skip to content

fix(integrations): classify hubspot BAD_HUB and 429 oauth refresh failures - #72609

Merged
mayteio merged 1 commit into
masterfrom
harley/hubspot-oauth-refresh-reasons
Jul 21, 2026
Merged

fix(integrations): classify hubspot BAD_HUB and 429 oauth refresh failures#72609
mayteio merged 1 commit into
masterfrom
harley/hubspot-oauth-refresh-reasons

Conversation

@mayteio

@mayteio mayteio commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Problem

The integration_oauth_refresh metric buckets failures by reason so alerting can tell credential breakage from provider trouble, and so dead grants can go terminal (an unbroken invalid_grant streak) instead of being retried forever.

HubSpot breaks both mechanisms for one failure shape. A grant whose portal (hub) was deleted or disconnected comes back as {"status": "BAD_HUB", "error": "access_denied", ...} with no invalid_grant code, so it buckets as other and never terminates. Those rows retry at the backoff cap forever, and because the cap is a fixed interval they synchronize into periodic retry herds. The herds are big enough to trip HubSpot's per-second rate limit, which then also fails healthy refreshes that land in the same second, and those 429s bucket as other too.

Changes

  • Map HubSpot's BAD_HUB shape (4xx + "status": "BAD_HUB") to invalid_grant, mirroring the existing reddit-ads special case for a dead grant reported without an OAuth error code. Dead hubs now go terminal after the usual streak and stop retrying. HubSpot's BAD_REFRESH_TOKEN responses already carry "error": "invalid_grant" and need no special case.
  • Bucket HTTP 429 as a new rate_limited reason for all kinds, checked after the credential shapes and before the 5xx bucket, so throttling reads as transient provider pressure rather than an unclassified failure. Downstream alerting (PostHog/charts#13344) treats rate_limited as provider-transient alongside http_5xx/network.

Note

Existing BAD_HUB rows start terminating only as they fail again after this deploys; the backlog drains over the following hours as each row accumulates its streak.

How did you test this code?

  • Extended the existing parameterized test_oauth_refresh_failure_reason with six cases; regressions each group catches:
    • hubspot_dead_hub_shape - the core mapping; fails without the new branch.
    • hubspot_shape_on_other_kind - the mapping must stay scoped to hubspot, like the reddit case.
    • hubspot_bad_hub_5xx_is_outage - a 5xx with a weird body is still an outage, not a dead grant.
    • hubspot_bad_refresh_token_still_oauth_code - the standard invalid_grant path keeps precedence.
    • rate_limited / rate_limited_any_kind - 429 buckets independently of body shape and kind.
  • hogli test posthog/models/test/test_integration_model.py -- -k refresh - 68 passed (covers backoff, terminal-state, and streak-reset behavior around the changed function).
  • No manual testing beyond reading production refresh-failure log bodies to confirm the exact shapes being classified.

Automatic notifications

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

Docs update

None needed - internal metric classification only.

🤖 Agent context

Autonomy: Human-driven (agent-assisted)

Authored with Claude Code. The failure shapes were taken verbatim from production refresh-failure logs (Loki) while investigating why hubspot's other failure bursts recur on a fixed period; the burst timing matched the backoff cap and the in-burst failures were RATE_LIMIT bodies, which is what motivated the separate rate_limited bucket rather than folding 429s into other or http_5xx. Considered gating BAD_HUB on exact status 400 like the reddit case, but chose < 500 since the logged shape is unambiguous and a provider-side 5xx that happens to echo a body should still bucket as an outage (covered by a test). Tests follow the file's existing parameterized pattern.

… failures

HubSpot reports a grant whose portal was deleted or disconnected as
{"status": "BAD_HUB", "error": "access_denied"} - no invalid_grant code, so
these rows bucket as 'other', never hit the invalid_grant terminal streak,
and are retried at the backoff cap forever. The cap synchronises that
zombie cohort into hourly retry herds that trip HubSpot's per-second rate
limit, which also fails healthy refreshes landing in the same second.

Map the BAD_HUB shape to invalid_grant for hubspot (mirroring the existing
reddit-ads special case) so dead hubs go terminal and the herd drains, and
bucket HTTP 429 as a new 'rate_limited' reason for all kinds so throttling
reads as transient provider pressure rather than an unclassified failure.
The cdp alerting split (PostHog/charts#13344) treats rate_limited as
provider-transient alongside http_5xx/network.
@mayteio mayteio self-assigned this Jul 21, 2026
@mayteio
mayteio marked this pull request as ready for review July 21, 2026 14:55
@greptile-apps

greptile-apps Bot commented Jul 21, 2026

Copy link
Copy Markdown
Contributor

Reviews (1): Last reviewed commit: "fix(integrations): classify hubspot BAD_..." | Re-trigger Greptile

@mayteio mayteio added the stamphog Request AI approval (no full review) label Jul 21, 2026

@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.

The "auth" title flag is incidental — this only reclassifies OAuth refresh failure reasons for metrics/retry-termination logic, not authentication or credential handling itself; the diff is small, well-tested with parameterized cases, and the author has STRONG familiarity with this exact code path.

  • Author wrote 100% of the modified lines and has 7 merged PRs in these paths (familiarity STRONG).
  • 👍 on the PR from greptile-apps[bot], hex-security-app[bot].
Gate mechanics and policy version
Gate Result
prerequisites all clear
deny-list no deny categories matched
size 16L, 1F substantive, 40L/2F incl. docs/generated/snapshots — within ceiling
tier T1-agent / T1b-small (40L, 2F, single-area, fix)
stamphog 2.0.0b3 .stamphog/policy.yml @ cb180e3 · reviewed head 277802a

@mayteio
mayteio enabled auto-merge (squash) July 21, 2026 15:01
@github-actions

Copy link
Copy Markdown
Contributor

🤖 CI report

Playwright — all passed

All tests passed.

View test results →

@mayteio
mayteio merged commit afae069 into master Jul 21, 2026
457 of 475 checks passed
@mayteio
mayteio deleted the harley/hubspot-oauth-refresh-reasons branch July 21, 2026 16:55
@deployment-status-posthog

deployment-status-posthog Bot commented Jul 21, 2026

Copy link
Copy Markdown

Deploy status

Environment Status Deployed At Workflow
dev ✅ Deployed 2026-07-21 17:31 UTC Run
prod-us ✅ Deployed 2026-07-21 17:52 UTC Run
prod-eu ✅ Deployed 2026-07-21 17:50 UTC Run

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.

1 participant