TelemetryService.ts 8.7 KB

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