-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathTelemetryService.ts
More file actions
315 lines (270 loc) · 9.57 KB
/
Copy pathTelemetryService.ts
File metadata and controls
315 lines (270 loc) · 9.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
import { ZodError } from "zod"
import {
type TelemetryClient,
type TelemetryPropertiesProvider,
TelemetryEventName,
type TelemetrySetting,
type ToolUsage,
} from "@roo-code/types"
/**
* TelemetryService wrapper class that defers initialization.
* This ensures that we only create the various clients after environment
* variables are loaded.
*/
export class TelemetryService {
constructor(private clients: TelemetryClient[]) {}
public register(client: TelemetryClient): void {
this.clients.push(client)
}
/**
* Sets the ClineProvider reference to use for global properties
* @param provider A ClineProvider instance to use
*/
public setProvider(provider: TelemetryPropertiesProvider): void {
// If client is initialized, pass the provider reference.
if (this.isReady) {
this.clients.forEach((client) => client.setProvider(provider))
}
}
/**
* Base method for all telemetry operations
* Checks if the service is initialized before performing any operation
* @returns Whether the service is ready to use
*/
private get isReady(): boolean {
return this.clients.length > 0
}
/**
* Updates the telemetry state based on user preferences and VSCode settings
* @param isOptedIn Whether the user is opted into telemetry
*/
public updateTelemetryState(isOptedIn: boolean): void {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.updateTelemetryState(isOptedIn))
}
/**
* Generic method to capture any type of event with specified properties
* @param eventName The event name to capture
* @param properties The event properties
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
public captureEvent(eventName: TelemetryEventName, properties?: Record<string, any>): void {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.capture({ event: eventName, properties }))
}
/**
* Captures an exception using PostHog's error tracking
* @param error The error to capture
* @param additionalProperties Additional properties to include with the exception
*/
public captureException(error: Error, additionalProperties?: Record<string, unknown>): void {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.captureException(error, additionalProperties))
}
public captureTaskCreated(taskId: string): void {
this.captureEvent(TelemetryEventName.TASK_CREATED, { taskId })
}
public captureTaskRestarted(taskId: string): void {
this.captureEvent(TelemetryEventName.TASK_RESTARTED, { 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 {
this.captureEvent(TelemetryEventName.TASK_CONVERSATION_MESSAGE, { taskId, source })
}
public captureLlmCompletion(
taskId: string,
properties: {
inputTokens: number
outputTokens: number
cacheWriteTokens: number
cacheReadTokens: number
cost?: number
},
): void {
this.captureEvent(TelemetryEventName.LLM_COMPLETION, { taskId, ...properties })
}
public captureModeSwitch(taskId: string, newMode: string): void {
this.captureEvent(TelemetryEventName.MODE_SWITCH, { taskId, newMode })
}
public captureToolUsage(taskId: string, tool: string): void {
this.captureEvent(TelemetryEventName.TOOL_USED, { taskId, tool })
}
public captureCheckpointCreated(taskId: string): void {
this.captureEvent(TelemetryEventName.CHECKPOINT_CREATED, { taskId })
}
public captureCheckpointDiffed(taskId: string): void {
this.captureEvent(TelemetryEventName.CHECKPOINT_DIFFED, { taskId })
}
public captureCheckpointRestored(taskId: string): void {
this.captureEvent(TelemetryEventName.CHECKPOINT_RESTORED, { taskId })
}
public captureContextCondensed(taskId: string, isAutomaticTrigger: boolean, usedCustomPrompt?: boolean): void {
this.captureEvent(TelemetryEventName.CONTEXT_CONDENSED, {
taskId,
isAutomaticTrigger,
...(usedCustomPrompt !== undefined && { usedCustomPrompt }),
})
}
public captureSlidingWindowTruncation(taskId: string): void {
this.captureEvent(TelemetryEventName.SLIDING_WINDOW_TRUNCATION, { taskId })
}
public captureCodeActionUsed(actionType: string): void {
this.captureEvent(TelemetryEventName.CODE_ACTION_USED, { actionType })
}
public capturePromptEnhanced(taskId?: string): void {
this.captureEvent(TelemetryEventName.PROMPT_ENHANCED, { ...(taskId && { taskId }) })
}
public captureSchemaValidationError({ schemaName, error }: { schemaName: string; error: ZodError }): void {
// https://zod.dev/ERROR_HANDLING?id=formatting-errors
this.captureEvent(TelemetryEventName.SCHEMA_VALIDATION_ERROR, { schemaName, error: error.format() })
}
public captureDiffApplicationError(taskId: string, consecutiveMistakeCount: number): void {
this.captureEvent(TelemetryEventName.DIFF_APPLICATION_ERROR, { taskId, consecutiveMistakeCount })
}
public captureShellIntegrationError(taskId: string): void {
this.captureEvent(TelemetryEventName.SHELL_INTEGRATION_ERROR, { taskId })
}
public captureConsecutiveMistakeError(taskId: string): void {
this.captureEvent(TelemetryEventName.CONSECUTIVE_MISTAKE_ERROR, { taskId })
}
/**
* Captures when a tab is shown due to user action
* @param tab The tab that was shown
*/
public captureTabShown(tab: string): void {
this.captureEvent(TelemetryEventName.TAB_SHOWN, { tab })
}
/**
* Captures when a setting is changed in ModesView
* @param settingName The name of the setting that was changed
*/
public captureModeSettingChanged(settingName: string): void {
this.captureEvent(TelemetryEventName.MODE_SETTINGS_CHANGED, { settingName })
}
/**
* Captures when a user creates a new custom mode
* @param modeSlug The slug of the custom mode
* @param modeName The name of the custom mode
*/
public captureCustomModeCreated(modeSlug: string, modeName: string): void {
this.captureEvent(TelemetryEventName.CUSTOM_MODE_CREATED, { modeSlug, modeName })
}
/**
* Captures a marketplace item installation event
* @param itemId The unique identifier of the marketplace item
* @param itemType The type of item (mode or mcp)
* @param itemName The human-readable name of the item
* @param target The installation target (project or global)
* @param properties Additional properties like hasParameters, installationMethod
*/
public captureMarketplaceItemInstalled(
itemId: string,
itemType: string,
itemName: string,
target: string,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
properties?: Record<string, any>,
): void {
this.captureEvent(TelemetryEventName.MARKETPLACE_ITEM_INSTALLED, {
itemId,
itemType,
itemName,
target,
...(properties || {}),
})
}
/**
* Captures a marketplace item removal event
* @param itemId The unique identifier of the marketplace item
* @param itemType The type of item (mode or mcp)
* @param itemName The human-readable name of the item
* @param target The removal target (project or global)
*/
public captureMarketplaceItemRemoved(itemId: string, itemType: string, itemName: string, target: string): void {
this.captureEvent(TelemetryEventName.MARKETPLACE_ITEM_REMOVED, {
itemId,
itemType,
itemName,
target,
})
}
/**
* Captures a title button click event
* @param button The button that was clicked
*/
public captureTitleButtonClicked(button: string): void {
this.captureEvent(TelemetryEventName.TITLE_BUTTON_CLICKED, { button })
}
/**
* Captures when telemetry settings are changed
* @param previousSetting The previous telemetry setting
* @param newSetting The new telemetry setting
*/
public captureTelemetrySettingsChanged(previousSetting: TelemetrySetting, newSetting: TelemetrySetting): void {
this.captureEvent(TelemetryEventName.TELEMETRY_SETTINGS_CHANGED, {
previousSetting,
newSetting,
})
}
/**
* Checks if telemetry is currently enabled
* @returns Whether telemetry is enabled
*/
public isTelemetryEnabled(): boolean {
return this.isReady && this.clients.some((client) => client.isTelemetryEnabled())
}
public async shutdown(): Promise<void> {
if (!this.isReady) {
return
}
this.clients.forEach((client) => client.shutdown())
}
private static _instance: TelemetryService | null = null
static createInstance(clients: TelemetryClient[] = []) {
if (this._instance) {
throw new Error("TelemetryService instance already created")
}
this._instance = new TelemetryService(clients)
return this._instance
}
static get instance() {
if (!this._instance) {
throw new Error("TelemetryService not initialized")
}
return this._instance
}
static hasInstance(): boolean {
return this._instance !== null
}
}