diff --git a/package-lock.json b/package-lock.json index afb46114..e71274d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,7 +18,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", - "node-machine-id": "^1.1.12", + "node-machine-id": "1.1.12", "openapi-fetch": "^0.13.5", "simple-oauth2": "^5.1.0", "yargs-parser": "^21.1.1", diff --git a/package.json b/package.json index 287e9a69..6e77412f 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,7 @@ "mongodb-log-writer": "^2.4.1", "mongodb-redact": "^1.1.6", "mongodb-schema": "^12.6.2", - "node-machine-id": "^1.1.12", + "node-machine-id": "1.1.12", "openapi-fetch": "^0.13.5", "simple-oauth2": "^5.1.0", "yargs-parser": "^21.1.1", diff --git a/src/deferred-promise.ts b/src/deferred-promise.ts new file mode 100644 index 00000000..1eb3f6e0 --- /dev/null +++ b/src/deferred-promise.ts @@ -0,0 +1,58 @@ +type DeferredPromiseOptions = { + timeout?: number; + onTimeout?: (resolve: (value: T) => void, reject: (reason: Error) => void) => void; +}; + +/** Creates a promise and exposes its resolve and reject methods, with an optional timeout. */ +export class DeferredPromise extends Promise { + resolve: (value: T) => void; + reject: (reason: unknown) => void; + private timeoutId?: NodeJS.Timeout; + + constructor( + executor: (resolve: (value: T) => void, reject: (reason: Error) => void) => void, + { timeout, onTimeout }: DeferredPromiseOptions = {} + ) { + let resolveFn: (value: T) => void; + let rejectFn: (reason?: unknown) => void; + + super((resolve, reject) => { + resolveFn = resolve; + rejectFn = reject; + }); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.resolve = resolveFn!; + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + this.reject = rejectFn!; + + if (timeout !== undefined && onTimeout) { + this.timeoutId = setTimeout(() => { + onTimeout(this.resolve, this.reject); + }, timeout); + } + + executor( + (value: T) => { + if (this.timeoutId) clearTimeout(this.timeoutId); + this.resolve(value); + }, + (reason: Error) => { + if (this.timeoutId) clearTimeout(this.timeoutId); + this.reject(reason); + } + ); + } + + static fromPromise(promise: Promise, options: DeferredPromiseOptions = {}): DeferredPromise { + return new DeferredPromise((resolve, reject) => { + promise + .then((value) => { + resolve(value); + }) + .catch((reason) => { + reject(reason as Error); + }); + }, options); + } +} diff --git a/src/index.ts b/src/index.ts index 9ab92038..20a60e53 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ import { config } from "./config.js"; import { Session } from "./session.js"; import { Server } from "./server.js"; import { packageInfo } from "./packageInfo.js"; +import { Telemetry } from "./telemetry/telemetry.js"; try { const session = new Session({ @@ -19,9 +20,12 @@ try { version: packageInfo.version, }); + const telemetry = Telemetry.create(session, config); + const server = new Server({ mcpServer, session, + telemetry, userConfig: config, }); diff --git a/src/logger.ts b/src/logger.ts index bdd439e1..fbffe85a 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -20,6 +20,7 @@ export const LogId = { telemetryEmitSuccess: mongoLogId(1_002_004), telemetryMetadataError: mongoLogId(1_002_005), telemetryDeviceIdFailure: mongoLogId(1_002_006), + telemetryDeviceIdTimeout: mongoLogId(1_002_007), toolExecute: mongoLogId(1_003_001), toolExecuteFailure: mongoLogId(1_003_002), diff --git a/src/server.ts b/src/server.ts index 38a1d82b..76f73826 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,6 +16,7 @@ export interface ServerOptions { session: Session; userConfig: UserConfig; mcpServer: McpServer; + telemetry: Telemetry; } export class Server { @@ -25,10 +26,10 @@ export class Server { public readonly userConfig: UserConfig; private readonly startTime: number; - constructor({ session, mcpServer, userConfig }: ServerOptions) { + constructor({ session, mcpServer, userConfig, telemetry }: ServerOptions) { this.startTime = Date.now(); this.session = session; - this.telemetry = new Telemetry(session, userConfig); + this.telemetry = telemetry; this.mcpServer = mcpServer; this.userConfig = userConfig; } @@ -93,6 +94,7 @@ export class Server { } async close(): Promise { + await this.telemetry.close(); await this.session.close(); await this.mcpServer.close(); } diff --git a/src/telemetry/constants.ts b/src/telemetry/constants.ts index 7fe85b75..998f6e24 100644 --- a/src/telemetry/constants.ts +++ b/src/telemetry/constants.ts @@ -1,11 +1,10 @@ import { packageInfo } from "../packageInfo.js"; import { type CommonStaticProperties } from "./types.js"; -import { getDeviceId } from "./device-id.js"; + /** * Machine-specific metadata formatted for telemetry */ export const MACHINE_METADATA: CommonStaticProperties = { - device_id: getDeviceId(), mcp_server_version: packageInfo.version, mcp_server_name: packageInfo.mcpServerName, platform: process.platform, diff --git a/src/telemetry/device-id.ts b/src/telemetry/device-id.ts deleted file mode 100644 index e9c48d63..00000000 --- a/src/telemetry/device-id.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { createHmac } from "crypto"; -import nodeMachineId from "node-machine-id"; -import logger, { LogId } from "../logger.js"; - -export function getDeviceId(): string { - try { - const originalId = nodeMachineId.machineIdSync(true); - // Create a hashed format from the all uppercase version of the machine ID - // to match it exactly with the denisbrodbeck/machineid library that Atlas CLI uses. - const hmac = createHmac("sha256", originalId.toUpperCase()); - - /** This matches the message used to create the hashes in Atlas CLI */ - const DEVICE_ID_HASH_MESSAGE = "atlascli"; - - hmac.update(DEVICE_ID_HASH_MESSAGE); - return hmac.digest("hex"); - } catch (error) { - logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error)); - return "unknown"; - } -} diff --git a/src/telemetry/eventCache.ts b/src/telemetry/eventCache.ts index 141e9b78..26fc1f82 100644 --- a/src/telemetry/eventCache.ts +++ b/src/telemetry/eventCache.ts @@ -1,5 +1,5 @@ -import { BaseEvent } from "./types.js"; import { LRUCache } from "lru-cache"; +import { BaseEvent } from "./types.js"; /** * Singleton class for in-memory telemetry event caching diff --git a/src/telemetry/telemetry.ts b/src/telemetry/telemetry.ts index 31760ff4..30a0363b 100644 --- a/src/telemetry/telemetry.ts +++ b/src/telemetry/telemetry.ts @@ -5,23 +5,101 @@ import logger, { LogId } from "../logger.js"; import { ApiClient } from "../common/atlas/apiClient.js"; import { MACHINE_METADATA } from "./constants.js"; import { EventCache } from "./eventCache.js"; +import { createHmac } from "crypto"; +import nodeMachineId from "node-machine-id"; +import { DeferredPromise } from "../deferred-promise.js"; type EventResult = { success: boolean; error?: Error; }; +export const DEVICE_ID_TIMEOUT = 3000; + export class Telemetry { - private readonly commonProperties: CommonProperties; + private isBufferingEvents: boolean = true; + /** Resolves when the device ID is retrieved or timeout occurs */ + public deviceIdPromise: DeferredPromise | undefined; + private eventCache: EventCache; + private getRawMachineId: () => Promise; - constructor( + private constructor( private readonly session: Session, private readonly userConfig: UserConfig, - private readonly eventCache: EventCache = EventCache.getInstance() + private readonly commonProperties: CommonProperties, + { eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise } ) { - this.commonProperties = { - ...MACHINE_METADATA, - }; + this.eventCache = eventCache; + this.getRawMachineId = getRawMachineId; + } + + static create( + session: Session, + userConfig: UserConfig, + { + commonProperties = { ...MACHINE_METADATA }, + eventCache = EventCache.getInstance(), + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + getRawMachineId = () => nodeMachineId.machineId(true), + }: { + eventCache?: EventCache; + getRawMachineId?: () => Promise; + commonProperties?: CommonProperties; + } = {} + ): Telemetry { + const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId }); + + void instance.start(); + return instance; + } + + private async start(): Promise { + if (!this.isTelemetryEnabled()) { + return; + } + this.deviceIdPromise = DeferredPromise.fromPromise(this.getDeviceId(), { + timeout: DEVICE_ID_TIMEOUT, + onTimeout: (resolve) => { + resolve("unknown"); + logger.debug(LogId.telemetryDeviceIdTimeout, "telemetry", "Device ID retrieval timed out"); + }, + }); + this.commonProperties.device_id = await this.deviceIdPromise; + + this.isBufferingEvents = false; + } + + public async close(): Promise { + this.deviceIdPromise?.resolve("unknown"); + this.isBufferingEvents = false; + await this.emitEvents(this.eventCache.getEvents()); + } + + /** + * @returns A hashed, unique identifier for the running device or `"unknown"` if not known. + */ + private async getDeviceId(): Promise { + try { + if (this.commonProperties.device_id) { + return this.commonProperties.device_id; + } + + const originalId: string = await this.getRawMachineId(); + + // Create a hashed format from the all uppercase version of the machine ID + // to match it exactly with the denisbrodbeck/machineid library that Atlas CLI uses. + const hmac = createHmac("sha256", originalId.toUpperCase()); + + /** This matches the message used to create the hashes in Atlas CLI */ + const DEVICE_ID_HASH_MESSAGE = "atlascli"; + + hmac.update(DEVICE_ID_HASH_MESSAGE); + return hmac.digest("hex"); + } catch (error) { + logger.debug(LogId.telemetryDeviceIdFailure, "telemetry", String(error)); + return "unknown"; + } } /** @@ -78,6 +156,11 @@ export class Telemetry { * Falls back to caching if both attempts fail */ private async emit(events: BaseEvent[]): Promise { + if (this.isBufferingEvents) { + this.eventCache.appendEvents(events); + return; + } + const cachedEvents = this.eventCache.getEvents(); const allEvents = [...cachedEvents, ...events]; diff --git a/src/telemetry/types.ts b/src/telemetry/types.ts index 76e1d4ae..d77cc010 100644 --- a/src/telemetry/types.ts +++ b/src/telemetry/types.ts @@ -53,7 +53,6 @@ export type ServerEvent = TelemetryEvent; * Interface for static properties, they can be fetched once and reused. */ export type CommonStaticProperties = { - device_id?: string; mcp_server_version: string; mcp_server_name: string; platform: string; diff --git a/tests/integration/helpers.ts b/tests/integration/helpers.ts index c4187318..b5c31b9b 100644 --- a/tests/integration/helpers.ts +++ b/tests/integration/helpers.ts @@ -5,6 +5,7 @@ import { UserConfig } from "../../src/config.js"; import { McpError } from "@modelcontextprotocol/sdk/types.js"; import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { Session } from "../../src/session.js"; +import { Telemetry } from "../../src/telemetry/telemetry.js"; import { config } from "../../src/config.js"; interface ParameterInfo { @@ -56,9 +57,14 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati apiClientSecret: userConfig.apiClientSecret, }); + userConfig.telemetry = "disabled"; + + const telemetry = Telemetry.create(session, userConfig); + mcpServer = new Server({ session, userConfig, + telemetry, mcpServer: new McpServer({ name: "test-server", version: "5.2.3", diff --git a/tests/integration/telemetry.test.ts b/tests/integration/telemetry.test.ts new file mode 100644 index 00000000..fe8e51ff --- /dev/null +++ b/tests/integration/telemetry.test.ts @@ -0,0 +1,29 @@ +import { createHmac } from "crypto"; +import { Telemetry } from "../../src/telemetry/telemetry.js"; +import { Session } from "../../src/session.js"; +import { config } from "../../src/config.js"; +import nodeMachineId from "node-machine-id"; + +describe("Telemetry", () => { + it("should resolve the actual machine ID", async () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + const actualId: string = await nodeMachineId.machineId(true); + + const actualHashedId = createHmac("sha256", actualId.toUpperCase()).update("atlascli").digest("hex"); + + const telemetry = Telemetry.create( + new Session({ + apiBaseUrl: "", + }), + config + ); + + expect(telemetry.getCommonProperties().device_id).toBe(undefined); + expect(telemetry["isBufferingEvents"]).toBe(true); + + await telemetry.deviceIdPromise; + + expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId); + expect(telemetry["isBufferingEvents"]).toBe(false); + }); +}); diff --git a/tests/integration/tools/mongodb/mongodbHelpers.ts b/tests/integration/tools/mongodb/mongodbHelpers.ts index 39ae86fa..11381802 100644 --- a/tests/integration/tools/mongodb/mongodbHelpers.ts +++ b/tests/integration/tools/mongodb/mongodbHelpers.ts @@ -76,8 +76,6 @@ export function setupMongoDBIntegrationTest(): MongoDBIntegrationTest { let dbsDir = path.join(tmpDir, "mongodb-runner", "dbs"); for (let i = 0; i < 10; i++) { try { - // TODO: Fix this type once mongodb-runner is updated. - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-unsafe-call mongoCluster = await MongoCluster.start({ tmpDir: dbsDir, logDir: path.join(tmpDir, "mongodb-runner", "logs"), diff --git a/tests/unit/deferred-promise.test.ts b/tests/unit/deferred-promise.test.ts new file mode 100644 index 00000000..c6011af1 --- /dev/null +++ b/tests/unit/deferred-promise.test.ts @@ -0,0 +1,72 @@ +import { DeferredPromise } from "../../src/deferred-promise.js"; +import { jest } from "@jest/globals"; + +describe("DeferredPromise", () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + afterEach(() => { + jest.useRealTimers(); + }); + + it("should resolve with the correct value", async () => { + const deferred = new DeferredPromise((resolve) => { + resolve("resolved value"); + }); + + await expect(deferred).resolves.toEqual("resolved value"); + }); + + it("should reject with the correct error", async () => { + const deferred = new DeferredPromise((_, reject) => { + reject(new Error("rejected error")); + }); + + await expect(deferred).rejects.toThrow("rejected error"); + }); + + it("should timeout if not resolved or rejected within the specified time", async () => { + const deferred = new DeferredPromise( + () => { + // Do not resolve or reject + }, + { timeout: 100, onTimeout: (resolve, reject) => reject(new Error("Promise timed out")) } + ); + + jest.advanceTimersByTime(100); + + await expect(deferred).rejects.toThrow("Promise timed out"); + }); + + it("should clear the timeout when resolved", async () => { + const deferred = new DeferredPromise( + (resolve) => { + setTimeout(() => resolve("resolved value"), 100); + }, + { timeout: 200 } + ); + + const promise = deferred.then((value) => { + expect(value).toBe("resolved value"); + }); + + jest.advanceTimersByTime(100); + await promise; + }); + + it("should clear the timeout when rejected", async () => { + const deferred = new DeferredPromise( + (_, reject) => { + setTimeout(() => reject(new Error("rejected error")), 100); + }, + { timeout: 200, onTimeout: (resolve, reject) => reject(new Error("Promise timed out")) } + ); + + const promise = deferred.catch((error) => { + expect(error).toEqual(new Error("rejected error")); + }); + + jest.advanceTimersByTime(100); + await promise; + }); +}); diff --git a/tests/unit/telemetry.test.ts b/tests/unit/telemetry.test.ts index bdb06326..969a4ee8 100644 --- a/tests/unit/telemetry.test.ts +++ b/tests/unit/telemetry.test.ts @@ -1,10 +1,12 @@ import { ApiClient } from "../../src/common/atlas/apiClient.js"; import { Session } from "../../src/session.js"; -import { Telemetry } from "../../src/telemetry/telemetry.js"; +import { DEVICE_ID_TIMEOUT, 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/config.js"; import { jest } from "@jest/globals"; +import logger, { LogId } from "../../src/logger.js"; +import { createHmac } from "crypto"; // Mock the ApiClient to avoid real API calls jest.mock("../../src/common/atlas/apiClient.js"); @@ -15,6 +17,9 @@ jest.mock("../../src/telemetry/eventCache.js"); const MockEventCache = EventCache as jest.MockedClass; describe("Telemetry", () => { + const machineId = "test-machine-id"; + const hashedMachineId = createHmac("sha256", machineId.toUpperCase()).update("atlascli").digest("hex"); + let mockApiClient: jest.Mocked; let mockEventCache: jest.Mocked; let session: Session; @@ -120,109 +125,194 @@ describe("Telemetry", () => { setAgentRunner: jest.fn().mockResolvedValue(undefined), } as unknown as Session; - // Create the telemetry instance with mocked dependencies - telemetry = new Telemetry(session, config, mockEventCache); + telemetry = Telemetry.create(session, config, { + eventCache: mockEventCache, + getRawMachineId: () => Promise.resolve(machineId), + }); + config.telemetry = "enabled"; }); - describe("when telemetry is enabled", () => { - it("should send events successfully", async () => { - const testEvent = createTestEvent(); + describe("sending events", () => { + describe("when telemetry is enabled", () => { + it("should send events successfully", async () => { + const testEvent = createTestEvent(); - await telemetry.emitEvents([testEvent]); + await telemetry.emitEvents([testEvent]); - verifyMockCalls({ - sendEventsCalls: 1, - clearEventsCalls: 1, - sendEventsCalledWith: [testEvent], + verifyMockCalls({ + sendEventsCalls: 1, + clearEventsCalls: 1, + sendEventsCalledWith: [testEvent], + }); }); - }); - it("should cache events when sending fails", async () => { - mockApiClient.sendEvents.mockRejectedValueOnce(new Error("API error")); + it("should cache events when sending fails", async () => { + mockApiClient.sendEvents.mockRejectedValueOnce(new Error("API error")); - const testEvent = createTestEvent(); + const testEvent = createTestEvent(); - await telemetry.emitEvents([testEvent]); + await telemetry.emitEvents([testEvent]); - verifyMockCalls({ - sendEventsCalls: 1, - appendEventsCalls: 1, - appendEventsCalledWith: [testEvent], + verifyMockCalls({ + sendEventsCalls: 1, + appendEventsCalls: 1, + appendEventsCalledWith: [testEvent], + }); }); - }); - it("should include cached events when sending", async () => { - const cachedEvent = createTestEvent({ - command: "cached-command", - component: "cached-component", - }); + it("should include cached events when sending", async () => { + const cachedEvent = createTestEvent({ + command: "cached-command", + component: "cached-component", + }); - const newEvent = createTestEvent({ - command: "new-command", - component: "new-component", + const newEvent = createTestEvent({ + command: "new-command", + component: "new-component", + }); + + // Set up mock to return cached events + mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]); + + await telemetry.emitEvents([newEvent]); + + verifyMockCalls({ + sendEventsCalls: 1, + clearEventsCalls: 1, + sendEventsCalledWith: [cachedEvent, newEvent], + }); }); - // Set up mock to return cached events - mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]); + it("should correctly add common properties to events", () => { + const commonProps = telemetry.getCommonProperties(); - await telemetry.emitEvents([newEvent]); + // Use explicit type assertion + const expectedProps: Record = { + mcp_client_version: "1.0.0", + mcp_client_name: "test-agent", + session_id: "test-session-id", + config_atlas_auth: "true", + config_connection_string: expect.any(String) as unknown as string, + device_id: hashedMachineId, + }; - verifyMockCalls({ - sendEventsCalls: 1, - clearEventsCalls: 1, - sendEventsCalledWith: [cachedEvent, newEvent], + expect(commonProps).toMatchObject(expectedProps); }); - }); - }); - describe("when telemetry is disabled", () => { - beforeEach(() => { - config.telemetry = "disabled"; - }); + describe("machine ID resolution", () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers(); + }); - it("should not send events", async () => { - const testEvent = createTestEvent(); + afterEach(() => { + jest.clearAllMocks(); + jest.useRealTimers(); + }); - await telemetry.emitEvents([testEvent]); + it("should successfully resolve the machine ID", async () => { + telemetry = Telemetry.create(session, config, { + getRawMachineId: () => Promise.resolve(machineId), + }); - verifyMockCalls(); - }); - }); + expect(telemetry["isBufferingEvents"]).toBe(true); + expect(telemetry.getCommonProperties().device_id).toBe(undefined); - it("should correctly add common properties to events", () => { - const commonProps = telemetry.getCommonProperties(); + await telemetry.deviceIdPromise; - // Use explicit type assertion - const expectedProps: Record = { - mcp_client_version: "1.0.0", - mcp_client_name: "test-agent", - session_id: "test-session-id", - config_atlas_auth: "true", - config_connection_string: expect.any(String) as unknown as string, - }; + expect(telemetry["isBufferingEvents"]).toBe(false); + expect(telemetry.getCommonProperties().device_id).toBe(hashedMachineId); + }); - expect(commonProps).toMatchObject(expectedProps); - }); + it("should handle machine ID resolution failure", async () => { + const loggerSpy = jest.spyOn(logger, "debug"); + + telemetry = Telemetry.create(session, config, { + getRawMachineId: () => Promise.reject(new Error("Failed to get device ID")), + }); + + expect(telemetry["isBufferingEvents"]).toBe(true); + expect(telemetry.getCommonProperties().device_id).toBe(undefined); + + await telemetry.deviceIdPromise; + + expect(telemetry["isBufferingEvents"]).toBe(false); + expect(telemetry.getCommonProperties().device_id).toBe("unknown"); + + expect(loggerSpy).toHaveBeenCalledWith( + LogId.telemetryDeviceIdFailure, + "telemetry", + "Error: Failed to get device ID" + ); + }); - describe("when DO_NOT_TRACK environment variable is set", () => { - let originalEnv: string | undefined; + it("should timeout if machine ID resolution takes too long", async () => { + const loggerSpy = jest.spyOn(logger, "debug"); - beforeEach(() => { - originalEnv = process.env.DO_NOT_TRACK; - process.env.DO_NOT_TRACK = "1"; + telemetry = Telemetry.create(session, config, { getRawMachineId: () => new Promise(() => {}) }); + + expect(telemetry["isBufferingEvents"]).toBe(true); + expect(telemetry.getCommonProperties().device_id).toBe(undefined); + + jest.advanceTimersByTime(DEVICE_ID_TIMEOUT / 2); + + // Make sure the timeout doesn't happen prematurely. + expect(telemetry["isBufferingEvents"]).toBe(true); + expect(telemetry.getCommonProperties().device_id).toBe(undefined); + + jest.advanceTimersByTime(DEVICE_ID_TIMEOUT); + + await telemetry.deviceIdPromise; + + expect(telemetry.getCommonProperties().device_id).toBe("unknown"); + expect(telemetry["isBufferingEvents"]).toBe(false); + expect(loggerSpy).toHaveBeenCalledWith( + LogId.telemetryDeviceIdTimeout, + "telemetry", + "Device ID retrieval timed out" + ); + }); + }); }); - afterEach(() => { - process.env.DO_NOT_TRACK = originalEnv; + describe("when telemetry is disabled", () => { + beforeEach(() => { + config.telemetry = "disabled"; + }); + + afterEach(() => { + config.telemetry = "enabled"; + }); + + it("should not send events", async () => { + const testEvent = createTestEvent(); + + await telemetry.emitEvents([testEvent]); + + verifyMockCalls(); + }); }); - it("should not send events", async () => { - const testEvent = createTestEvent(); + describe("when DO_NOT_TRACK environment variable is set", () => { + let originalEnv: string | undefined; + + beforeEach(() => { + originalEnv = process.env.DO_NOT_TRACK; + process.env.DO_NOT_TRACK = "1"; + }); + + afterEach(() => { + process.env.DO_NOT_TRACK = originalEnv; + }); - await telemetry.emitEvents([testEvent]); + it("should not send events", async () => { + const testEvent = createTestEvent(); - verifyMockCalls(); + await telemetry.emitEvents([testEvent]); + + verifyMockCalls(); + }); }); }); }); diff --git a/tsconfig.build.json b/tsconfig.build.json index dd65f91d..1fe57f10 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -8,7 +8,7 @@ "strict": true, "strictNullChecks": true, "esModuleInterop": true, - "types": ["node", "jest"], + "types": ["node"], "sourceMap": true, "skipLibCheck": true, "resolveJsonModule": true,