Skip to content
Open
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
28 changes: 26 additions & 2 deletions packages/telemetry/src/TelemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
type TelemetryPropertiesProvider,
TelemetryEventName,
type TelemetrySetting,
type ToolUsage,
} from "@roo-code/types"

/**
Expand Down Expand Up @@ -86,8 +87,31 @@ export class TelemetryService {
this.captureEvent(TelemetryEventName.TASK_RESTARTED, { taskId })
}

public captureTaskCompleted(taskId: string): void {
this.captureEvent(TelemetryEventName.TASK_COMPLETED, { taskId })
/**
* Captures task completion, summarizing per-task tool and message counts that
* were previously reported as separate per-turn events to reduce event volume.
*
* A single task may emit this more than once (e.g. an "idle" or "shutdown"
* installment followed by a final "attempt_completion" one). toolsUsed and
* messageCount are always deltas since the previous emission for that task,
* not running totals -- summing installments for a taskId reconstructs the
* full-task counts without double-counting.
*
* Note: "attempt_completion" means the model called that tool, not that the
* user accepted the result.
*/
public captureTaskCompleted(
taskId: string,
toolsUsed?: ToolUsage,
messageCount?: { user: number; assistant: number },
completionReason: "attempt_completion" | "idle" | "shutdown" = "attempt_completion",
): void {
this.captureEvent(TelemetryEventName.TASK_COMPLETED, {
taskId,
completionReason,
...(toolsUsed !== undefined && { toolsUsed }),
...(messageCount !== undefined && { messageCount }),
})
}

public captureConversationMessage(taskId: string, source: "user" | "assistant"): void {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// pnpm --filter @roo-code/telemetry test src/__tests__/TelemetryService.task-completed.test.ts

import { TelemetryEventName, type TelemetryClient } from "@roo-code/types"

import { TelemetryService } from "../TelemetryService"

describe("TelemetryService.captureTaskCompleted", () => {
let mockClient: TelemetryClient

beforeEach(() => {
mockClient = {
setProvider: vi.fn(),
capture: vi.fn().mockResolvedValue(undefined),
captureException: vi.fn().mockResolvedValue(undefined),
updateTelemetryState: vi.fn(),
isTelemetryEnabled: vi.fn().mockReturnValue(true),
shutdown: vi.fn().mockResolvedValue(undefined),
}
})

it("captures Task Completed with the taskId and a default 'attempt_completion' completionReason when no summary is provided", () => {
const service = new TelemetryService([mockClient])

service.captureTaskCompleted("task_1")

expect(mockClient.capture).toHaveBeenCalledWith({
event: TelemetryEventName.TASK_COMPLETED,
properties: { taskId: "task_1", completionReason: "attempt_completion" },
})
})

it("includes toolsUsed and messageCount summaries when provided", () => {
const service = new TelemetryService([mockClient])

service.captureTaskCompleted(
"task_1",
{ read_file: { attempts: 3, failures: 0 }, apply_diff: { attempts: 1, failures: 1 } },
{ user: 4, assistant: 5 },
)

expect(mockClient.capture).toHaveBeenCalledWith({
event: TelemetryEventName.TASK_COMPLETED,
properties: {
taskId: "task_1",
completionReason: "attempt_completion",
toolsUsed: { read_file: { attempts: 3, failures: 0 }, apply_diff: { attempts: 1, failures: 1 } },
messageCount: { user: 4, assistant: 5 },
},
})
})

it("includes the given completionReason for idle/shutdown installments", () => {
const service = new TelemetryService([mockClient])

service.captureTaskCompleted("task_1", { read_file: { attempts: 1, failures: 0 } }, undefined, "idle")

expect(mockClient.capture).toHaveBeenCalledWith({
event: TelemetryEventName.TASK_COMPLETED,
properties: {
taskId: "task_1",
completionReason: "idle",
toolsUsed: { read_file: { attempts: 1, failures: 0 } },
},
})
})
})
3 changes: 2 additions & 1 deletion src/__tests__/history-resume-delegation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ vi.mock("vscode", () => {
return { window, workspace, env, Uri, commands, ExtensionMode, version }
})

// Mock TelemetryService (needed by attemptCompletionTool's emitTaskCompleted)
// Mock TelemetryService (needed by attemptCompletionTool's emitPublicTaskCompleted)
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
Expand Down Expand Up @@ -1255,6 +1255,7 @@ describe("History resume delegation - parent metadata transitions", () => {
userMessageContent: [],
consecutiveMistakeCount: 0,
emitFinalTokenUsageUpdate: vi.fn(),
flushTelemetryInstallment: vi.fn(),
} as unknown as import("../core/task/Task").Task

const block = {
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/nested-delegation-resume.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ describe("Nested delegation resume (A → B → C)", () => {
userMessageContent: [],
consecutiveMistakeCount: 0,
emitFinalTokenUsageUpdate: vi.fn(),
flushTelemetryInstallment: vi.fn(),
} as unknown as Task

const blockC = {
Expand Down Expand Up @@ -250,6 +251,7 @@ describe("Nested delegation resume (A → B → C)", () => {
userMessageContent: [],
consecutiveMistakeCount: 0,
emitFinalTokenUsageUpdate: vi.fn(),
flushTelemetryInstallment: vi.fn(),
} as unknown as Task

const blockB = {
Expand Down
2 changes: 2 additions & 0 deletions src/__tests__/task-run-dispatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Runnable = {
metadata: { task?: string | null; images?: string[] | null }
resumeTaskFromHistory: () => Promise<void>
startTask: (task?: string, images?: string[]) => Promise<void>
startIdleTelemetryCheck: () => void
}

function makeRunnable(overrides: Partial<Runnable> = {}): Runnable & { run(): Promise<void> } {
Expand All @@ -27,6 +28,7 @@ function makeRunnable(overrides: Partial<Runnable> = {}): Runnable & { run(): Pr
metadata: { task: undefined, images: undefined },
resumeTaskFromHistory: vi.fn().mockResolvedValue(undefined),
startTask: vi.fn().mockResolvedValue(undefined),
startIdleTelemetryCheck: vi.fn(),
...overrides,
}
// Bind the real run() implementation from Task.prototype to our stand-in.
Expand Down
Loading
Loading