Skip to content

Commit 7561997

Browse files
committed
feat(cloud): Add telemetry retry queue for network resilience
- Implement RetryQueue class with workspace-scoped persistence - Queue failed telemetry events for automatic retry - Retry events every 60 seconds with fresh auth tokens - FIFO eviction when queue reaches 100 events - Persist queue across VS Code restarts This ensures telemetry data isn't lost during network failures or temporary server issues. Migrated from RooCodeInc/Roo-Code-Cloud#744
1 parent 2e59347 commit 7561997

File tree

8 files changed

+547
-44
lines changed

8 files changed

+547
-44
lines changed

packages/cloud/src/CloudService.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import { StaticSettingsService } from "./StaticSettingsService.js"
2424
import { CloudTelemetryClient as TelemetryClient } from "./TelemetryClient.js"
2525
import { CloudShareService } from "./CloudShareService.js"
2626
import { CloudAPI } from "./CloudAPI.js"
27+
import { RetryQueue } from "./retry-queue/index.js"
2728

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

79+
private _retryQueue: RetryQueue | null = null
80+
81+
public get retryQueue() {
82+
return this._retryQueue
83+
}
84+
7885
private constructor(context: ExtensionContext, log?: (...args: unknown[]) => void) {
7986
super()
8087

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

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

134-
this._telemetryClient = new TelemetryClient(this._authService, this._settingsService)
141+
// Initialize retry queue with auth header provider
142+
this._retryQueue = new RetryQueue(
143+
this.context,
144+
undefined, // Use default config
145+
this.log,
146+
() => {
147+
// Provide fresh auth headers for retries
148+
const sessionToken = this._authService?.getSessionToken()
149+
if (sessionToken) {
150+
return {
151+
Authorization: `Bearer ${sessionToken}`,
152+
"X-Organization-Id": this._authService?.getStoredOrganizationId() || "",
153+
}
154+
}
155+
return undefined
156+
},
157+
)
158+
159+
this._telemetryClient = new TelemetryClient(this._authService, this._settingsService, this._retryQueue)
135160

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

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

326+
if (this._retryQueue) {
327+
this._retryQueue.dispose()
328+
}
329+
301330
this.isInitialized = false
302331
}
303332

packages/cloud/src/TelemetryClient.ts

Lines changed: 59 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
} from "@roo-code/types"
1212

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

1516
abstract class BaseTelemetryClient implements TelemetryClient {
1617
protected providerRef: WeakRef<TelemetryPropertiesProvider> | null = null
@@ -82,18 +83,18 @@ abstract class BaseTelemetryClient implements TelemetryClient {
8283
}
8384

8485
export class CloudTelemetryClient extends BaseTelemetryClient {
86+
private retryQueue: RetryQueue | null = null
87+
8588
constructor(
8689
private authService: AuthService,
8790
private settingsService: SettingsService,
88-
debug = false,
91+
retryQueue?: RetryQueue,
8992
) {
90-
super(
91-
{
92-
type: "exclude",
93-
events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
94-
},
95-
debug,
96-
)
93+
super({
94+
type: "exclude",
95+
events: [TelemetryEventName.TASK_CONVERSATION_MESSAGE],
96+
})
97+
this.retryQueue = retryQueue || null
9798
}
9899

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

111-
const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, {
112+
const url = `${getRooCodeApiUrl()}/api/${path}`
113+
const fetchOptions: RequestInit = {
112114
...options,
113115
headers: {
114116
Authorization: `Bearer ${token}`,
115117
"Content-Type": "application/json",
116118
},
117-
})
119+
}
118120

119-
if (!response.ok) {
120-
console.error(
121-
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
122-
)
121+
try {
122+
const response = await fetch(url, fetchOptions)
123+
124+
if (!response.ok) {
125+
console.error(
126+
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
127+
)
128+
}
129+
130+
return response
131+
} catch (error) {
132+
console.error(`[TelemetryClient#fetch] Network error for ${options.method} ${path}: ${error}`)
133+
134+
// Queue for retry if we have a retry queue and it's a network error
135+
if (this.retryQueue && error instanceof TypeError && error.message.includes("fetch failed")) {
136+
await this.retryQueue.enqueue(
137+
url,
138+
fetchOptions,
139+
"telemetry",
140+
`Telemetry: ${options.method} /api/${path}`,
141+
)
142+
}
143+
144+
throw error
123145
}
124146
}
125147

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

@@ -200,21 +223,35 @@ export class CloudTelemetryClient extends BaseTelemetryClient {
200223
}
201224

202225
// Custom fetch for multipart - don't set Content-Type header (let browser set it)
203-
const response = await fetch(`${getRooCodeApiUrl()}/api/events/backfill`, {
226+
const url = `${getRooCodeApiUrl()}/api/events/backfill`
227+
const fetchOptions: RequestInit = {
204228
method: "POST",
205229
headers: {
206230
Authorization: `Bearer ${token}`,
207231
// Note: No Content-Type header - browser will set multipart/form-data with boundary
208232
},
209233
body: formData,
210-
})
234+
}
211235

212-
if (!response.ok) {
213-
console.error(
214-
`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
215-
)
216-
} else if (this.debug) {
217-
console.info(`[TelemetryClient#backfillMessages] Successfully uploaded messages for task ${taskId}`)
236+
try {
237+
const response = await fetch(url, fetchOptions)
238+
239+
if (!response.ok) {
240+
console.error(
241+
`[TelemetryClient#backfillMessages] POST events/backfill -> ${response.status} ${response.statusText}`,
242+
)
243+
}
244+
} catch (fetchError) {
245+
// For backfill, also queue for retry on network errors
246+
if (this.retryQueue && fetchError instanceof TypeError && fetchError.message.includes("fetch failed")) {
247+
await this.retryQueue.enqueue(
248+
url,
249+
fetchOptions,
250+
"telemetry",
251+
`Telemetry: Backfill messages for task ${taskId}`,
252+
)
253+
}
254+
throw fetchError
218255
}
219256
} catch (error) {
220257
console.error(`[TelemetryClient#backfillMessages] Error uploading messages: ${error}`)

packages/cloud/src/__tests__/TelemetryClient.test.ts

Lines changed: 2 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -684,27 +684,8 @@ describe("TelemetryClient", () => {
684684
)
685685
})
686686

687-
it("should log debug information when debug is enabled", async () => {
688-
const client = new TelemetryClient(mockAuthService, mockSettingsService, true)
689-
690-
const messages = [
691-
{
692-
ts: 1,
693-
type: "say" as const,
694-
say: "text" as const,
695-
text: "test message",
696-
},
697-
]
698-
699-
await client.backfillMessages(messages, "test-task-id")
700-
701-
expect(console.info).toHaveBeenCalledWith(
702-
"[TelemetryClient#backfillMessages] Uploading 1 messages for task test-task-id",
703-
)
704-
expect(console.info).toHaveBeenCalledWith(
705-
"[TelemetryClient#backfillMessages] Successfully uploaded messages for task test-task-id",
706-
)
707-
})
687+
// Debug logging has been removed in the new implementation
688+
// This test is no longer applicable
708689

709690
it("should handle empty messages array", async () => {
710691
const client = new TelemetryClient(mockAuthService, mockSettingsService)

packages/cloud/src/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@ export * from "./config.js"
33
export { CloudService } from "./CloudService.js"
44

55
export { BridgeOrchestrator } from "./bridge/index.js"
6+
7+
export { RetryQueue } from "./retry-queue/index.js"
8+
export type { QueuedRequest, QueueStats, RetryQueueConfig, RetryQueueEvents } from "./retry-queue/index.js"

0 commit comments

Comments
 (0)