Skip to content

Commit b7a25a9

Browse files
authored
chore: add telemetry unit tests (#109)
1 parent 7b9559c commit b7a25a9

File tree

1 file changed

+200
-0
lines changed

1 file changed

+200
-0
lines changed

tests/unit/telemetry.test.ts

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import { ApiClient } from "../../src/common/atlas/apiClient.js";
2+
import { Session } from "../../src/session.js";
3+
import { Telemetry } from "../../src/telemetry/telemetry.js";
4+
import { BaseEvent, TelemetryResult } from "../../src/telemetry/types.js";
5+
import { EventCache } from "../../src/telemetry/eventCache.js";
6+
import { config } from "../../src/config.js";
7+
8+
// Mock the ApiClient to avoid real API calls
9+
jest.mock("../../src/common/atlas/apiClient.js");
10+
const MockApiClient = ApiClient as jest.MockedClass<typeof ApiClient>;
11+
12+
// Mock EventCache to control and verify caching behavior
13+
jest.mock("../../src/telemetry/eventCache.js");
14+
const MockEventCache = EventCache as jest.MockedClass<typeof EventCache>;
15+
16+
describe("Telemetry", () => {
17+
let mockApiClient: jest.Mocked<ApiClient>;
18+
let mockEventCache: jest.Mocked<EventCache>;
19+
let session: Session;
20+
let telemetry: Telemetry;
21+
22+
// Helper function to create properly typed test events
23+
function createTestEvent(options?: {
24+
source?: string;
25+
result?: TelemetryResult;
26+
component?: string;
27+
category?: string;
28+
command?: string;
29+
duration_ms?: number;
30+
}): BaseEvent {
31+
return {
32+
timestamp: new Date().toISOString(),
33+
source: options?.source || "mdbmcp",
34+
properties: {
35+
component: options?.component || "test-component",
36+
duration_ms: options?.duration_ms || 100,
37+
result: options?.result || "success",
38+
category: options?.category || "test",
39+
command: options?.command || "test-command",
40+
},
41+
};
42+
}
43+
44+
// Helper function to verify mock calls to reduce duplication
45+
function verifyMockCalls({
46+
sendEventsCalls = 0,
47+
clearEventsCalls = 0,
48+
appendEventsCalls = 0,
49+
sendEventsCalledWith = undefined,
50+
appendEventsCalledWith = undefined,
51+
} = {}) {
52+
const { calls: sendEvents } = mockApiClient.sendEvents.mock;
53+
const { calls: clearEvents } = mockEventCache.clearEvents.mock;
54+
const { calls: appendEvents } = mockEventCache.appendEvents.mock;
55+
56+
expect(sendEvents.length).toBe(sendEventsCalls);
57+
expect(clearEvents.length).toBe(clearEventsCalls);
58+
expect(appendEvents.length).toBe(appendEventsCalls);
59+
60+
if (sendEventsCalledWith) {
61+
expect(sendEvents[0]?.[0]).toEqual(sendEventsCalledWith);
62+
}
63+
64+
if (appendEventsCalledWith) {
65+
expect(appendEvents[0]?.[0]).toEqual(appendEventsCalledWith);
66+
}
67+
}
68+
69+
beforeEach(() => {
70+
// Reset mocks before each test
71+
jest.clearAllMocks();
72+
73+
// Setup mocked API client
74+
mockApiClient = new MockApiClient() as jest.Mocked<ApiClient>;
75+
mockApiClient.sendEvents = jest.fn().mockResolvedValue(undefined);
76+
mockApiClient.hasCredentials = jest.fn().mockReturnValue(true);
77+
78+
// Setup mocked EventCache
79+
mockEventCache = new MockEventCache() as jest.Mocked<EventCache>;
80+
mockEventCache.getEvents = jest.fn().mockReturnValue([]);
81+
mockEventCache.clearEvents = jest.fn().mockResolvedValue(undefined);
82+
mockEventCache.appendEvents = jest.fn().mockResolvedValue(undefined);
83+
MockEventCache.getInstance = jest.fn().mockReturnValue(mockEventCache);
84+
85+
// Create a simplified session with our mocked API client
86+
session = {
87+
apiClient: mockApiClient,
88+
sessionId: "test-session-id",
89+
agentRunner: { name: "test-agent", version: "1.0.0" } as const,
90+
close: jest.fn().mockResolvedValue(undefined),
91+
setAgentRunner: jest.fn().mockResolvedValue(undefined),
92+
} as unknown as Session;
93+
94+
// Create the telemetry instance with mocked dependencies
95+
telemetry = new Telemetry(session, mockEventCache);
96+
97+
config.telemetry = "enabled";
98+
});
99+
100+
describe("when telemetry is enabled", () => {
101+
it("should send events successfully", async () => {
102+
const testEvent = createTestEvent();
103+
104+
await telemetry.emitEvents([testEvent]);
105+
106+
verifyMockCalls({
107+
sendEventsCalls: 1,
108+
clearEventsCalls: 1,
109+
sendEventsCalledWith: [testEvent],
110+
});
111+
});
112+
113+
it("should cache events when sending fails", async () => {
114+
mockApiClient.sendEvents.mockRejectedValueOnce(new Error("API error"));
115+
116+
const testEvent = createTestEvent();
117+
118+
await telemetry.emitEvents([testEvent]);
119+
120+
verifyMockCalls({
121+
sendEventsCalls: 1,
122+
appendEventsCalls: 1,
123+
appendEventsCalledWith: [testEvent],
124+
});
125+
});
126+
127+
it("should include cached events when sending", async () => {
128+
const cachedEvent = createTestEvent({
129+
command: "cached-command",
130+
component: "cached-component",
131+
});
132+
133+
const newEvent = createTestEvent({
134+
command: "new-command",
135+
component: "new-component",
136+
});
137+
138+
// Set up mock to return cached events
139+
mockEventCache.getEvents.mockReturnValueOnce([cachedEvent]);
140+
141+
await telemetry.emitEvents([newEvent]);
142+
143+
verifyMockCalls({
144+
sendEventsCalls: 1,
145+
clearEventsCalls: 1,
146+
sendEventsCalledWith: [cachedEvent, newEvent],
147+
});
148+
});
149+
});
150+
151+
describe("when telemetry is disabled", () => {
152+
beforeEach(() => {
153+
config.telemetry = "disabled";
154+
});
155+
156+
it("should not send events", async () => {
157+
const testEvent = createTestEvent();
158+
159+
await telemetry.emitEvents([testEvent]);
160+
161+
verifyMockCalls();
162+
});
163+
});
164+
165+
it("should correctly add common properties to events", () => {
166+
const commonProps = telemetry.getCommonProperties();
167+
168+
// Use explicit type assertion
169+
const expectedProps: Record<string, string> = {
170+
mcp_client_version: "1.0.0",
171+
mcp_client_name: "test-agent",
172+
session_id: "test-session-id",
173+
config_atlas_auth: "true",
174+
config_connection_string: expect.any(String) as unknown as string,
175+
};
176+
177+
expect(commonProps).toMatchObject(expectedProps);
178+
});
179+
180+
describe("when DO_NOT_TRACK environment variable is set", () => {
181+
let originalEnv: string | undefined;
182+
183+
beforeEach(() => {
184+
originalEnv = process.env.DO_NOT_TRACK;
185+
process.env.DO_NOT_TRACK = "1";
186+
});
187+
188+
afterEach(() => {
189+
process.env.DO_NOT_TRACK = originalEnv;
190+
});
191+
192+
it("should not send events", async () => {
193+
const testEvent = createTestEvent();
194+
195+
await telemetry.emitEvents([testEvent]);
196+
197+
verifyMockCalls();
198+
});
199+
});
200+
});

0 commit comments

Comments
 (0)