Compare commits

...

1 Commits

Author SHA1 Message Date
Trevor Hudson e038d3b150 Use identify to enhance distinct user segmentation
include backup id in front end

variables clarity
2025-05-23 11:14:06 -07:00
9 changed files with 139 additions and 214 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"claude-dev": patch
---
Use identify to enhance distinct user segmentation
+2 -8
View File
@@ -261,13 +261,6 @@ export class Controller {
}
}
})
// If user already opted in to telemetry, enable telemetry service
this.getStateToPostToWebview().then((state) => {
const { telemetrySetting } = state
const isOptedIn = telemetrySetting !== "disabled"
telemetryService.updateTelemetryState(isOptedIn)
})
break
case "showChatView": {
this.postMessageToWebview({
@@ -810,6 +803,7 @@ export class Controller {
const response = await axios.get("https://api.cline.bot/v1/mcp/marketplace", {
headers: {
"Content-Type": "application/json",
"User-Agent": "cline-vscode-extension",
},
})
@@ -1287,7 +1281,7 @@ export class Controller {
telemetrySetting,
planActSeparateModelsSetting,
enableCheckpointsSetting: enableCheckpointsSetting ?? true,
vscMachineId: vscode.env.machineId,
distinctId: telemetryService.distinctId,
globalClineRulesToggles: globalClineRulesToggles || {},
localClineRulesToggles: localClineRulesToggles || {},
localWindsurfRulesToggles: localWindsurfRulesToggles || {},
+11
View File
@@ -14,6 +14,7 @@ import { Controller } from "./core/controller"
import { ErrorService } from "./services/error/ErrorService"
import { initializeTestMode, cleanupTestMode } from "./services/test/TestMode"
import { telemetryService } from "./services/posthog/telemetry/TelemetryService"
import { v4 as uuidv4 } from "uuid"
/*
Built using https://github.com/microsoft/vscode-webview-ui-toolkit
@@ -75,6 +76,16 @@ export async function activate(context: vscode.ExtensionContext) {
console.error(`Error during post-update actions: ${errorMessage}, Stack trace: ${error.stack}`)
}
// backup id in case vscMachineID doesn't work
let installId = context.globalState.get<string>("installId")
if (!installId) {
installId = uuidv4()
await context.globalState.update("installId", installId)
}
telemetryService.captureExtensionActivated(installId)
context.subscriptions.push(
vscode.commands.registerCommand("cline.plusButtonClicked", async (webview: any) => {
const openChat = async (instance?: WebviewProvider) => {
@@ -9,7 +9,6 @@ class PostHogClientProvider {
this.client = new PostHog(posthogConfig.apiKey, {
host: posthogConfig.host,
enableExceptionAutocapture: false,
defaultOptIn: false,
})
}
@@ -7,7 +7,7 @@ import type { BrowserSettings } from "@shared/BrowserSettings"
import { posthogClientProvider } from "../PostHogClientProvider"
/**
* PostHogClient handles telemetry event tracking for the Cline extension
* TelemetryService handles telemetry event tracking for the Cline extension
* Uses PostHog analytics to track user interactions and system events
* Respects user privacy settings and VSCode's global telemetry configuration
*/
@@ -29,7 +29,7 @@ interface Collection {
*/
type TelemetryCategory = "checkpoints" | "browser"
class PostHogClient {
class TelemetryService {
// Map to control specific telemetry categories (event types)
private telemetryCategoryEnabled: Map<TelemetryCategory, boolean> = new Map([
["checkpoints", false], // Checkpoints telemetry disabled
@@ -41,6 +41,11 @@ class PostHogClient {
// Event constants for tracking user interactions and system events
private static readonly EVENTS = {
// Task-related events for tracking conversation and execution flow
USER: {
OPT_OUT: "user.opt_out",
EXTENSION_ACTIVATED: "user.extension_activated",
},
TASK: {
// Tracks when a new task/conversation is started
CREATED: "task.created",
@@ -83,37 +88,19 @@ class PostHogClient {
},
// UI interaction events for tracking user engagement
UI: {
// Tracks when user switches between API providers
PROVIDER_SWITCH: "ui.provider_switch",
// Tracks when images are attached to a conversation
IMAGE_ATTACHED: "ui.image_attached",
// Tracks general button click interactions
BUTTON_CLICK: "ui.button_click",
// Tracks when the marketplace view is opened
MARKETPLACE_OPENED: "ui.marketplace_opened",
// Tracks when settings panel is opened
SETTINGS_OPENED: "ui.settings_opened",
// Tracks when task history view is opened
HISTORY_OPENED: "ui.history_opened",
// Tracks when a task is removed from history
TASK_POPPED: "ui.task_popped",
// Tracks when a different model is selected
MODEL_SELECTED: "ui.model_selected",
// Tracks when planning mode is toggled on
PLAN_MODE_TOGGLED: "ui.plan_mode_toggled",
// Tracks when action mode is toggled on
ACT_MODE_TOGGLED: "ui.act_mode_toggled",
// Tracks when users use the "favorite" button in the model picker
MODEL_FAVORITE_TOGGLED: "ui.model_favorite_toggled",
},
}
/** Singleton instance of the PostHogClient */
private static instance: PostHogClient
/** Singleton instance of the TelemetryService */
private static instance: TelemetryService
/** PostHog client instance for sending analytics events */
private client: PostHog
/** Unique identifier for the current VSCode instance */
private distinctId: string = vscode.env.machineId
public distinctId: string = vscode.env.machineId
/** Whether telemetry is currently enabled based on user and VSCode settings */
private telemetryEnabled: boolean = false
/** Current version of the extension */
@@ -129,14 +116,18 @@ class PostHogClient {
this.client = posthogClientProvider.getClient()
}
private setDistinctId(installId: string) {
if (this.distinctId === "someValue.machineId") {
this.distinctId = installId
}
}
/**
* Updates the telemetry state based on user preferences and VSCode settings
* Only enables telemetry if both VSCode global telemetry is enabled and user has opted in
* @param didUserOptIn Whether the user has explicitly opted into telemetry
*/
public updateTelemetryState(didUserOptIn: boolean): void {
this.telemetryEnabled = false
public async updateTelemetryState(didUserOptIn: boolean): Promise<void> {
// First check global telemetry level - telemetry should only be enabled when level is "all"
const telemetryLevel = vscode.workspace.getConfiguration("telemetry").get<string>("telemetryLevel", "all")
const globalTelemetryEnabled = telemetryLevel === "all"
@@ -144,25 +135,53 @@ class PostHogClient {
// We only enable telemetry if global vscode telemetry is enabled
if (globalTelemetryEnabled) {
this.telemetryEnabled = didUserOptIn
} else {
// Show warning to user that global telemetry is disabled
void vscode.window
.showWarningMessage(
"VSCode telemetry is disabled. To enable telemetry for this extension, first enable VSCode telemetry in settings.",
"Open Settings",
)
.then((selection) => {
if (selection === "Open Settings") {
void vscode.commands.executeCommand("workbench.action.openSettings", "telemetry.telemetryLevel")
}
})
}
// Update PostHog client state based on telemetry preference
if (this.telemetryEnabled) {
this.client.optIn()
this.client.identify({ distinctId: this.distinctId })
} else {
this.client.capture({
distinctId: this.distinctId,
event: TelemetryService.EVENTS.USER.OPT_OUT,
properties: this.addProperties({}),
})
await new Promise((resolve) => setTimeout(resolve, 1000)) // Delay 1 second before opting out
this.client.optOut()
}
}
/**
* Gets or creates the singleton instance of PostHogClient
* @returns The PostHogClient instance
* Gets or creates the singleton instance of TelemetryService
* @returns The TelemetryService instance
*/
public static getInstance(): PostHogClient {
if (!PostHogClient.instance) {
PostHogClient.instance = new PostHogClient()
public static getInstance(): TelemetryService {
if (!TelemetryService.instance) {
TelemetryService.instance = new TelemetryService()
}
return TelemetryService.instance
}
private addProperties(properties: any): any {
return {
...properties,
extension_version: this.version,
is_dev: this.isDev,
}
return PostHogClient.instance
}
/**
@@ -171,13 +190,14 @@ class PostHogClient {
* @param collect If true, store the event in collectedEvents instead of sending to PostHog
*/
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 (!this.telemetryEnabled) {
return
}
if (collect) {
const taskId = event.properties.taskId
const propertiesWithVersion = this.addProperties(event.properties)
if (collect && taskId) {
const existingTask = this.collectedTasks.find((task) => task.taskId === taskId)
if (existingTask) {
existingTask.collection.push({
@@ -195,11 +215,20 @@ class PostHogClient {
],
})
}
} else if (this.telemetryEnabled) {
} else {
this.client.capture({ distinctId: this.distinctId, event: event.event, properties: propertiesWithVersion })
}
}
public captureExtensionActivated(installId: string) {
this.setDistinctId(installId)
if (this.telemetryEnabled) {
this.client.identify({ distinctId: this.distinctId })
this.client.capture({ distinctId: this.distinctId, event: TelemetryService.EVENTS.USER.EXTENSION_ACTIVATED })
}
}
// Task events
/**
* Records when a new task/conversation is started
@@ -210,7 +239,7 @@ class PostHogClient {
public captureTaskCreated(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CREATED,
event: TelemetryService.EVENTS.TASK.CREATED,
properties: { taskId, apiProvider },
},
collect,
@@ -226,7 +255,7 @@ class PostHogClient {
public captureTaskRestarted(taskId: string, apiProvider?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RESTARTED,
event: TelemetryService.EVENTS.TASK.RESTARTED,
properties: { taskId, apiProvider },
},
collect,
@@ -241,7 +270,7 @@ class PostHogClient {
public captureTaskCompleted(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.COMPLETED,
event: TelemetryService.EVENTS.TASK.COMPLETED,
properties: { taskId },
},
collect,
@@ -278,7 +307,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CONVERSATION_TURN,
event: TelemetryService.EVENTS.TASK.CONVERSATION_TURN,
properties,
},
collect,
@@ -295,7 +324,7 @@ class PostHogClient {
public captureTokenUsage(taskId: string, tokensIn: number, tokensOut: number, model: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOKEN_USAGE,
event: TelemetryService.EVENTS.TASK.TOKEN_USAGE,
properties: {
taskId,
tokensIn,
@@ -315,7 +344,7 @@ class PostHogClient {
public captureModeSwitch(taskId: string, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.MODE_SWITCH,
event: TelemetryService.EVENTS.TASK.MODE_SWITCH,
properties: {
taskId,
mode,
@@ -334,7 +363,7 @@ class PostHogClient {
console.info("TelemetryService: Capturing task feedback", { taskId, feedbackType })
this.capture(
{
event: PostHogClient.EVENTS.TASK.FEEDBACK,
event: TelemetryService.EVENTS.TASK.FEEDBACK,
properties: {
taskId,
feedbackType,
@@ -355,7 +384,7 @@ class PostHogClient {
public captureToolUsage(taskId: string, tool: string, autoApproved: boolean, success: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TOOL_USED,
event: TelemetryService.EVENTS.TASK.TOOL_USED,
properties: {
taskId,
tool,
@@ -385,7 +414,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.CHECKPOINT_USED,
event: TelemetryService.EVENTS.TASK.CHECKPOINT_USED,
properties: {
taskId,
action,
@@ -396,135 +425,6 @@ class PostHogClient {
)
}
// UI events
/**
* Records when the user switches between different API providers
* @param from Previous provider name
* @param to New provider name
* @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,
collect: boolean = false,
) {
this.capture(
{
event: PostHogClient.EVENTS.UI.PROVIDER_SWITCH,
properties: {
from,
to,
location,
taskId,
},
},
collect,
)
}
/**
* Records when images are attached to a conversation
* @param taskId Unique identifier for the task
* @param imageCount Number of images attached
*/
public captureImageAttached(taskId: string, imageCount: number, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.IMAGE_ATTACHED,
properties: {
taskId,
imageCount,
},
},
collect,
)
}
/**
* Records general button click interactions in the UI
* @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, 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, 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, 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, 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, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.TASK_POPPED,
properties: {
taskId,
},
},
collect,
)
}
/**
* Records when a diff edit (replace_in_file) operation fails
* @param taskId Unique identifier for the task
@@ -533,7 +433,7 @@ class PostHogClient {
public captureDiffEditFailure(taskId: string, modelId: string, errorType?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.DIFF_EDIT_FAILED,
event: TelemetryService.EVENTS.TASK.DIFF_EDIT_FAILED,
properties: {
taskId,
errorType,
@@ -553,7 +453,7 @@ class PostHogClient {
public captureModelSelected(model: string, provider: string, taskId?: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_SELECTED,
event: TelemetryService.EVENTS.UI.MODEL_SELECTED,
properties: {
model,
provider,
@@ -571,7 +471,7 @@ class PostHogClient {
public captureHistoricalTaskLoaded(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.HISTORICAL_LOADED,
event: TelemetryService.EVENTS.TASK.HISTORICAL_LOADED,
properties: {
taskId,
},
@@ -587,7 +487,7 @@ class PostHogClient {
public captureRetryClicked(taskId: string, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.RETRY_CLICKED,
event: TelemetryService.EVENTS.TASK.RETRY_CLICKED,
properties: {
taskId,
},
@@ -608,7 +508,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_START,
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_START,
properties: {
taskId,
viewport: browserSettings.viewport,
@@ -641,7 +541,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_TOOL_END,
event: TelemetryService.EVENTS.TASK.BROWSER_TOOL_END,
properties: {
taskId,
actionCount: stats.actionCount,
@@ -679,7 +579,7 @@ class PostHogClient {
this.capture(
{
event: PostHogClient.EVENTS.TASK.BROWSER_ERROR,
event: TelemetryService.EVENTS.TASK.BROWSER_ERROR,
properties: {
taskId,
errorType,
@@ -701,7 +601,7 @@ class PostHogClient {
public captureOptionSelected(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTION_SELECTED,
event: TelemetryService.EVENTS.TASK.OPTION_SELECTED,
properties: {
taskId,
qty,
@@ -721,7 +621,7 @@ class PostHogClient {
public captureOptionsIgnored(taskId: string, qty: number, mode: "plan" | "act", collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.OPTIONS_IGNORED,
event: TelemetryService.EVENTS.TASK.OPTIONS_IGNORED,
properties: {
taskId,
qty,
@@ -758,7 +658,7 @@ class PostHogClient {
) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.GEMINI_API_PERFORMANCE,
event: TelemetryService.EVENTS.TASK.GEMINI_API_PERFORMANCE,
properties: {
taskId,
modelId,
@@ -777,7 +677,7 @@ class PostHogClient {
public captureModelFavoritesUsage(model: string, isFavorited: boolean, collect: boolean = false) {
this.capture(
{
event: PostHogClient.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
event: TelemetryService.EVENTS.UI.MODEL_FAVORITE_TOGGLED,
properties: {
model,
isFavorited,
@@ -806,13 +706,17 @@ class PostHogClient {
}
public async sendCollectedEvents(taskId?: string): Promise<void> {
if (!this.telemetryEnabled) {
return
}
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,
event: TelemetryService.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId, events: task.collection },
},
false,
@@ -823,7 +727,7 @@ class PostHogClient {
for (const task of this.collectedTasks) {
this.capture(
{
event: PostHogClient.EVENTS.TASK.TASK_COLLECTION,
event: TelemetryService.EVENTS.TASK.TASK_COLLECTION,
properties: { taskId: task.taskId, events: task.collection },
},
false,
@@ -839,4 +743,4 @@ class PostHogClient {
}
}
export const telemetryService = PostHogClient.getInstance()
export const telemetryService = TelemetryService.getInstance()
+1 -1
View File
@@ -137,7 +137,7 @@ export interface ExtensionState {
photoURL: string | null
}
version: string
vscMachineId: string
distinctId: string
globalClineRulesToggles: ClineRulesToggles
localClineRulesToggles: ClineRulesToggles
localWorkflowToggles: ClineRulesToggles
+23 -11
View File
@@ -5,32 +5,44 @@ import { posthogConfig } from "@shared/services/config/posthog-config"
import { useExtensionState } from "./context/ExtensionStateContext"
export function CustomPostHogProvider({ children }: { children: ReactNode }) {
const { telemetrySetting, vscMachineId } = useExtensionState()
const { telemetrySetting, distinctId, version } = useExtensionState()
const isTelemetryEnabled = telemetrySetting !== "disabled"
useEffect(() => {
if (vscMachineId.length === 0) {
return
}
posthog.init(posthogConfig.apiKey, {
api_host: posthogConfig.host,
ui_host: posthogConfig.uiHost,
opt_out_capturing_by_default: true,
disable_session_recording: true,
capture_pageview: false,
capture_dead_clicks: true,
bootstrap: {
distinctID: vscMachineId,
})
}, [])
useEffect(() => {
if (distinctId.length === 0 || version.length === 0) {
return
}
posthog.set_config({
before_send: (payload: any) => {
if (payload?.properties) {
payload.properties.extension_version = version
payload.properties.distinct_id = distinctId
}
return payload
},
})
if (isTelemetryEnabled) {
const optedIn = posthog.has_opted_in_capturing()
const optedOut = posthog.has_opted_out_capturing()
if (isTelemetryEnabled && !optedIn) {
posthog.opt_in_capturing()
} else {
posthog.identify(distinctId)
} else if (!isTelemetryEnabled && !optedOut) {
posthog.opt_out_capturing()
}
}, [isTelemetryEnabled, vscMachineId])
}, [isTelemetryEnabled, distinctId, version])
return <PostHogProvider client={posthog}>{children}</PostHogProvider>
}
@@ -607,7 +607,7 @@ const SettingsView = ({ onDone, targetSection }: SettingsViewProps) => {
<div className="mb-[5px]">
<VSCodeCheckbox
className="mb-[5px]"
checked={telemetrySetting === "enabled"}
checked={telemetrySetting !== "disabled"}
onChange={(e: any) => {
const checked = e.target.checked === true
setTelemetrySetting(checked ? "enabled" : "disabled")
@@ -156,7 +156,7 @@ export const ExtensionStateContextProvider: React.FC<{
chatSettings: DEFAULT_CHAT_SETTINGS,
platform: DEFAULT_PLATFORM,
telemetrySetting: "unset",
vscMachineId: "",
distinctId: "",
planActSeparateModelsSetting: true,
enableCheckpointsSetting: true,
globalClineRulesToggles: {},