Skip to content

Commit 13a0349

Browse files
committed
refactor and address some comments
1 parent d148376 commit 13a0349

File tree

12 files changed

+392
-169
lines changed

12 files changed

+392
-169
lines changed

src/common/connectionManager.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { packageInfo } from "./packageInfo.js";
66
import ConnectionString from "mongodb-connection-string-url";
77
import { MongoClientOptions } from "mongodb";
88
import { ErrorCodes, MongoDBError } from "./errors.js";
9+
import { DeviceIdService } from "../helpers/deviceId.js";
10+
import { AppNameComponents } from "../helpers/connectionOptions.js";
911

1012
export interface AtlasClusterConnectionInfo {
1113
username: string;
@@ -67,11 +69,19 @@ export interface ConnectionManagerEvents {
6769

6870
export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
6971
private state: AnyConnectionState;
72+
private deviceId: DeviceIdService;
73+
private clientName: string;
7074

7175
constructor() {
7276
super();
7377

7478
this.state = { tag: "disconnected" };
79+
this.deviceId = DeviceIdService.getInstance();
80+
this.clientName = "unknown";
81+
}
82+
83+
setClientName(clientName: string): void {
84+
this.clientName = clientName;
7585
}
7686

7787
async connect(settings: ConnectionSettings): Promise<AnyConnectionState> {
@@ -84,14 +94,15 @@ export class ConnectionManager extends EventEmitter<ConnectionManagerEvents> {
8494
let serviceProvider: NodeDriverServiceProvider;
8595
try {
8696
settings = { ...settings };
97+
const appNameComponents: AppNameComponents = {
98+
appName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
99+
deviceId: this.deviceId.getDeviceId(),
100+
clientName: this.clientName,
101+
};
102+
87103
settings.connectionString = await setAppNameParamIfMissing({
88104
connectionString: settings.connectionString,
89-
components: {
90-
appName: `${packageInfo.mcpServerName} ${packageInfo.version}`,
91-
// deviceId: deviceId, //TODO: MCP-68 - get deviceId from session
92-
// clientName: this.clientInfo?.name || "unknown",
93-
clientName: "unknown", //TODO: MCP-68 - get client name from session
94-
},
105+
components: appNameComponents,
95106
});
96107

97108
serviceProvider = await NodeDriverServiceProvider.connect(settings.connectionString, {

src/common/session.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -72,20 +72,22 @@ export class Session extends EventEmitter<SessionEvents> {
7272

7373
setMcpClient(mcpClient: Implementation | undefined): void {
7474
if (!mcpClient) {
75-
this.mcpClient = undefined;
75+
this.connectionManager.setClientName("unknown");
7676
this.logger.debug({
7777
id: LogId.serverMcpClientSet,
7878
context: "session",
7979
message: "MCP client info not found",
8080
});
81-
return;
8281
}
8382

8483
this.mcpClient = {
85-
name: mcpClient.name || "unknown",
86-
version: mcpClient.version || "unknown",
87-
title: mcpClient.title || "unknown",
84+
name: mcpClient?.name || "unknown",
85+
version: mcpClient?.version || "unknown",
86+
title: mcpClient?.title || "unknown",
8887
};
88+
89+
// Set the client name on the connection manager for appName generation
90+
this.connectionManager.setClientName(this.mcpClient.name || "unknown");
8991
}
9092

9193
async disconnect(): Promise<void> {

src/helpers/connectionOptions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
11
import { MongoClientOptions } from "mongodb";
22
import ConnectionString from "mongodb-connection-string-url";
3-
import { getDeviceIdForConnection } from "./deviceId.js";
43

54
export interface AppNameComponents {
65
appName: string;
7-
deviceId?: string;
6+
deviceId?: Promise<string>;
87
clientName?: string;
98
}
109

@@ -30,7 +29,8 @@ export async function setAppNameParamIfMissing({
3029
return connectionStringUrl.toString();
3130
}
3231

33-
const deviceId = components.deviceId || (await getDeviceIdForConnection());
32+
const deviceId = components.deviceId ? await components.deviceId : "unknown";
33+
3434
const clientName = components.clientName || "unknown";
3535

3636
// Build the extended appName format: appName--deviceId--clientName

src/helpers/deviceId.ts

Lines changed: 148 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,156 @@
11
import { getDeviceId } from "@mongodb-js/device-id";
2-
import nodeMachineId from "node-machine-id";
3-
import { LogId, CompositeLogger } from "../common/logger.js";
2+
import { LogId, LoggerBase } from "../common/logger.js";
43

54
export const DEVICE_ID_TIMEOUT = 3000;
65

76
/**
8-
* Retrieves the device ID for telemetry purposes.
9-
* The device ID is generated using the machine ID and additional logic to handle errors.
10-
*
11-
* @returns Promise that resolves to the device ID string
12-
* If an error occurs during retrieval, the function returns "unknown".
13-
*
14-
* @example
15-
* ```typescript
16-
* const deviceId = await getDeviceIdForConnection(logger);
17-
* console.log(deviceId); // Outputs the device ID or "unknown" in case of failure
18-
* ```
7+
* Singleton class for managing device ID retrieval and caching.
8+
* Starts device ID calculation early and is shared across all services.
199
*/
20-
export async function getDeviceIdForConnection(logger: CompositeLogger): Promise<string> {
21-
const controller = new AbortController();
22-
23-
try {
24-
const deviceId = await getDeviceId({
25-
getMachineId: () => nodeMachineId.machineId(true),
26-
onError: (reason, error) => {
27-
switch (reason) {
28-
case "resolutionError":
29-
logger.debug(LogId.telemetryDeviceIdFailure, "deviceId", String(error));
30-
break;
31-
case "timeout":
32-
logger.debug(LogId.telemetryDeviceIdTimeout, "deviceId", "Device ID retrieval timed out");
33-
break;
34-
case "abort":
35-
// No need to log in the case of aborts
36-
break;
37-
}
38-
},
39-
abortSignal: controller.signal,
40-
});
41-
return deviceId;
42-
} catch (error) {
43-
logger.debug(LogId.telemetryDeviceIdFailure, "deviceId", `Failed to get device ID: ${String(error)}`);
44-
return "unknown";
10+
export class DeviceIdService {
11+
private static instance: DeviceIdService | undefined = undefined;
12+
private deviceId: string | undefined = undefined;
13+
private deviceIdPromise: Promise<string> | undefined = undefined;
14+
private abortController: AbortController | undefined = undefined;
15+
private logger: LoggerBase;
16+
private getMachineId: () => Promise<string>;
17+
18+
private constructor(logger: LoggerBase, getMachineId: () => Promise<string>) {
19+
this.logger = logger;
20+
this.getMachineId = getMachineId;
21+
// Start device ID calculation immediately
22+
this.startDeviceIdCalculation();
23+
}
24+
25+
/**
26+
* Initializes the DeviceIdService singleton with a logger.
27+
* A separated init method is used to use a single instance of the logger.
28+
* @param logger - The logger instance to use
29+
* @returns The DeviceIdService instance
30+
*/
31+
public static init(logger: LoggerBase, getMachineId: () => Promise<string>): DeviceIdService {
32+
if (DeviceIdService.instance) {
33+
return DeviceIdService.instance;
34+
}
35+
DeviceIdService.instance = new DeviceIdService(logger, getMachineId);
36+
return DeviceIdService.instance;
37+
}
38+
39+
/**
40+
* Gets the singleton instance of DeviceIdService.
41+
* @returns The DeviceIdService instance
42+
*/
43+
public static getInstance(): DeviceIdService {
44+
if (!DeviceIdService.instance) {
45+
throw Error("DeviceIdService not initialized");
46+
}
47+
return DeviceIdService.instance;
48+
}
49+
50+
/**
51+
* Resets the singleton instance (mainly for testing).
52+
*/
53+
static resetInstance(): void {
54+
DeviceIdService.instance = undefined;
55+
}
56+
57+
/**
58+
* Starts the device ID calculation process.
59+
* This method is called automatically in the constructor.
60+
*/
61+
private startDeviceIdCalculation(): void {
62+
if (this.deviceIdPromise) {
63+
return;
64+
}
65+
66+
this.abortController = new AbortController();
67+
this.deviceIdPromise = this.calculateDeviceId();
68+
}
69+
70+
/**
71+
* Gets the device ID, waiting for the calculation to complete if necessary.
72+
* @returns Promise that resolves to the device ID string
73+
*/
74+
public async getDeviceId(): Promise<string> {
75+
// Return cached value if available
76+
if (this.deviceId !== undefined) {
77+
return this.deviceId;
78+
}
79+
80+
// If calculation is already in progress, wait for it
81+
if (this.deviceIdPromise) {
82+
return this.deviceIdPromise;
83+
}
84+
85+
// If somehow we don't have a promise, raise an error
86+
throw new Error("Failed to get device ID");
87+
}
88+
/**
89+
* Aborts any ongoing device ID calculation.
90+
*/
91+
abortCalculation(): void {
92+
if (this.abortController) {
93+
this.abortController.abort();
94+
this.abortController = undefined;
95+
}
96+
this.deviceIdPromise = undefined;
97+
}
98+
99+
/**
100+
* Internal method that performs the actual device ID calculation.
101+
*/
102+
private async calculateDeviceId(): Promise<string> {
103+
if (!this.abortController) {
104+
throw new Error("Device ID calculation not started");
105+
}
106+
107+
try {
108+
const deviceId = await getDeviceId({
109+
getMachineId: this.getMachineId,
110+
onError: (reason, error) => {
111+
switch (reason) {
112+
case "resolutionError":
113+
this.logger.debug({
114+
id: LogId.telemetryDeviceIdFailure,
115+
context: "deviceId",
116+
message: `Device ID resolution error: ${String(error)}`,
117+
});
118+
break;
119+
case "timeout":
120+
this.logger.debug({
121+
id: LogId.telemetryDeviceIdTimeout,
122+
context: "deviceId",
123+
message: "Device ID retrieval timed out",
124+
});
125+
break;
126+
case "abort":
127+
// No need to log in the case of aborts
128+
break;
129+
}
130+
},
131+
abortSignal: this.abortController.signal,
132+
});
133+
134+
// Cache the result
135+
this.deviceId = deviceId;
136+
return deviceId;
137+
} catch (error) {
138+
// Check if this was an abort error
139+
if (error instanceof Error && error.name === "AbortError") {
140+
throw error; // Re-throw abort errors
141+
}
142+
143+
this.logger.debug({
144+
id: LogId.telemetryDeviceIdFailure,
145+
context: "deviceId",
146+
message: `Failed to get device ID: ${String(error)}`,
147+
});
148+
149+
// Cache the fallback value
150+
this.deviceId = "unknown";
151+
return "unknown";
152+
} finally {
153+
this.abortController = undefined;
154+
}
45155
}
46156
}

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ import { packageInfo } from "./common/packageInfo.js";
4242
import { StdioRunner } from "./transports/stdio.js";
4343
import { StreamableHttpRunner } from "./transports/streamableHttp.js";
4444
import { systemCA } from "@mongodb-js/devtools-proxy-support";
45+
import { DeviceIdService } from "./helpers/deviceId.js";
46+
import nodeMachineId from "node-machine-id";
4547

4648
async function main(): Promise<void> {
4749
systemCA().catch(() => undefined); // load system CA asynchronously as in mongosh
@@ -50,6 +52,7 @@ async function main(): Promise<void> {
5052
assertVersionMode();
5153

5254
const transportRunner = config.transport === "stdio" ? new StdioRunner(config) : new StreamableHttpRunner(config);
55+
const deviceId = DeviceIdService.init(transportRunner.logger, () => nodeMachineId.machineId(true));
5356

5457
const shutdown = (): void => {
5558
transportRunner.logger.info({
@@ -61,6 +64,7 @@ async function main(): Promise<void> {
6164
transportRunner
6265
.close()
6366
.then(() => {
67+
deviceId.abortCalculation();
6468
transportRunner.logger.info({
6569
id: LogId.serverClosed,
6670
context: "server",
@@ -69,6 +73,7 @@ async function main(): Promise<void> {
6973
process.exit(0);
7074
})
7175
.catch((error: unknown) => {
76+
deviceId.abortCalculation();
7277
transportRunner.logger.error({
7378
id: LogId.serverCloseFailure,
7479
context: "server",

src/telemetry/telemetry.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { LogId } from "../common/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 { getDeviceIdForConnection } from "../helpers/deviceId.js";
98
import { detectContainerEnv } from "../helpers/container.js";
9+
import { DeviceIdService } from "../helpers/deviceId.js";
1010

1111
type EventResult = {
1212
success: boolean;
@@ -18,14 +18,16 @@ export class Telemetry {
1818
/** Resolves when the setup is complete or a timeout occurs */
1919
public setupPromise: Promise<[string, boolean]> | undefined;
2020
private eventCache: EventCache;
21+
private deviceId: DeviceIdService;
2122

2223
private constructor(
2324
private readonly session: Session,
2425
private readonly userConfig: UserConfig,
2526
private readonly commonProperties: CommonProperties,
26-
{ eventCache }: { eventCache: EventCache }
27+
{ eventCache, deviceId }: { eventCache: EventCache; deviceId: DeviceIdService }
2728
) {
2829
this.eventCache = eventCache;
30+
this.deviceId = deviceId;
2931
}
3032

3133
static create(
@@ -34,12 +36,14 @@ export class Telemetry {
3436
{
3537
commonProperties = { ...MACHINE_METADATA },
3638
eventCache = EventCache.getInstance(),
39+
deviceId = DeviceIdService.getInstance(),
3740
}: {
3841
eventCache?: EventCache;
42+
deviceId?: DeviceIdService;
3943
commonProperties?: CommonProperties;
4044
} = {}
4145
): Telemetry {
42-
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache });
46+
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, deviceId });
4347

4448
void instance.setup();
4549
return instance;
@@ -49,10 +53,11 @@ export class Telemetry {
4953
if (!this.isTelemetryEnabled()) {
5054
return;
5155
}
52-
this.setupPromise = Promise.all([getDeviceIdForConnection(), detectContainerEnv()]);
53-
const [deviceId, containerEnv] = await this.setupPromise;
5456

55-
this.commonProperties.device_id = deviceId;
57+
this.setupPromise = Promise.all([this.deviceId.getDeviceId(), detectContainerEnv()]);
58+
const [deviceIdValue, containerEnv] = await this.setupPromise;
59+
60+
this.commonProperties.device_id = deviceIdValue;
5661
this.commonProperties.is_container_env = containerEnv;
5762

5863
this.isBufferingEvents = false;

0 commit comments

Comments
 (0)