diff --git a/packages/telemetry/package.json b/packages/telemetry/package.json index 42d0df387c0..0961ec14082 100644 --- a/packages/telemetry/package.json +++ b/packages/telemetry/package.json @@ -11,6 +11,13 @@ "clean": "rimraf dist .turbo" }, "dependencies": { + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.56.0", + "@opentelemetry/instrumentation": "^0.56.0", + "@opentelemetry/resources": "^1.29.0", + "@opentelemetry/sdk-trace-base": "^1.29.0", + "@opentelemetry/sdk-trace-node": "^1.29.0", + "@opentelemetry/semantic-conventions": "^1.29.0", "@roo-code/types": "workspace:^", "posthog-node": "^5.0.0", "zod": "^3.25.61" diff --git a/packages/telemetry/src/OpenTelemetryClient.ts b/packages/telemetry/src/OpenTelemetryClient.ts new file mode 100644 index 00000000000..2c5cc40f324 --- /dev/null +++ b/packages/telemetry/src/OpenTelemetryClient.ts @@ -0,0 +1,209 @@ +import { trace, SpanStatusCode, Tracer } from "@opentelemetry/api" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" +import { Resource } from "@opentelemetry/resources" +import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions" +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node" +import { BatchSpanProcessor, ConsoleSpanExporter } from "@opentelemetry/sdk-trace-base" +import { registerInstrumentations } from "@opentelemetry/instrumentation" + +import { type TelemetryEvent } from "@roo-code/types" + +import { BaseTelemetryClient } from "./BaseTelemetryClient" + +// Conditionally import vscode only when not in test environment +// eslint-disable-next-line @typescript-eslint/no-explicit-any +let vscode: any +try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + vscode = require("vscode") +} catch { + // In test environment, vscode is not available + vscode = { + extensions: { + getExtension: () => ({ packageJSON: { version: "test-version" } }), + }, + workspace: { + getConfiguration: () => ({ + get: () => "all", + }), + }, + } +} + +export interface OtelEndpoint { + url: string + headers?: Record + enabled: boolean +} + +/** + * OpenTelemetryClient handles telemetry event tracking using OpenTelemetry. + * Supports sending traces to multiple custom endpoints in addition to internal endpoints. + * Respects user privacy settings and VSCode's global telemetry configuration. + */ +export class OpenTelemetryClient extends BaseTelemetryClient { + private tracer: Tracer | null = null + private provider: NodeTracerProvider | null = null + private endpoints: OtelEndpoint[] = [] + private isInitialized = false + + constructor(debug = false) { + super(undefined, debug) + } + + /** + * Initialize or reinitialize the OpenTelemetry provider with the given endpoints + * @param endpoints Array of OTEL collector endpoints to send traces to + */ + public async initialize(endpoints: OtelEndpoint[]): Promise { + try { + // Shutdown existing provider if any + if (this.provider) { + await this.shutdown() + } + + this.endpoints = endpoints.filter((ep) => ep.enabled) + + // Create resource with service information + const version = + vscode?.extensions?.getExtension?.("rooveterinaryinc.roo-cline")?.packageJSON?.version || "unknown" + const resource = new Resource({ + [ATTR_SERVICE_NAME]: "roo-code", + [ATTR_SERVICE_VERSION]: version, + }) + + // Create provider + this.provider = new NodeTracerProvider({ + resource, + }) + + // Add exporters for each endpoint + for (const endpoint of this.endpoints) { + const exporter = new OTLPTraceExporter({ + url: endpoint.url, + headers: endpoint.headers || {}, + }) + + // Use BatchSpanProcessor for better performance + this.provider.addSpanProcessor(new BatchSpanProcessor(exporter)) + } + + // Add console exporter in debug mode + if (this.debug) { + this.provider.addSpanProcessor(new BatchSpanProcessor(new ConsoleSpanExporter())) + } + + // Register the provider + this.provider.register() + + // Register instrumentations + registerInstrumentations({ + instrumentations: [], + }) + + // Only get tracer if we have endpoints + if (this.endpoints.length > 0) { + // Get tracer with version + this.tracer = trace.getTracer("roo-code-telemetry", version) + this.isInitialized = true + } + + if (this.debug) { + console.info(`[OpenTelemetryClient#initialize] Initialized with ${this.endpoints.length} endpoints`) + } + } catch (error) { + console.error("[OpenTelemetry] Failed to initialize:", error) + // Don't throw - just log the error + } + } + + /** + * Update endpoints configuration + * This will reinitialize the provider with the new endpoints + * @param endpoints New array of OTEL collector endpoints + */ + public async updateEndpoints(endpoints: OtelEndpoint[]): Promise { + await this.initialize(endpoints) + } + + public override async capture(event: TelemetryEvent): Promise { + if (!this.isTelemetryEnabled() || !this.isInitialized || !this.tracer) { + if (this.debug) { + console.info( + `[OpenTelemetryClient#capture] Skipping event: ${event.event} (enabled: ${this.isTelemetryEnabled()}, initialized: ${this.isInitialized})`, + ) + } + return + } + + try { + if (this.debug) { + console.info(`[OpenTelemetryClient#capture] ${event.event}`) + } + + // Get event properties + const properties = await this.getEventProperties(event) + + // Create a span for the event + const span = this.tracer.startSpan(event.event) + + // Set attributes after creating the span + if (properties && Object.keys(properties).length > 0) { + span.setAttributes(properties) + } + + // Set span status to OK and end it immediately since these are point-in-time events + span.setStatus({ code: SpanStatusCode.OK }) + span.end() + } catch (error) { + console.error("[OpenTelemetry] Failed to capture event:", error) + // Don't throw - just log the error + } + } + + /** + * Updates the telemetry state based on user preferences and VSCode settings. + * Only enables telemetry if both VSCode global telemetry is enabled and + * user has opted in. + * @param didUserOptIn Whether the user has explicitly opted into telemetry + */ + public override updateTelemetryState(didUserOptIn: boolean): void { + this.telemetryEnabled = false + + // First check global telemetry level - telemetry should only be enabled when level is "all". + const telemetryLevel = + vscode?.workspace?.getConfiguration?.("telemetry")?.get?.("telemetryLevel", "all") || "all" + const globalTelemetryEnabled = telemetryLevel === "all" + + // We only enable telemetry if global vscode telemetry is enabled. + if (globalTelemetryEnabled) { + this.telemetryEnabled = didUserOptIn + } + + if (this.debug) { + console.info(`[OpenTelemetryClient#updateTelemetryState] Telemetry enabled: ${this.telemetryEnabled}`) + } + } + + public override async shutdown(): Promise { + if (this.provider) { + try { + await this.provider.shutdown() + } catch (error) { + console.error("[OpenTelemetry] Failed to shutdown:", error) + // Don't throw - just log the error + } + this.provider = null + this.tracer = null + this.isInitialized = false + } + } + + /** + * Get the currently configured endpoints + * @returns Array of configured OTEL endpoints + */ + public getEndpoints(): OtelEndpoint[] { + return [...this.endpoints] + } +} diff --git a/packages/telemetry/src/__tests__/OpenTelemetryClient.test.ts b/packages/telemetry/src/__tests__/OpenTelemetryClient.test.ts new file mode 100644 index 00000000000..8c302cfaa5f --- /dev/null +++ b/packages/telemetry/src/__tests__/OpenTelemetryClient.test.ts @@ -0,0 +1,490 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ + +// npx vitest run src/__tests__/OpenTelemetryClient.test.ts + +import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node" +import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http" +import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base" +import { registerInstrumentations } from "@opentelemetry/instrumentation" +import { trace, context } from "@opentelemetry/api" + +import { type TelemetryPropertiesProvider, TelemetryEventName } from "@roo-code/types" + +import { OpenTelemetryClient } from "../OpenTelemetryClient" + +// Define OtelEndpoint type locally for tests +interface OtelEndpoint { + url: string + headers?: Record + enabled: boolean +} + +// Mock OpenTelemetry modules +vi.mock("@opentelemetry/sdk-trace-node") +vi.mock("@opentelemetry/exporter-trace-otlp-http") +vi.mock("@opentelemetry/sdk-trace-base") +vi.mock("@opentelemetry/instrumentation") +vi.mock("@opentelemetry/api") + +describe("OpenTelemetryClient", () => { + let mockProvider: any + let mockTracer: any + let mockSpan: any + let mockExporter: any + let mockProcessor: any + + beforeEach(() => { + vi.clearAllMocks() + + // Mock span + mockSpan = { + setAttributes: vi.fn().mockReturnThis(), + setStatus: vi.fn().mockReturnThis(), + recordException: vi.fn().mockReturnThis(), + end: vi.fn(), + } + + // Mock tracer + mockTracer = { + startSpan: vi.fn().mockReturnValue(mockSpan), + } + + // Mock provider + mockProvider = { + register: vi.fn(), + addSpanProcessor: vi.fn(), + shutdown: vi.fn().mockResolvedValue(undefined), + } + + // Mock exporter + mockExporter = { + shutdown: vi.fn().mockResolvedValue(undefined), + } + + // Mock processor + mockProcessor = { + shutdown: vi.fn().mockResolvedValue(undefined), + } + + // Setup mocks + ;(NodeTracerProvider as any).mockImplementation(() => mockProvider) + ;(OTLPTraceExporter as any).mockImplementation(() => mockExporter) + ;(BatchSpanProcessor as any).mockImplementation(() => mockProcessor) + ;(registerInstrumentations as any).mockImplementation(() => {}) + ;(trace.getTracer as any) = vi.fn().mockReturnValue(mockTracer) + ;(context.with as any) = vi.fn((ctx, fn) => fn()) + ;(context.active as any) = vi.fn().mockReturnValue({}) + }) + + describe("initialize", () => { + it("should initialize with multiple endpoints", async () => { + const client = new OpenTelemetryClient() + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: { "x-api-key": "test-key-1" }, + enabled: true, + }, + { + url: "http://example.com:4318/v1/traces", + headers: { "x-api-key": "test-key-2" }, + enabled: true, + }, + { + url: "http://disabled.com:4318/v1/traces", + headers: {}, + enabled: false, // This one should be filtered out + }, + ] + + await client.initialize(endpoints) + + // Should create provider + expect(NodeTracerProvider).toHaveBeenCalledTimes(1) + expect(mockProvider.register).toHaveBeenCalledTimes(1) + + // Should create exporters only for enabled endpoints + expect(OTLPTraceExporter).toHaveBeenCalledTimes(2) + expect(OTLPTraceExporter).toHaveBeenCalledWith({ + url: "http://localhost:4318/v1/traces", + headers: { "x-api-key": "test-key-1" }, + }) + expect(OTLPTraceExporter).toHaveBeenCalledWith({ + url: "http://example.com:4318/v1/traces", + headers: { "x-api-key": "test-key-2" }, + }) + + // Should create processors for each enabled endpoint + expect(BatchSpanProcessor).toHaveBeenCalledTimes(2) + expect(mockProvider.addSpanProcessor).toHaveBeenCalledTimes(2) + + // Should register instrumentations + expect(registerInstrumentations).toHaveBeenCalledTimes(1) + }) + + it("should handle empty endpoints array", async () => { + const client = new OpenTelemetryClient() + await client.initialize([]) + + // Should still create provider but no exporters + expect(NodeTracerProvider).toHaveBeenCalledTimes(1) + expect(OTLPTraceExporter).not.toHaveBeenCalled() + expect(BatchSpanProcessor).not.toHaveBeenCalled() + }) + + it("should handle initialization errors gracefully", async () => { + const client = new OpenTelemetryClient() + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + // Make provider throw an error + ;(NodeTracerProvider as any).mockImplementation(() => { + throw new Error("Provider initialization failed") + }) + + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + + expect(consoleErrorSpy).toHaveBeenCalledWith("[OpenTelemetry] Failed to initialize:", expect.any(Error)) + + consoleErrorSpy.mockRestore() + }) + }) + + describe("capture", () => { + it("should create and end a span for captured events", async () => { + const client = new OpenTelemetryClient() + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + client.updateTelemetryState(true) + + const mockTelemetryProvider: TelemetryPropertiesProvider = { + getTelemetryProperties: vi.fn().mockResolvedValue({ + appVersion: "1.0.0", + vscodeVersion: "1.60.0", + platform: "darwin", + mode: "code", + }), + } + + client.setProvider(mockTelemetryProvider) + + await client.capture({ + event: TelemetryEventName.TASK_CREATED, + properties: { + customProp: "value", + duration: 1000, + }, + }) + + // Should get tracer + expect(trace.getTracer).toHaveBeenCalledWith("roo-code-telemetry", expect.any(String)) + + // Should start span with event name + expect(mockTracer.startSpan).toHaveBeenCalledWith(TelemetryEventName.TASK_CREATED) + + // Should set attributes from merged properties + expect(mockSpan.setAttributes).toHaveBeenCalledWith({ + appVersion: "1.0.0", + vscodeVersion: "1.60.0", + platform: "darwin", + mode: "code", + customProp: "value", + duration: 1000, + }) + + // Should end the span + expect(mockSpan.end).toHaveBeenCalledTimes(1) + }) + + it("should not capture when telemetry is disabled", async () => { + const client = new OpenTelemetryClient() + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + + // Clear the mock calls from initialization + vi.clearAllMocks() + + client.updateTelemetryState(false) // Disable telemetry + + await client.capture({ + event: TelemetryEventName.TASK_CREATED, + properties: { test: "value" }, + }) + + // Should not create any spans when telemetry is disabled + expect(mockTracer.startSpan).not.toHaveBeenCalled() + }) + + it("should not capture when not initialized", async () => { + const client = new OpenTelemetryClient() + client.updateTelemetryState(true) + + await client.capture({ + event: TelemetryEventName.TASK_CREATED, + properties: { test: "value" }, + }) + + // Should not create any spans + expect(trace.getTracer).not.toHaveBeenCalled() + expect(mockTracer.startSpan).not.toHaveBeenCalled() + }) + + it("should handle capture errors gracefully", async () => { + const client = new OpenTelemetryClient() + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + client.updateTelemetryState(true) + + // Make startSpan throw an error + mockTracer.startSpan.mockImplementation(() => { + throw new Error("Failed to start span") + }) + + await client.capture({ + event: TelemetryEventName.TASK_CREATED, + properties: { test: "value" }, + }) + + expect(consoleErrorSpy).toHaveBeenCalledWith("[OpenTelemetry] Failed to capture event:", expect.any(Error)) + + consoleErrorSpy.mockRestore() + }) + + it("should handle provider errors gracefully", async () => { + const client = new OpenTelemetryClient() + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + client.updateTelemetryState(true) + + const mockTelemetryProvider: TelemetryPropertiesProvider = { + getTelemetryProperties: vi.fn().mockRejectedValue(new Error("Provider error")), + } + + client.setProvider(mockTelemetryProvider) + + await client.capture({ + event: TelemetryEventName.TASK_CREATED, + properties: { customProp: "value" }, + }) + + // Should still create span with event properties only + expect(mockTracer.startSpan).toHaveBeenCalled() + expect(mockSpan.setAttributes).toHaveBeenCalledWith({ customProp: "value" }) + expect(mockSpan.end).toHaveBeenCalled() + + consoleErrorSpy.mockRestore() + }) + }) + + describe("updateTelemetryState", () => { + it("should enable telemetry when set to true", () => { + const client = new OpenTelemetryClient() + client.updateTelemetryState(true) + expect(client.isTelemetryEnabled()).toBe(true) + }) + + it("should disable telemetry when set to false", () => { + const client = new OpenTelemetryClient() + client.updateTelemetryState(false) + expect(client.isTelemetryEnabled()).toBe(false) + }) + }) + + describe("setProvider", () => { + it("should set the telemetry properties provider", () => { + const client = new OpenTelemetryClient() + const mockTelemetryProvider: TelemetryPropertiesProvider = { + getTelemetryProperties: vi.fn(), + } + + client.setProvider(mockTelemetryProvider) + + // Verify provider is set by attempting to capture with it + const getProviderSpy = vi.spyOn(mockTelemetryProvider, "getTelemetryProperties") + getProviderSpy.mockResolvedValue({ + appName: "test-app", + appVersion: "1.0.0", + vscodeVersion: "1.60.0", + platform: "darwin", + editorName: "vscode", + language: "en", + mode: "code", + }) + + // Initialize and enable to allow capture + client.updateTelemetryState(true) + + // Provider should be used during capture (though capture won't complete without initialization) + // This is just to verify the provider was set + expect(getProviderSpy).not.toHaveBeenCalled() // Not called yet since we haven't captured + }) + }) + + describe("shutdown", () => { + it("should shutdown the provider when initialized", async () => { + const client = new OpenTelemetryClient() + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + await client.shutdown() + + expect(mockProvider.shutdown).toHaveBeenCalledTimes(1) + }) + + it("should handle shutdown when not initialized", async () => { + const client = new OpenTelemetryClient() + + // Should not throw + await expect(client.shutdown()).resolves.toBeUndefined() + }) + + it("should handle shutdown errors gracefully", async () => { + const client = new OpenTelemetryClient() + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + + // Make shutdown throw an error + mockProvider.shutdown.mockRejectedValue(new Error("Shutdown failed")) + + await client.shutdown() + + expect(consoleErrorSpy).toHaveBeenCalledWith("[OpenTelemetry] Failed to shutdown:", expect.any(Error)) + + consoleErrorSpy.mockRestore() + }) + }) + + describe("integration scenarios", () => { + it("should handle multiple captures in sequence", async () => { + const client = new OpenTelemetryClient() + const endpoints: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints) + client.updateTelemetryState(true) + + // Capture multiple events + await client.capture({ + event: TelemetryEventName.TASK_CREATED, + properties: { taskId: "1" }, + }) + + await client.capture({ + event: TelemetryEventName.MODE_SWITCH, + properties: { from: "code", to: "debug" }, + }) + + await client.capture({ + event: TelemetryEventName.TASK_COMPLETED, + properties: { taskId: "1", duration: 5000 }, + }) + + // Should create 3 spans + expect(mockTracer.startSpan).toHaveBeenCalledTimes(3) + expect(mockSpan.end).toHaveBeenCalledTimes(3) + + // Verify different event names were used + expect(mockTracer.startSpan).toHaveBeenNthCalledWith(1, TelemetryEventName.TASK_CREATED) + expect(mockTracer.startSpan).toHaveBeenNthCalledWith(2, TelemetryEventName.MODE_SWITCH) + expect(mockTracer.startSpan).toHaveBeenNthCalledWith(3, TelemetryEventName.TASK_COMPLETED) + }) + + it("should reinitialize with new endpoints", async () => { + const client = new OpenTelemetryClient() + + // First initialization + const endpoints1: OtelEndpoint[] = [ + { + url: "http://localhost:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints1) + + // Clear mocks + vi.clearAllMocks() + + // Second initialization with different endpoints + const endpoints2: OtelEndpoint[] = [ + { + url: "http://example.com:4318/v1/traces", + headers: { "x-api-key": "new-key" }, + enabled: true, + }, + { + url: "http://another.com:4318/v1/traces", + headers: {}, + enabled: true, + }, + ] + + await client.initialize(endpoints2) + + // Should create new provider and exporters + expect(NodeTracerProvider).toHaveBeenCalledTimes(1) + expect(OTLPTraceExporter).toHaveBeenCalledTimes(2) + expect(BatchSpanProcessor).toHaveBeenCalledTimes(2) + }) + }) +}) diff --git a/packages/telemetry/src/index.ts b/packages/telemetry/src/index.ts index 8795ad46a2e..76603de3b99 100644 --- a/packages/telemetry/src/index.ts +++ b/packages/telemetry/src/index.ts @@ -1,3 +1,4 @@ export * from "./BaseTelemetryClient" export * from "./PostHogTelemetryClient" +export * from "./OpenTelemetryClient" export * from "./TelemetryService" diff --git a/packages/types/src/global-settings.ts b/packages/types/src/global-settings.ts index 39480e5a3d7..e48cff5860d 100644 --- a/packages/types/src/global-settings.ts +++ b/packages/types/src/global-settings.ts @@ -143,6 +143,18 @@ export const globalSettingsSchema = z.object({ remoteControlEnabled: z.boolean().optional(), + // OpenTelemetry configuration + otelEnabled: z.boolean().optional(), + otelEndpoints: z + .array( + z.object({ + url: z.string(), + headers: z.record(z.string(), z.string()).optional(), + enabled: z.boolean(), + }), + ) + .optional(), + mode: z.string().optional(), modeApiConfigs: z.record(z.string(), z.string()).optional(), customModes: z.array(modeConfigSchema).optional(), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0bc3f93bcbc..ed29b8bd2b4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -174,7 +174,7 @@ importers: version: 0.518.0(react@18.3.1) next: specifier: ^15.2.5 - version: 15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.2.5(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -277,7 +277,7 @@ importers: version: 0.518.0(react@18.3.1) next: specifier: ^15.2.5 - version: 15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + version: 15.2.5(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) next-themes: specifier: ^0.4.6 version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) @@ -401,7 +401,7 @@ importers: version: 0.13.0 drizzle-orm: specifier: ^0.44.1 - version: 0.44.1(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7) + version: 0.44.1(@libsql/client@0.15.8)(@opentelemetry/api@1.9.0)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7) execa: specifier: ^9.6.0 version: 9.6.0 @@ -482,6 +482,27 @@ importers: packages/telemetry: dependencies: + '@opentelemetry/api': + specifier: ^1.9.0 + version: 1.9.0 + '@opentelemetry/exporter-trace-otlp-http': + specifier: ^0.56.0 + version: 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/instrumentation': + specifier: ^0.56.0 + version: 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': + specifier: ^1.29.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': + specifier: ^1.29.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-node': + specifier: ^1.29.0 + version: 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': + specifier: ^1.29.0 + version: 1.36.0 '@roo-code/types': specifier: workspace:^ version: link:../types @@ -2277,6 +2298,118 @@ packages: '@open-draft/until@2.1.0': resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==} + '@opentelemetry/api-logs@0.56.0': + resolution: {integrity: sha512-Wr39+94UNNG3Ei9nv3pHd4AJ63gq5nSemMRpCd8fPwDL9rN3vK26lzxfH27mw16XzOSO+TpyQwBAMaLxaPWG0g==} + engines: {node: '>=14'} + + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/context-async-hooks@1.30.1': + resolution: {integrity: sha512-s5vvxXPVdjqS3kTLKMeBMvop9hbWkwzBpu+mUO2M7sZtlkyDJGwFe33wRKnbaYDo8ExRVBIIdwIGrqpxHuKttA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.29.0': + resolution: {integrity: sha512-gmT7vAreXl0DTHD2rVZcw3+l2g84+5XiHIqdBUxXbExymPCvSsGOpiwMmn8nkiJur28STV31wnhIDrzWDPzjfA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@1.30.1': + resolution: {integrity: sha512-OOCM2C/QIURhJMuKaekP3TRBxBKxG/TWWA0TL2J6nXUtDnuCtccy49LUJF8xPFXMX+0LMcxFpCo8M9cGY1W6rQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/exporter-trace-otlp-http@0.56.0': + resolution: {integrity: sha512-vqVuJvcwameA0r0cNrRzrZqPLB0otS+95g0XkZdiKOXUo81wYdY6r4kyrwz4nSChqTBEFm0lqi/H2OWGboOa6g==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.56.0': + resolution: {integrity: sha512-2KkGBKE+FPXU1F0zKww+stnlUxUTlBvLCiWdP63Z9sqXYeNI/ziNzsxAp4LAdUcTQmXjw1IWgvm5CAb/BHy99w==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-exporter-base@0.56.0': + resolution: {integrity: sha512-eURvv0fcmBE+KE1McUeRo+u0n18ZnUeSc7lDlW/dzlqFYasEbsztTK4v0Qf8C4vEY+aMTjPKUxBG0NX2Te3Pmw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/otlp-transformer@0.56.0': + resolution: {integrity: sha512-kVkH/W2W7EpgWWpyU5VnnjIdSD7Y7FljQYObAQSKdRcejiwMj2glypZtUdfq1LTJcv4ht0jyTrw1D3CCxssNtQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/propagator-b3@1.30.1': + resolution: {integrity: sha512-oATwWWDIJzybAZ4pO76ATN5N6FFbOA1otibAVlS8v90B4S1wClnhRUk7K+2CHAwN1JKYuj4jh/lpCEG5BAqFuQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/propagator-jaeger@1.30.1': + resolution: {integrity: sha512-Pj/BfnYEKIOImirH76M4hDaBSx6HyZ2CXUqk+Kj02m6BB80c/yo4BdWkn/1gDFfU+YPY+bPR2U0DKBfdxCKwmg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.29.0': + resolution: {integrity: sha512-s7mLXuHZE7RQr1wwweGcaRp3Q4UJJ0wazeGlc/N5/XSe6UyXfsh1UQGMADYeg7YwD+cEdMtU1yJAUXdnFzYzyQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/resources@1.30.1': + resolution: {integrity: sha512-5UxZqiAgLYGFjS4s9qm5mBVo433u+dSPUFWVWXmLAD4wB65oMCoXaJP1KJa9DIYYMeHu3z4BZcStG3LC593cWA==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-logs@0.56.0': + resolution: {integrity: sha512-OS0WPBJF++R/cSl+terUjQH5PebloidB1Jbbecgg2rnCmQbTST9xsRes23bLfDQVRvmegmHqDh884h0aRdJyLw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.4.0 <1.10.0' + + '@opentelemetry/sdk-metrics@1.29.0': + resolution: {integrity: sha512-MkVtuzDjXZaUJSuJlHn6BSXjcQlMvHcsDV7LjY4P6AJeffMa4+kIGDjzsCf6DkAh6Vqlwag5EWEam3KZOX5Drw==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.29.0': + resolution: {integrity: sha512-hEOpAYLKXF3wGJpXOtWsxEtqBgde0SCv+w+jvr3/UusR4ll3QrENEGnSl1WDCyRrpqOQ5NCNOvZch9UFVa7MnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@1.30.1': + resolution: {integrity: sha512-jVPgBbH1gCy2Lb7X0AVQ8XAfgg0pJ4nvl8/IiQA6nxOsPvS+0zMJaFSs2ltXe0J6C8dqjcnpyqINDJmU30+uOg==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/sdk-trace-node@1.30.1': + resolution: {integrity: sha512-cBjYOINt1JxXdpw1e5MlHmFRc5fgj4GW/86vsKFxJCJ8AL4PdVtYH41gWwl4qd4uQjqEL1oJVrXkSy5cnduAnQ==} + engines: {node: '>=14'} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.28.0': + resolution: {integrity: sha512-lp4qAiMTD4sNWW4DbKLBkfiMZ4jbAboJIGOQr5DvciMRI494OapieI9qiODpOt0XBr1LjIDy1xAGAnVs5supTA==} + engines: {node: '>=14'} + + '@opentelemetry/semantic-conventions@1.36.0': + resolution: {integrity: sha512-TtxJSRD8Ohxp6bKkhrm27JRHAxPczQA7idtcTOMYI+wQRRrfgqxHv1cFbCApcSnNjtXkmzFozn6jQtFrOmbjPQ==} + engines: {node: '>=14'} + '@oxc-resolver/binding-darwin-arm64@11.2.0': resolution: {integrity: sha512-ruKLkS+Dm/YIJaUhzEB7zPI+jh3EXxu0QnNV8I7t9jf0lpD2VnltuyRbhrbJEkksklZj//xCMyFFsILGjiU2Mg==} cpu: [arm64] @@ -2352,6 +2485,36 @@ packages: '@polka/url@1.0.0-next.29': resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==} + '@protobufjs/aspromise@1.1.2': + resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==} + + '@protobufjs/base64@1.1.2': + resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==} + + '@protobufjs/codegen@2.0.4': + resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==} + + '@protobufjs/eventemitter@1.1.0': + resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==} + + '@protobufjs/fetch@1.1.0': + resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==} + + '@protobufjs/float@1.0.2': + resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==} + + '@protobufjs/inquire@1.1.0': + resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==} + + '@protobufjs/path@1.1.2': + resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==} + + '@protobufjs/pool@1.1.0': + resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==} + + '@protobufjs/utf8@1.1.0': + resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==} + '@puppeteer/browsers@2.10.5': resolution: {integrity: sha512-eifa0o+i8dERnngJwKrfp3dEq7ia5XFyoqB17S4gK8GhsQE4/P8nxOfQSE0zQHxzzLo/cmF+7+ywEQ7wK7Fb+w==} engines: {node: '>=18'} @@ -3903,6 +4066,9 @@ packages: '@types/shell-quote@1.7.5': resolution: {integrity: sha512-+UE8GAGRPbJVQDdxi16dgadcBfQ+KG2vgZhV1+3A1XmHbmwcdwhCUwIdy+d3pAGrbvgRoVSjeI9vOWyq376Yzw==} + '@types/shimmer@1.2.0': + resolution: {integrity: sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==} + '@types/stack-utils@2.0.3': resolution: {integrity: sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==} @@ -4138,6 +4304,11 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -4589,6 +4760,9 @@ packages: resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} engines: {node: '>=8'} + cjs-module-lexer@1.4.3: + resolution: {integrity: sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==} + class-variance-authority@0.7.1: resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} @@ -6248,6 +6422,9 @@ packages: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} + import-in-the-middle@1.14.2: + resolution: {integrity: sha512-5tCuY9BV8ujfOpwtAGgsTx9CGUapcFMEEyByLv1B+v2+6DhAcw+Zr0nhQT7uwaZ7DiourxFEscghOR8e1aPLQw==} + imurmurhash@0.1.4: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} @@ -7022,6 +7199,9 @@ packages: resolution: {integrity: sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==} engines: {node: '>=18'} + long@5.3.2: + resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} + longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} @@ -7383,6 +7563,9 @@ packages: engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + monaco-vscode-textmate-theme-converter@0.1.7: resolution: {integrity: sha512-ZMsq1RPWwOD3pvXD0n+9ddnhfzZoiUMwNIWPNUqYqEiQeH2HjyZ9KYOdt/pqe0kkN8WnYWLrxT9C/SrtIsAu2Q==} hasBin: true @@ -8040,6 +8223,10 @@ packages: property-information@7.1.0: resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + protobufjs@7.5.3: + resolution: {integrity: sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==} + engines: {node: '>=12.0.0'} + proxy-addr@2.0.7: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} @@ -8350,6 +8537,10 @@ packages: resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} engines: {node: '>=0.10.0'} + require-in-the-middle@7.5.2: + resolution: {integrity: sha512-gAZ+kLqBdHarXB64XpAe2VCjB7rIRv+mU8tfRWziHRJ5umKsIHN2tLLv6EtMw7WCdP19S0ERVMldNvxYCHnhSQ==} + engines: {node: '>=8.6.0'} + resize-observer-polyfill@1.5.1: resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} @@ -8562,6 +8753,9 @@ packages: shiki@3.4.1: resolution: {integrity: sha512-PSnoczt+iWIOB4iRQ+XVPFtTuN1FcmuYzPgUBZTSv5pC6CozssIx2M4O5n4S9gJlUu9A3FxMU0ZPaHflky/6LA==} + shimmer@1.2.1: + resolution: {integrity: sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==} + side-channel-list@1.0.0: resolution: {integrity: sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==} engines: {node: '>= 0.4'} @@ -11487,6 +11681,127 @@ snapshots: '@open-draft/until@2.1.0': {} + '@opentelemetry/api-logs@0.56.0': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/api@1.9.0': {} + + '@opentelemetry/context-async-hooks@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + + '@opentelemetry/core@1.29.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/core@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/exporter-trace-otlp-http@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-exporter-base': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.29.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/instrumentation@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.56.0 + '@types/shimmer': 1.2.0 + import-in-the-middle: 1.14.2 + require-in-the-middle: 7.5.2 + semver: 7.7.2 + shimmer: 1.2.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/otlp-exporter-base@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/otlp-transformer': 0.56.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/otlp-transformer@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.56.0 + '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-logs': 0.56.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-metrics': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.29.0(@opentelemetry/api@1.9.0) + protobufjs: 7.5.3 + + '@opentelemetry/propagator-b3@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/propagator-jaeger@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + + '@opentelemetry/resources@1.29.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/resources@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-logs@0.56.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/api-logs': 0.56.0 + '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.29.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-metrics@1.29.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.29.0(@opentelemetry/api@1.9.0) + + '@opentelemetry/sdk-trace-base@1.29.0(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.29.0(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-base@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/resources': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/semantic-conventions': 1.28.0 + + '@opentelemetry/sdk-trace-node@1.30.1(@opentelemetry/api@1.9.0)': + dependencies: + '@opentelemetry/api': 1.9.0 + '@opentelemetry/context-async-hooks': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/core': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-b3': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/propagator-jaeger': 1.30.1(@opentelemetry/api@1.9.0) + '@opentelemetry/sdk-trace-base': 1.30.1(@opentelemetry/api@1.9.0) + semver: 7.7.2 + + '@opentelemetry/semantic-conventions@1.28.0': {} + + '@opentelemetry/semantic-conventions@1.36.0': {} + '@oxc-resolver/binding-darwin-arm64@11.2.0': optional: true @@ -11536,6 +11851,29 @@ snapshots: '@polka/url@1.0.0-next.29': {} + '@protobufjs/aspromise@1.1.2': {} + + '@protobufjs/base64@1.1.2': {} + + '@protobufjs/codegen@2.0.4': {} + + '@protobufjs/eventemitter@1.1.0': {} + + '@protobufjs/fetch@1.1.0': + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/inquire': 1.1.0 + + '@protobufjs/float@1.0.2': {} + + '@protobufjs/inquire@1.1.0': {} + + '@protobufjs/path@1.1.2': {} + + '@protobufjs/pool@1.1.0': {} + + '@protobufjs/utf8@1.1.0': {} + '@puppeteer/browsers@2.10.5': dependencies: debug: 4.4.1(supports-color@8.1.1) @@ -13242,7 +13580,6 @@ snapshots: '@types/node@20.19.9': dependencies: undici-types: 6.21.0 - optional: true '@types/node@22.15.29': dependencies: @@ -13269,6 +13606,8 @@ snapshots: '@types/shell-quote@1.7.5': {} + '@types/shimmer@1.2.0': {} + '@types/stack-utils@2.0.3': {} '@types/stacktrace-js@2.0.3': @@ -13604,6 +13943,10 @@ snapshots: mime-types: 3.0.1 negotiator: 1.0.0 + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1 @@ -14116,6 +14459,8 @@ snapshots: ci-info@3.9.0: {} + cjs-module-lexer@1.4.3: {} + class-variance-authority@0.7.1: dependencies: clsx: 2.1.1 @@ -14723,9 +15068,10 @@ snapshots: transitivePeerDependencies: - supports-color - drizzle-orm@0.44.1(@libsql/client@0.15.8)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7): + drizzle-orm@0.44.1(@libsql/client@0.15.8)(@opentelemetry/api@1.9.0)(better-sqlite3@11.10.0)(gel@2.1.0)(postgres@3.4.7): optionalDependencies: '@libsql/client': 0.15.8 + '@opentelemetry/api': 1.9.0 better-sqlite3: 11.10.0 gel: 2.1.0 postgres: 3.4.7 @@ -15999,6 +16345,13 @@ snapshots: parent-module: 1.0.1 resolve-from: 4.0.0 + import-in-the-middle@1.14.2: + dependencies: + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + cjs-module-lexer: 1.4.3 + module-details-from-path: 1.0.4 + imurmurhash@0.1.4: {} indent-string@4.0.0: {} @@ -16765,6 +17118,8 @@ snapshots: strip-ansi: 7.1.0 wrap-ansi: 9.0.0 + long@5.3.2: {} + longest-streak@3.1.0: {} loose-envify@1.4.0: @@ -17357,7 +17712,7 @@ snapshots: mlly@1.7.4: dependencies: - acorn: 8.14.1 + acorn: 8.15.0 pathe: 2.0.3 pkg-types: 1.3.1 ufo: 1.6.1 @@ -17385,6 +17740,8 @@ snapshots: yargs-parser: 21.1.1 yargs-unparser: 2.0.0 + module-details-from-path@1.0.4: {} + monaco-vscode-textmate-theme-converter@0.1.7(tslib@2.8.1): dependencies: commander: 8.3.0 @@ -17442,7 +17799,7 @@ snapshots: react: 18.3.1 react-dom: 18.3.1(react@18.3.1) - next@15.2.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + next@15.2.5(@opentelemetry/api@1.9.0)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@next/env': 15.2.5 '@swc/counter': 0.1.3 @@ -17462,6 +17819,7 @@ snapshots: '@next/swc-linux-x64-musl': 15.2.5 '@next/swc-win32-arm64-msvc': 15.2.5 '@next/swc-win32-x64-msvc': 15.2.5 + '@opentelemetry/api': 1.9.0 sharp: 0.33.5 transitivePeerDependencies: - '@babel/core' @@ -18058,6 +18416,21 @@ snapshots: property-information@7.1.0: {} + protobufjs@7.5.3: + dependencies: + '@protobufjs/aspromise': 1.1.2 + '@protobufjs/base64': 1.1.2 + '@protobufjs/codegen': 2.0.4 + '@protobufjs/eventemitter': 1.1.0 + '@protobufjs/fetch': 1.1.0 + '@protobufjs/float': 1.0.2 + '@protobufjs/inquire': 1.1.0 + '@protobufjs/path': 1.1.2 + '@protobufjs/pool': 1.1.0 + '@protobufjs/utf8': 1.1.0 + '@types/node': 20.19.9 + long: 5.3.2 + proxy-addr@2.0.7: dependencies: forwarded: 0.2.0 @@ -18507,6 +18880,14 @@ snapshots: require-directory@2.1.1: {} + require-in-the-middle@7.5.2: + dependencies: + debug: 4.4.1(supports-color@8.1.1) + module-details-from-path: 1.0.4 + resolve: 1.22.10 + transitivePeerDependencies: + - supports-color + resize-observer-polyfill@1.5.1: {} resolve-from@4.0.0: {} @@ -18784,6 +19165,8 @@ snapshots: '@shikijs/vscode-textmate': 10.0.2 '@types/hast': 3.0.4 + shimmer@1.2.1: {} + side-channel-list@1.0.0: dependencies: es-errors: 1.3.0 diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 7e25ae14dcd..db5afe6cb97 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -2611,5 +2611,13 @@ export const webviewMessageHandler = async ( } break } + case "otelEnabled": + await updateGlobalState("otelEnabled", message.bool ?? false) + await provider.postStateToWebview() + break + case "otelEndpoints": + await updateGlobalState("otelEndpoints", message.endpoints ?? []) + await provider.postStateToWebview() + break } } diff --git a/src/extension.ts b/src/extension.ts index 767e83c42dc..d8f4a0669c4 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -13,7 +13,7 @@ try { } import { CloudService, UnifiedBridgeService } from "@roo-code/cloud" -import { TelemetryService, PostHogTelemetryClient } from "@roo-code/telemetry" +import { TelemetryService, PostHogTelemetryClient, OpenTelemetryClient } from "@roo-code/telemetry" import "./utils/path" // Necessary to have access to String.prototype.toPosix. import { createOutputChannelLogger, createDualLogger } from "./utils/outputChannelLogger" @@ -94,6 +94,26 @@ export async function activate(context: vscode.ExtensionContext) { const contextProxy = await ContextProxy.getInstance(context) + // Initialize OpenTelemetry client if enabled + const otelClient = new OpenTelemetryClient() + try { + const otelEnabled = contextProxy.getValue("otelEnabled") ?? false + const otelEndpoints = contextProxy.getValue("otelEndpoints") ?? [] + + if (otelEnabled && otelEndpoints.length > 0) { + await otelClient.initialize(otelEndpoints) + telemetryService.register(otelClient) + outputChannel.appendLine( + `[OpenTelemetry] Initialized with ${otelEndpoints.filter((e) => e.enabled).length} endpoints`, + ) + } + } catch (error) { + console.warn("Failed to initialize OpenTelemetryClient:", error) + outputChannel.appendLine( + `[OpenTelemetry] Failed to initialize: ${error instanceof Error ? error.message : String(error)}`, + ) + } + // Initialize code index managers for all workspace folders const codeIndexManagers: CodeIndexManager[] = [] if (vscode.workspace.workspaceFolders) { diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index f9ac305e07f..7979516199c 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -121,6 +121,8 @@ export interface ExtensionMessage { | "showEditMessageDialog" | "commands" | "insertTextIntoTextarea" + | "otelEnabled" + | "otelEndpoints" text?: string payload?: any // Add a generic payload for now, can refine later action?: @@ -195,6 +197,8 @@ export interface ExtensionMessage { messageTs?: number context?: string commands?: Command[] + bool?: boolean + endpoints?: Array<{ url: string; headers?: Record; enabled: boolean }> } export type ExtensionState = Pick< @@ -272,6 +276,8 @@ export type ExtensionState = Pick< | "includeDiagnosticMessages" | "maxDiagnosticMessages" | "remoteControlEnabled" + | "otelEnabled" + | "otelEndpoints" > & { version: string clineMessages: ClineMessage[] diff --git a/src/shared/WebviewMessage.ts b/src/shared/WebviewMessage.ts index 2d94896bf5b..0ecd61453f2 100644 --- a/src/shared/WebviewMessage.ts +++ b/src/shared/WebviewMessage.ts @@ -211,6 +211,8 @@ export interface WebviewMessage { | "deleteCommand" | "createCommand" | "insertTextIntoTextarea" + | "otelEnabled" + | "otelEndpoints" text?: string editedMessageContent?: string tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account" @@ -254,6 +256,7 @@ export interface WebviewMessage { visibility?: ShareVisibility // For share visibility hasContent?: boolean // For checkRulesDirectoryResult checkOnly?: boolean // For deleteCustomMode check + endpoints?: Array<{ url: string; headers?: Record; enabled: boolean }> // For otelEndpoints codeIndexSettings?: { // Global state settings codebaseIndexEnabled: boolean diff --git a/webview-ui/src/components/settings/OpenTelemetrySettings.tsx b/webview-ui/src/components/settings/OpenTelemetrySettings.tsx new file mode 100644 index 00000000000..7218f7138dc --- /dev/null +++ b/webview-ui/src/components/settings/OpenTelemetrySettings.tsx @@ -0,0 +1,284 @@ +import { HTMLAttributes, useState } from "react" +import { VSCodeCheckbox, VSCodeTextField, VSCodeButton } from "@vscode/webview-ui-toolkit/react" +import { Activity, Plus, Trash2, ChevronDown, ChevronRight } from "lucide-react" + +import { SetCachedStateField } from "./types" +import { SectionHeader } from "./SectionHeader" +import { Section } from "./Section" + +interface OtelEndpoint { + url: string + headers?: Record + enabled: boolean +} + +type OpenTelemetrySettingsProps = HTMLAttributes & { + otelEnabled?: boolean + otelEndpoints?: OtelEndpoint[] + setCachedStateField: SetCachedStateField<"otelEnabled" | "otelEndpoints"> +} + +export const OpenTelemetrySettings = ({ + otelEnabled, + otelEndpoints = [], + setCachedStateField, + ...props +}: OpenTelemetrySettingsProps) => { + const [expandedEndpoints, setExpandedEndpoints] = useState>(new Set()) + + const handleAddEndpoint = () => { + const newEndpoint: OtelEndpoint = { + url: "", + headers: {}, + enabled: true, + } + setCachedStateField("otelEndpoints", [...otelEndpoints, newEndpoint]) + // Auto-expand the new endpoint + setExpandedEndpoints(new Set(Array.from(expandedEndpoints).concat(otelEndpoints.length))) + } + + const handleRemoveEndpoint = (index: number) => { + const updated = otelEndpoints.filter((_, i) => i !== index) + setCachedStateField("otelEndpoints", updated) + // Remove from expanded set + const newExpanded = new Set(expandedEndpoints) + newExpanded.delete(index) + setExpandedEndpoints(newExpanded) + } + + const handleUpdateEndpoint = (index: number, field: keyof OtelEndpoint, value: any) => { + const updated = [...otelEndpoints] + updated[index] = { ...updated[index], [field]: value } + setCachedStateField("otelEndpoints", updated) + } + + const handleAddHeader = (endpointIndex: number, key: string, value: string) => { + if (!key) return + const updated = [...otelEndpoints] + updated[endpointIndex] = { + ...updated[endpointIndex], + headers: { ...updated[endpointIndex].headers, [key]: value }, + } + setCachedStateField("otelEndpoints", updated) + } + + const handleRemoveHeader = (endpointIndex: number, key: string) => { + const updated = [...otelEndpoints] + const headers = { ...updated[endpointIndex].headers } + delete headers[key] + updated[endpointIndex] = { ...updated[endpointIndex], headers } + setCachedStateField("otelEndpoints", updated) + } + + const toggleExpanded = (index: number) => { + const newExpanded = new Set(expandedEndpoints) + if (newExpanded.has(index)) { + newExpanded.delete(index) + } else { + newExpanded.add(index) + } + setExpandedEndpoints(newExpanded) + } + + return ( +
+ +
+ +
OpenTelemetry
+
+
+ +
+
+ setCachedStateField("otelEnabled", e.target.checked)} + data-testid="otel-enabled-checkbox"> + Enable OpenTelemetry + +
+ Send telemetry traces to custom OpenTelemetry collector endpoints +
+
+ + {otelEnabled && ( +
+
+ + + + Add Endpoint + +
+ + {otelEndpoints.length === 0 ? ( +
+ No endpoints configured. Click "Add Endpoint" to add a custom OTEL collector. +
+ ) : ( +
+ {otelEndpoints.map((endpoint, index) => { + const isExpanded = expandedEndpoints.has(index) + return ( +
+
+ + +
+
+ + handleUpdateEndpoint(index, "enabled", e.target.checked) + } + data-testid={`endpoint-${index}-enabled`}> + Endpoint {index + 1} + + +
+ + {isExpanded && ( +
+
+ + + handleUpdateEndpoint( + index, + "url", + e.target.value, + ) + } + className="w-full" + data-testid={`endpoint-${index}-url`} + /> +
+ +
+ +
+ {Object.entries(endpoint.headers || {}).map( + ([key, value]) => ( +
+ + { + const newHeaders = { + ...endpoint.headers, + } + delete newHeaders[key] + newHeaders[key] = e.target.value + handleUpdateEndpoint( + index, + "headers", + newHeaders, + ) + }} + /> + +
+ ), + )} + +
+ + + { + const keyInput = + document.getElementById( + `new-header-key-${index}`, + ) as HTMLInputElement + const valueInput = + document.getElementById( + `new-header-value-${index}`, + ) as HTMLInputElement + if ( + keyInput && + valueInput && + keyInput.value + ) { + handleAddHeader( + index, + keyInput.value, + valueInput.value, + ) + keyInput.value = "" + valueInput.value = "" + } + }}> + Add + +
+
+
+
+ )} +
+
+
+ ) + })} +
+ )} + +
+ Note: Changes to endpoints require restarting VS Code to take effect. +
+
+ )} +
+
+ ) +} diff --git a/webview-ui/src/components/settings/SettingsView.tsx b/webview-ui/src/components/settings/SettingsView.tsx index 630b59485d7..ccaf6b93604 100644 --- a/webview-ui/src/components/settings/SettingsView.tsx +++ b/webview-ui/src/components/settings/SettingsView.tsx @@ -23,6 +23,7 @@ import { Info, MessageSquare, LucideIcon, + Activity, } from "lucide-react" import type { ProviderSettings, ExperimentId } from "@roo-code/types" @@ -65,6 +66,7 @@ import { LanguageSettings } from "./LanguageSettings" import { About } from "./About" import { Section } from "./Section" import PromptsSettings from "./PromptsSettings" +import { OpenTelemetrySettings } from "./OpenTelemetrySettings" import { cn } from "@/lib/utils" export const settingsTabsContainer = "flex flex-1 overflow-hidden [&.narrow_.tab-label]:hidden" @@ -87,6 +89,7 @@ const sectionNames = [ "contextManagement", "terminal", "prompts", + "opentelemetry", "experimental", "language", "about", @@ -183,6 +186,8 @@ const SettingsView = forwardRef(({ onDone, t includeDiagnosticMessages, maxDiagnosticMessages, includeTaskHistoryInEnhance, + otelEnabled, + otelEndpoints, } = cachedState const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration]) @@ -345,6 +350,8 @@ const SettingsView = forwardRef(({ onDone, t vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration }) vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting }) vscode.postMessage({ type: "profileThresholds", values: profileThresholds }) + vscode.postMessage({ type: "otelEnabled", bool: otelEnabled ?? false }) + vscode.postMessage({ type: "otelEndpoints", endpoints: otelEndpoints ?? [] }) setChangeDetected(false) } } @@ -422,6 +429,7 @@ const SettingsView = forwardRef(({ onDone, t { id: "contextManagement", icon: Database }, { id: "terminal", icon: SquareTerminal }, { id: "prompts", icon: MessageSquare }, + { id: "opentelemetry", icon: Activity }, { id: "experimental", icon: FlaskConical }, { id: "language", icon: Globe }, { id: "about", icon: Info }, @@ -716,6 +724,15 @@ const SettingsView = forwardRef(({ onDone, t /> )} + {/* OpenTelemetry Section */} + {activeTab === "opentelemetry" && ( + + )} + {/* Experimental Section */} {activeTab === "experimental" && ( diff --git a/webview-ui/src/context/ExtensionStateContext.tsx b/webview-ui/src/context/ExtensionStateContext.tsx index 12f13bdf555..26299f964c7 100644 --- a/webview-ui/src/context/ExtensionStateContext.tsx +++ b/webview-ui/src/context/ExtensionStateContext.tsx @@ -149,6 +149,10 @@ export interface ExtensionStateContextType extends ExtensionState { setMaxDiagnosticMessages: (value: number) => void includeTaskHistoryInEnhance?: boolean setIncludeTaskHistoryInEnhance: (value: boolean) => void + otelEnabled?: boolean + setOtelEnabled: (value: boolean) => void + otelEndpoints?: Array<{ url: string; headers?: Record; enabled: boolean }> + setOtelEndpoints: (value: Array<{ url: string; headers?: Record; enabled: boolean }>) => void } export const ExtensionStateContext = createContext(undefined) @@ -269,6 +273,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode global: {}, }) const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(false) + const [otelEnabled, setOtelEnabled] = useState(false) + const [otelEndpoints, setOtelEndpoints] = useState< + Array<{ url: string; headers?: Record; enabled: boolean }> + >([]) const setListApiConfigMeta = useCallback( (value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })), @@ -306,6 +314,13 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode if ((newState as any).includeTaskHistoryInEnhance !== undefined) { setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance) } + // Update OTEL settings if present in state message + if ((newState as any).otelEnabled !== undefined) { + setOtelEnabled((newState as any).otelEnabled) + } + if ((newState as any).otelEndpoints !== undefined) { + setOtelEndpoints((newState as any).otelEndpoints) + } // Handle marketplace data if present in state message if (newState.marketplaceItems !== undefined) { setMarketplaceItems(newState.marketplaceItems) @@ -522,6 +537,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode }, includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance, + otelEnabled, + setOtelEnabled, + otelEndpoints, + setOtelEndpoints, } return {children}