diff --git a/packages/cloud/src/CloudService.ts b/packages/cloud/src/CloudService.ts index 65d6fd3e87..66fbcc9b07 100644 --- a/packages/cloud/src/CloudService.ts +++ b/packages/cloud/src/CloudService.ts @@ -39,6 +39,7 @@ export class CloudService extends EventEmitter implements vs private shareService: ShareService | null = null private connectionMonitor: ConnectionMonitor | null = null private queueManager: TelemetryQueueManager | null = null + private connectionRestoredDebounceTimer: NodeJS.Timeout | null = null private isInitialized = false private log: (...args: unknown[]) => void @@ -103,26 +104,26 @@ export class CloudService extends EventEmitter implements vs try { const { ContextProxy } = await import("../../../src/core/config/ContextProxy") isQueueEnabled = ContextProxy.instance.getValue("telemetryQueueEnabled") ?? true - } catch (_error) { + } catch (error) { // Default to enabled if we can't access settings - this.log("[CloudService] Could not access telemetryQueueEnabled setting, defaulting to enabled") + this.log("[CloudService] Could not access telemetryQueueEnabled setting:", error) + isQueueEnabled = true } if (isQueueEnabled) { // Set up connection monitoring with debouncing - let connectionRestoredDebounceTimer: NodeJS.Timeout | null = null const connectionRestoredDebounceDelay = 3000 // 3 seconds this.connectionMonitor.onConnectionRestored(() => { this.log("[CloudService] Connection restored, scheduling queue processing") // Clear any existing timer - if (connectionRestoredDebounceTimer) { - clearTimeout(connectionRestoredDebounceTimer) + if (this.connectionRestoredDebounceTimer) { + clearTimeout(this.connectionRestoredDebounceTimer) } // Schedule queue processing with debounce - connectionRestoredDebounceTimer = setTimeout(() => { + this.connectionRestoredDebounceTimer = setTimeout(() => { this.queueManager ?.processQueue() .then(() => { @@ -321,6 +322,11 @@ export class CloudService extends EventEmitter implements vs if (this.connectionMonitor) { this.connectionMonitor.dispose() } + // Clean up any pending debounce timer + if (this.connectionRestoredDebounceTimer) { + clearTimeout(this.connectionRestoredDebounceTimer) + this.connectionRestoredDebounceTimer = null + } this.isInitialized = false } diff --git a/packages/cloud/src/ConnectionMonitor.ts b/packages/cloud/src/ConnectionMonitor.ts index a84a93f113..edf701c3d1 100644 --- a/packages/cloud/src/ConnectionMonitor.ts +++ b/packages/cloud/src/ConnectionMonitor.ts @@ -6,6 +6,7 @@ export class ConnectionMonitor extends EventEmitter { private checkInterval: NodeJS.Timeout | null = null private readonly healthCheckEndpoint = "/api/health" private readonly defaultCheckInterval = 30000 // 30 seconds + private readonly defaultTimeoutMs = 5000 // 5 seconds constructor() { super() @@ -17,7 +18,7 @@ export class ConnectionMonitor extends EventEmitter { public async checkConnection(): Promise { try { const controller = new AbortController() - const timeoutId = setTimeout(() => controller.abort(), 5000) // 5 second timeout + const timeoutId = setTimeout(() => controller.abort(), this.defaultTimeoutMs) const response = await fetch(`${getRooCodeApiUrl()}${this.healthCheckEndpoint}`, { method: "GET", diff --git a/packages/cloud/src/TelemetryClient.ts b/packages/cloud/src/TelemetryClient.ts index caa9e55846..8b5a9c433c 100644 --- a/packages/cloud/src/TelemetryClient.ts +++ b/packages/cloud/src/TelemetryClient.ts @@ -18,6 +18,7 @@ export class TelemetryClient extends BaseTelemetryClient { private isQueueEnabled: boolean = false private log: (...args: unknown[]) => void private processQueueDebounceTimer: NodeJS.Timeout | null = null + private processQueueAbortController: AbortController | null = null private readonly processQueueDebounceDelay = 5000 // 5 seconds constructor( @@ -123,8 +124,16 @@ export class TelemetryClient extends BaseTelemetryClient { if (this.processQueueDebounceTimer) { clearTimeout(this.processQueueDebounceTimer) } + if (this.processQueueAbortController) { + this.processQueueAbortController.abort() + } + this.processQueueAbortController = new AbortController() + const signal = this.processQueueAbortController.signal this.processQueueDebounceTimer = setTimeout(() => { + if (signal.aborted) { + return + } this.queueManager.processQueue().catch((error) => { this.log(`[TelemetryClient#debouncedProcessQueue] Error processing queue: ${error}`) }) @@ -218,6 +227,11 @@ export class TelemetryClient extends BaseTelemetryClient { clearTimeout(this.processQueueDebounceTimer) this.processQueueDebounceTimer = null } + // Abort any pending operations + if (this.processQueueAbortController) { + this.processQueueAbortController.abort() + this.processQueueAbortController = null + } // Process any remaining queued events before shutdown if queue is enabled if (this.isQueueEnabled) { @@ -244,18 +258,25 @@ export class TelemetryClient extends BaseTelemetryClient { // Process each event individually to maintain compatibility for (const queuedEvent of events) { - const payload = { - type: queuedEvent.event.event, - properties: await this.getEventProperties(queuedEvent.event), - } + try { + const payload = { + type: queuedEvent.event.event, + properties: await this.getEventProperties(queuedEvent.event), + } - const result = rooCodeTelemetryEventSchema.safeParse(payload) - if (!result.success) { - this.log(`[TelemetryClient#processBatchedEvents] Invalid telemetry event: ${result.error.message}`) - continue - } + const result = rooCodeTelemetryEventSchema.safeParse(payload) + if (!result.success) { + this.log(`[TelemetryClient#processBatchedEvents] Invalid telemetry event: ${result.error.message}`) + continue + } - await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) }) + await this.fetch(`events`, { method: "POST", body: JSON.stringify(result.data) }) + } catch (error) { + // Log the error but continue processing other events + this.log(`[TelemetryClient#processBatchedEvents] Error processing event ${queuedEvent.id}: ${error}`) + // Re-throw to let the queue manager handle retry logic + throw error + } } } } diff --git a/packages/cloud/src/TelemetryQueueManager.ts b/packages/cloud/src/TelemetryQueueManager.ts index 1d99ab81f5..0dba5feba2 100644 --- a/packages/cloud/src/TelemetryQueueManager.ts +++ b/packages/cloud/src/TelemetryQueueManager.ts @@ -4,6 +4,7 @@ import { ContextProxy } from "../../../src/core/config/ContextProxy" export class TelemetryQueueManager { private static instance: TelemetryQueueManager + private static readonly ABSOLUTE_MAX_QUEUE_SIZE = 5000 private queue: QueuedTelemetryEvent[] = [] private isProcessing = false private maxQueueSize = 1000 @@ -37,7 +38,10 @@ export class TelemetryQueueManager { */ public async addToQueue(event: TelemetryEvent, priority: "high" | "normal" = "normal"): Promise { const queuedEvent: QueuedTelemetryEvent = { - id: crypto.randomUUID(), + id: + typeof crypto !== "undefined" && crypto.randomUUID + ? crypto.randomUUID() + : `fallback-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, event, timestamp: Date.now(), retryCount: 0, @@ -185,8 +189,11 @@ export class TelemetryQueueManager { if (storedQueue && Array.isArray(storedQueue)) { // Add validation for queue size to prevent memory issues - if (storedQueue.length > this.maxQueueSize * 2) { - this.log("[TelemetryQueueManager] Queue size exceeds safety limit, truncating to max size") + const effectiveMaxSize = Math.min(this.maxQueueSize * 2, TelemetryQueueManager.ABSOLUTE_MAX_QUEUE_SIZE) + if (storedQueue.length > effectiveMaxSize) { + this.log( + `[TelemetryQueueManager] Queue size (${storedQueue.length}) exceeds safety limit (${effectiveMaxSize}), truncating to max size`, + ) this.queue = (storedQueue as QueuedTelemetryEvent[]).slice(-this.maxQueueSize) } else { this.queue = storedQueue as QueuedTelemetryEvent[] diff --git a/webview-ui/src/i18n/locales/ca/account.json b/webview-ui/src/i18n/locales/ca/account.json index 4b140ca0d0..b60dc6e51b 100644 --- a/webview-ui/src/i18n/locales/ca/account.json +++ b/webview-ui/src/i18n/locales/ca/account.json @@ -11,5 +11,5 @@ "cloudBenefitSharing": "Funcions de compartició i col·laboració", "cloudBenefitMetrics": "Mètriques d'ús basades en tasques, tokens i costos", "visitCloudWebsite": "Visita Roo Code Cloud", - "offlineWarning": "Ara mateix estàs sense connexió. Els esdeveniments de telemetria s'encularan i s'enviaran quan es restableixi la connexió." + "offlineWarning": "Ara mateix estàs sense connexió. Els esdeveniments de telemetria s'encolaran i s'enviaran quan es restableixi la connexió." }