Skip to content

[Fix] PostHog captures one exception per API error - #2330

Draft
zoomote2[bot] wants to merge 1 commit into
mainfrom
fix/telemetry-single-capture-513
Draft

zoomote2[bot] wants to merge 1 commit into
mainfrom
fix/telemetry-single-capture-513

Conversation

@zoomote2

@zoomote2 zoomote2 Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

​Created by Roomote. Follow up by mentioning @roomote, in the web UI, or in Discord.

Related GitHub Issue

Closes: #513

Description

Providers caught API errors, sent one $exception event, and re-threw the same failure. The retry loop in Task#attemptApiRequest re-entered the provider on every attempt. One persistent API error produced three to six $exception events. PR #1702 fixed Bugs 1 and 3 of issue #513. This PR fixes Bug 2 and Bug 4 and adds client-side deduplication.

This PR picks Option B from the issue. TelemetryService#captureException now enforces one capture per error occurrence with two guards:

  1. Identity. ApiProviderError gains a captured flag. The first capture sets it. Re-throws of the same instance are no-ops. This guard also covers Bug 4.
  2. Fingerprint window. Each retry attempt builds a fresh error object, so identity cannot see it. Identical errors (provider, model, operation, code, message) inside a 30-second sliding window collapse into the first capture. The cache holds at most 100 fingerprints, oldest evicted.

Providers keep their catch blocks and their captures. No telemetry is removed. The issue asked for an audit of capture paths outside the retry loop before this choice. completePrompt and similar paths capture once at the provider and keep doing so. The policy does not drop that telemetry.

Bug 4: OpenRouterHandler#handleStreamingError stays the single capture point for mid-stream OpenRouter errors. It now throws the same ApiProviderError instance it captured, so an enclosing capture of that instance is a no-op through the flag. The caller-facing message text is unchanged. One payload note: the PostHog message now carries the OpenRouter API Error <code>: prefix, because one object is both captured and thrown.

Test Procedure

New tests:

  • packages/telemetry/src/__tests__/TelemetryService.exception-single-capture.test.ts. Pins the flag, the window, window expiry, sliding refresh, distinct errors, plain errors, and the not-ready path. Eight tests.
  • src/core/task/__tests__/Task.spec.ts adds a fault-injection test. It drives the real retry loop in attemptApiRequest. The provider seam captures and throws on attempts one and two and recovers on attempt three. The test asserts exactly one client captureException call and a successful recovery.
  • The same file adds a mistake-limit guard test. It asserts the named Consecutive Mistake Error event fires and captureException never runs. ConsecutiveMistakeError therefore never reaches $exception.
  • src/api/providers/__tests__/openrouter.spec.ts expects the captured message with the OpenRouter API Error <code>: prefix on the four handleStreamingError paths.

Commands and results:

  • pnpm --filter @roo-code/telemetry test: 59 tests pass.
  • pnpm --dir src exec vitest run api/providers/__tests__/openrouter.spec.ts: 41 tests pass.
  • pnpm --dir src exec vitest run core/task/__tests__/Task.spec.ts: 204 tests pass.
  • pnpm test from the repository root: 565 test files pass, zero failures. Two unrelated extension.spec.ts tests flaked once under full-suite parallelism. The same flake reproduces on a clean main tree and is not caused by this change.
  • tsc --noEmit passes in src, @roo-code/types, and @roo-code/telemetry.
  • ESLint with --prune-suppressions passes on every changed file. No suppression count increases.

Pre-Submission Checklist

  • Issue Linked: This PR is linked to an approved GitHub Issue (see "Related GitHub Issue" above).
  • Scope: My changes are focused on the linked issue (one major feature/fix per PR).
  • Self-Review: I reviewed my own code with care.
  • Writing Style: The PR title and description use Simplified Technical English (ASD-STE100): short sentences, plain words, active voice, one idea per sentence, no contractions.
  • Testing: New and/or updated tests have been added to cover my changes (if applicable).
  • Test Helpers (test changes only): Shared test helpers (src/test-utils/, @/utils/test-utils) are used only for mechanical duplication; test intent and scenario-specific mocks remain visible.
  • Visual Snapshot (UI changes only): N/A. No UI changes.
  • Documentation Impact: I checked if my changes need documentation updates (see "Documentation Updates" section below).
  • Contribution Guidelines: I read and agree to the Contributor Guidelines.

Visual Snapshots

N/A. No UI changes.

Videos (interaction / animation only)

N/A.

Documentation Updates

  • No documentation updates are required.

Additional Notes

  • Why Option B and not Option A: Option A must remove captures from every provider and add new capture points for mid-stream errors and every completePrompt call site. The retry loop has no bounded final failure under auto-approval. Option B removes no telemetry and keeps every capture site in place.
  • Trade-off: two distinct failures with the same fingerprint inside the 30-second window collapse into one event. This matches the issue goal of one $exception per distinct occurrence and follows the PostHog guidance on deduplication in retry loops. An identical error re-captures once a quiet gap exceeds the window.
  • The captured flag reads as undefined until the first capture, so untouched error instances stay unchanged.

Get in Touch

Discord: 0x7777777_.

@coderabbitai

coderabbitai Bot commented Nov 5, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are limited based on label configuration.

🏷️ Required labels (at least one): 1

  • coderabbit-review-active

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

Run configuration

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

Share

@zoomote2

zoomote2 Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor Author

2 issues outstanding. See session

  • src/api/providers/openrouter.ts:221-223 — Throwing the captured ApiProviderError lets PostHogTelemetryClient#captureException synchronously rewrite error.message (numeric status triggers the isOpenAISdkError branch) before the throw runs, so caller-facing text loses the OpenRouter API Error <code>: prefix whenever telemetry is enabled and the error is not 429-filtered; production PostHog payload also loses the prefix, and the spec assertions can't catch it because the telemetry module is mocked.
  • packages/telemetry/src/TelemetryService.ts:39 — The 30s dedupe window is shorter than the retry loop's exponential backoff gaps (5s doubling to a 600s cap), so from the 4th retry onward each attempt re-captures; a persistent error under auto-approval still emits roughly one $exception per backoff interval instead of one total.

Reviewed 2b1b074

@github-actions

github-actions Bot commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

Review status

This PR was opened by an automated account. A human maintainer must verify the change intent, provenance, and validation before merging.

Current step: Mark the PR ready. Required CI must pass before CodeRabbit starts.

Review-state labels are managed by this workflow; do not edit them manually. community-approved is managed the same way — do not add or remove it manually. It signals a fresh community code approval for the current head as an advisory priority only; maintainer review is still required.

@codecov

codecov Bot commented Nov 5, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Comment on lines 221 to +223
TelemetryService.instance.captureException(apiError)

throw new Error(`OpenRouter API Error ${error?.code}: ${rawErrorMessage}`)
throw apiError

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Throwing the captured instance lets the telemetry client rewrite the caller-facing message before the throw runs.

PostHogTelemetryClient.captureException synchronously sets error.message = errorMessage (packages/telemetry/src/PostHogTelemetryClient.ts:120) before forwarding to PostHog, and getErrorMessage takes the isOpenAISdkError branch for this instance because Object.assign gave it a numeric status — it returns error.error?.metadata?.raw || error.error?.message || error.message, i.e. the unprefixed raw text. The async client body executes synchronously up to its first await, so the rewrite lands on this instance before line 223 throws it.

Consequences whenever error?.code is a number:

  • With telemetry enabled and the error not 429-filtered, callers see "Rate limit exceeded" (or the metadata.raw text) instead of "OpenRouter API Error 429: Rate limit exceeded" — the mid-stream streaming_failed message, api_req_failed prompt, and backoff header all show the changed text. With telemetry disabled, or for filtered 429s, the client returns before the rewrite and the prefix survives — user-visible text now depends on telemetry state.
  • The PR's payload note ("the PostHog message now carries the OpenRouter API Error <code>: prefix") doesn't hold in production: the real client strips it again before sending. The updated openrouter.spec.ts assertions can't catch this because they run against a mocked @roo-code/telemetry, and Task.spec.ts also uses a mock client.

Capturing a detached copy keeps the caller-facing message and the production PostHog payload both identical to their pre-PR values (the copy still takes the client's rewrite, the thrown instance is untouched):

Suggested change
TelemetryService.instance.captureException(apiError)
throw new Error(`OpenRouter API Error ${error?.code}: ${rawErrorMessage}`)
throw apiError
// Capture a detached copy: PostHogTelemetryClient#captureException
// rewrites error.message in place (preferring the upstream raw text),
// and this instance is about to be thrown to callers.
TelemetryService.instance.captureException(
Object.assign(
new ApiProviderError(apiError.message, providerIdentifiers.openrouter, modelId, operation, error?.code),
{ status: error?.code, error },
),
)
throw apiError

An enclosing catch that captures the thrown instance is still deduped by the fingerprint window (same provider/model/operation/code/message, same stack, well inside 30s), so the single-capture policy for Bug 4 holds either way.

* error keeps the window alive while it keeps happening, so one persistent
* failure surfaces as one $exception event instead of one per retry.
*/
const EXCEPTION_DEDUPE_WINDOW_MS = 30 * 1000

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The 30s window is shorter than the retry gaps it needs to cover, so a persistent error still emits more than one $exception.

Task#backoffAndAnnounce (src/core/task/Task.ts:6061-6121) doubles requestDelaySeconds || 5 per attempt up to MAX_EXPONENTIAL_BACKOFF_SECONDS = 600, and a 429 RetryInfo can set the delay directly. The inter-attempt gaps run 5s, 10s, 20s, 40s, 80s, ... — from the 4th retry onward each gap exceeds EXCEPTION_DEDUPE_WINDOW_MS, the window expires between attempts, and every later attempt re-captures. A persistent error under auto-approval therefore produces events at attempts 1, 4, 5, 6, ... (roughly one per backoff interval) instead of one total, and the comment above ("one persistent failure surfaces as one $exception event") overstates the suppression for exactly the scenario issue #513 targets.

Either size the window to cover the backoff schedule (larger false-collapse trade-off to weigh), or keep 30s and document the residual rate (about one event per window expiry for long-stuck retries, which PostHog's server-side burst throttling then caps).

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] PostHog telemetry: exception inflation from double-capture and control-flow misuse of captureException

1 participant