Skip to content

Commit 2d3c184

Browse files
committed
Expose a hook to specify telemetry hosting mode
1 parent f78fb4c commit 2d3c184

File tree

3 files changed

+149
-126
lines changed

3 files changed

+149
-126
lines changed

src/telemetry/telemetry.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ type EventResult = {
1414
};
1515

1616
export class Telemetry {
17+
public static hostingMode?: string;
18+
1719
private isBufferingEvents: boolean = true;
1820
/** Resolves when the setup is complete or a timeout occurs */
1921
public setupPromise: Promise<[string, boolean]> | undefined;
@@ -42,6 +44,7 @@ export class Telemetry {
4244
commonProperties?: CommonProperties;
4345
} = {}
4446
): Telemetry {
47+
commonProperties.hosting_mode = Telemetry.hostingMode;
4548
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, deviceId });
4649

4750
void instance.setup();

src/telemetry/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,5 @@ export type CommonProperties = {
7373
config_atlas_auth?: TelemetryBoolSet;
7474
config_connection_string?: TelemetryBoolSet;
7575
session_id?: string;
76+
hosting_mode?: string;
7677
} & CommonStaticProperties;

tests/unit/telemetry.test.ts

Lines changed: 145 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
import { ApiClient } from "../../src/common/atlas/apiClient.js";
22
import type { Session } from "../../src/common/session.js";
33
import { Telemetry } from "../../src/telemetry/telemetry.js";
4-
import type { BaseEvent, TelemetryResult } from "../../src/telemetry/types.js";
4+
import type { BaseEvent, CommonProperties, TelemetryEvent, TelemetryResult } from "../../src/telemetry/types.js";
55
import { EventCache } from "../../src/telemetry/eventCache.js";
66
import { config } from "../../src/common/config.js";
77
import { afterEach, beforeEach, describe, it, vi, expect } from "vitest";
88
import { NullLogger } from "../../src/common/logger.js";
99
import type { MockedFunction } from "vitest";
1010
import type { DeviceId } from "../../src/helpers/deviceId.js";
11+
import { expectDefined } from "../integration/helpers.js";
1112

1213
// Mock the ApiClient to avoid real API calls
1314
vi.mock("../../src/common/atlas/apiClient.js");
@@ -29,6 +30,7 @@ describe("Telemetry", () => {
2930
};
3031
let session: Session;
3132
let telemetry: Telemetry;
33+
let mockDeviceId: DeviceId;
3234

3335
// Helper function to create properly typed test events
3436
function createTestEvent(options?: {
@@ -115,7 +117,7 @@ describe("Telemetry", () => {
115117
mockEventCache.appendEvents = vi.fn().mockResolvedValue(undefined);
116118
MockEventCache.getInstance = vi.fn().mockReturnValue(mockEventCache as unknown as EventCache);
117119

118-
const mockDeviceId = {
120+
mockDeviceId = {
119121
get: vi.fn().mockResolvedValue("test-device-id"),
120122
} as unknown as DeviceId;
121123

@@ -137,183 +139,200 @@ describe("Telemetry", () => {
137139
config.telemetry = "enabled";
138140
});
139141

140-
describe("sending events", () => {
141-
describe("when telemetry is enabled", () => {
142-
it("should send events successfully", async () => {
143-
const testEvent = createTestEvent();
142+
describe("when telemetry is enabled", () => {
143+
it("should send events successfully", async () => {
144+
const testEvent = createTestEvent();
144145

145-
await telemetry.setupPromise;
146+
await telemetry.setupPromise;
146147

147-
await telemetry.emitEvents([testEvent]);
148+
await telemetry.emitEvents([testEvent]);
148149

149-
verifyMockCalls({
150-
sendEventsCalls: 1,
151-
clearEventsCalls: 1,
152-
sendEventsCalledWith: [testEvent],
153-
});
150+
verifyMockCalls({
151+
sendEventsCalls: 1,
152+
clearEventsCalls: 1,
153+
sendEventsCalledWith: [testEvent],
154154
});
155+
});
155156

156-
it("should cache events when sending fails", async () => {
157-
mockApiClient.sendEvents.mockRejectedValueOnce(new Error("API error"));
157+
it("should cache events when sending fails", async () => {
158+
mockApiClient.sendEvents.mockRejectedValueOnce(new Error("API error"));
158159

159-
const testEvent = createTestEvent();
160+
const testEvent = createTestEvent();
160161

161-
await telemetry.setupPromise;
162+
await telemetry.setupPromise;
162163

163-
await telemetry.emitEvents([testEvent]);
164+
await telemetry.emitEvents([testEvent]);
164165

165-
verifyMockCalls({
166-
sendEventsCalls: 1,
167-
appendEventsCalls: 1,
168-
appendEventsCalledWith: [testEvent],
169-
});
166+
verifyMockCalls({
167+
sendEventsCalls: 1,
168+
appendEventsCalls: 1,
169+
appendEventsCalledWith: [testEvent],
170170
});
171+
});
171172

172-
it("should include cached events when sending", async () => {
173-
const cachedEvent = createTestEvent({
174-
command: "cached-command",
175-
component: "cached-component",
176-
});
173+
it("should include cached events when sending", async () => {
174+
const cachedEvent = createTestEvent({
175+
command: "cached-command",
176+
component: "cached-component",
177+
});
177178

178-
const newEvent = createTestEvent({
179-
command: "new-command",
180-
component: "new-component",
181-
});
179+
const newEvent = createTestEvent({
180+
command: "new-command",
181+
component: "new-component",
182+
});
182183

183-
// Set up mock to return cached events
184-
mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]);
184+
// Set up mock to return cached events
185+
mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]);
185186

186-
await telemetry.setupPromise;
187+
await telemetry.setupPromise;
187188

188-
await telemetry.emitEvents([newEvent]);
189+
await telemetry.emitEvents([newEvent]);
189190

190-
verifyMockCalls({
191-
sendEventsCalls: 1,
192-
clearEventsCalls: 1,
193-
sendEventsCalledWith: [cachedEvent, newEvent],
194-
});
191+
verifyMockCalls({
192+
sendEventsCalls: 1,
193+
clearEventsCalls: 1,
194+
sendEventsCalledWith: [cachedEvent, newEvent],
195195
});
196+
});
196197

197-
it("should correctly add common properties to events", async () => {
198-
await telemetry.setupPromise;
198+
it("should correctly add common properties to events", async () => {
199+
await telemetry.setupPromise;
200+
201+
const commonProps = telemetry.getCommonProperties();
199202

200-
const commonProps = telemetry.getCommonProperties();
203+
// Use explicit type assertion
204+
const expectedProps: Record<string, string> = {
205+
mcp_client_version: "1.0.0",
206+
mcp_client_name: "test-agent",
207+
session_id: "test-session-id",
208+
config_atlas_auth: "true",
209+
config_connection_string: expect.any(String) as unknown as string,
210+
device_id: "test-device-id",
211+
};
201212

202-
// Use explicit type assertion
203-
const expectedProps: Record<string, string> = {
204-
mcp_client_version: "1.0.0",
205-
mcp_client_name: "test-agent",
206-
session_id: "test-session-id",
207-
config_atlas_auth: "true",
208-
config_connection_string: expect.any(String) as unknown as string,
209-
device_id: "test-device-id",
210-
};
213+
expect(commonProps).toMatchObject(expectedProps);
214+
});
211215

212-
expect(commonProps).toMatchObject(expectedProps);
216+
it("should add hostingMode to events if set", async () => {
217+
Telemetry.hostingMode = "vscode-extension";
218+
telemetry = Telemetry.create(session, config, mockDeviceId, {
219+
eventCache: mockEventCache as unknown as EventCache,
213220
});
221+
await telemetry.setupPromise;
222+
223+
const commonProps = telemetry.getCommonProperties();
224+
expect(commonProps.hosting_mode).toBe("vscode-extension");
225+
226+
await telemetry.emitEvents([createTestEvent()]);
227+
228+
const calls = mockApiClient.sendEvents.mock.calls;
229+
expect(calls).toHaveLength(1);
230+
const event = calls[0]?.[0][0];
231+
expectDefined(event);
232+
expect((event as TelemetryEvent<CommonProperties>).properties.hosting_mode).toBe("vscode-extension");
233+
});
214234

215-
describe("device ID resolution", () => {
216-
beforeEach(() => {
217-
vi.clearAllMocks();
218-
});
235+
describe("device ID resolution", () => {
236+
beforeEach(() => {
237+
vi.clearAllMocks();
238+
});
219239

220-
afterEach(() => {
221-
vi.clearAllMocks();
222-
});
240+
afterEach(() => {
241+
vi.clearAllMocks();
242+
});
223243

224-
it("should successfully resolve the device ID", async () => {
225-
const mockDeviceId = {
226-
get: vi.fn().mockResolvedValue("test-device-id"),
227-
} as unknown as DeviceId;
244+
it("should successfully resolve the device ID", async () => {
245+
const mockDeviceId = {
246+
get: vi.fn().mockResolvedValue("test-device-id"),
247+
} as unknown as DeviceId;
228248

229-
telemetry = Telemetry.create(session, config, mockDeviceId);
249+
telemetry = Telemetry.create(session, config, mockDeviceId);
230250

231-
expect(telemetry["isBufferingEvents"]).toBe(true);
232-
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
251+
expect(telemetry["isBufferingEvents"]).toBe(true);
252+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
233253

234-
await telemetry.setupPromise;
254+
await telemetry.setupPromise;
235255

236-
expect(telemetry["isBufferingEvents"]).toBe(false);
237-
expect(telemetry.getCommonProperties().device_id).toBe("test-device-id");
238-
});
256+
expect(telemetry["isBufferingEvents"]).toBe(false);
257+
expect(telemetry.getCommonProperties().device_id).toBe("test-device-id");
258+
});
239259

240-
it("should handle device ID resolution failure gracefully", async () => {
241-
const mockDeviceId = {
242-
get: vi.fn().mockResolvedValue("unknown"),
243-
} as unknown as DeviceId;
260+
it("should handle device ID resolution failure gracefully", async () => {
261+
const mockDeviceId = {
262+
get: vi.fn().mockResolvedValue("unknown"),
263+
} as unknown as DeviceId;
244264

245-
telemetry = Telemetry.create(session, config, mockDeviceId);
265+
telemetry = Telemetry.create(session, config, mockDeviceId);
246266

247-
expect(telemetry["isBufferingEvents"]).toBe(true);
248-
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
267+
expect(telemetry["isBufferingEvents"]).toBe(true);
268+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
249269

250-
await telemetry.setupPromise;
270+
await telemetry.setupPromise;
251271

252-
expect(telemetry["isBufferingEvents"]).toBe(false);
253-
// Should use "unknown" as fallback when device ID resolution fails
254-
expect(telemetry.getCommonProperties().device_id).toBe("unknown");
255-
});
272+
expect(telemetry["isBufferingEvents"]).toBe(false);
273+
// Should use "unknown" as fallback when device ID resolution fails
274+
expect(telemetry.getCommonProperties().device_id).toBe("unknown");
275+
});
256276

257-
it("should handle device ID timeout gracefully", async () => {
258-
const mockDeviceId = {
259-
get: vi.fn().mockResolvedValue("unknown"),
260-
} as unknown as DeviceId;
277+
it("should handle device ID timeout gracefully", async () => {
278+
const mockDeviceId = {
279+
get: vi.fn().mockResolvedValue("unknown"),
280+
} as unknown as DeviceId;
261281

262-
telemetry = Telemetry.create(session, config, mockDeviceId);
282+
telemetry = Telemetry.create(session, config, mockDeviceId);
263283

264-
expect(telemetry["isBufferingEvents"]).toBe(true);
265-
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
284+
expect(telemetry["isBufferingEvents"]).toBe(true);
285+
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
266286

267-
await telemetry.setupPromise;
287+
await telemetry.setupPromise;
268288

269-
expect(telemetry["isBufferingEvents"]).toBe(false);
270-
// Should use "unknown" as fallback when device ID times out
271-
expect(telemetry.getCommonProperties().device_id).toBe("unknown");
272-
});
289+
expect(telemetry["isBufferingEvents"]).toBe(false);
290+
// Should use "unknown" as fallback when device ID times out
291+
expect(telemetry.getCommonProperties().device_id).toBe("unknown");
273292
});
274293
});
294+
});
275295

276-
describe("when telemetry is disabled", () => {
277-
beforeEach(() => {
278-
config.telemetry = "disabled";
279-
});
296+
describe("when telemetry is disabled", () => {
297+
beforeEach(() => {
298+
config.telemetry = "disabled";
299+
});
280300

281-
afterEach(() => {
282-
config.telemetry = "enabled";
283-
});
301+
afterEach(() => {
302+
config.telemetry = "enabled";
303+
});
284304

285-
it("should not send events", async () => {
286-
const testEvent = createTestEvent();
305+
it("should not send events", async () => {
306+
const testEvent = createTestEvent();
287307

288-
await telemetry.emitEvents([testEvent]);
308+
await telemetry.emitEvents([testEvent]);
289309

290-
verifyMockCalls();
291-
});
310+
verifyMockCalls();
292311
});
312+
});
293313

294-
describe("when DO_NOT_TRACK environment variable is set", () => {
295-
let originalEnv: string | undefined;
314+
describe("when DO_NOT_TRACK environment variable is set", () => {
315+
let originalEnv: string | undefined;
296316

297-
beforeEach(() => {
298-
originalEnv = process.env.DO_NOT_TRACK;
299-
process.env.DO_NOT_TRACK = "1";
300-
});
317+
beforeEach(() => {
318+
originalEnv = process.env.DO_NOT_TRACK;
319+
process.env.DO_NOT_TRACK = "1";
320+
});
301321

302-
afterEach(() => {
303-
if (originalEnv) {
304-
process.env.DO_NOT_TRACK = originalEnv;
305-
} else {
306-
delete process.env.DO_NOT_TRACK;
307-
}
308-
});
322+
afterEach(() => {
323+
if (originalEnv) {
324+
process.env.DO_NOT_TRACK = originalEnv;
325+
} else {
326+
delete process.env.DO_NOT_TRACK;
327+
}
328+
});
309329

310-
it("should not send events", async () => {
311-
const testEvent = createTestEvent();
330+
it("should not send events", async () => {
331+
const testEvent = createTestEvent();
312332

313-
await telemetry.emitEvents([testEvent]);
333+
await telemetry.emitEvents([testEvent]);
314334

315-
verifyMockCalls();
316-
});
335+
verifyMockCalls();
317336
});
318337
});
319338
});

0 commit comments

Comments
 (0)