Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
31 changes: 30 additions & 1 deletion packages/cloud/src/CloudService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { StaticSettingsService } from "./StaticSettingsService.js"
import { CloudTelemetryClient as TelemetryClient } from "./TelemetryClient.js"
import { CloudShareService } from "./CloudShareService.js"
import { CloudAPI } from "./CloudAPI.js"
import { RetryQueue } from "./retry-queue/index.js"

type AuthStateChangedPayload = CloudServiceEvents["auth-state-changed"][0]
type AuthUserInfoPayload = CloudServiceEvents["user-info"][0]
Expand Down Expand Up @@ -75,6 +76,12 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
return this._cloudAPI
}

private _retryQueue: RetryQueue | null = null

public get retryQueue() {
return this._retryQueue
}

private constructor(context: ExtensionContext, log?: (...args: unknown[]) => void) {
super()

Expand Down Expand Up @@ -131,7 +138,25 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di

this._cloudAPI = new CloudAPI(this._authService, this.log)

this._telemetryClient = new TelemetryClient(this._authService, this._settingsService)
// Initialize retry queue with auth header provider
this._retryQueue = new RetryQueue(
this.context,
undefined, // Use default config
this.log,
() => {
// Provide fresh auth headers for retries
const sessionToken = this._authService?.getSessionToken()
if (sessionToken) {
return {
Authorization: `Bearer ${sessionToken}`,
"X-Organization-Id": this._authService?.getStoredOrganizationId() || "",
}
}
return undefined
},
)

this._telemetryClient = new TelemetryClient(this._authService, this._settingsService, this._retryQueue)

this._shareService = new CloudShareService(this._cloudAPI, this._settingsService, this.log)

Expand Down Expand Up @@ -298,6 +323,10 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements Di
this.settingsService.dispose()
}

if (this._retryQueue) {
this._retryQueue.dispose()
}

this.isInitialized = false
}

Expand Down
81 changes: 59 additions & 22 deletions packages/cloud/src/TelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
} from "@roo-code/types"

import { getRooCodeApiUrl } from "./config.js"
import type { RetryQueue } from "./retry-queue/index.js"

abstract class BaseTelemetryClient implements TelemetryClient {
protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
Expand Down Expand Up @@ -82,18 +83,18 @@ abstract class BaseTelemetryClient implements TelemetryClient {
}

export class CloudTelemetryClient extends BaseTelemetryClient {
private retryQueue: RetryQueue | null = null

constructor(
private authService: AuthService,
private settingsService: SettingsService,
debug = false,
retryQueue?: RetryQueue,
) {
super(
{
type: "exclude",
events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
},
debug,
)
super({
type: "exclude",
events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
})
this.retryQueue = retryQueue || null
}

private async fetch(path: string, options: RequestInit) {
Expand All @@ -108,18 +109,39 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
return
}

const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, {
const url = `${getRooCodeApiUrl()}/api/${path}`
const fetchOptions: RequestInit = {
...options,
headers: {
Authorization: `Bearer ${token}`,
"Content-Type": "application/json",
},
})
}

if (!response.ok) {
console.error(
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
)
try {
const response = await fetch(url, fetchOptions)

if (!response.ok) {
console.error(
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
)
}

return response
} catch (error) {
console.error(`[TelemetryClient#fetch] Network error for ${options.method} ${path}: ${error}`)

// Queue for retry if we have a retry queue and it's a network error
if (this.retryQueue && error instanceof TypeError && error.message.includes("fetch failed")) {
await this.retryQueue.enqueue(
url,
fetchOptions,
"telemetry",
`Telemetry: ${options.method} /api/${path}`,
)
}

throw error
}
}

Expand Down Expand Up @@ -158,6 +180,7 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
})
} catch (error) {
console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`)
// Error is already queued for retry in the fetch method
}
}

Expand Down Expand Up @@ -200,21 +223,35 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
}

// Custom fetch for multipart - don't set Content-Type header (let browser set it)
const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, {
const url = `${getRooCodeApiUrl()}/api/events/backfill`
const fetchOptions: RequestInit = {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
// Note: No Content-Type header - browser will set multipart/form-data with boundary
},
body: formData,
})
}

if (!response.ok) {
console.error(
`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
)
} else if (this.debug) {
console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`)
try {
const response = await fetch(url, fetchOptions)

if (!response.ok) {
console.error(
`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
)
}
} catch (fetchError) {
// For backfill, also queue for retry on network errors
if (this.retryQueue && fetchError instanceof TypeError && fetchError.message.includes("fetch failed")) {
await this.retryQueue.enqueue(
url,
fetchOptions,
"telemetry",
`Telemetry: Backfill messages for task ${taskId}`,
)
}
throw fetchError
}
} catch (error) {
console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`)
Expand Down
23 changes: 2 additions & 21 deletions packages/cloud/src/__tests__/TelemetryClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -684,27 +684,8 @@ describe("TelemetryClient", () => {
)
})

it("should log debug information when debug is enabled", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService, true)

const messages = [
{
ts: 1,
type: "say" as const,
say: "text" as const,
text: "test message",
},
]

await client.backfillMessages(messages, "test-task-id")

expect(console.info).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id",
)
expect(console.info).toHaveBeenCalledWith(
"[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id",
)
})
// Debug logging has been removed in the new implementation
// This test is no longer applicable

it("should handle empty messages array", async () => {
const client = new TelemetryClient(mockAuthService, mockSettingsService)
Expand Down
3 changes: 3 additions & 0 deletions packages/cloud/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,6 @@ export * from "./config.js"
export { CloudService } from "./CloudService.js"

export { BridgeOrchestrator } from "./bridge/index.js"

export { RetryQueue } from "./retry-queue/index.js"
export type { QueuedRequest, QueueStats, RetryQueueConfig, RetryQueueEvents } from "./retry-queue/index.js"
Loading
Loading