Skip to content

Commit a480c94

Browse files
committed
MCP-2: add is_container_env to telemetry
1 parent ba6350c commit a480c94

File tree

6 files changed

+89
-44
lines changed

6 files changed

+89
-44
lines changed

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ try {
2020
version: packageInfo.version,
2121
});
2222

23-
const telemetry = Telemetry.create(session, config);
23+
const telemetry = Telemetry.create({ session, userConfig: config });
2424

2525
const server = new Server({
2626
mcpServer,

src/telemetry/telemetry.ts

Lines changed: 54 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { MACHINE_METADATA } from "./constants.js";
77
import { EventCache } from "./eventCache.js";
88
import nodeMachineId from "node-machine-id";
99
import { getDeviceId } from "@mongodb-js/device-id";
10+
import fs from "fs/promises";
1011

1112
type EventResult = {
1213
success: boolean;
@@ -15,38 +16,36 @@ type EventResult = {
1516

1617
export const DEVICE_ID_TIMEOUT = 3000;
1718

19+
export type TelemetryOptions = {
20+
session: Session;
21+
userConfig: UserConfig;
22+
commonProperties?: CommonProperties;
23+
eventCache?: EventCache;
24+
getRawMachineId?: () => Promise<string>;
25+
};
26+
1827
export class Telemetry {
1928
private isBufferingEvents: boolean = true;
2029
/** Resolves when the device ID is retrieved or timeout occurs */
2130
public deviceIdPromise: Promise<string> | undefined;
2231
private deviceIdAbortController = new AbortController();
23-
private eventCache: EventCache;
24-
private getRawMachineId: () => Promise<string>;
2532

2633
private constructor(
2734
private readonly session: Session,
2835
private readonly userConfig: UserConfig,
2936
private readonly commonProperties: CommonProperties,
30-
{ eventCache, getRawMachineId }: { eventCache: EventCache; getRawMachineId: () => Promise<string> }
31-
) {
32-
this.eventCache = eventCache;
33-
this.getRawMachineId = getRawMachineId;
34-
}
35-
36-
static create(
37-
session: Session,
38-
userConfig: UserConfig,
39-
{
40-
commonProperties = { ...MACHINE_METADATA },
41-
eventCache = EventCache.getInstance(),
42-
getRawMachineId = () => nodeMachineId.machineId(true),
43-
}: {
44-
eventCache?: EventCache;
45-
getRawMachineId?: () => Promise<string>;
46-
commonProperties?: CommonProperties;
47-
} = {}
48-
): Telemetry {
49-
const instance = new Telemetry(session, userConfig, commonProperties, { eventCache, getRawMachineId });
37+
private readonly eventCache: EventCache,
38+
private readonly getRawMachineId: () => Promise<string>
39+
) {}
40+
41+
static create({ session, userConfig, commonProperties, eventCache, getRawMachineId }: TelemetryOptions): Telemetry {
42+
const instance = new Telemetry(
43+
session,
44+
userConfig,
45+
commonProperties || { ...MACHINE_METADATA },
46+
eventCache || EventCache.getInstance(),
47+
getRawMachineId || (() => nodeMachineId.machineId(true))
48+
);
5049

5150
void instance.start();
5251
return instance;
@@ -106,17 +105,47 @@ export class Telemetry {
106105
* Gets the common properties for events
107106
* @returns Object containing common properties for all events
108107
*/
109-
public getCommonProperties(): CommonProperties {
108+
public async getCommonProperties(): Promise<CommonProperties> {
110109
return {
111110
...this.commonProperties,
112111
mcp_client_version: this.session.agentRunner?.version,
113112
mcp_client_name: this.session.agentRunner?.name,
114113
session_id: this.session.sessionId,
115114
config_atlas_auth: this.session.apiClient.hasCredentials() ? "true" : "false",
116115
config_connection_string: this.userConfig.connectionString ? "true" : "false",
116+
is_container_env: (await this.isContainerized()) ? "true" : "false",
117117
};
118118
}
119119

120+
private async fileExists(filePath: string): Promise<boolean> {
121+
try {
122+
await fs.stat(filePath);
123+
return true; // File exists
124+
} catch (e: unknown) {
125+
if (
126+
e instanceof Error &&
127+
(
128+
e as Error & {
129+
code: string;
130+
}
131+
).code === "ENOENT"
132+
) {
133+
return false; // File does not exist
134+
}
135+
throw e; // Re-throw unexpected errors
136+
}
137+
}
138+
139+
private async isContainerized(): Promise<boolean> {
140+
for (const file of ["/.dockerenv", "/run/.containerenv", "/var/run/.containerenv"]) {
141+
const fileExists = await this.fileExists(file);
142+
if (fileExists) {
143+
return true;
144+
}
145+
}
146+
return !!process.env.container;
147+
}
148+
120149
/**
121150
* Checks if telemetry is currently enabled
122151
* This is a method rather than a constant to capture runtime config changes
@@ -177,10 +206,11 @@ export class Telemetry {
177206
*/
178207
private async sendEvents(client: ApiClient, events: BaseEvent[]): Promise<EventResult> {
179208
try {
209+
const commonProperties = await this.getCommonProperties();
180210
await client.sendEvents(
181211
events.map((event) => ({
182212
...event,
183-
properties: { ...this.getCommonProperties(), ...event.properties },
213+
properties: { ...commonProperties, ...event.properties },
184214
}))
185215
);
186216
return { success: true };

src/telemetry/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,4 +71,5 @@ export type CommonProperties = {
7171
config_atlas_auth?: TelemetryBoolSet;
7272
config_connection_string?: TelemetryBoolSet;
7373
session_id?: string;
74+
is_container_env?: TelemetryBoolSet;
7475
} & CommonStaticProperties;

tests/integration/helpers.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ export function setupIntegrationTest(getUserConfig: () => UserConfig): Integrati
6767

6868
userConfig.telemetry = "disabled";
6969

70-
const telemetry = Telemetry.create(session, userConfig);
70+
const telemetry = Telemetry.create({ session, userConfig });
7171

7272
mcpServer = new Server({
7373
session,

tests/integration/telemetry.test.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,21 @@ describe("Telemetry", () => {
1010

1111
const actualHashedId = createHmac("sha256", actualId.toUpperCase()).update("atlascli").digest("hex");
1212

13-
const telemetry = Telemetry.create(
14-
new Session({
13+
const telemetry = Telemetry.create({
14+
session: new Session({
1515
apiBaseUrl: "",
1616
}),
17-
config
18-
);
17+
userConfig: config,
18+
});
1919

20-
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
20+
const commonProperties = await telemetry.getCommonProperties();
21+
22+
expect(commonProperties.device_id).toBe(undefined);
2123
expect(telemetry["isBufferingEvents"]).toBe(true);
2224

2325
await telemetry.deviceIdPromise;
2426

25-
expect(telemetry.getCommonProperties().device_id).toBe(actualHashedId);
27+
expect(commonProperties.device_id).toBe(actualHashedId);
2628
expect(telemetry["isBufferingEvents"]).toBe(false);
2729
});
2830
});

tests/unit/telemetry.test.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe("Telemetry", () => {
5555
}
5656

5757
// Helper function to verify mock calls to reduce duplication
58-
function verifyMockCalls({
58+
async function verifyMockCalls({
5959
sendEventsCalls = 0,
6060
clearEventsCalls = 0,
6161
appendEventsCalls = 0,
@@ -77,11 +77,13 @@ describe("Telemetry", () => {
7777
expect(appendEvents.length).toBe(appendEventsCalls);
7878

7979
if (sendEventsCalledWith) {
80+
const commonProperties = await telemetry.getCommonProperties();
81+
8082
expect(sendEvents[0]?.[0]).toEqual(
8183
sendEventsCalledWith.map((event) => ({
8284
...event,
8385
properties: {
84-
...telemetry.getCommonProperties(),
86+
...commonProperties,
8587
...event.properties,
8688
},
8789
}))
@@ -125,7 +127,9 @@ describe("Telemetry", () => {
125127
setAgentRunner: jest.fn().mockResolvedValue(undefined),
126128
} as unknown as Session;
127129

128-
telemetry = Telemetry.create(session, config, {
130+
telemetry = Telemetry.create({
131+
session,
132+
userConfig: config,
129133
eventCache: mockEventCache,
130134
getRawMachineId: () => Promise.resolve(machineId),
131135
});
@@ -140,7 +144,7 @@ describe("Telemetry", () => {
140144

141145
await telemetry.emitEvents([testEvent]);
142146

143-
verifyMockCalls({
147+
await verifyMockCalls({
144148
sendEventsCalls: 1,
145149
clearEventsCalls: 1,
146150
sendEventsCalledWith: [testEvent],
@@ -154,7 +158,7 @@ describe("Telemetry", () => {
154158

155159
await telemetry.emitEvents([testEvent]);
156160

157-
verifyMockCalls({
161+
await verifyMockCalls({
158162
sendEventsCalls: 1,
159163
appendEventsCalls: 1,
160164
appendEventsCalledWith: [testEvent],
@@ -177,15 +181,15 @@ describe("Telemetry", () => {
177181

178182
await telemetry.emitEvents([newEvent]);
179183

180-
verifyMockCalls({
184+
await verifyMockCalls({
181185
sendEventsCalls: 1,
182186
clearEventsCalls: 1,
183187
sendEventsCalledWith: [cachedEvent, newEvent],
184188
});
185189
});
186190

187191
it("should correctly add common properties to events", () => {
188-
const commonProps = telemetry.getCommonProperties();
192+
const commonProps = await telemetry.getCommonProperties();
189193

190194
// Use explicit type assertion
191195
const expectedProps: Record<string, string> = {
@@ -212,7 +216,9 @@ describe("Telemetry", () => {
212216
});
213217

214218
it("should successfully resolve the machine ID", async () => {
215-
telemetry = Telemetry.create(session, config, {
219+
telemetry = Telemetry.create({
220+
session,
221+
userConfig: config,
216222
getRawMachineId: () => Promise.resolve(machineId),
217223
});
218224

@@ -228,7 +234,9 @@ describe("Telemetry", () => {
228234
it("should handle machine ID resolution failure", async () => {
229235
const loggerSpy = jest.spyOn(logger, "debug");
230236

231-
telemetry = Telemetry.create(session, config, {
237+
telemetry = Telemetry.create({
238+
session,
239+
userConfig: config,
232240
getRawMachineId: () => Promise.reject(new Error("Failed to get device ID")),
233241
});
234242

@@ -250,7 +258,11 @@ describe("Telemetry", () => {
250258
it("should timeout if machine ID resolution takes too long", async () => {
251259
const loggerSpy = jest.spyOn(logger, "debug");
252260

253-
telemetry = Telemetry.create(session, config, { getRawMachineId: () => new Promise(() => {}) });
261+
telemetry = Telemetry.create({
262+
session,
263+
userConfig: config,
264+
getRawMachineId: () => new Promise(() => {}),
265+
});
254266

255267
expect(telemetry["isBufferingEvents"]).toBe(true);
256268
expect(telemetry.getCommonProperties().device_id).toBe(undefined);
@@ -290,7 +302,7 @@ describe("Telemetry", () => {
290302

291303
await telemetry.emitEvents([testEvent]);
292304

293-
verifyMockCalls();
305+
await verifyMockCalls();
294306
});
295307
});
296308

@@ -315,7 +327,7 @@ describe("Telemetry", () => {
315327

316328
await telemetry.emitEvents([testEvent]);
317329

318-
verifyMockCalls();
330+
await verifyMockCalls();
319331
});
320332
});
321333
});

0 commit comments

Comments
 (0)