Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 33 additions & 2 deletions packages/telemetry/src/PostHogTelemetryClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import { PostHog } from "posthog-node"
import * as vscode from "vscode"

import { TelemetryEventName, type TelemetryEvent } from "@roo-code/types"
import {
TelemetryEventName,
type TelemetryEvent,
getErrorStatusCode,
getOpenAISdkErrorMessage,
shouldReportApiErrorToTelemetry,
isApiProviderError,
extractApiProviderErrorProperties,
} from "@roo-code/types"

import { BaseTelemetryClient } from "./BaseTelemetryClient"

Expand Down Expand Up @@ -70,11 +78,34 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
return
}

// Extract error status code and message for filtering
const errorCode = getErrorStatusCode(error)
const errorMessage = getOpenAISdkErrorMessage(error) ?? error.message

// Filter out expected errors (e.g., 429 rate limits)
if (!shouldReportApiErrorToTelemetry(errorCode, errorMessage)) {
if (this.debug) {
console.info(
`[PostHogTelemetryClient#captureException] Filtering out expected error: ${errorCode} - ${errorMessage}`,
)
}
return
}

if (this.debug) {
console.info(`[PostHogTelemetryClient#captureException] ${error.message}`)
}

this.client.captureException(error, this.distinctId, additionalProperties)
// Auto-extract properties from ApiProviderError and merge with additionalProperties.
// Explicit additionalProperties take precedence over auto-extracted properties.
let mergedProperties = additionalProperties

if (isApiProviderError(error)) {
const extractedProperties = extractApiProviderErrorProperties(error)
mergedProperties = { ...extractedProperties, ...additionalProperties }
}

this.client.captureException(error, this.distinctId, mergedProperties)
}

/**
Expand Down
171 changes: 170 additions & 1 deletion packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
import * as vscode from "vscode"
import { PostHog } from "posthog-node"

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

import { PostHogTelemetryClient } from "../PostHogTelemetryClient"

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

mockPostHogClient = {
capture: vi.fn(),
captureException: vi.fn(),
optIn: vi.fn(),
optOut: vi.fn(),
shutdown: vi.fn().mockResolvedValue(undefined),
Expand Down Expand Up @@ -373,4 +374,172 @@ describe("PostHogTelemetryClient", () => {
expect(mockPostHogClient.shutdown).toHaveBeenCalled()
})
})

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",
})
})

it("should filter out 429 rate limit errors (via status property)", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

// Create an error with status property (like OpenAI SDK errors)
const error = Object.assign(new Error("Rate limit exceeded"), { status: 429 })
client.captureException(error)

// Should NOT capture 429 errors
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})

it("should filter out errors with '429' in message", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const error = new Error("429 Rate limit exceeded: free-models-per-day")
client.captureException(error)

// Should NOT capture errors with 429 in message
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})

it("should filter out errors containing 'rate limit' (case insensitive)", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const error = new Error("Request failed due to Rate Limit")
client.captureException(error)

// Should NOT capture rate limit errors
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})

it("should capture non-rate-limit errors", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const error = new Error("Internal server error")
client.captureException(error)

expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
})

it("should capture errors with non-429 status codes", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const error = Object.assign(new Error("Internal server error"), { status: 500 })
client.captureException(error)

expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", undefined)
})

it("should use nested error message from OpenAI SDK error structure", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

// Create an error with nested metadata (like OpenRouter upstream errors)
const error = Object.assign(new Error("Request failed"), {
status: 429,
error: {
message: "Error details",
metadata: { raw: "Rate limit exceeded: free-models-per-day" },
},
})
client.captureException(error)

// Should NOT capture - the nested metadata.raw contains rate limit message
expect(mockPostHogClient.captureException).not.toHaveBeenCalled()
})

it("should auto-extract properties from ApiProviderError", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage", 500)
client.captureException(error)

expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
provider: "OpenRouter",
modelId: "gpt-4",
operation: "createMessage",
errorCode: 500,
})
})

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", () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const error = new ApiProviderError("Test error", "OpenRouter", "gpt-4", "createMessage")
client.captureException(error, { customProperty: "value" })

expect(mockPostHogClient.captureException).toHaveBeenCalledWith(error, "test-machine-id", {
provider: "OpenRouter",
modelId: "gpt-4",
operation: "createMessage",
customProperty: "value",
})
})

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", () => {
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",
})
})
})
})
Loading
Loading