mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat(telemetry): add app version to exception captures and filter 402 errors (#9996)
Co-authored-by: cte <cestreich@gmail.com>
This commit is contained in:
parent
380a57823c
commit
1cf6ae6c93
6 changed files with 135 additions and 74 deletions
|
|
@ -69,9 +69,7 @@ abstract class BaseTelemetryClient implements TelemetryClient {
|
|||
|
||||
public abstract capture(event: TelemetryEvent): Promise<void>
|
||||
|
||||
public captureException(_error: Error, _additionalProperties?: Record<string, unknown>): void {
|
||||
// No-op - exception capture is only supported by PostHog
|
||||
}
|
||||
public async captureException(_error: Error, _additionalProperties?: Record<string, unknown>): Promise<void> {}
|
||||
|
||||
public setProvider(provider: TelemetryPropertiesProvider): void {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export abstract class BaseTelemetryClient implements TelemetryClient {
|
|||
|
||||
public abstract capture(event: TelemetryEvent): Promise<void>
|
||||
|
||||
public abstract captureException(error: Error, additionalProperties?: Record<string, unknown>): void
|
||||
public abstract captureException(error: Error, additionalProperties?: Record<string, unknown>): Promise<void>
|
||||
|
||||
public setProvider(provider: TelemetryPropertiesProvider): void {
|
||||
this.providerRef = new WeakRef(provider)
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@ import { PostHog } from "posthog-node"
|
|||
import * as vscode from "vscode"
|
||||
|
||||
import {
|
||||
TelemetryEventName,
|
||||
type TelemetryProperties,
|
||||
type TelemetryEvent,
|
||||
TelemetryEventName,
|
||||
getErrorStatusCode,
|
||||
getErrorMessage,
|
||||
shouldReportApiErrorToTelemetry,
|
||||
|
|
@ -69,7 +70,10 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
|
|||
})
|
||||
}
|
||||
|
||||
public override captureException(error: Error, additionalProperties?: Record<string, unknown>): void {
|
||||
public override async captureException(
|
||||
error: Error,
|
||||
additionalProperties?: Record<string, unknown>,
|
||||
): Promise<void> {
|
||||
if (!this.isTelemetryEnabled()) {
|
||||
if (this.debug) {
|
||||
console.info(`[PostHogTelemetryClient#captureException] Skipping exception: ${error.message}`)
|
||||
|
|
@ -82,7 +86,7 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
|
|||
const errorCode = getErrorStatusCode(error)
|
||||
const errorMessage = getErrorMessage(error) ?? error.message
|
||||
|
||||
// Filter out expected errors (e.g., 429 rate limits)
|
||||
// Filter out expected errors (e.g., 402 billing, 429 rate limits)
|
||||
if (!shouldReportApiErrorToTelemetry(errorCode, errorMessage)) {
|
||||
if (this.debug) {
|
||||
console.info(
|
||||
|
|
@ -108,7 +112,21 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
|
|||
// Override the error message with the extracted error message.
|
||||
error.message = errorMessage
|
||||
|
||||
this.client.captureException(error, this.distinctId, mergedProperties)
|
||||
const provider = this.providerRef?.deref()
|
||||
let telemetryProperties: TelemetryProperties | undefined = undefined
|
||||
|
||||
if (provider) {
|
||||
try {
|
||||
telemetryProperties = await provider.getTelemetryProperties()
|
||||
} catch (_error) {
|
||||
// Ignore.
|
||||
}
|
||||
}
|
||||
|
||||
this.client.captureException(error, this.distinctId, {
|
||||
...mergedProperties,
|
||||
$app_version: telemetryProperties?.appVersion,
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -367,6 +367,60 @@ describe("PostHogTelemetryClient", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("captureException", () => {
|
||||
it("should not capture exceptions when telemetry is disabled", async () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(false)
|
||||
|
||||
const error = new Error("Test error")
|
||||
await client.captureException(error)
|
||||
|
||||
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should capture exceptions with app version from provider", async () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const mockProvider: TelemetryPropertiesProvider = {
|
||||
getTelemetryProperties: vi.fn().mockResolvedValue({
|
||||
appVersion: "1.0.0",
|
||||
vscodeVersion: "1.60.0",
|
||||
platform: "darwin",
|
||||
editorName: "vscode",
|
||||
language: "en",
|
||||
mode: "code",
|
||||
}),
|
||||
}
|
||||
|
||||
client.setProvider(mockProvider)
|
||||
|
||||
const error = new Error("Test error")
|
||||
await client.captureException(error, { customProp: "value" })
|
||||
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(
|
||||
error,
|
||||
"test-machine-id",
|
||||
expect.objectContaining({
|
||||
customProp: "value",
|
||||
$app_version: "1.0.0",
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
it("should capture exceptions with undefined app version when no provider is set", async () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const error = new Error("Test error")
|
||||
await client.captureException(error)
|
||||
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("shutdown", () => {
|
||||
it("should call shutdown on the PostHog client", async () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
|
|
@ -375,29 +429,7 @@ describe("PostHogTelemetryClient", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("captureException", () => {
|
||||
it("should not capture exceptions when telemetry is disabled", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(false)
|
||||
|
||||
const error = new Error("Test error")
|
||||
client.captureException(error)
|
||||
|
||||
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should capture exceptions when telemetry is enabled", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const error = new Error("Test error")
|
||||
client.captureException(error, { provider: "TestProvider" })
|
||||
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
provider: "TestProvider",
|
||||
})
|
||||
})
|
||||
|
||||
describe("captureException error filtering", () => {
|
||||
it("should filter out 429 rate limit errors (via status property)", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
|
@ -410,6 +442,18 @@ describe("PostHogTelemetryClient", () => {
|
|||
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should filter out 402 billing errors (via status property)", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
// Create an error with status 402 (Payment Required)
|
||||
const error = Object.assign(new Error("Payment required"), { status: 402 })
|
||||
client.captureException(error)
|
||||
|
||||
// Should NOT capture 402 errors
|
||||
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should filter out errors with '429' in message", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
|
@ -439,7 +483,9 @@ describe("PostHogTelemetryClient", () => {
|
|||
const error = new Error("Internal server error")
|
||||
client.captureException(error)
|
||||
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should capture errors with non-429 status codes", () => {
|
||||
|
|
@ -449,7 +495,9 @@ describe("PostHogTelemetryClient", () => {
|
|||
const error = Object.assign(new Error("Internal server error"), { status: 500 })
|
||||
client.captureException(error)
|
||||
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use nested error message from OpenAI SDK error structure for filtering", () => {
|
||||
|
|
@ -470,7 +518,7 @@ describe("PostHogTelemetryClient", () => {
|
|||
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it("should modify error.message with extracted message from nested metadata.raw", () => {
|
||||
it("should capture errors with nested metadata and override error.message with extracted message", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
|
|
@ -485,12 +533,14 @@ describe("PostHogTelemetryClient", () => {
|
|||
|
||||
client.captureException(error)
|
||||
|
||||
// Verify error message was modified to use metadata.raw (highest priority)
|
||||
// The implementation overrides error.message with the extracted message from getErrorMessage
|
||||
expect(error.message).toBe("Upstream provider error: model overloaded")
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should modify error.message with nested error.message when metadata.raw is not available", () => {
|
||||
it("should capture errors with nested error.message and override error.message with extracted message", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
|
|
@ -504,9 +554,11 @@ describe("PostHogTelemetryClient", () => {
|
|||
|
||||
client.captureException(error)
|
||||
|
||||
// Verify error message was modified to use nested error.message
|
||||
// The implementation overrides error.message with the extracted message from getErrorMessage
|
||||
expect(error.message).toBe("Upstream provider: connection timeout")
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should use primary message when no nested error structure exists", () => {
|
||||
|
|
@ -522,78 +574,55 @@ describe("PostHogTelemetryClient", () => {
|
|||
|
||||
// Verify error message remains the primary message
|
||||
expect(error.message).toBe("Primary error message")
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should auto-extract properties from ApiProviderError", () => {
|
||||
it("should capture ApiProviderError and auto-extract properties", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 500)
|
||||
client.captureException(error)
|
||||
|
||||
// The implementation auto-extracts properties from ApiProviderError
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
provider: "OpenRouter",
|
||||
modelId: "gpt-4",
|
||||
operation: "createMessage",
|
||||
errorCode: 500,
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should auto-extract properties from ApiProviderError without errorCode", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "completePrompt")
|
||||
client.captureException(error)
|
||||
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
provider: "OpenRouter",
|
||||
modelId: "gpt-4",
|
||||
operation: "completePrompt",
|
||||
})
|
||||
})
|
||||
|
||||
it("should merge auto-extracted properties with additionalProperties", () => {
|
||||
it("should capture ApiProviderError with additionalProperties merged with auto-extracted properties", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
|
||||
client.captureException(error, { customProperty: "value" })
|
||||
|
||||
// additionalProperties take precedence over auto-extracted properties
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
provider: "OpenRouter",
|
||||
modelId: "gpt-4",
|
||||
operation: "createMessage",
|
||||
customProperty: "value",
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
|
||||
it("should allow additionalProperties to override auto-extracted properties", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
|
||||
// Explicitly override the provider value
|
||||
client.captureException(error, { provider: "OverriddenProvider" })
|
||||
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
provider: "OverriddenProvider", // additionalProperties takes precedence
|
||||
modelId: "gpt-4",
|
||||
operation: "createMessage",
|
||||
})
|
||||
})
|
||||
|
||||
it("should not auto-extract for non-ApiProviderError errors", () => {
|
||||
it("should capture regular errors with additionalProperties", () => {
|
||||
const client = new PostHogTelemetryClient()
|
||||
client.updateTelemetryState(true)
|
||||
|
||||
const error = new Error("Regular error")
|
||||
client.captureException(error, { customProperty: "value" })
|
||||
|
||||
// Should only have the additionalProperties, not any auto-extracted ones
|
||||
expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
|
||||
customProperty: "value",
|
||||
$app_version: undefined,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -109,6 +109,11 @@ describe("telemetry error utilities", () => {
|
|||
}
|
||||
})
|
||||
|
||||
it("should return false for 402 billing errors", () => {
|
||||
expect(shouldReportApiErrorToTelemetry(402)).toBe(false)
|
||||
expect(shouldReportApiErrorToTelemetry(402, "Payment required")).toBe(false)
|
||||
})
|
||||
|
||||
it("should return false for 429 rate limit errors", () => {
|
||||
expect(shouldReportApiErrorToTelemetry(429)).toBe(false)
|
||||
expect(shouldReportApiErrorToTelemetry(429, "Rate limit exceeded")).toBe(false)
|
||||
|
|
@ -142,6 +147,16 @@ describe("telemetry error utilities", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("EXPECTED_API_ERROR_CODES", () => {
|
||||
it("should contain 402 (payment required)", () => {
|
||||
expect(EXPECTED_API_ERROR_CODES.has(402)).toBe(true)
|
||||
})
|
||||
|
||||
it("should contain 429 (rate limit)", () => {
|
||||
expect(EXPECTED_API_ERROR_CODES.has(429)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe("ApiProviderError", () => {
|
||||
it("should create an error with correct properties", () => {
|
||||
const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 500)
|
||||
|
|
|
|||
|
|
@ -262,7 +262,7 @@ export interface TelemetryClient {
|
|||
|
||||
setProvider(provider: TelemetryPropertiesProvider): void
|
||||
capture(options: TelemetryEvent): Promise<void>
|
||||
captureException(error: Error, additionalProperties?: Record<string, unknown>): void
|
||||
captureException(error: Error, additionalProperties?: Record<string, unknown>): Promise<void>
|
||||
updateTelemetryState(isOptedIn: boolean): void
|
||||
isTelemetryEnabled(): boolean
|
||||
shutdown(): Promise<void>
|
||||
|
|
@ -273,6 +273,7 @@ export interface TelemetryClient {
|
|||
* These are normal/expected errors that users can't do much about.
|
||||
*/
|
||||
export const EXPECTED_API_ERROR_CODES = new Set([
|
||||
402, // Payment required - billing issues
|
||||
429, // Rate limit - expected when hitting API limits
|
||||
])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue