Skip to content
Closed
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
10 changes: 10 additions & 0 deletions packages/telemetry/src/TelemetryService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<typeof vi.fn>
setProvider: ReturnType<typeof vi.fn>
updateTelemetryState: ReturnType<typeof vi.fn>
isTelemetryEnabled: ReturnType<typeof vi.fn>
shutdown: ReturnType<typeof vi.fn>
}

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()
})
})
})
2 changes: 2 additions & 0 deletions packages/types/src/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}

/**
Expand Down Expand Up @@ -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,
}),
Expand Down
3 changes: 1 addition & 2 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,7 +900,6 @@ export class Task extends EventEmitter<TaskEvents> 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.
Expand All @@ -926,7 +925,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
}
}
}

public approveAsk({ text, images }: { text?: string; images?: string[] } = {}) {
this.handleWebviewAskResponse("yesButtonClicked", text, images)
}
Expand Down
14 changes: 14 additions & 0 deletions webview-ui/src/components/chat/ChatTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -299,6 +301,12 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}

if (type === ContextMenuOptionType.Mode && value) {
// Track telemetry for mode selection from context menu
telemetryClient.capture(TelemetryEventName.SLASH_COMMAND_USED, {
commandType: "mode",
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The commandType value should be "mode_switch" not "mode" to match the type signature of TelemetryService.captureSlashCommandUsed(taskId, commandType: "custom" | "mode_switch", commandName). This mismatch will cause the telemetry data to be inconsistent with the intended classification of mode switch commands.

commandName: value,
})
Comment on lines +305 to +308
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The telemetry capture is missing the taskId property. According to the backend TelemetryService.captureSlashCommandUsed() signature and the telemetry schema, this event requires taskId, commandType, and commandName properties. The webview needs access to the current task ID to properly track slash command usage. Consider passing the task ID through the extension state or event context.


// Handle mode selection.
setMode(value)
setInputValue("")
Expand All @@ -308,6 +316,12 @@ export const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}

if (type === ContextMenuOptionType.Command && value) {
// Track telemetry for slash command usage from context menu
telemetryClient.capture(TelemetryEventName.SLASH_COMMAND_USED, {
commandType: "custom",
commandName: value,
})
Comment on lines +320 to +323
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This telemetry capture is also missing the taskId property. The TelemetryService.captureSlashCommandUsed() method requires three parameters: taskId, commandType, and commandName. Without the task ID, the telemetry data won't be properly associated with the task context.


// Handle command selection.
setSelectedMenuIndex(-1)
setInputValue("")
Expand Down
Loading