Compare commits

...

8 Commits

Author SHA1 Message Date
arafatkatze 1d9b9445a0 Lower border radius 2025-05-06 09:12:20 +04:00
Ara 21c3c33fed Update src/services/telemetry/TelemetryService.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-06 10:36:29 +05:30
Trevor Hudson fb43add6df Update src/core/controller/task/clearTask.ts
Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-05-05 16:25:16 -07:00
Trevor Hudson 362b05b25b remove check to send events anytime a new task is created while on an existing task 2025-05-05 16:13:10 -07:00
Trevor Hudson 59894c40e9 remove commented out parts 2025-05-02 18:56:18 -07:00
Trevor Hudson 88980a8e1b changeset 2025-05-02 18:26:01 -07:00
Trevor Hudson bd52febe90 collect messages 2025-05-01 23:05:36 -07:00
Trevor Hudson 9395d25f7a add collection method 2025-05-01 19:17:49 -07:00
7 changed files with 378 additions and 197 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Allow option to collect events to send them in a bundle to avoid sending too many events
+3
View File
@@ -1793,6 +1793,9 @@ Here is the project's README to help you get started:\n\n${mcpDetails.readmeCont
}
async clearTask() {
if (this.task) {
await telemetryService.sendCollectedEvents(this.task.taskId)
}
this.task?.abortTask()
this.task = undefined // removes reference to it, so once promises end it will be garbage collected
}
+1
View File
@@ -8,6 +8,7 @@ import { Empty, EmptyRequest } from "../../../shared/proto/common"
* @returns Empty response
*/
export async function clearTask(controller: Controller, _request: EmptyRequest): Promise<Empty> {
// clearTask is called here when the user closes the task
await controller.clearTask()
await controller.postStateToWebview()
return Empty.create()
+15 -3
View File
@@ -3543,7 +3543,7 @@ export class Task {
content: userContent,
})
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user")
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "user", true)
// since we sent off a placeholder api_req_started message to update the webview while waiting to actually start the API request (to load potential details for example), we need to update the text of that message
const lastApiReqIndex = findLastIndex(this.clineMessages, (m) => m.say === "api_req_started")
@@ -3620,7 +3620,13 @@ export class Task {
updateApiReqMsg(cancelReason, streamingFailedMessage)
await this.saveClineMessagesAndUpdateHistory()
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
)
// signals to provider that it can retrieve the saved messages from disk, as abortTask can not be awaited on in nature
this.didFinishAbortingStream = true
@@ -3763,7 +3769,13 @@ export class Task {
// need to save assistant responses to file before proceeding to tool use since user can exit at any moment and we wouldn't be able to save the assistant's response
let didEndLoop = false
if (assistantMessage.length > 0) {
telemetryService.captureConversationTurnEvent(this.taskId, currentProviderId, this.api.getModel().id, "assistant")
telemetryService.captureConversationTurnEvent(
this.taskId,
currentProviderId,
this.api.getModel().id,
"assistant",
true,
)
await this.addToApiConversationHistory({
role: "assistant",
+4 -3
View File
@@ -434,11 +434,12 @@ export function activate(context: vscode.ExtensionContext) {
const { IS_DEV, DEV_WORKSPACE_FOLDER } = process.env
// This method is called when your extension is deactivated
export function deactivate() {
export async function deactivate() {
await telemetryService.sendCollectedEvents()
// Clean up test mode
cleanupTestMode()
telemetryService.shutdown()
await telemetryService.shutdown()
Logger.log("Cline extension deactivated")
}
+349 -191
View File
@@ -10,7 +10,20 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
* Uses PostHog analytics to track user interactions and system events
* Respects user privacy settings and VSCode's global telemetry configuration
*/
interface CollectedTasks {
taskId: string
collection: Collection[]
}
interface Collection {
event: string
properties: any
}
class PostHogClient {
// Stores events when collect=true
private collectedTasks: CollectedTasks[] = []
// Event constants for tracking user interactions and system events
private static readonly EVENTS = {
// Task-related events for tracking conversation and execution flow
@@ -49,6 +62,8 @@ class PostHogClient {
BROWSER_TOOL_END: "task.browser_tool_end",
// Tracks when browser errors occur
BROWSER_ERROR: "task.browser_error",
// Collection of all task events
TASK_COLLECTION: "task.collection",
},
// UI interaction events for tracking user engagement
UI: {
@@ -87,6 +102,8 @@ class PostHogClient {
private telemetryEnabled: boolean = false
/** Current version of the extension */
private readonly version: string = extensionVersion
/** Whether the extension is running in development mode */
private readonly isDev = process.env.IS_DEV
/**
* Private constructor to enforce singleton pattern
@@ -136,17 +153,36 @@ class PostHogClient {
}
/**
* Captures a telemetry event if telemetry is enabled
* Captures a telemetry event if telemetry is enabled or collects if collect=true
* @param event The event to capture with its properties
* @param collect If true, store the event in collectedEvents instead of sending to PostHog
*/
public capture(event: { event: string; properties?: any }): void {
// Only send events if telemetry is enabled
if (this.telemetryEnabled) {
// Include extension version in all event properties
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
public capture(event: { event: string; properties?: any }, collect: boolean = false): void {
const taskId = event.properties.taskId
const propertiesWithVersion = {
...event.properties,
extension_version: this.version,
is_dev: this.isDev,
}
if (collect) {
const existingTask = this.collectedTasks.find((task) => task.taskId === taskId)
if (existingTask) {
existingTask.collection.push({
event: event.event,
properties: propertiesWithVersion,
})
} else {
this.collectedTasks.push({
taskId,
collection: [
{
event: event.event,
properties: propertiesWithVersion,
},
],
})
}
} else if (this.telemetryEnabled) {
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: propertiesWithVersion })
}
}
@@ -155,34 +191,48 @@ class PostHogClient {
/**
* Records when a new task/conversation is started
* @param taskId Unique identifier for the new task
* @param apiProvider Optional API provider
* @param collect If true, collect event instead of sending
*/
public captureTaskCreated(taskId: string, apiProvider?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
})
public captureTaskCreated(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
},
collect,
)
}
/**
* Records when a task/conversation is restarted
* @param taskId Unique identifier for the new task
* @param apiProvider Optional API provider
* @param collect If true, collect event instead of sending
*/
public captureTaskRestarted(taskId: string, apiProvider?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
})
public captureTaskRestarted(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
},
collect,
)
}
/**
* Records when cline calls the task completion_result tool signifying that cline is done with the task
* @param taskId Unique identifier for the task
* @param collect If true, collect event instead of sending
*/
public captureTaskCompleted(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.COMPLETED,
properties: { taskId },
})
public captureTaskCompleted(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.COMPLETED,
properties: { taskId },
},
collect,
)
}
/**
@@ -197,6 +247,7 @@ class PostHogClient {
provider: string = "unknown",
model: string = "unknown",
source: "user" | "assistant",
collect: boolean = false,
) {
// Ensure required parameters are provided
if (!taskId || !provider || !model || !source) {
@@ -212,10 +263,13 @@ class PostHogClient {
timestamp: new Date().toISOString(), // Add timestamp for message sequencing
}
this.capture({
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
properties,
})
this.capture(
{
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
properties,
},
collect,
)
}
/**
@@ -226,16 +280,19 @@ class PostHogClient {
* @param tokensOut Number of output tokens generated
* @param model The model used for token calculation
*/
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
tokensOut,
model,
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
tokensOut,
model,
},
},
})
collect,
)
}
/**
@@ -243,14 +300,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param mode The mode being switched to (plan or act)
*/
public captureModeSwitch(taskId: string, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
public captureModeSwitch(taskId: string, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
},
},
})
collect,
)
}
/**
@@ -258,15 +318,18 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param feedbackType The type of feedback ("thumbs_up" or "thumbs_down")
*/
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType) {
public captureTaskFeedback(taskId: string, feedbackType: TaskFeedbackType, collect: boolean = false) {
console.info("TelemetryService: Capturing task feedback", { taskId, feedbackType })
this.capture({
event: PostHogClient.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
this.capture(
{
event: PostHogClient.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
},
},
})
collect,
)
}
// Tool events
@@ -277,16 +340,19 @@ class PostHogClient {
* @param autoApproved Whether the tool was auto-approved based on settings
* @param success Whether the tool execution was successful
*/
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean) {
this.capture({
event: PostHogClient.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
autoApproved,
success,
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
autoApproved,
success,
},
},
})
collect,
)
}
/**
@@ -299,15 +365,19 @@ class PostHogClient {
taskId: string,
action: "shadow_git_initialized" | "commit_created" | "restored" | "diff_generated",
durationMs?: number,
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
durationMs,
this.capture(
{
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
durationMs,
},
},
})
collect,
)
}
// UI events
@@ -318,16 +388,25 @@ class PostHogClient {
* @param location Where the switch occurred (settings panel or bottom bar)
* @param taskId Optional task identifier if switch occurred during a task
*/
public captureProviderSwitch(from: string, to: string, location: "settings" | "bottom", taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
public captureProviderSwitch(
from: string,
to: string,
location: "settings" | "bottom",
taskId?: string,
collect: boolean = false,
) {
this.capture(
{
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
},
},
})
collect,
)
}
/**
@@ -335,14 +414,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param imageCount Number of images attached
*/
public captureImageAttached(taskId: string, imageCount: number) {
this.capture({
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
public captureImageAttached(taskId: string, imageCount: number, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
},
},
})
collect,
)
}
/**
@@ -350,66 +432,81 @@ class PostHogClient {
* @param button Identifier for the button that was clicked
* @param taskId Optional task identifier if click occurred during a task
*/
public captureButtonClick(button: string, taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.BUTTON_CLICK,
properties: {
button,
taskId,
public captureButtonClick(button: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.BUTTON_CLICK,
properties: {
button,
taskId,
},
},
})
collect,
)
}
/**
* Records when the marketplace view is opened
* @param taskId Optional task identifier if marketplace was opened during a task
*/
public captureMarketplaceOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.MARKETPLACE_OPENED,
properties: {
taskId,
public captureMarketplaceOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MARKETPLACE_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the settings panel is opened
* @param taskId Optional task identifier if settings were opened during a task
*/
public captureSettingsOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.SETTINGS_OPENED,
properties: {
taskId,
public captureSettingsOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.SETTINGS_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the task history view is opened
* @param taskId Optional task identifier if history was opened during a task
*/
public captureHistoryOpened(taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.HISTORY_OPENED,
properties: {
taskId,
public captureHistoryOpened(taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.HISTORY_OPENED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when a task is removed from the task history
* @param taskId Unique identifier for the task being removed
*/
public captureTaskPopped(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
public captureTaskPopped(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
@@ -417,14 +514,17 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param errorType Type of error that occurred (e.g., "search_not_found", "invalid_format")
*/
public captureDiffEditFailure(taskId: string, errorType?: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
public captureDiffEditFailure(taskId: string, errorType?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
},
},
})
collect,
)
}
/**
@@ -433,41 +533,50 @@ class PostHogClient {
* @param provider Provider of the selected model
* @param taskId Optional task identifier if model was selected during a task
*/
public captureModelSelected(model: string, provider: string, taskId?: string) {
this.capture({
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
taskId,
public captureModelSelected(model: string, provider: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
taskId,
},
},
})
collect,
)
}
/**
* Records when a historical task is loaded from storage
* @param taskId Unique identifier for the historical task
*/
public captureHistoricalTaskLoaded(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
public captureHistoricalTaskLoaded(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
* Records when the retry button is clicked for failed operations
* @param taskId Unique identifier for the task being retried
*/
public captureRetryClicked(taskId: string) {
this.capture({
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
public captureRetryClicked(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
},
},
})
collect,
)
}
/**
@@ -475,17 +584,20 @@ class PostHogClient {
* @param taskId Unique identifier for the task
* @param browserSettings The browser settings being used
*/
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
isRemote: !!browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
timestamp: new Date().toISOString(),
public captureBrowserToolStart(taskId: string, browserSettings: BrowserSettings, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
isRemote: !!browserSettings.remoteBrowserEnabled,
remoteBrowserHost: browserSettings.remoteBrowserHost,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -500,17 +612,21 @@ class PostHogClient {
duration: number
actions?: string[]
},
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
duration: stats.duration,
actions: stats.actions,
timestamp: new Date().toISOString(),
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
duration: stats.duration,
actions: stats.actions,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -530,17 +646,21 @@ class PostHogClient {
isRemote?: boolean
[key: string]: any
},
collect: boolean = false,
) {
this.capture({
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
errorMessage,
context,
timestamp: new Date().toISOString(),
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
errorMessage,
context,
timestamp: new Date().toISOString(),
},
},
})
collect,
)
}
/**
@@ -549,15 +669,18 @@ class PostHogClient {
* @param qty The quantity of options that were presented
* @param mode The mode in which the option was selected ("plan" or "act")
*/
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
mode,
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
mode,
},
},
})
collect,
)
}
/**
@@ -566,15 +689,18 @@ class PostHogClient {
* @param qty The quantity of options that were presented
* @param mode The mode in which the custom response was provided ("plan" or "act")
*/
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act") {
this.capture({
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
mode,
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
mode,
},
},
})
collect,
)
}
/**
@@ -582,20 +708,52 @@ class PostHogClient {
* @param model The name of the model the user has interacted with
* @param isFavorited Whether the model is being favorited (true) or unfavorited (false)
*/
public captureModelFavoritesUsage(model: string, isFavorited: boolean) {
this.capture({
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
public captureModelFavoritesUsage(model: string, isFavorited: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
},
},
})
collect,
)
}
public isTelemetryEnabled(): boolean {
return this.telemetryEnabled
}
public async sendCollectedEvents(taskId?: string): Promise<void> {
if (this.collectedTasks.length > 0) {
if (taskId) {
const task = this.collectedTasks.find((t) => t.taskId === taskId)
if (task) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId, events: task.collection },
},
false,
)
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== taskId)
}
} else {
for (const task of this.collectedTasks) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId: task.taskId, events: task.collection },
},
false,
)
this.collectedTasks = this.collectedTasks.filter((t) => t.taskId !== task.taskId)
}
}
}
}
public async shutdown(): Promise<void> {
await this.client.shutdown()
}
+1
View File
@@ -0,0 +1 @@
25463