diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index ccf0eb41..5d0ad827 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -7,6 +7,7 @@ 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 fs from "fs/promises"; type EventResult = { success: boolean; @@ -17,8 +18,8 @@ export const DEVICE_ID_TIMEOUT = 3000; export class Telemetry { private isBufferingEvents: boolean = true; - /** Resolves when the device ID is retrieved or timeout occurs */ - public deviceIdPromise: Promise | undefined; + /** 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; @@ -48,33 +49,62 @@ export class Telemetry { ): Telemetry { const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId }); - void instance.start(); + void instance.setup(); return instance; } - private async start(): Promise { - if (!this.isTelemetryEnabled()) { - return; + private async isContainerEnv(): Promise { + if (process.platform !== "linux") { + return false; // we only support linux containers for now + } + + if (process.env.container) { + return true; } - this.deviceIdPromise = 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; + + const exists = await Promise.all( + ["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"].map(async (file) => { + try { + await fs.access(file); + return true; + } catch { + return false; } - }, - abortSignal: this.deviceIdAbortController.signal, - }); + }) + ); - this.commonProperties.device_id = await this.deviceIdPromise; + return exists.includes(true); + } + + private async setup(): Promise { + 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, + }), + this.isContainerEnv(), + ]); + + const [deviceId, containerEnv] = await this.setupPromise; + + this.commonProperties.device_id = deviceId; + this.commonProperties.is_container_env = containerEnv; this.isBufferingEvents = false; } diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index d77cc010..862441fd 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -66,6 +66,7 @@ export type CommonStaticProperties = { */ export type CommonProperties = { device_id?: string; + is_container_env?: boolean; mcp_client_version?: string; mcp_client_name?: string; config_atlas_auth?: TelemetryBoolSet; diff --git a/tests/integration/telemetry.test.ts b/tests/integration/telemetry.test.ts index 522c1154..881a8915 100644 --- a/tests/integration/telemetry.test.ts +++ b/tests/integration/telemetry.test.ts @@ -20,7 +20,7 @@ describe("Telemetry", () => { expect(telemetry.getCommonProperties().device_id).toBe(undefined); expect(telemetry["isBufferingEvents"]).toBe(true); - await telemetry.deviceIdPromise; + await telemetry.setupPromise; expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId); expect(telemetry["isBufferingEvents"]).toBe(false); diff --git a/tests/unit/telemetry.test.ts b/tests/unit/telemetry.test.ts index c1ae28ea..1898c4a6 100644 --- a/tests/unit/telemetry.test.ts +++ b/tests/unit/telemetry.test.ts @@ -138,6 +138,8 @@ describe("Telemetry", () => { it("should send events successfully", async () => { const testEvent = createTestEvent(); + await telemetry.setupPromise; + await telemetry.emitEvents([testEvent]); verifyMockCalls({ @@ -152,6 +154,8 @@ describe("Telemetry", () => { const testEvent = createTestEvent(); + await telemetry.setupPromise; + await telemetry.emitEvents([testEvent]); verifyMockCalls({ @@ -175,6 +179,8 @@ describe("Telemetry", () => { // Set up mock to return cached events mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]); + await telemetry.setupPromise; + await telemetry.emitEvents([newEvent]); verifyMockCalls({ @@ -184,7 +190,9 @@ describe("Telemetry", () => { }); }); - it("should correctly add common properties to events", () => { + it("should correctly add common properties to events", async () => { + await telemetry.setupPromise; + const commonProps = telemetry.getCommonProperties(); // Use explicit type assertion @@ -219,7 +227,7 @@ describe("Telemetry", () => { expect(telemetry["isBufferingEvents"]).toBe(true); expect(telemetry.getCommonProperties().device_id).toBe(undefined); - await telemetry.deviceIdPromise; + await telemetry.setupPromise; expect(telemetry["isBufferingEvents"]).toBe(false); expect(telemetry.getCommonProperties().device_id).toBe(hashedMachineId); @@ -235,7 +243,7 @@ describe("Telemetry", () => { expect(telemetry["isBufferingEvents"]).toBe(true); expect(telemetry.getCommonProperties().device_id).toBe(undefined); - await telemetry.deviceIdPromise; + await telemetry.setupPromise; expect(telemetry["isBufferingEvents"]).toBe(false); expect(telemetry.getCommonProperties().device_id).toBe("unknown"); @@ -263,7 +271,7 @@ describe("Telemetry", () => { jest.advanceTimersByTime(DEVICE_ID_TIMEOUT); - await telemetry.deviceIdPromise; + await telemetry.setupPromise; expect(telemetry.getCommonProperties().device_id).toBe("unknown"); expect(telemetry["isBufferingEvents"]).toBe(false);