Skip to content

Commit fda020a

Browse files
daniel-lxsroomotecte
authored
fix: filter out 429 rate limit errors from API error telemetry (#9987)
Co-authored-by: Roo Code <[email protected]> Co-authored-by: cte <[email protected]>
1 parent 5a4315f commit fda020a

File tree

6 files changed

+837
-58
lines changed

6 files changed

+837
-58
lines changed

packages/telemetry/src/PostHogTelemetryClient.ts

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
import { PostHog } from "posthog-node"
22
import * as vscode from "vscode"
33

4-
import { TelemetryEventName, type TelemetryEvent } from "@roo-code/types"
4+
import {
5+
TelemetryEventName,
6+
type TelemetryEvent,
7+
getErrorStatusCode,
8+
getErrorMessage,
9+
shouldReportApiErrorToTelemetry,
10+
isApiProviderError,
11+
extractApiProviderErrorProperties,
12+
} from "@roo-code/types"
513

614
import { BaseTelemetryClient } from "./BaseTelemetryClient"
715

@@ -70,11 +78,37 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
7078
return
7179
}
7280

81+
// Extract error status code and message for filtering.
82+
const errorCode = getErrorStatusCode(error)
83+
const errorMessage = getErrorMessage(error) ?? error.message
84+
85+
// Filter out expected errors (e.g., 429 rate limits)
86+
if (!shouldReportApiErrorToTelemetry(errorCode, errorMessage)) {
87+
if (this.debug) {
88+
console.info(
89+
`[PostHogTelemetryClient#captureException] Filtering out expected error: ${errorCode} - ${errorMessage}`,
90+
)
91+
}
92+
return
93+
}
94+
7395
if (this.debug) {
7496
console.info(`[PostHogTelemetryClient#captureException] ${error.message}`)
7597
}
7698

77-
this.client.captureException(error, this.distinctId, additionalProperties)
99+
// Auto-extract properties from ApiProviderError and merge with additionalProperties.
100+
// Explicit additionalProperties take precedence over auto-extracted properties.
101+
let mergedProperties = additionalProperties
102+
103+
if (isApiProviderError(error)) {
104+
const extractedProperties = extractApiProviderErrorProperties(error)
105+
mergedProperties = { ...extractedProperties, ...additionalProperties }
106+
}
107+
108+
// Override the error message with the extracted error message.
109+
error.message = errorMessage
110+
111+
this.client.captureException(error, this.distinctId, mergedProperties)
78112
}
79113

80114
/**

packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts

Lines changed: 226 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/* eslint-disable @typescript-eslint/no-explicit-any */
22

3-
// npx vitest run src/__tests__/PostHogTelemetryClient.test.ts
3+
// pnpm --filter @roo-code/telemetry test src/__tests__/PostHogTelemetryClient.test.ts
44

55
import * as vscode from "vscode"
66
import { PostHog } from "posthog-node"
77

8-
import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types"
8+
import { type TelemetryPropertiesProvider, TelemetryEventName, ApiProviderError } from "@roo-code/types"
99

1010
import { PostHogTelemetryClient } from "../PostHogTelemetryClient"
1111

@@ -32,6 +32,7 @@ describe("PostHogTelemetryClient", () => {
3232

3333
mockPostHogClient = {
3434
capture: vi.fn(),
35+
captureException: vi.fn(),
3536
optIn: vi.fn(),
3637
optOut: vi.fn(),
3738
shutdown: vi.fn().mockResolvedValue(undefined),
@@ -373,4 +374,227 @@ describe("PostHogTelemetryClient", () => {
373374
expect(mockPostHogClient.shutdown).toHaveBeenCalled()
374375
})
375376
})
377+
378+
describe("captureException", () => {
379+
it("should not capture exceptions when telemetry is disabled", () => {
380+
const client = new PostHogTelemetryClient()
381+
client.updateTelemetryState(false)
382+
383+
const error = new Error("Test error")
384+
client.captureException(error)
385+
386+
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
387+
})
388+
389+
it("should capture exceptions when telemetry is enabled", () => {
390+
const client = new PostHogTelemetryClient()
391+
client.updateTelemetryState(true)
392+
393+
const error = new Error("Test error")
394+
client.captureException(error, { provider: "TestProvider" })
395+
396+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
397+
provider: "TestProvider",
398+
})
399+
})
400+
401+
it("should filter out 429 rate limit errors (via status property)", () => {
402+
const client = new PostHogTelemetryClient()
403+
client.updateTelemetryState(true)
404+
405+
// Create an error with status property (like OpenAI SDK errors)
406+
const error = Object.assign(new Error("Rate limit exceeded"), { status: 429 })
407+
client.captureException(error)
408+
409+
// Should NOT capture 429 errors
410+
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
411+
})
412+
413+
it("should filter out errors with '429' in message", () => {
414+
const client = new PostHogTelemetryClient()
415+
client.updateTelemetryState(true)
416+
417+
const error = new Error("429 Rate limit exceeded: free-models-per-day")
418+
client.captureException(error)
419+
420+
// Should NOT capture errors with 429 in message
421+
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
422+
})
423+
424+
it("should filter out errors containing 'rate limit' (case insensitive)", () => {
425+
const client = new PostHogTelemetryClient()
426+
client.updateTelemetryState(true)
427+
428+
const error = new Error("Request failed due to Rate Limit")
429+
client.captureException(error)
430+
431+
// Should NOT capture rate limit errors
432+
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
433+
})
434+
435+
it("should capture non-rate-limit errors", () => {
436+
const client = new PostHogTelemetryClient()
437+
client.updateTelemetryState(true)
438+
439+
const error = new Error("Internal server error")
440+
client.captureException(error)
441+
442+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
443+
})
444+
445+
it("should capture errors with non-429 status codes", () => {
446+
const client = new PostHogTelemetryClient()
447+
client.updateTelemetryState(true)
448+
449+
const error = Object.assign(new Error("Internal server error"), { status: 500 })
450+
client.captureException(error)
451+
452+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
453+
})
454+
455+
it("should use nested error message from OpenAI SDK error structure for filtering", () => {
456+
const client = new PostHogTelemetryClient()
457+
client.updateTelemetryState(true)
458+
459+
// Create an error with nested metadata (like OpenRouter upstream errors)
460+
const error = Object.assign(new Error("Request failed"), {
461+
status: 429,
462+
error: {
463+
message: "Error details",
464+
metadata: { raw: "Rate limit exceeded: free-models-per-day" },
465+
},
466+
})
467+
client.captureException(error)
468+
469+
// Should NOT capture - the nested metadata.raw contains rate limit message
470+
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
471+
})
472+
473+
it("should modify error.message with extracted message from nested metadata.raw", () => {
474+
const client = new PostHogTelemetryClient()
475+
client.updateTelemetryState(true)
476+
477+
// Create an OpenAI SDK-like error with nested metadata (non-rate-limit error)
478+
const error = Object.assign(new Error("Generic request failed"), {
479+
status: 500,
480+
error: {
481+
message: "Nested error message",
482+
metadata: { raw: "Upstream provider error: model overloaded" },
483+
},
484+
})
485+
486+
client.captureException(error)
487+
488+
// Verify error message was modified to use metadata.raw (highest priority)
489+
expect(error.message).toBe("Upstream provider error: model overloaded")
490+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
491+
})
492+
493+
it("should modify error.message with nested error.message when metadata.raw is not available", () => {
494+
const client = new PostHogTelemetryClient()
495+
client.updateTelemetryState(true)
496+
497+
// Create an OpenAI SDK-like error with nested message but no metadata.raw
498+
const error = Object.assign(new Error("Generic request failed"), {
499+
status: 500,
500+
error: {
501+
message: "Upstream provider: connection timeout",
502+
},
503+
})
504+
505+
client.captureException(error)
506+
507+
// Verify error message was modified to use nested error.message
508+
expect(error.message).toBe("Upstream provider: connection timeout")
509+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
510+
})
511+
512+
it("should use primary message when no nested error structure exists", () => {
513+
const client = new PostHogTelemetryClient()
514+
client.updateTelemetryState(true)
515+
516+
// Create an OpenAI SDK-like error without nested error object
517+
const error = Object.assign(new Error("Primary error message"), {
518+
status: 500,
519+
})
520+
521+
client.captureException(error)
522+
523+
// Verify error message remains the primary message
524+
expect(error.message).toBe("Primary error message")
525+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
526+
})
527+
528+
it("should auto-extract properties from ApiProviderError", () => {
529+
const client = new PostHogTelemetryClient()
530+
client.updateTelemetryState(true)
531+
532+
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 500)
533+
client.captureException(error)
534+
535+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
536+
provider: "OpenRouter",
537+
modelId: "gpt-4",
538+
operation: "createMessage",
539+
errorCode: 500,
540+
})
541+
})
542+
543+
it("should auto-extract properties from ApiProviderError without errorCode", () => {
544+
const client = new PostHogTelemetryClient()
545+
client.updateTelemetryState(true)
546+
547+
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "completePrompt")
548+
client.captureException(error)
549+
550+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
551+
provider: "OpenRouter",
552+
modelId: "gpt-4",
553+
operation: "completePrompt",
554+
})
555+
})
556+
557+
it("should merge auto-extracted properties with additionalProperties", () => {
558+
const client = new PostHogTelemetryClient()
559+
client.updateTelemetryState(true)
560+
561+
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
562+
client.captureException(error, { customProperty: "value" })
563+
564+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
565+
provider: "OpenRouter",
566+
modelId: "gpt-4",
567+
operation: "createMessage",
568+
customProperty: "value",
569+
})
570+
})
571+
572+
it("should allow additionalProperties to override auto-extracted properties", () => {
573+
const client = new PostHogTelemetryClient()
574+
client.updateTelemetryState(true)
575+
576+
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
577+
// Explicitly override the provider value
578+
client.captureException(error, { provider: "OverriddenProvider" })
579+
580+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
581+
provider: "OverriddenProvider", // additionalProperties takes precedence
582+
modelId: "gpt-4",
583+
operation: "createMessage",
584+
})
585+
})
586+
587+
it("should not auto-extract for non-ApiProviderError errors", () => {
588+
const client = new PostHogTelemetryClient()
589+
client.updateTelemetryState(true)
590+
591+
const error = new Error("Regular error")
592+
client.captureException(error, { customProperty: "value" })
593+
594+
// Should only have the additionalProperties, not any auto-extracted ones
595+
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
596+
customProperty: "value",
597+
})
598+
})
599+
})
376600
})

0 commit comments

Comments
 (0)