- 
                Notifications
    You must be signed in to change notification settings 
- Fork 153
feat: core telemetry functionality #87
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 10 commits
9cf0447
              1b8ea02
              af714b4
              40aaef0
              8adc20d
              cff01e0
              5336e4a
              76e2a6f
              1850676
              3a5d4c1
              23fff05
              754ce8d
              8301e43
              994b698
              3bfc9f2
              6d92021
              7997c54
              f18b916
              a9cd835
              6813518
              65fad1d
              7e98c33
              5209073
              2e33584
              d92adf1
              8ec7d9c
              6ad2a19
              807109d
              f9a46f9
              599201d
              7a889b4
              eb360e2
              710b131
              a15eeb6
              90caa25
              89113a5
              143f898
              f751a30
              0927d28
              fb0b8af
              9e625aa
              a61d9b4
              2bd00cf
              3636fde
              0df870c
              4b7563d
              6dd1f79
              807b2ca
              139c3ee
              3a05d31
              8505d91
              024a3d1
              File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
|  | @@ -11,6 +11,7 @@ interface UserConfig { | |
| apiBaseUrl?: string; | ||
| apiClientId?: string; | ||
| apiClientSecret?: string; | ||
| telemetry?: "enabled" | "disabled"; | ||
|         
                  blva marked this conversation as resolved.
              Show resolved
            Hide resolved | ||
| logPath: string; | ||
| connectionString?: string; | ||
| connectOptions: { | ||
|  | @@ -39,9 +40,20 @@ const mergedUserConfig = { | |
| ...getCliConfig(), | ||
| }; | ||
|  | ||
| const machineMetadata = { | ||
| device_id: "id", // TODO: use @mongodb-js/machine-id | ||
| platform: process.platform, | ||
| arch: process.arch, | ||
| os_type: process.platform, | ||
| os_version: process.version, | ||
| }; | ||
|  | ||
| const config = { | ||
| ...mergedUserConfig, | ||
| ...machineMetadata, | ||
|         
                  blva marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| version: packageJson.version, | ||
| mcpServerName: "MdbMcpServer", | ||
|          | ||
| isTelemetryEnabled: true, | ||
|         
                  blva marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| }; | ||
|  | ||
| export default config; | ||
|         
                  gagik marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
|  | ||
| Original file line number | Diff line number | Diff line change | 
|---|---|---|
| @@ -0,0 +1,203 @@ | ||
| import { Session } from "../session.js"; | ||
| import { BaseEvent, type ToolEvent } from "./types.js"; | ||
| import pkg from "../../package.json" with { type: "json" }; | ||
| import config from "../config.js"; | ||
| import logger from "../logger.js"; | ||
| import { mongoLogId } from "mongodb-log-writer"; | ||
| import { ApiClient } from "../common/atlas/apiClient.js"; | ||
| import fs from "fs/promises"; | ||
| import path from "path"; | ||
|  | ||
| const TELEMETRY_ENABLED = config.telemetry !== "disabled"; | ||
| const CACHE_FILE = path.join(process.cwd(), ".telemetry-cache.json"); | ||
|  | ||
| interface TelemetryError extends Error { | ||
| code?: string; | ||
| } | ||
|  | ||
| type EventResult = { | ||
| success: boolean; | ||
| error?: Error; | ||
| }; | ||
|  | ||
| type CommonProperties = { | ||
| device_id: string; | ||
| mcp_server_version: string; | ||
| mcp_server_name: string; | ||
| mcp_client_version?: string; | ||
| mcp_client_name?: string; | ||
| platform: string; | ||
| arch: string; | ||
| os_type: string; | ||
| os_version?: string; | ||
| session_id?: string; | ||
| }; | ||
|  | ||
| export class Telemetry { | ||
| private readonly commonProperties: CommonProperties; | ||
|  | ||
| constructor(private readonly session: Session) { | ||
| // Ensure all required properties are present | ||
|         
                  blva marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| this.commonProperties = Object.freeze({ | ||
|         
                  blva marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| device_id: config.device_id, | ||
| mcp_server_version: pkg.version, | ||
| mcp_server_name: config.mcpServerName, | ||
| mcp_client_version: this.session.agentClientVersion, | ||
| mcp_client_name: this.session.agentClientName, | ||
| platform: config.platform, | ||
| arch: config.arch, | ||
| os_type: config.os_type, | ||
| os_version: config.os_version, | ||
| }); | ||
| } | ||
|  | ||
| /** | ||
| * Emits a tool event with timing and error information | ||
| * @param command - The command being executed | ||
| * @param category - Category of the command | ||
| * @param startTime - Start time in milliseconds | ||
| * @param result - Whether the command succeeded or failed | ||
| * @param error - Optional error if the command failed | ||
| */ | ||
| public async emitToolEvent( | ||
|         
                  blva marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| command: string, | ||
| category: string, | ||
| startTime: number, | ||
| result: "success" | "failure", | ||
| error?: Error | ||
| ): Promise<void> { | ||
| if (!TELEMETRY_ENABLED) { | ||
| logger.debug(mongoLogId(1_000_000), "telemetry", "Telemetry is disabled, skipping event."); | ||
| return; | ||
| } | ||
|  | ||
| const event = this.createToolEvent(command, category, startTime, result, error); | ||
| await this.emit([event]); | ||
| } | ||
|  | ||
| /** | ||
| * Creates a tool event with common properties and timing information | ||
| */ | ||
| private createToolEvent( | ||
|         
                  blva marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| command: string, | ||
| category: string, | ||
| startTime: number, | ||
| result: "success" | "failure", | ||
| error?: Error | ||
| ): ToolEvent { | ||
| const duration = Date.now() - startTime; | ||
|  | ||
| const event: ToolEvent = { | ||
| timestamp: new Date().toISOString(), | ||
| source: "mdbmcp", | ||
| properties: { | ||
| ...this.commonProperties, | ||
| command, | ||
| category, | ||
| duration_ms: duration, | ||
| session_id: this.session.sessionId, | ||
| result, | ||
| ...(error && { | ||
| error_type: error.name, | ||
| error_code: error.message, | ||
| }), | ||
| }, | ||
| }; | ||
|  | ||
| return event; | ||
| } | ||
|  | ||
| /** | ||
| * Attempts to emit events through authenticated and unauthenticated clients | ||
| * Falls back to caching if both attempts fail | ||
| */ | ||
| private async emit(events: BaseEvent[]): Promise<void> { | ||
| const cachedEvents = await this.readCache(); | ||
| const allEvents = [...cachedEvents, ...events]; | ||
|  | ||
| logger.debug( | ||
| mongoLogId(1_000_000), | ||
| "telemetry", | ||
| `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)` | ||
| ); | ||
|  | ||
| const result = await this.sendEvents(this.session.apiClient, allEvents); | ||
| if (result.success) { | ||
| await this.clearCache(); | ||
| return; | ||
| } | ||
|  | ||
| logger.warning(mongoLogId(1_000_000), "telemetry", `Error sending event to client: ${result.error}`); | ||
| await this.cacheEvents(allEvents); | ||
| } | ||
|  | ||
| /** | ||
| * Attempts to send events through the provided API client | ||
| */ | ||
| private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> { | ||
| try { | ||
| await client.sendEvents(events); | ||
| return { success: true }; | ||
| } catch (error) { | ||
| return { | ||
| success: false, | ||
| error: error instanceof Error ? error : new Error(String(error)), | ||
| }; | ||
| } | ||
| } | ||
|  | ||
| /** | ||
| * Reads cached events from disk | ||
| * Returns empty array if no cache exists or on read error | ||
| */ | ||
| private async readCache(): Promise<BaseEvent[]> { | ||
|         
                  blva marked this conversation as resolved.
              Outdated
          
            Show resolved
            Hide resolved | ||
| try { | ||
| const data = await fs.readFile(CACHE_FILE, "utf-8"); | ||
| return JSON.parse(data) as BaseEvent[]; | ||
| } catch (error) { | ||
| const typedError = error as TelemetryError; | ||
| if (typedError.code !== "ENOENT") { | ||
| logger.warning( | ||
| mongoLogId(1_000_000), | ||
| "telemetry", | ||
| `Error reading telemetry cache: ${typedError.message}` | ||
| ); | ||
| } | ||
| return []; | ||
| } | ||
| } | ||
|  | ||
| /** | ||
| * Caches events to disk for later sending | ||
| */ | ||
| private async cacheEvents(events: BaseEvent[]): Promise<void> { | ||
| try { | ||
| await fs.writeFile(CACHE_FILE, JSON.stringify(events, null, 2)); | ||
| logger.debug(mongoLogId(1_000_000), "telemetry", `Cached ${events.length} events for later sending`); | ||
| } catch (error) { | ||
| logger.warning( | ||
| mongoLogId(1_000_000), | ||
| "telemetry", | ||
| `Failed to cache telemetry events: ${error instanceof Error ? error.message : String(error)}` | ||
| ); | ||
| } | ||
| } | ||
|  | ||
| /** | ||
| * Clears the event cache after successful sending | ||
| */ | ||
| private async clearCache(): Promise<void> { | ||
| try { | ||
| await fs.unlink(CACHE_FILE); | ||
| } catch (error) { | ||
| const typedError = error as TelemetryError; | ||
| if (typedError.code !== "ENOENT") { | ||
| logger.warning( | ||
| mongoLogId(1_000_000), | ||
| "telemetry", | ||
| `Error clearing telemetry cache: ${typedError.message}` | ||
| ); | ||
| } | ||
| } | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.