Skip to content

Commit dcdec0b

Browse files
committed
feat: align with Atlas CLI hashing and resolve device ID asynchronously
1 parent 5df10b6 commit dcdec0b

File tree

3 files changed

+170
-29
lines changed

3 files changed

+170
-29
lines changed

packages/logging/src/logging-and-telemetry.spec.ts

+113-7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import type { MongoshBus } from '@mongosh/types';
77
import type { Writable } from 'stream';
88
import type { MongoshLoggingAndTelemetry } from '.';
99
import { setupLoggingAndTelemetry } from '.';
10+
import { getDeviceId } from './logging-and-telemetry';
11+
import sinon from 'sinon';
12+
import type { MongoshLoggingAndTelemetryArguments } from './types';
13+
import { eventually } from '../../../testing/eventually';
1014

1115
describe('MongoshLoggingAndTelemetry', function () {
1216
let logOutput: any[];
@@ -32,20 +36,25 @@ describe('MongoshLoggingAndTelemetry', function () {
3236

3337
let loggingAndTelemetry: MongoshLoggingAndTelemetry;
3438

35-
beforeEach(function () {
36-
logOutput = [];
37-
analyticsOutput = [];
38-
bus = new EventEmitter();
39-
40-
loggingAndTelemetry = setupLoggingAndTelemetry({
41-
bus,
39+
const testLoggingArguments: Omit<MongoshLoggingAndTelemetryArguments, 'bus'> =
40+
{
4241
analytics,
4342
deviceId: 'test-device',
4443
userTraits: {
4544
platform: process.platform,
4645
arch: process.arch,
4746
},
4847
mongoshVersion: '1.0.0',
48+
};
49+
50+
beforeEach(function () {
51+
logOutput = [];
52+
analyticsOutput = [];
53+
bus = new EventEmitter();
54+
55+
loggingAndTelemetry = setupLoggingAndTelemetry({
56+
...testLoggingArguments,
57+
bus,
4958
});
5059

5160
logger = new MongoLogWriter(logId, `/tmp/${logId}_log`, {
@@ -193,6 +202,94 @@ describe('MongoshLoggingAndTelemetry', function () {
193202
]);
194203
});
195204

205+
describe('device ID', function () {
206+
let loggingAndTelemetry: MongoshLoggingAndTelemetry;
207+
let bus: EventEmitter;
208+
beforeEach(function () {
209+
bus = new EventEmitter();
210+
loggingAndTelemetry = setupLoggingAndTelemetry({
211+
...testLoggingArguments,
212+
bus,
213+
deviceId: undefined,
214+
});
215+
});
216+
217+
it('uses device ID "unknown" if it fails to resolve it', async function () {
218+
loggingAndTelemetry.attachLogger(logger);
219+
// eslint-disable-next-line @typescript-eslint/no-var-requires
220+
sinon.stub(require('child_process'), 'exec').throws();
221+
222+
bus.emit('mongosh:new-user', { userId, anonymousId: userId });
223+
224+
await eventually(() => {
225+
expect(analyticsOutput).deep.equal([
226+
[
227+
'identify',
228+
{
229+
deviceId: undefined,
230+
anonymousId: userId,
231+
traits: {
232+
platform: process.platform,
233+
arch: process.arch,
234+
session_id: logId,
235+
},
236+
},
237+
],
238+
[
239+
'identify',
240+
{
241+
deviceId: 'unknown',
242+
anonymousId: userId,
243+
traits: {
244+
platform: process.platform,
245+
arch: process.arch,
246+
session_id: logId,
247+
},
248+
},
249+
],
250+
]);
251+
});
252+
sinon.restore();
253+
});
254+
255+
it('automatically sets up device ID for telemetry', async function () {
256+
loggingAndTelemetry.attachLogger(logger);
257+
258+
bus.emit('mongosh:new-user', { userId, anonymousId: userId });
259+
260+
const deviceId = await getDeviceId();
261+
262+
await eventually(() => {
263+
expect(analyticsOutput).deep.equal([
264+
[
265+
'identify',
266+
{
267+
deviceId: undefined,
268+
anonymousId: userId,
269+
traits: {
270+
platform: process.platform,
271+
arch: process.arch,
272+
session_id: logId,
273+
},
274+
},
275+
],
276+
[
277+
'identify',
278+
{
279+
deviceId,
280+
anonymousId: userId,
281+
traits: {
282+
platform: process.platform,
283+
arch: process.arch,
284+
session_id: logId,
285+
},
286+
},
287+
],
288+
]);
289+
});
290+
});
291+
});
292+
196293
it('detaching logger leads to no logging but persists analytics', function () {
197294
loggingAndTelemetry.attachLogger(logger);
198295

@@ -1060,4 +1157,13 @@ describe('MongoshLoggingAndTelemetry', function () {
10601157
],
10611158
]);
10621159
});
1160+
1161+
describe('getDeviceId()', function () {
1162+
it('is consistent on the same machine', async function () {
1163+
const idA = await getDeviceId();
1164+
const idB = await getDeviceId();
1165+
1166+
expect(idA).equals(idB);
1167+
});
1168+
});
10631169
});

packages/logging/src/logging-and-telemetry.ts

+56-22
Original file line numberDiff line numberDiff line change
@@ -52,29 +52,35 @@ import type {
5252
MongoshLoggingAndTelemetryArguments,
5353
MongoshTrackingProperties,
5454
} from './types';
55-
import { machineIdSync } from 'node-machine-id';
55+
import { machineId } from 'node-machine-id';
56+
import { createHmac } from 'crypto';
5657

5758
export function setupLoggingAndTelemetry(
5859
props: MongoshLoggingAndTelemetryArguments
5960
): MongoshLoggingAndTelemetry {
60-
if (!props.deviceId) {
61-
try {
62-
props.deviceId = machineIdSync();
63-
} catch (error) {
64-
props.bus.emit(
65-
'mongosh:error',
66-
new Error('Failed to get device ID'),
67-
'telemetry'
68-
);
69-
}
70-
}
71-
7261
const loggingAndTelemetry = new LoggingAndTelemetry(props);
7362

7463
loggingAndTelemetry.setup();
7564
return loggingAndTelemetry;
7665
}
7766

67+
/**
68+
* @returns A hashed, unique identifier for the running device.
69+
* @throws If something goes wrong when getting the device ID.
70+
*/
71+
export async function getDeviceId(): Promise<string> {
72+
// Create a hashed format from the all uppercase version of the machine ID
73+
// to match it exactly with the denisbrodbeck/machineid library that Atlas CLI uses.
74+
const originalId = (await machineId(true)).toUpperCase();
75+
const hmac = createHmac('sha256', originalId);
76+
77+
/** This matches the message used to create the hashes in Atlas CLI */
78+
const DEVICE_ID_HASH_MESSAGE = 'atlascli';
79+
80+
hmac.update(DEVICE_ID_HASH_MESSAGE);
81+
return hmac.digest('hex');
82+
}
83+
7884
class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
7985
private static dummyLogger = new MongoLogWriter(
8086
'',
@@ -170,6 +176,7 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
170176
usesShellOption: false,
171177
telemetryAnonymousId: undefined,
172178
userId: undefined,
179+
deviceId: undefined,
173180
};
174181

175182
private setupBusEventListeners(): void {
@@ -207,15 +214,47 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
207214
session_id: this.log.logId,
208215
});
209216

217+
/** Eventually sets up the device ID and re-identifies the user. */
218+
const getCurrentDeviceId = async (): Promise<string> => {
219+
try {
220+
this.busEventState.deviceId ??= this.deviceId ?? (await getDeviceId());
221+
} catch (error) {
222+
this.bus.emit('mongosh:error', error as Error, 'telemetry');
223+
this.busEventState.deviceId = 'unknown';
224+
}
225+
return this.busEventState.deviceId;
226+
};
227+
210228
const getTelemetryUserIdentity = (): MongoshAnalyticsIdentity => {
211229
return {
230+
deviceId: this.busEventState.deviceId ?? this.deviceId,
212231
anonymousId:
213232
this.busEventState.telemetryAnonymousId ??
214233
(this.busEventState.userId as string),
215-
deviceId: this.deviceId,
216234
};
217235
};
218236

237+
const identifyUser = async (): Promise<void> => {
238+
// We first instantly identify the user with the
239+
// current user information we have.
240+
this.analytics.identify({
241+
...getTelemetryUserIdentity(),
242+
traits: getUserTraits(),
243+
});
244+
245+
if (!this.busEventState.deviceId) {
246+
// If the Device ID had not been resolved yet,
247+
// we wait to resolve it and re-identify the user.
248+
this.busEventState.deviceId ??= await getCurrentDeviceId();
249+
250+
this.analytics.identify({
251+
...getTelemetryUserIdentity(),
252+
deviceId: await getCurrentDeviceId(),
253+
traits: getUserTraits(),
254+
});
255+
}
256+
};
257+
219258
onBus('mongosh:start-mongosh-repl', (ev: StartMongoshReplEvent) => {
220259
this.log.info(
221260
'MONGOSH',
@@ -301,10 +340,8 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
301340
}
302341
this.busEventState.telemetryAnonymousId =
303342
newTelemetryUserIdentity.anonymousId;
304-
this.analytics.identify({
305-
...getTelemetryUserIdentity(),
306-
traits: getUserTraits(),
307-
});
343+
344+
void identifyUser();
308345
}
309346
);
310347

@@ -320,10 +357,7 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
320357
} else {
321358
this.busEventState.userId = updatedTelemetryUserIdentity.userId;
322359
}
323-
this.analytics.identify({
324-
...getTelemetryUserIdentity(),
325-
traits: getUserTraits(),
326-
});
360+
void identifyUser();
327361
this.log.info(
328362
'MONGOSH',
329363
mongoLogId(1_000_000_005),

packages/logging/src/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -34,4 +34,5 @@ export type LoggingAndTelemetryBusEventState = {
3434
usesShellOption: boolean;
3535
telemetryAnonymousId: string | undefined;
3636
userId: string | undefined;
37+
deviceId: string | undefined;
3738
};

0 commit comments

Comments
 (0)