Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/common/orpc/schemas/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,14 @@ const StreamCompletedPropertiesSchema = z.object({
output_tokens_b2: z.number(),
});

const CompactionCompletedPropertiesSchema = z.object({
model: z.string(),
duration_b2: z.number(),
input_tokens_b2: z.number(),
output_tokens_b2: z.number(),
compaction_source: z.enum(["manual", "idle"]),
});

const ProviderConfiguredPropertiesSchema = z.object({
provider: z.string(),
keyType: z.string(),
Expand Down Expand Up @@ -125,6 +133,10 @@ export const TelemetryEventSchema = z.discriminatedUnion("event", [
event: z.literal("stream_completed"),
properties: StreamCompletedPropertiesSchema,
}),
z.object({
event: z.literal("compaction_completed"),
properties: CompactionCompletedPropertiesSchema,
}),
z.object({
event: z.literal("provider_configured"),
properties: ProviderConfiguredPropertiesSchema,
Expand Down
17 changes: 17 additions & 0 deletions src/common/telemetry/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,22 @@ export interface StreamCompletedPayload {
output_tokens_b2: number;
}

/**
* Compaction completion event - tracks when history compaction finishes
*/
export interface CompactionCompletedPayload {
/** Model used for compaction */
model: string;
/** Duration in seconds, rounded to nearest power of 2 */
duration_b2: number;
/** Input tokens (pre-compaction history size), rounded to nearest power of 2 */
input_tokens_b2: number;
/** Output tokens (post-compaction summary size), rounded to nearest power of 2 */
output_tokens_b2: number;
/** Whether this compaction was user-triggered vs idle */
compaction_source: "manual" | "idle";
}

/**
* Provider configuration event - tracks when users set up providers
* Note: Only tracks that a key was set, never the actual value
Expand Down Expand Up @@ -211,6 +227,7 @@ export type TelemetryEventPayload =
| { event: "workspace_switched"; properties: WorkspaceSwitchedPayload }
| { event: "message_sent"; properties: MessageSentPayload }
| { event: "stream_completed"; properties: StreamCompletedPayload }
| { event: "compaction_completed"; properties: CompactionCompletedPayload }
| { event: "provider_configured"; properties: ProviderConfiguredPayload }
| { event: "command_used"; properties: CommandUsedPayload }
| { event: "voice_transcription"; properties: VoiceTranscriptionPayload }
Expand Down
4 changes: 4 additions & 0 deletions src/node/services/agentSession.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { createRuntime } from "@/node/runtime/runtimeFactory";
import { MessageQueue } from "./messageQueue";
import type { StreamEndEvent } from "@/common/types/stream";
import { CompactionHandler } from "./compactionHandler";
import type { TelemetryService } from "./telemetryService";
import type { BackgroundProcessManager } from "./backgroundProcessManager";
import { computeDiff } from "@/node/utils/diff";
import { AttachmentService } from "./attachmentService";
Expand Down Expand Up @@ -89,6 +90,7 @@ interface AgentSessionOptions {
partialService: PartialService;
aiService: AIService;
initStateManager: InitStateManager;
telemetryService?: TelemetryService;
backgroundProcessManager: BackgroundProcessManager;
/** Called when compaction completes (e.g., to clear idle compaction pending state) */
onCompactionComplete?: () => void;
Expand Down Expand Up @@ -147,6 +149,7 @@ export class AgentSession {
partialService,
aiService,
initStateManager,
telemetryService,
backgroundProcessManager,
onCompactionComplete,
onPostCompactionStateChange,
Expand All @@ -169,6 +172,7 @@ export class AgentSession {
this.compactionHandler = new CompactionHandler({
workspaceId: this.workspaceId,
historyService: this.historyService,
telemetryService,
partialService: this.partialService,
emitter: this.emitter,
onCompactionComplete,
Expand Down
41 changes: 41 additions & 0 deletions src/node/services/compactionHandler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import type { PartialService } from "./partialService";
import type { EventEmitter } from "events";
import { createMuxMessage, type MuxMessage } from "@/common/types/message";
import type { StreamEndEvent } from "@/common/types/stream";
import type { TelemetryService } from "./telemetryService";
import type { TelemetryEventPayload } from "@/common/telemetry/payload";
import { Ok, Err, type Result } from "@/common/types/result";

interface EmittedEvent {
Expand Down Expand Up @@ -117,6 +119,8 @@ describe("CompactionHandler", () => {
let mockHistoryService: ReturnType<typeof createMockHistoryService>;
let mockPartialService: ReturnType<typeof createMockPartialService>;
let mockEmitter: EventEmitter;
let telemetryCapture: ReturnType<typeof mock>;
let telemetryService: TelemetryService;
let emittedEvents: EmittedEvent[];
const workspaceId = "test-workspace";

Expand All @@ -125,12 +129,18 @@ describe("CompactionHandler", () => {
mockEmitter = emitter;
emittedEvents = events;

telemetryCapture = mock((_payload: TelemetryEventPayload) => {
void _payload;
});
telemetryService = { capture: telemetryCapture } as unknown as TelemetryService;

mockHistoryService = createMockHistoryService();
mockPartialService = createMockPartialService();

handler = new CompactionHandler({
workspaceId,
historyService: mockHistoryService as unknown as HistoryService,
telemetryService,
partialService: mockPartialService as unknown as PartialService,
emitter: mockEmitter,
});
Expand Down Expand Up @@ -160,6 +170,37 @@ describe("CompactionHandler", () => {
expect(result).toBe(false);
});

it("should capture compaction_completed telemetry on successful compaction", async () => {
const compactionReq = createCompactionRequest();
setupSuccessfulCompaction(mockHistoryService, [compactionReq]);

const event = createStreamEndEvent("Summary", {
duration: 1500,
// Prefer contextUsage (context size) over total usage.
contextUsage: { inputTokens: 1000, outputTokens: 333, totalTokens: undefined },
});

await handler.handleCompletion(event);

expect(telemetryCapture.mock.calls).toHaveLength(1);
const payload = telemetryCapture.mock.calls[0][0] as TelemetryEventPayload;
expect(payload.event).toBe("compaction_completed");
if (payload.event !== "compaction_completed") {
throw new Error("Expected compaction_completed payload");
}

expect(payload.properties).toEqual({
model: "claude-3-5-sonnet-20241022",
// 1.5s -> 2
duration_b2: 2,
// 1000 -> 1024
input_tokens_b2: 1024,
// 333 -> 512
output_tokens_b2: 512,
compaction_source: "manual",
});
});

it("should return true when successful", async () => {
const compactionReq = createCompactionRequest();
mockHistoryService.mockGetHistory(Ok([compactionReq]));
Expand Down
23 changes: 23 additions & 0 deletions src/node/services/compactionHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { Ok, Err } from "@/common/types/result";
import type { LanguageModelV2Usage } from "@ai-sdk/provider";

import { createMuxMessage, type MuxMessage } from "@/common/types/message";
import type { TelemetryService } from "@/node/services/telemetryService";
import { roundToBase2 } from "@/common/telemetry/utils";
import { log } from "@/node/services/log";
import {
extractEditedFileDiffs,
Expand All @@ -20,6 +22,7 @@ interface CompactionHandlerOptions {
workspaceId: string;
historyService: HistoryService;
partialService: PartialService;
telemetryService?: TelemetryService;
emitter: EventEmitter;
/** Called when compaction completes successfully (e.g., to clear idle compaction pending state) */
onCompactionComplete?: () => void;
Expand All @@ -37,6 +40,7 @@ export class CompactionHandler {
private readonly workspaceId: string;
private readonly historyService: HistoryService;
private readonly partialService: PartialService;
private readonly telemetryService?: TelemetryService;
private readonly emitter: EventEmitter;
private readonly processedCompactionRequestIds: Set<string> = new Set<string>();
private readonly onCompactionComplete?: () => void;
Expand All @@ -50,6 +54,7 @@ export class CompactionHandler {
this.workspaceId = options.workspaceId;
this.historyService = options.historyService;
this.partialService = options.partialService;
this.telemetryService = options.telemetryService;
this.emitter = options.emitter;
this.onCompactionComplete = options.onCompactionComplete;
}
Expand Down Expand Up @@ -130,6 +135,24 @@ export class CompactionHandler {
return false;
}

const durationSecs =
typeof event.metadata.duration === "number" ? event.metadata.duration / 1000 : 0;
const inputTokens =
event.metadata.contextUsage?.inputTokens ?? event.metadata.usage?.inputTokens ?? 0;
const outputTokens =
event.metadata.contextUsage?.outputTokens ?? event.metadata.usage?.outputTokens ?? 0;

this.telemetryService?.capture({
event: "compaction_completed",
properties: {
model: event.metadata.model,
duration_b2: roundToBase2(durationSecs),
input_tokens_b2: roundToBase2(inputTokens ?? 0),
output_tokens_b2: roundToBase2(outputTokens ?? 0),
compaction_source: isIdleCompaction ? "idle" : "manual",
},
});

// Notify that compaction completed (clears idle compaction pending state)
this.onCompactionComplete?.();

Expand Down
1 change: 1 addition & 0 deletions src/node/services/serviceContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class ServiceContainer {
this.menuEventService = new MenuEventService();
this.voiceService = new VoiceService(config);
this.telemetryService = new TelemetryService(config.rootDir);
this.workspaceService.setTelemetryService(this.telemetryService);
this.experimentsService = new ExperimentsService({
telemetryService: this.telemetryService,
muxHome: config.rootDir,
Expand Down
7 changes: 7 additions & 0 deletions src/node/services/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import type { PartialService } from "@/node/services/partialService";
import type { AIService } from "@/node/services/aiService";
import type { InitStateManager } from "@/node/services/initStateManager";
import type { ExtensionMetadataService } from "@/node/services/ExtensionMetadataService";
import type { TelemetryService } from "@/node/services/telemetryService";
import type { ExperimentsService } from "@/node/services/experimentsService";
import { EXPERIMENT_IDS, EXPERIMENTS } from "@/common/constants/experiments";
import type { MCPServerManager } from "@/node/services/mcpServerManager";
Expand Down Expand Up @@ -120,6 +121,7 @@ export class WorkspaceService extends EventEmitter {
this.setupMetadataListeners();
}

private telemetryService?: TelemetryService;
private experimentsService?: ExperimentsService;
private mcpServerManager?: MCPServerManager;
// Optional terminal service for cleanup on workspace removal
Expand All @@ -133,6 +135,10 @@ export class WorkspaceService extends EventEmitter {
this.mcpServerManager = manager;
}

setTelemetryService(telemetryService: TelemetryService): void {
this.telemetryService = telemetryService;
}

setExperimentsService(experimentsService: ExperimentsService): void {
this.experimentsService = experimentsService;
}
Expand Down Expand Up @@ -301,6 +307,7 @@ export class WorkspaceService extends EventEmitter {
historyService: this.historyService,
partialService: this.partialService,
aiService: this.aiService,
telemetryService: this.telemetryService,
initStateManager: this.initStateManager,
backgroundProcessManager: this.backgroundProcessManager,
onCompactionComplete: () => {
Expand Down