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

mongodb-js / mongodb-mcp-server / 15759142556

19 Jun 2025 01:32PM UTC coverage: 73.23%. First build
15759142556

Pull #298

github

web-flow
Merge 814ccb9b3 into 54effbbe4
Pull Request #298: chore: [MCP-2] add is_container_env to telemetry

210 of 378 branches covered (55.56%)

Branch coverage included in aggregate %.

38 of 55 new or added lines in 3 files covered. (69.09%)

783 of 978 relevant lines covered (80.06%)

56.26 hits per line

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

63.27
/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
async function fileExists(filePath: string): Promise<boolean> {
NEW
13
    try {
×
NEW
14
        await fs.access(filePath, fs.constants.F_OK);
×
NEW
15
        return true; // File exists
×
16
    } catch (e: unknown) {
NEW
17
        if (
×
18
            e instanceof Error &&
×
19
            (
20
                e as Error & {
21
                    code: string;
22
                }
23
            ).code === "ENOENT"
24
        ) {
NEW
25
            return false; // File does not exist
×
26
        }
NEW
27
        throw e; // Re-throw unexpected errors
×
28
    }
29
}
30

31
async function isContainerized(): Promise<boolean> {
NEW
32
    if (process.env.container) {
×
NEW
33
        return true;
×
34
    }
35

NEW
36
    const exists = await Promise.all(["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"].map(fileExists));
×
37

NEW
38
    return exists.includes(true);
×
39
}
40

41
export class Telemetry {
42
    private deviceIdAbortController = new AbortController();
41✔
43
    private eventCache: EventCache;
44
    private getRawMachineId: () => Promise<string>;
45
    private getContainerEnv: () => Promise<boolean>;
46
    private cachedCommonProperties?: CommonProperties;
47
    private flushing: boolean = false;
41✔
48

49
    private constructor(
50
        private readonly session: Session,
41✔
51
        private readonly userConfig: UserConfig,
41✔
52
        {
53
            eventCache,
54
            getRawMachineId,
55
            getContainerEnv,
56
        }: {
57
            eventCache: EventCache;
58
            getRawMachineId: () => Promise<string>;
59
            getContainerEnv: () => Promise<boolean>;
60
        }
61
    ) {
62
        this.eventCache = eventCache;
41✔
63
        this.getRawMachineId = getRawMachineId;
41✔
64
        this.getContainerEnv = getContainerEnv;
41✔
65
    }
66

67
    static create(
68
        session: Session,
69
        userConfig: UserConfig,
70
        {
30✔
71
            eventCache = EventCache.getInstance(),
30✔
72
            getRawMachineId = () => nodeMachineId.machineId(true),
✔
73
            getContainerEnv = isContainerized,
30✔
74
        }: {
75
            eventCache?: EventCache;
76
            getRawMachineId?: () => Promise<string>;
77
            getContainerEnv?: () => Promise<boolean>;
78
        } = {}
79
    ): Telemetry {
80
        const instance = new Telemetry(session, userConfig, {
41✔
81
            eventCache,
82
            getRawMachineId,
83
            getContainerEnv,
84
        });
85

86
        return instance;
41✔
87
    }
88

89
    public async close(): Promise<void> {
90
        this.deviceIdAbortController.abort();
30✔
91
        await this.flush();
30✔
92
    }
93

94
    /**
95
     * Emits events through the telemetry pipeline
96
     * @param events - The events to emit
97
     */
98
    public emitEvents(events: BaseEvent[]): void {
99
        void this.flush(events);
69✔
100
    }
101

102
    /**
103
     * Gets the common properties for events
104
     * @returns Object containing common properties for all events
105
     */
106
    private async getCommonProperties(): Promise<CommonProperties> {
107
        if (!this.cachedCommonProperties) {
7✔
108
            let deviceId: string | undefined;
109
            try {
7✔
110
                deviceId = await getDeviceId({
7✔
111
                    getMachineId: () => this.getRawMachineId(),
7✔
112
                    onError: (reason, error) => {
113
                        switch (reason) {
2!
114
                            case "resolutionError":
115
                                logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error));
1✔
116
                                break;
1✔
117
                            case "timeout":
118
                                logger.debug(
1✔
119
                                    LogId.telemetryDeviceIdTimeout,
120
                                    "telemetry",
121
                                    "Device ID retrieval timed out"
122
                                );
123
                                break;
1✔
124
                            case "abort":
125
                                // No need to log in the case of aborts
NEW
126
                                break;
×
127
                        }
128
                    },
129
                    abortSignal: this.deviceIdAbortController.signal,
130
                });
131
            } catch (error: unknown) {
NEW
132
                const err = error instanceof Error ? error : new Error(String(error));
×
NEW
133
                logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", err.message);
×
134
            }
135
            let containerEnv: boolean | undefined;
136
            try {
7✔
137
                containerEnv = await this.getContainerEnv();
7✔
138
            } catch (error: unknown) {
NEW
139
                const err = error instanceof Error ? error : new Error(String(error));
×
NEW
140
                logger.debug(LogId.telemetryContainerEnvFailure, "telemetry", err.message);
×
141
            }
142
            this.cachedCommonProperties = {
7✔
143
                ...MACHINE_METADATA,
144
                mcp_client_version: this.session.agentRunner?.version,
145
                mcp_client_name: this.session.agentRunner?.name,
146
                session_id: this.session.sessionId,
147
                config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
7!
148
                config_connection_string: this.userConfig.connectionString ? "true" : "false",
7!
149
                is_container_env: containerEnv ? "true" : "false",
7!
150
                device_id: deviceId,
151
            };
152
        }
153

154
        return this.cachedCommonProperties;
7✔
155
    }
156

157
    /**
158
     * Checks if telemetry is currently enabled
159
     * This is a method rather than a constant to capture runtime config changes
160
     *
161
     * Follows the Console Do Not Track standard (https://consoledonottrack.com/)
162
     * by respecting the DO_NOT_TRACK environment variable
163
     */
164
    public isTelemetryEnabled(): boolean {
165
        // Check if telemetry is explicitly disabled in config
166
        if (this.userConfig.telemetry === "disabled") {
331✔
167
            return false;
323✔
168
        }
169

170
        const doNotTrack = "DO_NOT_TRACK" in process.env;
8✔
171
        return !doNotTrack;
8✔
172
    }
173

174
    /**
175
     * Attempts to flush events through authenticated and unauthenticated clients
176
     * Falls back to caching if both attempts fail
177
     */
178
    public async flush(events?: BaseEvent[]): Promise<void> {
179
        if (!this.isTelemetryEnabled()) {
99✔
180
            logger.info(LogId.telemetryEmitFailure, "telemetry", `Telemetry is disabled.`);
92✔
181
            return;
92✔
182
        }
183

184
        if (this.flushing) {
7!
NEW
185
            this.eventCache.appendEvents(events ?? []);
×
186
            return;
×
187
        }
188

189
        this.flushing = true;
7✔
190

191
        try {
7✔
192
            const cachedEvents = this.eventCache.getEvents();
7✔
193
            const allEvents = [...cachedEvents, ...(events ?? [])];
7!
194

195
            logger.debug(
7✔
196
                LogId.telemetryEmitStart,
197
                "telemetry",
198
                `Attempting to send ${allEvents.length} events (${cachedEvents.length} cached)`
199
            );
200

201
            await this.sendEvents(this.session.apiClient, allEvents);
7✔
202
            this.eventCache.clearEvents();
6✔
203
            logger.debug(
6✔
204
                LogId.telemetryEmitSuccess,
205
                "telemetry",
206
                `Sent ${allEvents.length} events successfully: ${JSON.stringify(allEvents, null, 2)}`
207
            );
208
        } catch (error: unknown) {
209
            logger.debug(
1✔
210
                LogId.telemetryEmitFailure,
211
                "telemetry",
212
                `Error sending event to client: ${error instanceof Error ? error.message : String(error)}`
1!
213
            );
214
            this.eventCache.appendEvents(events ?? []);
1!
215
        }
216

217
        this.flushing = false;
7✔
218
    }
219

220
    /**
221
     * Attempts to send events through the provided API client
222
     */
223
    private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<void> {
224
        const commonProperties = await this.getCommonProperties();
7✔
225

226
        await client.sendEvents(
7✔
227
            events.map((event) => ({
8✔
228
                ...event,
229
                properties: { ...commonProperties, ...event.properties },
230
            }))
231
        );
232
    }
233
}
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