diff --git a/packages/telemetry/src/TelemetryService.ts b/packages/telemetry/src/TelemetryService.ts index 5ea4cef936fa..cf2aa6076e1a 100644 --- a/packages/telemetry/src/TelemetryService.ts +++ b/packages/telemetry/src/TelemetryService.ts @@ -243,6 +243,16 @@ export class TelemetryService { }) } + /** + * Captures a slash command usage event + * @param taskId The task ID where the command was used + * @param commandType The type of command (custom or mode_switch) + * @param commandName The name of the command used + */ + public captureSlashCommandUsed(taskId: string, commandType: "custom" | "mode_switch", commandName: string): void { + this.captureEvent(TelemetryEventName.SLASH_COMMAND_USED, { taskId, commandType, commandName }) + } + /** * Checks if telemetry is currently enabled * @returns Whether telemetry is enabled diff --git a/packages/telemetry/src/__tests__/TelemetryService.slashCommands.test.ts b/packages/telemetry/src/__tests__/TelemetryService.slashCommands.test.ts new file mode 100644 index 000000000000..e6aa17e6f5bf --- /dev/null +++ b/packages/telemetry/src/__tests__/TelemetryService.slashCommands.test.ts @@ -0,0 +1,117 @@ +// npx vitest run packages/telemetry/src/__tests__/TelemetryService.slashCommands.test.ts + +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { TelemetryService } from "../TelemetryService" +import { TelemetryEventName } from "@roo-code/types" + +describe("TelemetryService - Slash Commands", () => { + let telemetryService: TelemetryService + let mockClient: { + capture: ReturnType + setProvider: ReturnType + updateTelemetryState: ReturnType + isTelemetryEnabled: ReturnType + shutdown: ReturnType + } + + beforeEach(() => { + // Reset the singleton instance + ;(TelemetryService as unknown as { _instance: TelemetryService | null })._instance = null + + mockClient = { + capture: vi.fn(), + setProvider: vi.fn(), + updateTelemetryState: vi.fn(), + isTelemetryEnabled: vi.fn().mockReturnValue(true), + shutdown: vi.fn(), + } + + telemetryService = TelemetryService.createInstance([mockClient]) + }) + + afterEach(() => { + // Clean up singleton instance after each test + ;(TelemetryService as unknown as { _instance: TelemetryService | null })._instance = null + }) + + describe("captureSlashCommandUsed", () => { + it("should capture custom slash command usage", () => { + const taskId = "test-task-123" + const commandType = "custom" + const commandName = "deploy" + + telemetryService.captureSlashCommandUsed(taskId, commandType, commandName) + + expect(mockClient.capture).toHaveBeenCalledWith({ + event: TelemetryEventName.SLASH_COMMAND_USED, + properties: { + taskId, + commandType, + commandName, + }, + }) + }) + + it("should capture mode switch slash command usage", () => { + const taskId = "test-task-456" + const commandType = "mode_switch" + const commandName = "code" + + telemetryService.captureSlashCommandUsed(taskId, commandType, commandName) + + expect(mockClient.capture).toHaveBeenCalledWith({ + event: TelemetryEventName.SLASH_COMMAND_USED, + properties: { + taskId, + commandType, + commandName, + }, + }) + }) + + it("should handle multiple slash command captures", () => { + const taskId = "test-task-789" + + telemetryService.captureSlashCommandUsed(taskId, "custom", "build") + telemetryService.captureSlashCommandUsed(taskId, "mode_switch", "debug") + telemetryService.captureSlashCommandUsed(taskId, "custom", "test") + + expect(mockClient.capture).toHaveBeenCalledTimes(3) + expect(mockClient.capture).toHaveBeenNthCalledWith(1, { + event: TelemetryEventName.SLASH_COMMAND_USED, + properties: { + taskId, + commandType: "custom", + commandName: "build", + }, + }) + expect(mockClient.capture).toHaveBeenNthCalledWith(2, { + event: TelemetryEventName.SLASH_COMMAND_USED, + properties: { + taskId, + commandType: "mode_switch", + commandName: "debug", + }, + }) + expect(mockClient.capture).toHaveBeenNthCalledWith(3, { + event: TelemetryEventName.SLASH_COMMAND_USED, + properties: { + taskId, + commandType: "custom", + commandName: "test", + }, + }) + }) + + it("should not capture when service is not ready", () => { + // Reset the instance to test empty service + ;(TelemetryService as unknown as { _instance: TelemetryService | null })._instance = null + const emptyService = TelemetryService.createInstance([]) + + emptyService.captureSlashCommandUsed("task-id", "custom", "command") + + // Should not throw and should not call any client methods + expect(mockClient.capture).not.toHaveBeenCalled() + }) + }) +}) diff --git a/packages/types/src/telemetry.ts b/packages/types/src/telemetry.ts index 29612d42a2f8..e55ee74ba735 100644 --- a/packages/types/src/telemetry.ts +++ b/packages/types/src/telemetry.ts @@ -72,6 +72,7 @@ export enum TelemetryEventName { CONSECUTIVE_MISTAKE_ERROR = "Consecutive Mistake Error", CODE_INDEX_ERROR = "Code Index Error", TELEMETRY_SETTINGS_CHANGED = "Telemetry Settings Changed", + SLASH_COMMAND_USED = "Slash Command Used", } /** @@ -201,6 +202,7 @@ export const rooCodeTelemetryEventSchema = z.discriminatedUnion("type", [ TelemetryEventName.TAB_SHOWN, TelemetryEventName.MODE_SETTINGS_CHANGED, TelemetryEventName.CUSTOM_MODE_CREATED, + TelemetryEventName.SLASH_COMMAND_USED, ]), properties: telemetryPropertiesSchema, }), diff --git a/src/core/task/Task.ts b/src/core/task/Task.ts index 8beaf1235e8c..c256b95be91a 100644 --- a/src/core/task/Task.ts +++ b/src/core/task/Task.ts @@ -900,7 +900,6 @@ export class Task extends EventEmitter implements TaskLike { this.askResponse = askResponse this.askResponseText = text this.askResponseImages = images - // Create a checkpoint whenever the user sends a message. // Use allowEmpty=true to ensure a checkpoint is recorded even if there are no file changes. // Suppress the checkpoint_saved chat row for this particular checkpoint to keep the timeline clean. @@ -926,7 +925,7 @@ export class Task extends EventEmitter implements TaskLike { } } } - + public approveAsk({ text, images }: { text?: string; images?: string[] } = {}) { this.handleWebviewAskResponse("yesButtonClicked", text, images) } diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index c7813372fa79..3ccc22c0c761 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -22,6 +22,8 @@ import { import { cn } from "@src/lib/utils" import { convertToMentionPath } from "@src/utils/path-mentions" import { StandardTooltip } from "@src/components/ui" +import { telemetryClient } from "@src/utils/TelemetryClient" +import { TelemetryEventName } from "@roo-code/types" import Thumbnails from "../common/Thumbnails" import { ModeSelector } from "./ModeSelector" @@ -299,6 +301,12 @@ export const ChatTextArea = forwardRef( } if (type === ContextMenuOptionType.Mode && value) { + // Track telemetry for mode selection from context menu + telemetryClient.capture(TelemetryEventName.SLASH_COMMAND_USED, { + commandType: "mode", + commandName: value, + }) + // Handle mode selection. setMode(value) setInputValue("") @@ -308,6 +316,12 @@ export const ChatTextArea = forwardRef( } if (type === ContextMenuOptionType.Command && value) { + // Track telemetry for slash command usage from context menu + telemetryClient.capture(TelemetryEventName.SLASH_COMMAND_USED, { + commandType: "custom", + commandName: value, + }) + // Handle command selection. setSelectedMenuIndex(-1) setInputValue("")