Skip to content

Commit 0a270e3

Browse files
committed
feat: Add persistent retry queue for failed telemetry events
- Implement TelemetryQueue class with persistent storage using VSCode globalState API - Add automatic retry with exponential backoff (max 5 retries) - Queue events when offline and process when connection restored - Add connection monitoring with periodic health checks - Update UI to show connection status in account icon - Add comprehensive test coverage for queue functionality - Integrate queue seamlessly with existing TelemetryClient Fixes #4940
1 parent ebfd384 commit 0a270e3

File tree

14 files changed

+1363
-15
lines changed

14 files changed

+1363
-15
lines changed

packages/cloud/src/CloudService.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
8888
}
8989

9090
this.telemetryClient = new TelemetryClient(this.authService, this.settingsService)
91+
this.telemetryClient.setContext(this.context)
9192
this.shareService = new ShareService(this.authService, this.settingsService, this.log)
9293

9394
try {
@@ -196,6 +197,16 @@ export class CloudService extends EventEmitter<CloudServiceEvents> implements vs
196197
this.telemetryClient!.capture(event)
197198
}
198199

200+
public getTelemetryConnectionStatus(): "online" | "offline" {
201+
this.ensureInitialized()
202+
return this.telemetryClient!.getConnectionStatus()
203+
}
204+
205+
public getTelemetryQueueSize(): number {
206+
this.ensureInitialized()
207+
return this.telemetryClient!.getQueueSize()
208+
}
209+
199210
// ShareService
200211

201212
public async shareTask(

packages/cloud/src/TelemetryClient.ts

Lines changed: 120 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as vscode from "vscode"
12
import {
23
TelemetryEventName,
34
type TelemetryEvent,
@@ -9,8 +10,13 @@ import { BaseTelemetryClient } from "@roo-code/telemetry"
910
import { getRooCodeApiUrl } from "./Config"
1011
import type { AuthService } from "./auth"
1112
import type { SettingsService } from "./SettingsService"
13+
import { TelemetryQueue } from "./TelemetryQueue"
1214

1315
export class TelemetryClient extends BaseTelemetryClient {
16+
private telemetryQueue: TelemetryQueue | null = null
17+
private context: vscode.ExtensionContext | null = null
18+
private isOnline = true
19+
1420
constructor(
1521
private authService: AuthService,
1622
private settingsService: SettingsService,
@@ -25,27 +31,56 @@ export class TelemetryClient extends BaseTelemetryClient {
2531
)
2632
}
2733

28-
private async fetch(path: string, options: RequestInit) {
34+
public setContext(context: vscode.ExtensionContext): void {
35+
this.context = context
36+
37+
try {
38+
this.telemetryQueue = new TelemetryQueue(context)
39+
40+
// Process any queued events on initialization
41+
this.processQueuedEvents()
42+
} catch (error) {
43+
console.error(`Failed to initialize telemetry queue: ${error}`)
44+
// Continue without queue functionality
45+
this.telemetryQueue = null
46+
}
47+
}
48+
49+
private async fetch(path: string, options: RequestInit): Promise<boolean> {
2950
if (!this.authService.isAuthenticated()) {
30-
return
51+
return false
3152
}
3253

3354
const token = this.authService.getSessionToken()
3455

3556
if (!token) {
3657
console.error(`[TelemetryClient#fetch] Unauthorized: No session token available.`)
37-
return
58+
return false
3859
}
3960

40-
const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, {
41-
...options,
42-
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
43-
})
61+
try {
62+
const response = await fetch(`${getRooCodeApiUrl()}/api/${path}`, {
63+
...options,
64+
headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
65+
})
4466

45-
if (!response.ok) {
46-
console.error(
47-
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
48-
)
67+
const isSuccess = response.ok
68+
69+
if (!isSuccess) {
70+
console.error(
71+
`[TelemetryClient#fetch] ${options.method} ${path} -> ${response.status} ${response.statusText}`,
72+
)
73+
}
74+
75+
// Update connection status based on response
76+
this.updateConnectionStatus(isSuccess || response.status < 500)
77+
78+
return isSuccess
79+
} catch (error) {
80+
// Network error - we're offline
81+
console.error(`[TelemetryClient#fetch] Network error: ${error}`)
82+
this.updateConnectionStatus(false)
83+
return false
4984
}
5085
}
5186

@@ -78,9 +113,19 @@ export class TelemetryClient extends BaseTelemetryClient {
78113
}
79114

80115
try {
81-
await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) })
116+
const success = await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) })
117+
118+
if (!success && this.telemetryQueue) {
119+
// Failed to send, add to queue
120+
await this.telemetryQueue.enqueue(event)
121+
}
82122
} catch (error) {
83123
console.error(`[TelemetryClient#capture] Error sending telemetry event: ${error}`)
124+
125+
// Add to queue on error
126+
if (this.telemetryQueue) {
127+
await this.telemetryQueue.enqueue(event)
128+
}
84129
}
85130
}
86131

@@ -165,5 +210,67 @@ export class TelemetryClient extends BaseTelemetryClient {
165210
return true
166211
}
167212

168-
public override async shutdown() {}
213+
public override async shutdown() {
214+
if (this.telemetryQueue) {
215+
this.telemetryQueue.dispose()
216+
}
217+
}
218+
219+
private updateConnectionStatus(isOnline: boolean): void {
220+
this.isOnline = isOnline
221+
222+
if (this.telemetryQueue) {
223+
this.telemetryQueue.updateConnectionStatus(isOnline)
224+
225+
// If we're back online, process queued events
226+
if (isOnline) {
227+
this.processQueuedEvents()
228+
}
229+
}
230+
}
231+
232+
private async processQueuedEvents(): Promise<void> {
233+
if (!this.telemetryQueue) {
234+
return
235+
}
236+
237+
await this.telemetryQueue.processQueue(async (event) => {
238+
// Reuse the capture logic but send directly
239+
const payload = {
240+
type: event.event,
241+
properties: await this.getEventProperties(event),
242+
}
243+
244+
const result = rooCodeTelemetryEventSchema.safeParse(payload)
245+
246+
if (!result.success) {
247+
// Invalid event, don't retry
248+
return true
249+
}
250+
251+
return await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) })
252+
})
253+
}
254+
255+
public async checkConnection(): Promise<void> {
256+
// Simple health check to update connection status
257+
try {
258+
const response = await fetch(`${getRooCodeApiUrl()}/api/health`, {
259+
method: "GET",
260+
headers: { "Content-Type": "application/json" },
261+
})
262+
263+
this.updateConnectionStatus(response.ok)
264+
} catch {
265+
this.updateConnectionStatus(false)
266+
}
267+
}
268+
269+
public getConnectionStatus(): "online" | "offline" {
270+
return this.telemetryQueue?.getConnectionStatus() || "online"
271+
}
272+
273+
public getQueueSize(): number {
274+
return this.telemetryQueue?.getQueueSize() || 0
275+
}
169276
}

0 commit comments

Comments
 (0)