Skip to content
Closed
Show file tree
Hide file tree
Changes from all 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
49 changes: 33 additions & 16 deletions packages/cloud/src/TelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,29 +58,46 @@ export class TelemetryClient extends BaseTelemetryClient {
return
}

const payload = {
type: event.event,
properties: await this.getEventProperties(event),
}
// Try to send directly first
const success = await this.captureWithRetry(event)

if (this.debug) {
console.info(`[TelemetryClient#capture] ${JSON.stringify(payload)}`)
// If failed and queue is available, add to queue
if (!success && this.queue) {
await this.queue.addEvent(event, "cloud")
}
}

const result = rooCodeTelemetryEventSchema.safeParse(payload)
/**
* Attempts to capture an event with retry capability
* @param event The telemetry event to capture
* @returns True if the event was successfully sent, false if it should be retried
*/
protected override async captureWithRetry(event: TelemetryEvent): Promise<boolean> {
try {
const payload = {
type: event.event,
properties: await this.getEventProperties(event),
}

if (!result.success) {
console.error(
`[TelemetryClient#capture] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`,
)
if (this.debug) {
console.info(`[TelemetryClient#captureWithRetry] ${JSON.stringify(payload)}`)
}

return
}
const result = rooCodeTelemetryEventSchema.safeParse(payload)

if (!result.success) {
console.error(
`[TelemetryClient#captureWithRetry] Invalid telemetry event: ${result.error.message} - ${JSON.stringify(payload)}`,
)
// Don't retry invalid events
return true
}

try {
await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) })
} catch (error) {
console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`)
return true
} catch (_error) {
// Return false to trigger queue retry
return false
}
}

Expand Down
213 changes: 213 additions & 0 deletions packages/cloud/src/__tests__/TelemetryClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,4 +735,217 @@ describe("TelemetryClient", () => {
expect(fileContent).toBe("[]")
})
})

describe("captureWithRetry", () => {
it("should return true when event is captured successfully", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)

const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}

client.setProvider(mockProvider)

const captureWithRetry = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<boolean>
>(client, "captureWithRetry").bind(client)

const result = await captureWithRetry({
event: TelemetryEventName.TASK_CREATED,
properties: { taskId: "test-task-id" },
})

expect(result).toBe(true)
expect(mockFetch).toHaveBeenCalled()
})

it("should return true for invalid events (don't retry invalid events)", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)

const captureWithRetry = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<boolean>
>(client, "captureWithRetry").bind(client)

const result = await captureWithRetry({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" }, // Invalid properties
})

expect(result).toBe(true) // Don't retry invalid events
expect(mockFetch).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(expect.stringContaining("Invalid telemetry event"))
})

it("should return false when fetch fails", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)

mockFetch.mockRejectedValue(new Error("Network error"))

const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}

client.setProvider(mockProvider)

const captureWithRetry = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<boolean>
>(client, "captureWithRetry").bind(client)

const result = await captureWithRetry({
event: TelemetryEventName.TASK_CREATED,
properties: { taskId: "test-task-id" },
})

expect(result).toBe(false)
})
})

describe("queue integration", () => {
it("should add event to queue when captureWithRetry fails", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)

// Create a mock queue
const mockQueue = {
addEvent: vi.fn(),
} as any

client.setQueue(mockQueue)

// Make captureWithRetry fail
mockFetch.mockRejectedValue(new Error("Network error"))

const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}

client.setProvider(mockProvider)

await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { taskId: "test-task-id" },
})

expect(mockQueue.addEvent).toHaveBeenCalledWith(
{
event: TelemetryEventName.TASK_CREATED,
properties: { taskId: "test-task-id" },
},
"cloud",
)
})

it("should not add event to queue when captureWithRetry succeeds", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)

// Create a mock queue
const mockQueue = {
addEvent: vi.fn(),
} as any

client.setQueue(mockQueue)

const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}

client.setProvider(mockProvider)

await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { taskId: "test-task-id" },
})

expect(mockQueue.addEvent).not.toHaveBeenCalled()
})

it("should not fail when queue is not set", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)

// Make captureWithRetry fail
mockFetch.mockRejectedValue(new Error("Network error"))

const providerProperties = {
appName: "roo-code",
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
}

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue(providerProperties),
}

client.setProvider(mockProvider)

// Should not throw even without queue
await expect(
client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { taskId: "test-task-id" },
}),
).resolves.toBeUndefined()
})

it("should not add invalid events to queue", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)

// Create a mock queue
const mockQueue = {
addEvent: vi.fn(),
} as any

client.setQueue(mockQueue)

await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" }, // Invalid properties
})

// Should not add invalid events to queue
expect(mockQueue.addEvent).not.toHaveBeenCalled()
})
})
})
25 changes: 25 additions & 0 deletions packages/telemetry/src/BaseTelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@ import {
TelemetryPropertiesProvider,
TelemetryEventSubscription,
} from "@roo-code/types"
import { TelemetryQueue } from "./TelemetryQueue"

export abstract class BaseTelemetryClient implements TelemetryClient {
protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
protected telemetryEnabled: boolean = false
protected queue?: TelemetryQueue

constructor(
public readonly subscription?: TelemetryEventSubscription,
Expand Down Expand Up @@ -59,6 +61,29 @@ export abstract class BaseTelemetryClient implements TelemetryClient {

public abstract capture(event: TelemetryEvent): Promise<void>

/**
* Attempts to capture an event with retry capability
* @param event The telemetry event to capture
* @returns True if the event was successfully sent, false if it should be retried
*/
protected abstract captureWithRetry(event: TelemetryEvent): Promise<boolean>

/**
* Gets the queue instance if available
* @returns The TelemetryQueue instance or undefined
*/
protected getQueue(): TelemetryQueue | undefined {
return this.queue
}

/**
* Sets the queue instance for this client
* @param queue The TelemetryQueue instance
*/
public setQueue(queue: TelemetryQueue): void {
this.queue = queue
}

public setProvider(provider: TelemetryPropertiesProvider): void {
this.providerRef = new WeakRef(provider)
}
Expand Down
36 changes: 31 additions & 5 deletions packages/telemetry/src/PostHogTelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,11 +54,37 @@ export class PostHogTelemetryClient extends BaseTelemetryClient {
console.info(`[PostHogTelemetryClient#capture] ${event.event}`)
}

this.client.capture({
distinctId: this.distinctId,
event: event.event,
properties: await this.getEventProperties(event),
})
// Try to send directly first
const success = await this.captureWithRetry(event)

// If failed and queue is available, add to queue
if (!success && this.queue) {
await this.queue.addEvent(event, "posthog")
}
}

/**
* Attempts to capture an event with retry capability
* @param event The telemetry event to capture
* @returns True if the event was successfully sent, false if it should be retried
*/
protected override async captureWithRetry(event: TelemetryEvent): Promise<boolean> {
try {
// PostHog client has its own internal queue, but we need to detect if it's failing
// We'll wrap the capture call and check for errors
this.client.capture({
distinctId: this.distinctId,
event: event.event,
properties: await this.getEventProperties(event),
})

// PostHog's capture is async but doesn't return a promise by default
// We assume success - PostHog has its own internal queue
return true
} catch (_error) {
// Return false to trigger queue retry
return false
}
}

/**
Expand Down
Loading