diff --git a/src/common/session.ts b/src/common/session.ts index 2a75af33..93920b8e 100644 --- a/src/common/session.ts +++ b/src/common/session.ts @@ -102,9 +102,13 @@ export class Session extends EventEmitter { } async connectToMongoDB(connectionString: string, connectOptions: ConnectOptions): Promise { - connectionString = setAppNameParamIfMissing({ + // Use the extended appName format with deviceId and clientName + connectionString = await setAppNameParamIfMissing({ connectionString, - defaultAppName: `${packageInfo.mcpServerName} ${packageInfo.version}`, + components: { + appName: `${packageInfo.mcpServerName} ${packageInfo.version}`, + clientName: this.agentRunner?.name || "unknown", + }, }); try { diff --git a/src/helpers/connectionOptions.ts b/src/helpers/connectionOptions.ts index 10b1ecc8..0d7abf3e 100644 --- a/src/helpers/connectionOptions.ts +++ b/src/helpers/connectionOptions.ts @@ -1,20 +1,42 @@ import { MongoClientOptions } from "mongodb"; import ConnectionString from "mongodb-connection-string-url"; +import { getDeviceIdForConnection } from "./deviceId.js"; -export function setAppNameParamIfMissing({ +export interface AppNameComponents { + appName: string; + deviceId?: string; + clientName?: string; +} + +/** + * Sets the appName parameter with the extended format: appName--deviceId--clientName + * Only sets the appName if it's not already present in the connection string + * @param connectionString - The connection string to modify + * @param components - The components to build the appName from + * @returns The modified connection string + */ +export async function setAppNameParamIfMissing({ connectionString, - defaultAppName, + components, }: { connectionString: string; - defaultAppName?: string; -}): string { + components: AppNameComponents; +}): Promise { const connectionStringUrl = new ConnectionString(connectionString); - const searchParams = connectionStringUrl.typedSearchParams(); - if (!searchParams.has("appName") && defaultAppName !== undefined) { - searchParams.set("appName", defaultAppName); + // Only set appName if it's not already present + if (searchParams.has("appName")) { + return connectionStringUrl.toString(); } + const deviceId = components.deviceId || (await getDeviceIdForConnection()); + const clientName = components.clientName || "unknown"; + + // Build the extended appName format: appName--deviceId--clientName + const extendedAppName = `${components.appName}--${deviceId}--${clientName}`; + + searchParams.set("appName", extendedAppName); + return connectionStringUrl.toString(); } diff --git a/src/helpers/deviceId.ts b/src/helpers/deviceId.ts new file mode 100644 index 00000000..c3239cad --- /dev/null +++ b/src/helpers/deviceId.ts @@ -0,0 +1,46 @@ +import { getDeviceId } from "@mongodb-js/device-id"; +import nodeMachineId from "node-machine-id"; +import logger, { LogId } from "../common/logger.js"; + +export const DEVICE_ID_TIMEOUT = 3000; + +/** + * Retrieves the device ID for telemetry purposes. + * The device ID is generated using the machine ID and additional logic to handle errors. + * + * @returns Promise that resolves to the device ID string + * If an error occurs during retrieval, the function returns "unknown". + * + * @example + * ```typescript + * const deviceId = await getDeviceIdForConnection(); + * console.log(deviceId); // Outputs the device ID or "unknown" in case of failure + * ``` + */ +export async function getDeviceIdForConnection(): Promise { + const controller = new AbortController(); + + try { + const deviceId = await getDeviceId({ + getMachineId: () => nodeMachineId.machineId(true), + onError: (reason, error) => { + switch (reason) { + case "resolutionError": + logger.debug(LogId.telemetryDeviceIdFailure, "deviceId", String(error)); + break; + case "timeout": + logger.debug(LogId.telemetryDeviceIdTimeout, "deviceId", "Device ID retrieval timed out"); + break; + case "abort": + // No need to log in the case of aborts + break; + } + }, + abortSignal: controller.signal, + }); + return deviceId; + } catch (error) { + logger.debug(LogId.telemetryDeviceIdFailure, "deviceId", `Failed to get device ID: ${String(error)}`); + return "unknown"; + } +} diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index eb759edc..0264eb8c 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -5,8 +5,7 @@ import logger, { LogId } from "../common/logger.js"; import { ApiClient } from "../common/atlas/apiClient.js"; import { MACHINE_METADATA } from "./constants.js"; import { EventCache } from "./eventCache.js"; -import nodeMachineId from "node-machine-id"; -import { getDeviceId } from "@mongodb-js/device-id"; +import { getDeviceIdForConnection } from "../helpers/deviceId.js"; import { detectContainerEnv } from "../helpers/container.js"; type EventResult = { @@ -14,24 +13,19 @@ type EventResult = { error?: Error; }; -export const DEVICE_ID_TIMEOUT = 3000; - export class Telemetry { private isBufferingEvents: boolean = true; /** Resolves when the setup is complete or a timeout occurs */ public setupPromise: Promise<[string, boolean]> | undefined; - private deviceIdAbortController = new AbortController(); private eventCache: EventCache; - private getRawMachineId: () => Promise; private constructor( private readonly session: Session, private readonly userConfig: UserConfig, private readonly commonProperties: CommonProperties, - { eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise } + { eventCache }: { eventCache: EventCache } ) { this.eventCache = eventCache; - this.getRawMachineId = getRawMachineId; } static create( @@ -40,14 +34,12 @@ export class Telemetry { { commonProperties = { ...MACHINE_METADATA }, eventCache = EventCache.getInstance(), - getRawMachineId = () => nodeMachineId.machineId(true), }: { eventCache?: EventCache; - getRawMachineId?: () => Promise; commonProperties?: CommonProperties; } = {} ): Telemetry { - const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId }); + const instance = new Telemetry(session, userConfig, commonProperties, { eventCache }); void instance.setup(); return instance; @@ -57,26 +49,7 @@ export class Telemetry { if (!this.isTelemetryEnabled()) { return; } - this.setupPromise = Promise.all([ - getDeviceId({ - getMachineId: () => this.getRawMachineId(), - onError: (reason, error) => { - switch (reason) { - case "resolutionError": - logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error)); - break; - case "timeout": - logger.debug(LogId.telemetryDeviceIdTimeout, "telemetry", "Device ID retrieval timed out"); - break; - case "abort": - // No need to log in the case of aborts - break; - } - }, - abortSignal: this.deviceIdAbortController.signal, - }), - detectContainerEnv(), - ]); + this.setupPromise = Promise.all([getDeviceIdForConnection(), detectContainerEnv()]); const [deviceId, containerEnv] = await this.setupPromise; @@ -87,7 +60,6 @@ export class Telemetry { } public async close(): Promise { - this.deviceIdAbortController.abort(); this.isBufferingEvents = false; await this.emitEvents(this.eventCache.getEvents()); } diff --git a/src/tools/atlas/connect/connectCluster.ts b/src/tools/atlas/connect/connectCluster.ts index e83c3040..aa32064b 100644 --- a/src/tools/atlas/connect/connectCluster.ts +++ b/src/tools/atlas/connect/connectCluster.ts @@ -6,6 +6,8 @@ import { generateSecurePassword } from "../../../helpers/generatePassword.js"; import logger, { LogId } from "../../../common/logger.js"; import { inspectCluster } from "../../../common/atlas/cluster.js"; import { ensureCurrentIpInAccessList } from "../../../common/atlas/accessListUtils.js"; +import { setAppNameParamIfMissing } from "../../../helpers/connectionOptions.js"; +import { packageInfo } from "../../../common/packageInfo.js"; const EXPIRY_MS = 1000 * 60 * 60 * 12; // 12 hours @@ -120,7 +122,15 @@ export class ConnectClusterTool extends AtlasToolBase { cn.username = username; cn.password = password; cn.searchParams.set("authSource", "admin"); - return cn.toString(); + + const connectionStringWithAuth = cn.toString(); + return await setAppNameParamIfMissing({ + connectionString: connectionStringWithAuth, + components: { + appName: `${packageInfo.mcpServerName} ${packageInfo.version}`, + clientName: this.session.agentRunner?.name || "unknown", + }, + }); } private async connectToCluster(projectId: string, clusterName: string, connectionString: string): Promise { diff --git a/tests/integration/telemetry.test.ts b/tests/integration/telemetry.test.ts index d3a944f1..3846f387 100644 --- a/tests/integration/telemetry.test.ts +++ b/tests/integration/telemetry.test.ts @@ -1,15 +1,12 @@ -import { createHmac } from "crypto"; import { Telemetry } from "../../src/telemetry/telemetry.js"; import { Session } from "../../src/common/session.js"; import { config } from "../../src/common/config.js"; -import nodeMachineId from "node-machine-id"; +import { getDeviceIdForConnection } from "../../src/helpers/deviceId.js"; import { describe, expect, it } from "vitest"; describe("Telemetry", () => { - it("should resolve the actual machine ID", async () => { - const actualId: string = await nodeMachineId.machineId(true); - - const actualHashedId = createHmac("sha256", actualId.toUpperCase()).update("atlascli").digest("hex"); + it("should resolve the actual device ID", async () => { + const actualDeviceId = await getDeviceIdForConnection(); const telemetry = Telemetry.create( new Session({ @@ -23,7 +20,7 @@ describe("Telemetry", () => { await telemetry.setupPromise; - expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId); + expect(telemetry.getCommonProperties().device_id).toBe(actualDeviceId); expect(telemetry["isBufferingEvents"]).toBe(false); }); }); diff --git a/tests/integration/tools/mongodb/connect/connect.test.ts b/tests/integration/tools/mongodb/connect/connect.test.ts index d8be8e5a..c50313c6 100644 --- a/tests/integration/tools/mongodb/connect/connect.test.ts +++ b/tests/integration/tools/mongodb/connect/connect.test.ts @@ -7,7 +7,12 @@ import { } from "../../../helpers.js"; import { config } from "../../../../../src/common/config.js"; import { defaultTestConfig, setupIntegrationTest } from "../../../helpers.js"; -import { beforeEach, describe, expect, it } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; + +// Mock the deviceId utility for consistent testing +vi.mock("../../../../../src/helpers/deviceId.js", () => ({ + getDeviceIdForConnection: vi.fn().mockResolvedValue("test-device-id"), +})); describeWithMongoDB( "SwitchConnection tool", diff --git a/tests/unit/common/session.test.ts b/tests/unit/common/session.test.ts index 73236c5f..7d2113f7 100644 --- a/tests/unit/common/session.test.ts +++ b/tests/unit/common/session.test.ts @@ -4,6 +4,10 @@ import { Session } from "../../../src/common/session.js"; import { config } from "../../../src/common/config.js"; vi.mock("@mongosh/service-provider-node-driver"); +vi.mock("../../../src/helpers/deviceId.js", () => ({ + getDeviceIdForConnection: vi.fn().mockResolvedValue("test-device-id"), +})); + const MockNodeDriverServiceProvider = vi.mocked(NodeDriverServiceProvider); describe("Session", () => { @@ -50,7 +54,9 @@ describe("Session", () => { expect(connectMock).toHaveBeenCalledOnce(); const connectionString = connectMock.mock.calls[0]?.[0]; if (testCase.expectAppName) { - expect(connectionString).toContain("appName=MongoDB+MCP+Server"); + // Check for the extended appName format: appName--deviceId--clientName + expect(connectionString).toContain("appName=MongoDB+MCP+Server+"); + expect(connectionString).toContain("--test-device-id--"); } else { expect(connectionString).not.toContain("appName=MongoDB+MCP+Server"); } @@ -68,5 +74,31 @@ describe("Session", () => { expect(connectionConfig?.proxy).toEqual({ useEnvironmentVariableProxies: true }); expect(connectionConfig?.applyProxyToOIDC).toEqual(true); }); + + it("should include client name when agent runner is set", async () => { + session.setAgentRunner({ name: "test-client", version: "1.0.0" }); + + await session.connectToMongoDB("mongodb://localhost:27017", config.connectOptions); + expect(session.serviceProvider).toBeDefined(); + + const connectMock = MockNodeDriverServiceProvider.connect; + expect(connectMock).toHaveBeenCalledOnce(); + const connectionString = connectMock.mock.calls[0]?.[0]; + + // Should include the client name in the appName + expect(connectionString).toContain("--test-device-id--test-client"); + }); + + it("should use 'unknown' for client name when agent runner is not set", async () => { + await session.connectToMongoDB("mongodb://localhost:27017", config.connectOptions); + expect(session.serviceProvider).toBeDefined(); + + const connectMock = MockNodeDriverServiceProvider.connect; + expect(connectMock).toHaveBeenCalledOnce(); + const connectionString = connectMock.mock.calls[0]?.[0]; + + // Should use 'unknown' for client name when agent runner is not set + expect(connectionString).toContain("--test-device-id--unknown"); + }); }); }); diff --git a/tests/unit/helpers/connectionOptions.test.ts b/tests/unit/helpers/connectionOptions.test.ts new file mode 100644 index 00000000..2e2fee2d --- /dev/null +++ b/tests/unit/helpers/connectionOptions.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it, vi } from "vitest"; +import { setAppNameParamIfMissing } from "../../../src/helpers/connectionOptions.js"; + +// Mock the deviceId utility +vi.mock("../../../src/helpers/deviceId.js", () => ({ + getDeviceIdForConnection: vi.fn().mockResolvedValue("test-device-id"), +})); + +describe("Connection Options", () => { + describe("setAppNameParamIfMissing", () => { + it("should set extended appName when no appName is present", async () => { + const connectionString = "mongodb://localhost:27017"; + const result = await setAppNameParamIfMissing({ + connectionString, + components: { + appName: "TestApp", + clientName: "TestClient", + }, + }); + + expect(result).toContain("appName=TestApp--test-device-id--TestClient"); + }); + + it("should not modify connection string when appName is already present", async () => { + const connectionString = "mongodb://localhost:27017?appName=ExistingApp"; + const result = await setAppNameParamIfMissing({ + connectionString, + components: { + appName: "TestApp", + clientName: "TestClient", + }, + }); + + // The ConnectionString library normalizes URLs, so we need to check the content rather than exact equality + expect(result).toContain("appName=ExistingApp"); + expect(result).not.toContain("TestApp--test-device-id--TestClient"); + }); + + it("should use provided deviceId when available", async () => { + const connectionString = "mongodb://localhost:27017"; + const result = await setAppNameParamIfMissing({ + connectionString, + components: { + appName: "TestApp", + deviceId: "custom-device-id", + clientName: "TestClient", + }, + }); + + expect(result).toContain("appName=TestApp--custom-device-id--TestClient"); + }); + + it("should use 'unknown' for clientName when not provided", async () => { + const connectionString = "mongodb://localhost:27017"; + const result = await setAppNameParamIfMissing({ + connectionString, + components: { + appName: "TestApp", + }, + }); + + expect(result).toContain("appName=TestApp--test-device-id--unknown"); + }); + + it("should use deviceId utility when deviceId is not provided", async () => { + const connectionString = "mongodb://localhost:27017"; + const result = await setAppNameParamIfMissing({ + connectionString, + components: { + appName: "TestApp", + clientName: "TestClient", + }, + }); + + expect(result).toContain("appName=TestApp--test-device-id--TestClient"); + }); + + it("should preserve other query parameters", async () => { + const connectionString = "mongodb://localhost:27017?retryWrites=true&w=majority"; + const result = await setAppNameParamIfMissing({ + connectionString, + components: { + appName: "TestApp", + clientName: "TestClient", + }, + }); + + expect(result).toContain("retryWrites=true"); + expect(result).toContain("w=majority"); + expect(result).toContain("appName=TestApp--test-device-id--TestClient"); + }); + }); +}); diff --git a/tests/unit/helpers/deviceId.test.ts b/tests/unit/helpers/deviceId.test.ts new file mode 100644 index 00000000..c9144005 --- /dev/null +++ b/tests/unit/helpers/deviceId.test.ts @@ -0,0 +1,198 @@ +/* eslint-disable @typescript-eslint/no-unsafe-assignment, @typescript-eslint/unbound-method */ +import { describe, expect, it, vi, beforeEach, afterEach } from "vitest"; +import { getDeviceIdForConnection, DEVICE_ID_TIMEOUT } from "../../../src/helpers/deviceId.js"; +import { getDeviceId } from "@mongodb-js/device-id"; +import nodeMachineId from "node-machine-id"; +import logger, { LogId } from "../../../src/common/logger.js"; + +// Mock the dependencies +vi.mock("@mongodb-js/device-id"); +vi.mock("node-machine-id"); +vi.mock("../../../src/common/logger.js"); + +const MockGetDeviceId = vi.mocked(getDeviceId); +const MockNodeMachineId = vi.mocked(nodeMachineId); +const MockLogger = vi.mocked(logger); + +describe("Device ID Helper", () => { + beforeEach(() => { + vi.clearAllMocks(); + MockLogger.debug = vi.fn(); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe("getDeviceIdForConnection", () => { + it("should successfully retrieve device ID", async () => { + const mockDeviceId = "test-device-id-123"; + const mockMachineId = "machine-id-456"; + + MockNodeMachineId.machineId.mockResolvedValue(mockMachineId); + MockGetDeviceId.mockResolvedValue(mockDeviceId); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe(mockDeviceId); + expect(MockGetDeviceId).toHaveBeenCalledWith({ + getMachineId: expect.any(Function), + onError: expect.any(Function), + abortSignal: expect.any(AbortSignal), + }); + + // Verify the getMachineId function works + const callArgs = MockGetDeviceId.mock.calls[0]?.[0]; + if (callArgs?.getMachineId) { + const getMachineIdFn = callArgs.getMachineId; + expect(await getMachineIdFn()).toBe(mockMachineId); + } + }); + + it("should return 'unknown' when getDeviceId throws an error", async () => { + MockNodeMachineId.machineId.mockResolvedValue("machine-id"); + MockGetDeviceId.mockRejectedValue(new Error("Device ID resolution failed")); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe("unknown"); + expect(MockLogger.debug).toHaveBeenCalledWith( + LogId.telemetryDeviceIdFailure, + "deviceId", + "Failed to get device ID: Error: Device ID resolution failed" + ); + }); + + it("should handle resolution error callback", async () => { + const mockMachineId = "machine-id"; + MockNodeMachineId.machineId.mockResolvedValue(mockMachineId); + MockGetDeviceId.mockImplementation((options) => { + // Simulate a resolution error + if (options.onError) { + options.onError("resolutionError", new Error("Resolution failed")); + } + return Promise.resolve("device-id"); + }); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe("device-id"); + expect(MockLogger.debug).toHaveBeenCalledWith( + LogId.telemetryDeviceIdFailure, + "deviceId", + "Error: Resolution failed" + ); + }); + + it("should handle timeout error callback", async () => { + const mockMachineId = "machine-id"; + MockNodeMachineId.machineId.mockResolvedValue(mockMachineId); + MockGetDeviceId.mockImplementation((options) => { + // Simulate a timeout error + if (options.onError) { + options.onError("timeout", new Error("Timeout")); + } + return Promise.resolve("device-id"); + }); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe("device-id"); + expect(MockLogger.debug).toHaveBeenCalledWith( + LogId.telemetryDeviceIdTimeout, + "deviceId", + "Device ID retrieval timed out" + ); + }); + + it("should handle timeout with timer advancement", async () => { + vi.useFakeTimers(); + + const mockMachineId = "machine-id"; + MockNodeMachineId.machineId.mockResolvedValue(mockMachineId); + MockGetDeviceId.mockImplementation((options) => { + vi.advanceTimersByTime(DEVICE_ID_TIMEOUT / 2); + if (options.onError) { + options.onError("timeout", new Error("Timeout")); + } + return Promise.resolve("device-id"); + }); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe("device-id"); + expect(MockLogger.debug).toHaveBeenCalledWith( + LogId.telemetryDeviceIdTimeout, + "deviceId", + "Device ID retrieval timed out" + ); + + vi.useRealTimers(); + }); + + it("should handle abort error callback without logging", async () => { + const mockMachineId = "machine-id"; + MockNodeMachineId.machineId.mockResolvedValue(mockMachineId); + MockGetDeviceId.mockImplementation((options) => { + // Simulate an abort error + if (options.onError) { + options.onError("abort", new Error("Aborted")); + } + return Promise.resolve("device-id"); + }); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe("device-id"); + // Should not log abort errors + expect(MockLogger.debug).not.toHaveBeenCalledWith( + LogId.telemetryDeviceIdFailure, + "deviceId", + expect.stringContaining("Aborted") + ); + }); + + it("should handle machine ID generation failure", async () => { + MockNodeMachineId.machineId.mockImplementation(() => { + throw new Error("Machine ID generation failed"); + }); + // Also mock getDeviceId to throw to ensure we get the fallback + MockGetDeviceId.mockRejectedValue(new Error("Device ID failed")); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe("unknown"); + expect(MockLogger.debug).toHaveBeenCalledWith( + LogId.telemetryDeviceIdFailure, + "deviceId", + "Failed to get device ID: Error: Device ID failed" + ); + }); + + it("should use AbortController signal", async () => { + MockNodeMachineId.machineId.mockResolvedValue("machine-id"); + MockGetDeviceId.mockResolvedValue("device-id"); + + await getDeviceIdForConnection(); + + const callArgs = MockGetDeviceId.mock.calls[0]?.[0]; + if (callArgs) { + expect(callArgs.abortSignal).toBeInstanceOf(AbortSignal); + } + }); + + it("should handle non-Error exceptions", async () => { + MockNodeMachineId.machineId.mockResolvedValue("machine-id"); + MockGetDeviceId.mockRejectedValue("String error"); + + const result = await getDeviceIdForConnection(); + + expect(result).toBe("unknown"); + expect(MockLogger.debug).toHaveBeenCalledWith( + LogId.telemetryDeviceIdFailure, + "deviceId", + "Failed to get device ID: String error" + ); + }); + }); +}); diff --git a/tests/unit/telemetry.test.ts b/tests/unit/telemetry.test.ts index be1aeb9c..e0589cf7 100644 --- a/tests/unit/telemetry.test.ts +++ b/tests/unit/telemetry.test.ts @@ -1,12 +1,10 @@ import { ApiClient } from "../../src/common/atlas/apiClient.js"; import { Session } from "../../src/common/session.js"; -import { DEVICE_ID_TIMEOUT, Telemetry } from "../../src/telemetry/telemetry.js"; +import { Telemetry } from "../../src/telemetry/telemetry.js"; import { BaseEvent, TelemetryResult } from "../../src/telemetry/types.js"; import { EventCache } from "../../src/telemetry/eventCache.js"; import { config } from "../../src/common/config.js"; import { afterEach, beforeEach, describe, it, vi, expect } from "vitest"; -import logger, { LogId } from "../../src/common/logger.js"; -import { createHmac } from "crypto"; import type { MockedFunction } from "vitest"; // Mock the ApiClient to avoid real API calls @@ -17,10 +15,12 @@ const MockApiClient = vi.mocked(ApiClient); vi.mock("../../src/telemetry/eventCache.js"); const MockEventCache = vi.mocked(EventCache); -describe("Telemetry", () => { - const machineId = "test-machine-id"; - const hashedMachineId = createHmac("sha256", machineId.toUpperCase()).update("atlascli").digest("hex"); +// Mock the deviceId utility +vi.mock("../../src/helpers/deviceId.js", () => ({ + getDeviceIdForConnection: vi.fn().mockResolvedValue("test-device-id"), +})); +describe("Telemetry", () => { let mockApiClient: { sendEvents: MockedFunction<(events: BaseEvent[]) => Promise>; hasCredentials: MockedFunction<() => boolean>; @@ -129,7 +129,6 @@ describe("Telemetry", () => { telemetry = Telemetry.create(session, config, { eventCache: mockEventCache as unknown as EventCache, - getRawMachineId: () => Promise.resolve(machineId), }); config.telemetry = "enabled"; @@ -204,27 +203,23 @@ describe("Telemetry", () => { session_id: "test-session-id", config_atlas_auth: "true", config_connection_string: expect.any(String) as unknown as string, - device_id: hashedMachineId, + device_id: "test-device-id", }; expect(commonProps).toMatchObject(expectedProps); }); - describe("machine ID resolution", () => { + describe("device ID resolution", () => { beforeEach(() => { vi.clearAllMocks(); - vi.useFakeTimers(); }); afterEach(() => { vi.clearAllMocks(); - vi.useRealTimers(); }); - it("should successfully resolve the machine ID", async () => { - telemetry = Telemetry.create(session, config, { - getRawMachineId: () => Promise.resolve(machineId), - }); + it("should successfully resolve the device ID", async () => { + telemetry = Telemetry.create(session, config); expect(telemetry["isBufferingEvents"]).toBe(true); expect(telemetry.getCommonProperties().device_id).toBe(undefined); @@ -232,15 +227,15 @@ describe("Telemetry", () => { await telemetry.setupPromise; expect(telemetry["isBufferingEvents"]).toBe(false); - expect(telemetry.getCommonProperties().device_id).toBe(hashedMachineId); + expect(telemetry.getCommonProperties().device_id).toBe("test-device-id"); }); - it("should handle machine ID resolution failure", async () => { - const loggerSpy = vi.spyOn(logger, "debug"); + it("should handle device ID resolution failure gracefully", async () => { + // Mock the deviceId utility to return "unknown" for this test + const { getDeviceIdForConnection } = await import("../../src/helpers/deviceId.js"); + vi.mocked(getDeviceIdForConnection).mockResolvedValueOnce("unknown"); - telemetry = Telemetry.create(session, config, { - getRawMachineId: () => Promise.reject(new Error("Failed to get device ID")), - }); + telemetry = Telemetry.create(session, config); expect(telemetry["isBufferingEvents"]).toBe(true); expect(telemetry.getCommonProperties().device_id).toBe(undefined); @@ -248,40 +243,25 @@ describe("Telemetry", () => { await telemetry.setupPromise; expect(telemetry["isBufferingEvents"]).toBe(false); + // Should use "unknown" as fallback when device ID resolution fails expect(telemetry.getCommonProperties().device_id).toBe("unknown"); - - expect(loggerSpy).toHaveBeenCalledWith( - LogId.telemetryDeviceIdFailure, - "telemetry", - "Error: Failed to get device ID" - ); }); - it("should timeout if machine ID resolution takes too long", async () => { - const loggerSpy = vi.spyOn(logger, "debug"); - - telemetry = Telemetry.create(session, config, { getRawMachineId: () => new Promise(() => {}) }); - - expect(telemetry["isBufferingEvents"]).toBe(true); - expect(telemetry.getCommonProperties().device_id).toBe(undefined); + it("should handle device ID timeout gracefully", async () => { + // Mock the deviceId utility to return "unknown" for this test + const { getDeviceIdForConnection } = await import("../../src/helpers/deviceId.js"); + vi.mocked(getDeviceIdForConnection).mockResolvedValueOnce("unknown"); - vi.advanceTimersByTime(DEVICE_ID_TIMEOUT / 2); + telemetry = Telemetry.create(session, config); - // Make sure the timeout doesn't happen prematurely. expect(telemetry["isBufferingEvents"]).toBe(true); expect(telemetry.getCommonProperties().device_id).toBe(undefined); - vi.advanceTimersByTime(DEVICE_ID_TIMEOUT); - await telemetry.setupPromise; - expect(telemetry.getCommonProperties().device_id).toBe("unknown"); expect(telemetry["isBufferingEvents"]).toBe(false); - expect(loggerSpy).toHaveBeenCalledWith( - LogId.telemetryDeviceIdTimeout, - "telemetry", - "Device ID retrieval timed out" - ); + // Should use "unknown" as fallback when device ID times out + expect(telemetry.getCommonProperties().device_id).toBe("unknown"); }); }); });