@@ -5,119 +5,206 @@ import config from '../config.js';
55import logger from '../logger.js' ;
66import { mongoLogId } from 'mongodb-log-writer' ;
77import { ApiClient } from '../common/atlas/apiClient.js' ;
8- import { ApiClientError } from '../common/atlas/apiClientError.js' ;
98import fs from 'fs/promises' ;
109import path from 'path' ;
1110
12- const isTelemetryEnabled = config . telemetry === 'enabled ';
11+ const TELEMETRY_ENABLED = config . telemetry !== 'disabled ';
1312const CACHE_FILE = path . join ( process . cwd ( ) , '.telemetry-cache.json' ) ;
1413
14+ interface TelemetryError extends Error {
15+ code ?: string ;
16+ }
17+
18+ type EventResult = {
19+ success : boolean ;
20+ error ?: Error ;
21+ } ;
22+
23+ type CommonProperties = {
24+ device_id : string ;
25+ mcp_server_version : string ;
26+ mcp_server_name : string ;
27+ mcp_client_version ?: string ;
28+ mcp_client_name ?: string ;
29+ platform : string ;
30+ arch : string ;
31+ os_type : string ;
32+ os_version ?: string ;
33+ session_id ?: string ;
34+ } ;
35+
1536export class Telemetry {
16- constructor ( private readonly session : Session ) { }
37+ private readonly commonProperties : CommonProperties ;
1738
18- private readonly commonProperties = {
39+ constructor ( private readonly session : Session ) {
40+ // Ensure all required properties are present
41+ this . commonProperties = Object . freeze ( {
42+ device_id : config . device_id ,
1943 mcp_server_version : pkg . version ,
2044 mcp_server_name : config . mcpServerName ,
2145 mcp_client_version : this . session . agentClientVersion ,
2246 mcp_client_name : this . session . agentClientName ,
23- session_id : this . session . sessionId ,
24- device_id : config . device_id ,
2547 platform : config . platform ,
2648 arch : config . arch ,
2749 os_type : config . os_type ,
2850 os_version : config . os_version ,
29- } ;
51+ } ) ;
52+ }
3053
31- async emitToolEvent ( command : string , category : string , startTime : number , result : 'success' | 'failure' , error ?: Error ) : Promise < void > {
32- if ( ! isTelemetryEnabled ) {
33- logger . debug ( mongoLogId ( 1_000_000 ) , "telemetry" , `Telemetry is disabled, skipping event.` ) ;
54+ /**
55+ * Emits a tool event with timing and error information
56+ * @param command - The command being executed
57+ * @param category - Category of the command
58+ * @param startTime - Start time in milliseconds
59+ * @param result - Whether the command succeeded or failed
60+ * @param error - Optional error if the command failed
61+ */
62+ public async emitToolEvent (
63+ command : string ,
64+ category : string ,
65+ startTime : number ,
66+ result : 'success' | 'failure' ,
67+ error ?: Error
68+ ) : Promise < void > {
69+ if ( ! TELEMETRY_ENABLED ) {
70+ logger . debug ( mongoLogId ( 1_000_000 ) , "telemetry" , "Telemetry is disabled, skipping event." ) ;
3471 return ;
3572 }
3673
74+ const event = this . createToolEvent ( command , category , startTime , result , error ) ;
75+ await this . emit ( [ event ] ) ;
76+ }
77+
78+ /**
79+ * Creates a tool event with common properties and timing information
80+ */
81+ private createToolEvent (
82+ command : string ,
83+ category : string ,
84+ startTime : number ,
85+ result : 'success' | 'failure' ,
86+ error ?: Error
87+ ) : ToolEvent {
3788 const duration = Date . now ( ) - startTime ;
3889
3990 const event : ToolEvent = {
4091 timestamp : new Date ( ) . toISOString ( ) ,
4192 source : 'mdbmcp' ,
4293 properties : {
4394 ...this . commonProperties ,
44- command : command ,
45- category : category ,
95+ command,
96+ category,
4697 duration_ms : duration ,
47- result : result
98+ session_id : this . session . sessionId ,
99+ result,
100+ ...( error && {
101+ error_type : error . name ,
102+ error_code : error . message
103+ } )
48104 }
49105 } ;
50106
51- if ( result === 'failure' ) {
52- event . properties . error_type = error ?. name ;
53- event . properties . error_code = error ?. message ;
54- }
55-
56- await this . emit ( [ event ] ) ;
107+ return event ;
57108 }
58109
110+ /**
111+ * Attempts to emit events through authenticated and unauthenticated clients
112+ * Falls back to caching if both attempts fail
113+ */
59114 private async emit ( events : BaseEvent [ ] ) : Promise < void > {
60- // First try to read any cached events
61115 const cachedEvents = await this . readCache ( ) ;
62116 const allEvents = [ ...cachedEvents , ...events ] ;
63117
64- logger . debug ( mongoLogId ( 1_000_000 ) , "telemetry" , `Attempting to send ${ allEvents . length } events (${ cachedEvents . length } cached)` ) ;
118+ logger . debug (
119+ mongoLogId ( 1_000_000 ) ,
120+ "telemetry" ,
121+ `Attempting to send ${ allEvents . length } events (${ cachedEvents . length } cached)`
122+ ) ;
65123
66- try {
67- if ( this . session . apiClient ) {
68- await this . session . apiClient . sendEvents ( allEvents ) ;
69- // If successful, clear the cache
70- await this . clearCache ( ) ;
71- return ;
72- }
73- } catch ( error ) {
74- logger . warning ( mongoLogId ( 1_000_000 ) , "telemetry" , `Error sending event to authenticated client: ${ error } ` ) ;
75- // Cache the events that failed to send
76- await this . cacheEvents ( allEvents ) ;
124+ const result = await this . sendEvents ( this . session . apiClient , allEvents ) ;
125+ if ( result . success ) {
126+ await this . clearCache ( ) ;
127+ return ;
77128 }
78129
79- // Try unauthenticated client as fallback
130+ logger . warning (
131+ mongoLogId ( 1_000_000 ) ,
132+ "telemetry" ,
133+ `Error sending event to client: ${ result . error } `
134+ ) ;
135+ await this . cacheEvents ( allEvents ) ;
136+ }
137+
138+ /**
139+ * Attempts to send events through the provided API client
140+ */
141+ private async sendEvents ( client : ApiClient , events : BaseEvent [ ] ) : Promise < EventResult > {
80142 try {
81- const tempApiClient = new ApiClient ( {
82- baseUrl : config . apiBaseUrl ,
83- } ) ;
84- await tempApiClient . sendEvents ( allEvents ) ;
85- // If successful, clear the cache
86- await this . clearCache ( ) ;
143+ await client . sendEvents ( events ) ;
144+ return { success : true } ;
87145 } catch ( error ) {
88- logger . warning ( mongoLogId ( 1_000_000 ) , "telemetry" , `Error sending event to unauthenticated client: ${ error } ` ) ;
89- // Cache the events that failed to send
90- await this . cacheEvents ( allEvents ) ;
146+ return {
147+ success : false ,
148+ error : error instanceof Error ? error : new Error ( String ( error ) )
149+ } ;
91150 }
92151 }
93152
153+ /**
154+ * Reads cached events from disk
155+ * Returns empty array if no cache exists or on read error
156+ */
94157 private async readCache ( ) : Promise < BaseEvent [ ] > {
95158 try {
96159 const data = await fs . readFile ( CACHE_FILE , 'utf-8' ) ;
97- return JSON . parse ( data ) ;
160+ return JSON . parse ( data ) as BaseEvent [ ] ;
98161 } catch ( error ) {
99- if ( ( error as NodeJS . ErrnoException ) . code !== 'ENOENT' ) {
100- logger . warning ( mongoLogId ( 1_000_000 ) , "telemetry" , `Error reading telemetry cache: ${ error } ` ) ;
162+ const typedError = error as TelemetryError ;
163+ if ( typedError . code !== 'ENOENT' ) {
164+ logger . warning (
165+ mongoLogId ( 1_000_000 ) ,
166+ "telemetry" ,
167+ `Error reading telemetry cache: ${ typedError . message } `
168+ ) ;
101169 }
102170 return [ ] ;
103171 }
104172 }
105173
174+ /**
175+ * Caches events to disk for later sending
176+ */
106177 private async cacheEvents ( events : BaseEvent [ ] ) : Promise < void > {
107178 try {
108179 await fs . writeFile ( CACHE_FILE , JSON . stringify ( events , null , 2 ) ) ;
109- logger . debug ( mongoLogId ( 1_000_000 ) , "telemetry" , `Cached ${ events . length } events for later sending` ) ;
180+ logger . debug (
181+ mongoLogId ( 1_000_000 ) ,
182+ "telemetry" ,
183+ `Cached ${ events . length } events for later sending`
184+ ) ;
110185 } catch ( error ) {
111- logger . warning ( mongoLogId ( 1_000_000 ) , "telemetry" , `Failed to cache telemetry events: ${ error } ` ) ;
186+ logger . warning (
187+ mongoLogId ( 1_000_000 ) ,
188+ "telemetry" ,
189+ `Failed to cache telemetry events: ${ error instanceof Error ? error . message : String ( error ) } `
190+ ) ;
112191 }
113192 }
114193
194+ /**
195+ * Clears the event cache after successful sending
196+ */
115197 private async clearCache ( ) : Promise < void > {
116198 try {
117199 await fs . unlink ( CACHE_FILE ) ;
118200 } catch ( error ) {
119- if ( ( error as NodeJS . ErrnoException ) . code !== 'ENOENT' ) {
120- logger . warning ( mongoLogId ( 1_000_000 ) , "telemetry" , `Error clearing telemetry cache: ${ error } ` ) ;
201+ const typedError = error as TelemetryError ;
202+ if ( typedError . code !== 'ENOENT' ) {
203+ logger . warning (
204+ mongoLogId ( 1_000_000 ) ,
205+ "telemetry" ,
206+ `Error clearing telemetry cache: ${ typedError . message } `
207+ ) ;
121208 }
122209 }
123210 }
0 commit comments