Roo-Code/src/extension.ts
daniel-lxs c1d4b9ad55
feat: add telemetry queue persistence system
- Implement TelemetryQueueManager for persistent event storage
- Add QueuedTelemetryClient base class with retry logic
- Update PostHogTelemetryClient to use queuing system
- Store queue per-workspace to avoid conflicts
- Add exponential backoff retry (1s to 60s max)
- Events persist for 24 hours before expiring
- Queue limited to 100 events to manage file size
- Add comprehensive tests for queue functionality
- Disable PostHog's internal queue for better control

This ensures telemetry events are not lost during network
outages or server downtime, with events persisted to disk
and retried automatically when connectivity is restored.
2025-08-15 11:28:11 -05:00

294 lines
10 KiB
TypeScript

import * as vscode from "vscode"
import * as dotenvx from "@dotenvx/dotenvx"
import * as path from "path"
// Load environment variables from .env file
try {
// Specify path to .env file in the project root directory
const envPath = path.join(__dirname, "..", ".env")
dotenvx.config({ path: envPath })
} catch (e) {
// Silently handle environment loading errors
console.warn("Failed to load environment variables:", e)
}
import { CloudService, ExtensionBridgeService } from "@roo-code/cloud"
import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry"
import "./utils/path" // Necessary to have access to String.prototype.toPosix.
import { createOutputChannelLogger, createDualLogger } from "./utils/outputChannelLogger"
import { Package } from "./shared/package"
import { formatLanguage } from "./shared/language"
import { ContextProxy } from "./core/config/ContextProxy"
import { ClineProvider } from "./core/webview/ClineProvider"
import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
import { TerminalRegistry } from "./integrations/terminal/TerminalRegistry"
import { McpServerManager } from "./services/mcp/McpServerManager"
import { CodeIndexManager } from "./services/code-index/manager"
import { MdmService } from "./services/mdm/MdmService"
import { migrateSettings } from "./utils/migrateSettings"
import { autoImportSettings } from "./utils/autoImportSettings"
import { isRemoteControlEnabled } from "./utils/remoteControl"
import { API } from "./extension/api"
import {
handleUri,
registerCommands,
registerCodeActions,
registerTerminalActions,
CodeActionProvider,
} from "./activate"
import { initializeI18n } from "./i18n"
/**
* Built using https://github.com/microsoft/vscode-webview-ui-toolkit
*
* Inspired by:
* - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/default/weather-webview
* - https://github.com/microsoft/vscode-webview-ui-toolkit-samples/tree/main/frameworks/hello-world-react-cra
*/
let outputChannel: vscode.OutputChannel
let extensionContext: vscode.ExtensionContext
// This method is called when your extension is activated.
// Your extension is activated the very first time the command is executed.
export async function activate(context: vscode.ExtensionContext) {
extensionContext = context
outputChannel = vscode.window.createOutputChannel(Package.outputChannel)
context.subscriptions.push(outputChannel)
outputChannel.appendLine(`${Package.name} extension activated - ${JSON.stringify(Package)}`)
// Migrate old settings to new
await migrateSettings(context, outputChannel)
// Initialize telemetry service.
const telemetryService = TelemetryService.createInstance()
try {
// Enable debug mode for telemetry during development
const debugTelemetry = process.env.DEBUG_TELEMETRY === "true" || process.env.NODE_ENV === "development"
telemetryService.register(new PostHogTelemetryClient(context, debugTelemetry))
} catch (error) {
console.warn("Failed to register PostHogTelemetryClient:", error)
}
// Create logger for cloud services.
const cloudLogger = createDualLogger(createOutputChannelLogger(outputChannel))
// Initialize MDM service
const mdmService = await MdmService.createInstance(cloudLogger)
// Initialize i18n for internationalization support.
initializeI18n(context.globalState.get("language") ?? formatLanguage(vscode.env.language))
// Initialize terminal shell execution handlers.
TerminalRegistry.initialize()
// Get default commands from configuration.
const defaultCommands = vscode.workspace.getConfiguration(Package.name).get<string[]>("allowedCommands") || []
// Initialize global state if not already set.
if (!context.globalState.get("allowedCommands")) {
context.globalState.update("allowedCommands", defaultCommands)
}
const contextProxy = await ContextProxy.getInstance(context)
// Initialize code index managers for all workspace folders
const codeIndexManagers: CodeIndexManager[] = []
if (vscode.workspace.workspaceFolders) {
for (const folder of vscode.workspace.workspaceFolders) {
const manager = CodeIndexManager.getInstance(context, folder.uri.fsPath)
if (manager) {
codeIndexManagers.push(manager)
try {
await manager.initialize(contextProxy)
} catch (error) {
outputChannel.appendLine(
`[CodeIndexManager] Error during background CodeIndexManager configuration/indexing for ${folder.uri.fsPath}: ${error.message || error}`,
)
}
context.subscriptions.push(manager)
}
}
}
// Initialize Roo Code Cloud service.
const cloudService = await CloudService.createInstance(context, cloudLogger)
try {
if (cloudService.telemetryClient) {
TelemetryService.instance.register(cloudService.telemetryClient)
}
} catch (error) {
outputChannel.appendLine(
`[CloudService] Failed to register TelemetryClient: ${error instanceof Error ? error.message : String(error)}`,
)
}
const postStateListener = () => ClineProvider.getVisibleInstance()?.postStateToWebview()
cloudService.on("auth-state-changed", postStateListener)
cloudService.on("settings-updated", postStateListener)
cloudService.on("user-info", async ({ userInfo }) => {
postStateListener()
const bridgeConfig = await cloudService.cloudAPI?.bridgeConfig().catch(() => undefined)
if (!bridgeConfig) {
outputChannel.appendLine("[CloudService] Failed to get bridge config")
return
}
ExtensionBridgeService.handleRemoteControlState(
userInfo,
contextProxy.getValue("remoteControlEnabled"),
{ ...bridgeConfig, provider, sessionId: vscode.env.sessionId },
(message: string) => outputChannel.appendLine(message),
)
})
// Add to subscriptions for proper cleanup on deactivate.
context.subscriptions.push(cloudService)
const provider = new ClineProvider(context, outputChannel, "sidebar", contextProxy, mdmService)
TelemetryService.instance.setProvider(provider)
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(ClineProvider.sideBarId, provider, {
webviewOptions: { retainContextWhenHidden: true },
}),
)
// Auto-import configuration if specified in settings.
try {
await autoImportSettings(outputChannel, {
providerSettingsManager: provider.providerSettingsManager,
contextProxy: provider.contextProxy,
customModesManager: provider.customModesManager,
})
} catch (error) {
outputChannel.appendLine(
`[AutoImport] Error during auto-import: ${error instanceof Error ? error.message : String(error)}`,
)
}
registerCommands({ context, outputChannel, provider })
/**
* We use the text document content provider API to show the left side for diff
* view by creating a virtual document for the original content. This makes it
* readonly so users know to edit the right side if they want to keep their changes.
*
* This API allows you to create readonly documents in VSCode from arbitrary
* sources, and works by claiming an uri-scheme for which your provider then
* returns text contents. The scheme must be provided when registering a
* provider and cannot change afterwards.
*
* Note how the provider doesn't create uris for virtual documents - its role
* is to provide contents given such an uri. In return, content providers are
* wired into the open document logic so that providers are always considered.
*
* https://code.visualstudio.com/api/extension-guides/virtual-documents
*/
const diffContentProvider = new (class implements vscode.TextDocumentContentProvider {
provideTextDocumentContent(uri: vscode.Uri): string {
return Buffer.from(uri.query, "base64").toString("utf-8")
}
})()
context.subscriptions.push(
vscode.workspace.registerTextDocumentContentProvider(DIFF_VIEW_URI_SCHEME, diffContentProvider),
)
context.subscriptions.push(vscode.window.registerUriHandler({ handleUri }))
// Register code actions provider.
context.subscriptions.push(
vscode.languages.registerCodeActionsProvider({ pattern: "**/*" }, new CodeActionProvider(), {
providedCodeActionKinds: CodeActionProvider.providedCodeActionKinds,
}),
)
registerCodeActions(context)
registerTerminalActions(context)
// Allows other extensions to activate once Roo is ready.
vscode.commands.executeCommand(`${Package.name}.activationCompleted`)
// Implements the `RooCodeAPI` interface.
const socketPath = process.env.ROO_CODE_IPC_SOCKET_PATH
const enableLogging = typeof socketPath === "string"
// Watch the core files and automatically reload the extension host.
if (process.env.NODE_ENV === "development") {
const watchPaths = [
{ path: context.extensionPath, pattern: "**/*.ts" },
{ path: path.join(context.extensionPath, "../packages/types"), pattern: "**/*.ts" },
{ path: path.join(context.extensionPath, "../packages/telemetry"), pattern: "**/*.ts" },
{ path: path.join(context.extensionPath, "node_modules/@roo-code/cloud"), pattern: "**/*" },
]
console.log(
`♻️♻️♻️ Core auto-reloading: Watching for changes in ${watchPaths.map(({ path }) => path).join(", ")}`,
)
// Create a debounced reload function to prevent excessive reloads
let reloadTimeout: NodeJS.Timeout | undefined
const DEBOUNCE_DELAY = 1_000
const debouncedReload = (uri: vscode.Uri) => {
if (reloadTimeout) {
clearTimeout(reloadTimeout)
}
console.log(`♻️ ${uri.fsPath} changed; scheduling reload...`)
reloadTimeout = setTimeout(() => {
console.log(`♻️ Reloading host after debounce delay...`)
vscode.commands.executeCommand("workbench.action.reloadWindow")
}, DEBOUNCE_DELAY)
}
watchPaths.forEach(({ path: watchPath, pattern }) => {
const relPattern = new vscode.RelativePattern(vscode.Uri.file(watchPath), pattern)
const watcher = vscode.workspace.createFileSystemWatcher(relPattern, false, false, false)
// Listen to all change types to ensure symlinked file updates trigger reloads.
watcher.onDidChange(debouncedReload)
watcher.onDidCreate(debouncedReload)
watcher.onDidDelete(debouncedReload)
context.subscriptions.push(watcher)
})
// Clean up the timeout on deactivation
context.subscriptions.push({
dispose: () => {
if (reloadTimeout) {
clearTimeout(reloadTimeout)
}
},
})
}
return new API(outputChannel, provider, socketPath, enableLogging)
}
// This method is called when your extension is deactivated.
export async function deactivate() {
outputChannel.appendLine(`${Package.name} extension deactivated`)
const bridgeService = ExtensionBridgeService.getInstance()
if (bridgeService) {
await bridgeService.disconnect()
}
await McpServerManager.cleanup(extensionContext)
TelemetryService.instance.shutdown()
TerminalRegistry.cleanup()
}