@@ -5,119 +5,206 @@ import config from '../config.js';
5
5
import logger from '../logger.js' ;
6
6
import { mongoLogId } from 'mongodb-log-writer' ;
7
7
import { ApiClient } from '../common/atlas/apiClient.js' ;
8
- import { ApiClientError } from '../common/atlas/apiClientError.js' ;
9
8
import fs from 'fs/promises' ;
10
9
import path from 'path' ;
11
10
12
- const isTelemetryEnabled = config . telemetry === 'enabled ';
11
+ const TELEMETRY_ENABLED = config . telemetry !== 'disabled ';
13
12
const CACHE_FILE = path . join ( process . cwd ( ) , '.telemetry-cache.json' ) ;
14
13
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
+
15
36
export class Telemetry {
16
- constructor ( private readonly session : Session ) { }
37
+ private readonly commonProperties : CommonProperties ;
17
38
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 ,
19
43
mcp_server_version : pkg . version ,
20
44
mcp_server_name : config . mcpServerName ,
21
45
mcp_client_version : this . session . agentClientVersion ,
22
46
mcp_client_name : this . session . agentClientName ,
23
- session_id : this . session . sessionId ,
24
- device_id : config . device_id ,
25
47
platform : config . platform ,
26
48
arch : config . arch ,
27
49
os_type : config . os_type ,
28
50
os_version : config . os_version ,
29
- } ;
51
+ } ) ;
52
+ }
30
53
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." ) ;
34
71
return ;
35
72
}
36
73
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 {
37
88
const duration = Date . now ( ) - startTime ;
38
89
39
90
const event : ToolEvent = {
40
91
timestamp : new Date ( ) . toISOString ( ) ,
41
92
source : 'mdbmcp' ,
42
93
properties : {
43
94
...this . commonProperties ,
44
- command : command ,
45
- category : category ,
95
+ command,
96
+ category,
46
97
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
+ } )
48
104
}
49
105
} ;
50
106
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 ;
57
108
}
58
109
110
+ /**
111
+ * Attempts to emit events through authenticated and unauthenticated clients
112
+ * Falls back to caching if both attempts fail
113
+ */
59
114
private async emit ( events : BaseEvent [ ] ) : Promise < void > {
60
- // First try to read any cached events
61
115
const cachedEvents = await this . readCache ( ) ;
62
116
const allEvents = [ ...cachedEvents , ...events ] ;
63
117
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
+ ) ;
65
123
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 ;
77
128
}
78
129
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 > {
80
142
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 } ;
87
145
} 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
+ } ;
91
150
}
92
151
}
93
152
153
+ /**
154
+ * Reads cached events from disk
155
+ * Returns empty array if no cache exists or on read error
156
+ */
94
157
private async readCache ( ) : Promise < BaseEvent [ ] > {
95
158
try {
96
159
const data = await fs . readFile ( CACHE_FILE , 'utf-8' ) ;
97
- return JSON . parse ( data ) ;
160
+ return JSON . parse ( data ) as BaseEvent [ ] ;
98
161
} 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
+ ) ;
101
169
}
102
170
return [ ] ;
103
171
}
104
172
}
105
173
174
+ /**
175
+ * Caches events to disk for later sending
176
+ */
106
177
private async cacheEvents ( events : BaseEvent [ ] ) : Promise < void > {
107
178
try {
108
179
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
+ ) ;
110
185
} 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
+ ) ;
112
191
}
113
192
}
114
193
194
+ /**
195
+ * Clears the event cache after successful sending
196
+ */
115
197
private async clearCache ( ) : Promise < void > {
116
198
try {
117
199
await fs . unlink ( CACHE_FILE ) ;
118
200
} 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
+ ) ;
121
208
}
122
209
}
123
210
}
0 commit comments