Skip to content

Commit 80e71af

Browse files
committed
fix
1 parent 7eee5df commit 80e71af

File tree

11 files changed

+109
-40
lines changed

11 files changed

+109
-40
lines changed

package-lock.json

Lines changed: 10 additions & 25 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
"mongodb-log-writer": "^2.4.1",
7070
"mongodb-redact": "^1.1.6",
7171
"mongodb-schema": "^12.6.2",
72-
"native-machine-id": "^0.1.0",
72+
"node-machine-id": "1.1.12",
7373
"openapi-fetch": "^0.13.5",
7474
"simple-oauth2": "^5.1.0",
7575
"yargs-parser": "^21.1.1",

src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { config } from "./config.js";
77
import { Session } from "./session.js";
88
import { Server } from "./server.js";
99
import { packageInfo } from "./packageInfo.js";
10+
import { Telemetry } from "./telemetry/telemetry.js";
1011

1112
try {
1213
const session = new Session({
@@ -19,9 +20,12 @@ try {
1920
version: packageInfo.version,
2021
});
2122

23+
const telemetry = Telemetry.create(session);
24+
2225
const server = new Server({
2326
mcpServer,
2427
session,
28+
telemetry,
2529
userConfig: config,
2630
});
2731

src/logger.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export const LogId = {
1717
telemetryEmitFailure: mongoLogId(1_002_002),
1818
telemetryEmitStart: mongoLogId(1_002_003),
1919
telemetryEmitSuccess: mongoLogId(1_002_004),
20+
telemetryMachineIdFailure: mongoLogId(1_002_005),
2021

2122
toolExecute: mongoLogId(1_003_001),
2223
toolExecuteFailure: mongoLogId(1_003_002),

src/server.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export interface ServerOptions {
1616
session: Session;
1717
userConfig: UserConfig;
1818
mcpServer: McpServer;
19+
telemetry: Telemetry;
1920
}
2021

2122
export class Server {
@@ -25,10 +26,10 @@ export class Server {
2526
public readonly userConfig: UserConfig;
2627
private readonly startTime: number;
2728

28-
constructor({ session, mcpServer, userConfig }: ServerOptions) {
29+
constructor({ session, mcpServer, userConfig, telemetry }: ServerOptions) {
2930
this.startTime = Date.now();
3031
this.session = session;
31-
this.telemetry = new Telemetry(session);
32+
this.telemetry = telemetry;
3233
this.mcpServer = mcpServer;
3334
this.userConfig = userConfig;
3435
}

src/telemetry/constants.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
1-
import { getMachineIdSync } from "native-machine-id";
21
import { packageInfo } from "../packageInfo.js";
32
import { type CommonStaticProperties } from "./types.js";
43
/**
54
* Machine-specific metadata formatted for telemetry
65
*/
76
export const MACHINE_METADATA: CommonStaticProperties = {
8-
device_id: getMachineIdSync(),
97
mcp_server_version: packageInfo.version,
108
mcp_server_name: packageInfo.mcpServerName,
119
platform: process.platform,

src/telemetry/eventCache.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { BaseEvent } from "./types.js";
21
import { LRUCache } from "lru-cache";
2+
import { BaseEvent } from "./types.js";
33

44
/**
55
* Singleton class for in-memory telemetry event caching

src/telemetry/telemetry.ts

Lines changed: 75 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,22 +5,85 @@ import logger, { LogId } from "../logger.js";
55
import { ApiClient } from "../common/atlas/apiClient.js";
66
import { MACHINE_METADATA } from "./constants.js";
77
import { EventCache } from "./eventCache.js";
8+
import { createHmac } from "crypto";
9+
import { machineId } from "node-machine-id";
810

911
type EventResult = {
1012
success: boolean;
1113
error?: Error;
1214
};
1315

1416
export class Telemetry {
15-
private readonly commonProperties: CommonProperties;
17+
private isBufferingEvents: boolean = true;
18+
private resolveDeviceId: (deviceId: string) => void = () => {};
1619

17-
constructor(
20+
private constructor(
1821
private readonly session: Session,
19-
private readonly eventCache: EventCache = EventCache.getInstance()
20-
) {
21-
this.commonProperties = {
22-
...MACHINE_METADATA,
23-
};
22+
private readonly commonProperties: CommonProperties,
23+
private readonly eventCache: EventCache
24+
) {}
25+
26+
static create(
27+
session: Session,
28+
commonProperties: CommonProperties = MACHINE_METADATA,
29+
eventCache: EventCache = EventCache.getInstance()
30+
): Telemetry {
31+
const instance = new Telemetry(session, commonProperties, eventCache);
32+
33+
void instance.start();
34+
return instance;
35+
}
36+
37+
private async start(): Promise<void> {
38+
this.commonProperties.device_id = await this.getDeviceId();
39+
40+
this.isBufferingEvents = false;
41+
await this.emitEvents(this.eventCache.getEvents());
42+
}
43+
44+
public async close(): Promise<void> {
45+
this.resolveDeviceId("unknown");
46+
this.isBufferingEvents = false;
47+
await this.emitEvents(this.eventCache.getEvents());
48+
}
49+
50+
private async machineIdWithTimeout(): Promise<string> {
51+
try {
52+
return Promise.race<string>([
53+
machineId(true),
54+
new Promise<string>((resolve, reject) => {
55+
this.resolveDeviceId = resolve;
56+
setTimeout(() => {
57+
reject(new Error("Timeout getting machine ID"));
58+
}, 3000);
59+
}),
60+
]);
61+
} catch (error) {
62+
logger.debug(LogId.telemetryMachineIdFailure, "telemetry", `Error getting machine ID: ${String(error)}`);
63+
return "unknown";
64+
}
65+
}
66+
67+
/**
68+
* @returns A hashed, unique identifier for the running device or `undefined` if not known.
69+
*/
70+
private async getDeviceId(): Promise<string> {
71+
if (this.commonProperties.device_id) {
72+
return this.commonProperties.device_id;
73+
}
74+
75+
// Create a hashed format from the all uppercase version of the machine ID
76+
// to match it exactly with the denisbrodbeck/machineid library that Atlas CLI uses.
77+
78+
const originalId = (await this.machineIdWithTimeout()).toUpperCase();
79+
80+
const hmac = createHmac("sha256", originalId);
81+
82+
/** This matches the message used to create the hashes in Atlas CLI */
83+
const DEVICE_ID_HASH_MESSAGE = "atlascli";
84+
85+
hmac.update(DEVICE_ID_HASH_MESSAGE);
86+
return hmac.digest("hex");
2487
}
2588

2689
/**
@@ -84,6 +147,11 @@ export class Telemetry {
84147
* Falls back to caching if both attempts fail
85148
*/
86149
private async emit(events: BaseEvent[]): Promise<void> {
150+
if (this.isBufferingEvents) {
151+
this.eventCache.appendEvents(events);
152+
return;
153+
}
154+
87155
const cachedEvents = this.eventCache.getEvents();
88156
const allEvents = [...cachedEvents, ...events];
89157

src/telemetry/types.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ export type ServerEvent = TelemetryEvent<ServerEventProperties>;
5353
* Interface for static properties, they can be fetched once and reused.
5454
*/
5555
export type CommonStaticProperties = {
56-
device_id?: string;
5756
mcp_server_version: string;
5857
mcp_server_name: string;
5958
platform: string;

tests/integration/helpers.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { UserConfig } from "../../src/config.js";
55
import { McpError } from "@modelcontextprotocol/sdk/types.js";
66
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
77
import { Session } from "../../src/session.js";
8+
import { Telemetry } from "../../src/telemetry/telemetry.js";
89

910
interface ParameterInfo {
1011
name: string;
@@ -52,9 +53,13 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
5253
});
5354

5455
userConfig.telemetry = "disabled";
56+
57+
const telemetry = Telemetry.create(session);
58+
5559
mcpServer = new Server({
5660
session,
5761
userConfig,
62+
telemetry,
5863
mcpServer: new McpServer({
5964
name: "test-server",
6065
version: "1.2.3",

tests/unit/telemetry.test.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Telemetry } from "../../src/telemetry/telemetry.js";
44
import { BaseEvent, TelemetryResult } from "../../src/telemetry/types.js";
55
import { EventCache } from "../../src/telemetry/eventCache.js";
66
import { config } from "../../src/config.js";
7+
import { MACHINE_METADATA } from "../../src/telemetry/constants.js";
78

89
// Mock the ApiClient to avoid real API calls
910
jest.mock("../../src/common/atlas/apiClient.js");
@@ -113,7 +114,14 @@ describe("Telemetry", () => {
113114
} as unknown as Session;
114115

115116
// Create the telemetry instance with mocked dependencies
116-
telemetry = new Telemetry(session, mockEventCache);
117+
telemetry = Telemetry.create(
118+
session,
119+
{
120+
...MACHINE_METADATA,
121+
device_id: "test-device-id",
122+
},
123+
mockEventCache
124+
);
117125

118126
config.telemetry = "enabled";
119127
});

0 commit comments

Comments
 (0)