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

mongodb-js / mongodb-mcp-server / 25663339143

11 May 2026 09:58AM UTC coverage: 79.681% (-2.3%) from 81.951%
25663339143

push

github

web-flow
chore: skip test execution on Windows runners (#1173)

1791 of 2500 branches covered (71.64%)

Branch coverage included in aggregate %.

3362 of 3967 relevant lines covered (84.75%)

162.56 hits per line

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

75.95
/src/setup/setupTelemetry.ts
1
import { randomUUID } from "crypto";
2
import { ApiClient } from "../common/atlas/apiClient.js";
3
import type { Keychain } from "../common/keychain.js";
4
import { NullLogger } from "../common/logging/index.js";
5
import { DeviceId } from "../helpers/deviceId.js";
6
import { Telemetry } from "../telemetry/telemetry.js";
7
import type { SkillsInstallOutcome } from "./installSkills.js";
8
import type {
9
    SetupStage,
10
    SetupEvent,
11
    SetupEventProperties,
12
    TelemetryBoolSet,
13
    TelemetryResult,
14
} from "../telemetry/types.js";
15

16
/**
17
 * Context accumulated as the user progresses through the setup wizard. Each
18
 * step adds to it, and every emitted event carries the full context so each
19
 * event is independently queryable downstream.
20
 */
21
export type SetupTelemetryContext = Omit<
22
    SetupEventProperties,
23
    "stage" | "setup_session_id" | "last_step" | "error_type" | "total_duration_ms"
24
>;
25

26
export const toBoolSet = (value: boolean | undefined): TelemetryBoolSet | undefined => {
1✔
27
    if (value === undefined) {
10!
28
        return undefined;
×
29
    }
30

31
    return value ? "true" : "false";
10✔
32
};
33

34
/**
35
 * Per-run helper that owns the setup telemetry session: assigns the
36
 * `setup_session_id`, tracks wall-clock durations, and
37
 * accumulates context so every event carries the full set of known flags.
38
 *
39
 * One instance is constructed per `runSetup` invocation. Callers emit typed
40
 * events at each logical step and call {@link flush} before the process
41
 * exits so buffered events are best-effort sent.
42
 */
43
export class SetupTelemetry {
44
    private readonly setupSessionId: string = randomUUID();
13✔
45
    private readonly startedAt: number = Date.now();
13✔
46
    private stepStartedAt: number = this.startedAt;
13✔
47
    private lastStep: SetupStage | undefined;
48
    private context: SetupTelemetryContext = {};
13✔
49

50
    /**
51
     * Builds a fully-wired {@link SetupTelemetry} for the setup CLI: a silent
52
     * logger (so telemetry's internal logging doesn't leak into the
53
     * interactive wizard), a fresh {@link DeviceId}, an unauthenticated
54
     * {@link ApiClient}, and a {@link Telemetry} instance.
55
     */
56
    public static create(
57
        config: { apiBaseUrl: string; telemetry: "enabled" | "disabled" },
58
        keychain: Keychain
59
    ): SetupTelemetry {
60
        const logger = new NullLogger();
×
61
        const deviceId = DeviceId.create(logger);
×
62
        const apiClient = new ApiClient({ baseUrl: config.apiBaseUrl }, logger);
×
63
        const telemetry = Telemetry.create({
×
64
            logger,
65
            deviceId,
66
            apiClient,
67
            keychain,
68
            enabled: config.telemetry === "enabled",
69
        });
70
        return new SetupTelemetry(telemetry, deviceId);
×
71
    }
72

73
    /**
74
     * Direct construction is primarily for tests that want to inject a mock
75
     * telemetry pipeline. Production code should use {@link SetupTelemetry.create}.
76
     */
77
    public constructor(
78
        private readonly telemetry: Telemetry,
13✔
79
        private readonly deviceId: DeviceId
13✔
80
    ) {}
81

82
    /**
83
     * Merges new context values into the accumulated context. Subsequent
84
     * events will automatically carry the updated values.
85
     */
86
    public updateContext(patch: Partial<SetupTelemetryContext>): void {
87
        this.context = { ...this.context, ...patch };
13✔
88
    }
89

90
    /**
91
     * Emits a single setup event. `duration_ms` is computed from the time
92
     * elapsed since the previous step (or setup start), and `result`
93
     * defaults to "success" — callers pass "failure" only when the step's
94
     * own code path failed (e.g. writing the editor config threw).
95
     */
96
    private emit(
97
        stage: SetupStage,
98
        extra: Partial<SetupEventProperties> = {},
26✔
99
        result: TelemetryResult = "success"
26✔
100
    ): void {
101
        const now = Date.now();
26✔
102
        const event: SetupEvent = {
26✔
103
            timestamp: new Date(now).toISOString(),
104
            source: "mdbmcp",
105
            properties: {
106
                component: "setup",
107
                category: "setup",
108
                duration_ms: now - this.stepStartedAt,
109
                result,
110
                stage,
111
                setup_session_id: this.setupSessionId,
112
                ...this.context,
113
                ...extra,
114
            },
115
        };
116

117
        this.telemetry.emitEvents([event]);
26✔
118

119
        this.stepStartedAt = now;
26✔
120
        this.lastStep = stage;
26✔
121
    }
122

123
    public emitStarted(): void {
124
        this.emit("started");
8✔
125
    }
126

127
    public emitPrerequisitesChecked(props: { nodeVersionOk: boolean; hasDocker?: boolean }): void {
128
        this.updateContext({
1✔
129
            node_version_ok: toBoolSet(props.nodeVersionOk),
130
            has_docker: toBoolSet(props.hasDocker),
131
        });
132
        this.emit("prerequisites_checked");
1✔
133
    }
134

135
    public emitAiToolSelected(aiTool: string): void {
136
        this.updateContext({ ai_tool: aiTool });
3✔
137
        this.emit("ai_tool_selected");
3✔
138
    }
139

140
    public emitReadOnlySelected(isReadOnly: boolean): void {
141
        this.updateContext({ read_only_mode: toBoolSet(isReadOnly) });
1✔
142
        this.emit("read_only_selected");
1✔
143
    }
144

145
    public emitConnectionStringEntered(props: {
146
        provided: boolean;
147
        tested: boolean;
148
        attempts: number;
149
        testResult?: TelemetryResult;
150
    }): void {
151
        this.updateContext({
3✔
152
            connection_string_provided: toBoolSet(props.provided),
153
            connection_string_tested: toBoolSet(props.tested),
154
            connection_test_attempts: props.attempts,
155
        });
156
        // If the user tested their connection string, surface the final
157
        // test result on this step event (success/failure). If they skipped
158
        // the test, the step itself still "succeeded" — the user chose not
159
        // to validate — so we default to success.
160
        this.emit("connection_string_entered", {}, props.testResult ?? "success");
3✔
161
    }
162

163
    public emitServiceAccountIdEntered(provided: boolean): void {
164
        this.updateContext({ service_account_id_provided: toBoolSet(provided) });
×
165
        this.emit("service_account_id_entered");
×
166
    }
167

168
    public emitServiceAccountSecretEntered(provided: boolean): void {
169
        this.updateContext({ service_account_secret_provided: toBoolSet(provided) });
×
170
        this.emit("service_account_secret_entered");
×
171
    }
172

173
    public emitCredentialsValidated(): void {
174
        this.emit("credentials_validated");
×
175
    }
176

177
    public emitEditorConfigured(props: {
178
        usedDefaultConfigPath: boolean;
179
        result: TelemetryResult;
180
        error?: unknown;
181
    }): void {
182
        this.updateContext({
1✔
183
            used_default_config_path: toBoolSet(props.usedDefaultConfigPath),
184
        });
185
        this.emit("editor_configured", props.error ? { error_type: errorName(props.error) } : {}, props.result);
1!
186
    }
187

188
    public emitSkillsInstallPrompted(outcome: SkillsInstallOutcome): void {
189
        const patch: Partial<SetupEventProperties> = { skills_install_status: outcome.status };
4✔
190
        if (outcome.status === "skipped") {
4✔
191
            patch.skills_skip_reason = outcome.reason;
1✔
192
        } else if (outcome.status === "failed") {
3✔
193
            patch.skills_install_exit_code = outcome.exitCode;
1✔
194
        }
195
        this.updateContext(patch);
4✔
196
        this.emit("skills_install_prompted");
4✔
197
    }
198

199
    public emitOpenConfigPrompted(props: { opened: boolean; result: TelemetryResult; error?: unknown }): void {
200
        this.updateContext({ opened_config_file: toBoolSet(props.opened) });
×
201
        this.emit("open_config_prompted", props.error ? { error_type: errorName(props.error) } : {}, props.result);
×
202
    }
203

204
    public emitCompleted(): void {
205
        this.emit("completed", { total_duration_ms: Date.now() - this.startedAt });
3✔
206
    }
207

208
    /**
209
     * Emits a cancellation event (e.g. the user hit Ctrl+C). The `result` is
210
     * "success" because the cancellation itself was handled gracefully — the
211
     * distinct `stage: "cancelled"` is what analytics use to separate
212
     * abandoned runs from completed ones.
213
     */
214
    public emitCancelled(): void {
215
        this.emit("cancelled", {
1✔
216
            last_stage: this.lastStep,
217
            total_duration_ms: Date.now() - this.startedAt,
218
        });
219
    }
220

221
    public emitFailed(error: unknown): void {
222
        this.emit(
1✔
223
            "failed",
224
            {
225
                last_stage: this.lastStep,
226
                error_type: errorName(error),
227
                total_duration_ms: Date.now() - this.startedAt,
228
            },
229
            "failure"
230
        );
231
    }
232

233
    /**
234
     * Best-effort flush of any buffered events before the process exits. Also
235
     * closes the owned {@link DeviceId}.
236
     */
237
    public async flush(): Promise<void> {
238
        try {
1✔
239
            await this.telemetry.close();
1✔
240
        } catch {
241
            // Ignore errors from telemetry.close()
242
        } finally {
243
            try {
1✔
244
                this.deviceId.close();
1✔
245
            } catch {
246
                // Ignore errors - it's best-effort
247
            }
248
        }
249
    }
250
}
251

252
const errorName = (error: unknown): string => {
1✔
253
    if (error && typeof error === "object" && "name" in error && typeof error.name === "string") {
2!
254
        return error.name;
2✔
255
    }
256
    return "unknown";
×
257
};
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

© 2026 Coveralls, Inc