• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mongodb-js / mongodb-mcp-server / 16056820806

03 Jul 2025 05:24PM UTC coverage: 73.126% (-1.2%) from 74.286%
16056820806

Pull #339

github

web-flow
Merge 1ea261a8c into d7d4aa9ae
Pull Request #339: chore: reinstate telemetry/docker change after revert (MCP-49)

231 of 394 branches covered (58.63%)

Branch coverage included in aggregate %.

17 of 21 new or added lines in 1 file covered. (80.95%)

15 existing lines in 1 file now uncovered.

803 of 1020 relevant lines covered (78.73%)

58.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

67.78
/src/telemetry/telemetry.ts
1
import { Session } from "../session.js";
2
import { BaseEvent, CommonProperties } from "./types.js";
3
import { UserConfig } from "../config.js";
4
import logger, { LogId } from "../logger.js";
5
import { ApiClient } from "../common/atlas/apiClient.js";
6
import { MACHINE_METADATA } from "./constants.js";
7
import { EventCache } from "./eventCache.js";
8
import nodeMachineId from "node-machine-id";
9
import { getDeviceId } from "@mongodb-js/device-id";
10
import fs from "fs/promises";
11

12
type EventResult = {
13
    success: boolean;
14
    error?: Error;
15
};
16

17
export const DEVICE_ID_TIMEOUT = 3000;
36✔
18

19
export class Telemetry {
20
    private isBufferingEvents: boolean = true;
46✔
21
    /** Resolves when the device ID is retrieved or timeout occurs */
22
    public dataPromise: Promise<[string, boolean]> | undefined;
23
    private deviceIdAbortController = new AbortController();
46✔
24
    private eventCache: EventCache;
25
    private getRawMachineId: () => Promise<string>;
26

27
    private constructor(
28
        private readonly session: Session,
46✔
29
        private readonly userConfig: UserConfig,
46✔
30
        private readonly commonProperties: CommonProperties,
46✔
31
        { eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
32
    ) {
33
        this.eventCache = eventCache;
46✔
34
        this.getRawMachineId = getRawMachineId;
46✔
35
    }
36

37
    static create(
38
        session: Session,
39
        userConfig: UserConfig,
40
        {
34✔
41
            commonProperties = { ...MACHINE_METADATA },
46✔
42
            eventCache = EventCache.getInstance(),
37✔
43
            getRawMachineId = () => nodeMachineId.machineId(true),
1✔
44
        }: {
45
            eventCache?: EventCache;
46
            getRawMachineId?: () => Promise<string>;
47
            commonProperties?: CommonProperties;
48
        } = {}
49
    ): Telemetry {
50
        const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId });
46✔
51

52
        void instance.start();
46✔
53
        return instance;
46✔
54
    }
55

56
    private async isContainerEnv(): Promise<boolean> {
57
        if (process.platform !== "linux") {
13!
NEW
58
            return false; // we only support linux containers for now
×
59
        }
60

61
        if (process.env.container) {
13!
NEW
62
            return true;
×
63
        }
64

65
        const exists = await Promise.all(["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"].map(async (file) => {
13✔
66
            try {
39✔
67
                await fs.access(file);
39✔
NEW
68
                return true;
×
69
            } catch {
70
                return false;
33✔
71
            }
72
        }));
73

74
        return exists.includes(true);
11✔
75
    }
76

77
    private async start(): Promise<void> {
78
        if (!this.isTelemetryEnabled()) {
46✔
79
            return;
33✔
80
        }
81
        this.dataPromise = Promise.all([getDeviceId({
13✔
82
            getMachineId: () => this.getRawMachineId(),
13✔
83
            onError: (reason, error) => {
84
                switch (reason) {
2!
85
                    case "resolutionError":
86
                        logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
1✔
87
                        break;
1✔
88
                    case "timeout":
89
                        logger.debug(LogId.telemetryDeviceIdTimeout, "telemetry", "Device ID retrieval timed out");
1✔
90
                        break;
1✔
91
                    case "abort":
92
                        // No need to log in the case of aborts
NEW
93
                        break;
×
94
                }
95
            },
96
            abortSignal: this.deviceIdAbortController.signal,
97
        }), this.isContainerEnv()]);
98

99
        const [deviceId, containerEnv] = await this.dataPromise;
13✔
100

101
        this.commonProperties.device_id = deviceId;
11✔
102
        this.commonProperties.is_container_env = containerEnv;
11✔
103

104
        this.isBufferingEvents = false;
11✔
105
    }
106

107
    public async close(): Promise<void> {
108
        this.deviceIdAbortController.abort();
33✔
109
        this.isBufferingEvents = false;
33✔
110
        await this.emitEvents(this.eventCache.getEvents());
33✔
111
    }
112

113
    /**
114
     * Emits events through the telemetry pipeline
115
     * @param events - The events to emit
116
     */
117
    public async emitEvents(events: BaseEvent[]): Promise<void> {
118
        try {
104✔
119
            if (!this.isTelemetryEnabled()) {
104✔
120
                logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
101✔
121
                return;
101✔
122
            }
123

124
            await this.emit(events);
3✔
125
        } catch {
UNCOV
126
            logger.debug(LogId.telemetryEmitFailure, "telemetry", `Error emitting telemetry events.`);
×
127
        }
128
    }
129

130
    /**
131
     * Gets the common properties for events
132
     * @returns Object containing common properties for all events
133
     */
134
    public getCommonProperties(): CommonProperties {
135
        return {
10✔
136
            ...this.commonProperties,
137
            mcp_client_version: this.session.agentRunner?.version,
138
            mcp_client_name: this.session.agentRunner?.name,
139
            session_id: this.session.sessionId,
140
            config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
10✔
141
            config_connection_string: this.userConfig.connectionString ? "true" : "false",
10!
142
        };
143
    }
144

145
    /**
146
     * Checks if telemetry is currently enabled
147
     * This is a method rather than a constant to capture runtime config changes
148
     *
149
     * Follows the Console Do Not Track standard (https://consoledonottrack.com/)
150
     * by respecting the DO_NOT_TRACK environment variable
151
     */
152
    public isTelemetryEnabled(): boolean {
153
        // Check if telemetry is explicitly disabled in config
154
        if (this.userConfig.telemetry === "disabled") {
416✔
155
            return false;
399✔
156
        }
157

158
        const doNotTrack = "DO_NOT_TRACK" in process.env;
17✔
159
        return !doNotTrack;
17✔
160
    }
161

162
    /**
163
     * Attempts to emit events through authenticated and unauthenticated clients
164
     * Falls back to caching if both attempts fail
165
     */
166
    private async emit(events: BaseEvent[]): Promise<void> {
167
        if (this.isBufferingEvents) {
3✔
168
            this.eventCache.appendEvents(events);
3✔
169
            return;
3✔
170
        }
171

UNCOV
172
        const cachedEvents = this.eventCache.getEvents();
×
173
        const allEvents = [...cachedEvents, ...events];
×
174

UNCOV
175
        logger.debug(
×
176
            LogId.telemetryEmitStart,
177
            "telemetry",
178
            `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
179
        );
180

UNCOV
181
        const result = await this.sendEvents(this.session.apiClient, allEvents);
×
UNCOV
182
        if (result.success) {
×
UNCOV
183
            this.eventCache.clearEvents();
×
UNCOV
184
            logger.debug(
×
185
                LogId.telemetryEmitSuccess,
186
                "telemetry",
187
                `Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
188
            );
UNCOV
189
            return;
×
190
        }
191

UNCOV
192
        logger.debug(
×
193
            LogId.telemetryEmitFailure,
194
            "telemetry",
195
            `Error sending event to client: ${result.error instanceof Error ? result.error.message : String(result.error)}`
×
196
        );
UNCOV
197
        this.eventCache.appendEvents(events);
×
198
    }
199

200
    /**
201
     * Attempts to send events through the provided API client
202
     */
203
    private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> {
UNCOV
204
        try {
×
UNCOV
205
            await client.sendEvents(
×
UNCOV
206
                events.map((event) => ({
×
207
                    ...event,
208
                    properties: { ...this.getCommonProperties(), ...event.properties },
209
                }))
210
            );
UNCOV
211
            return { success: true };
×
212
        } catch (error) {
UNCOV
213
            return {
×
214
                success: false,
215
                error: error instanceof Error ? error : new Error(String(error)),
×
216
            };
217
        }
218
    }
219
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc