Skip to content

Commit de7518b

Browse files
authored
chore(telemetry): add device ID resolution COMPASS-8443 (#1034)
1 parent ff83831 commit de7518b

File tree

7 files changed

+500
-430
lines changed

7 files changed

+500
-430
lines changed

package-lock.json

+14
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -1318,6 +1318,7 @@
13181318
"@mongodb-js/compass-components": "^1.34.8",
13191319
"@mongodb-js/connection-form": "1.47.8",
13201320
"@mongodb-js/connection-info": "^0.11.9",
1321+
"@mongodb-js/device-id": "^0.2.0",
13211322
"@mongodb-js/mongodb-constants": "^0.11.1",
13221323
"@mongosh/browser-runtime-electron": "^3.10.1",
13231324
"@mongosh/i18n": "^2.9.1",
@@ -1338,6 +1339,7 @@
13381339
"mongodb-log-writer": "^2.4.1",
13391340
"mongodb-query-parser": "^4.3.2",
13401341
"mongodb-schema": "^12.6.2",
1342+
"node-machine-id": "1.1.12",
13411343
"numeral": "^2.0.6",
13421344
"query-string": "^7.1.3",
13431345
"react": "^18.3.1",

src/explorer/helpTree.ts

+1-2
Original file line numberDiff line numberDiff line change
@@ -116,8 +116,7 @@ export default class HelpTree
116116
iconName: 'report',
117117
});
118118

119-
const telemetryUserIdentity =
120-
this._telemetryService?.getTelemetryUserIdentity();
119+
const telemetryUserIdentity = this._telemetryService?.userIdentity;
121120

122121
const atlas = new HelpLinkTreeItem({
123122
title: 'Create Free Atlas Cluster',

src/mdbExtensionController.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ export default class MDBExtensionController implements vscode.Disposable {
180180
this._explorerController.activateConnectionsTreeView();
181181
this._helpExplorer.activateHelpTreeView();
182182
this._playgroundsExplorer.activatePlaygroundsTreeView();
183-
this._telemetryService.activateSegmentAnalytics();
183+
void this._telemetryService.activateSegmentAnalytics();
184184
this._participantController.createParticipant(this._context);
185185

186186
await this._connectionController.loadSavedConnections();

src/telemetry/telemetryService.ts

+54-19
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import path from 'path';
22
import * as vscode from 'vscode';
33
import { config } from 'dotenv';
44
import type { DataService } from 'mongodb-data-service';
5-
import fs from 'fs';
5+
import fs from 'fs/promises';
66
import { Analytics as SegmentAnalytics } from '@segment/analytics-node';
77
import { throttle } from 'lodash';
88

@@ -18,6 +18,10 @@ import {
1818
SidePanelOpenedTelemetryEvent,
1919
ParticipantResponseFailedTelemetryEvent,
2020
} from './telemetryEvents';
21+
import { getDeviceId } from '@mongodb-js/device-id';
22+
23+
// eslint-disable-next-line @typescript-eslint/no-var-requires
24+
const nodeMachineId = require('node-machine-id');
2125

2226
const log = createLogger('telemetry');
2327
// eslint-disable-next-line @typescript-eslint/no-var-requires
@@ -26,19 +30,25 @@ const { version } = require('../../package.json');
2630
export type SegmentProperties = {
2731
event: string;
2832
anonymousId: string;
33+
deviceId?: string;
2934
properties: Record<string, any>;
3035
};
3136

3237
/**
3338
* This controller manages telemetry.
3439
*/
3540
export class TelemetryService {
36-
_segmentAnalytics?: SegmentAnalytics;
37-
_segmentAnonymousId: string;
38-
_segmentKey?: string; // The segment API write key.
39-
40-
private _context: vscode.ExtensionContext;
41-
private _shouldTrackTelemetry: boolean; // When tests run the extension, we don't want to track telemetry.
41+
private _segmentAnalytics?: SegmentAnalytics;
42+
public _segmentKey?: string; // The segment API write key.
43+
private eventBuffer: TelemetryEvent[] = [];
44+
private isBufferingEvents = true;
45+
public userIdentity: {
46+
anonymousId: string;
47+
deviceId?: string;
48+
};
49+
private resolveDeviceId: ((value: string) => void) | undefined;
50+
private readonly _context: vscode.ExtensionContext;
51+
private readonly _shouldTrackTelemetry: boolean; // When tests run the extension, we don't want to track telemetry.
4252

4353
constructor(
4454
storageController: StorageController,
@@ -48,11 +58,12 @@ export class TelemetryService {
4858
const { anonymousId } = storageController.getUserIdentity();
4959
this._context = context;
5060
this._shouldTrackTelemetry = shouldTrackTelemetry || false;
51-
this._segmentAnonymousId = anonymousId;
52-
this._segmentKey = this._readSegmentKey();
61+
this.userIdentity = {
62+
anonymousId,
63+
};
5364
}
5465

55-
private _readSegmentKey(): string | undefined {
66+
private async readSegmentKey(): Promise<string | undefined> {
5667
config({ path: path.join(this._context.extensionPath, '.env') });
5768

5869
try {
@@ -61,18 +72,21 @@ export class TelemetryService {
6172
'./constants.json'
6273
);
6374
// eslint-disable-next-line no-sync
64-
const constantsFile = fs.readFileSync(segmentKeyFileLocation, 'utf8');
75+
const constantsFile = await fs.readFile(segmentKeyFileLocation, {
76+
encoding: 'utf8',
77+
});
6578
const { segmentKey } = JSON.parse(constantsFile) as {
6679
segmentKey?: string;
6780
};
6881
return segmentKey;
6982
} catch (error) {
7083
log.error('Failed to read segmentKey from the constants file', error);
71-
return;
84+
return undefined;
7285
}
7386
}
7487

75-
activateSegmentAnalytics(): void {
88+
async activateSegmentAnalytics(): Promise<void> {
89+
this._segmentKey = await this.readSegmentKey();
7690
if (!this._segmentKey) {
7791
return;
7892
}
@@ -83,12 +97,20 @@ export class TelemetryService {
8397
flushInterval: 10000, // 10 seconds is the default libraries' value.
8498
});
8599

86-
const segmentProperties = this.getTelemetryUserIdentity();
87-
this._segmentAnalytics.identify(segmentProperties);
88-
log.info('Segment analytics activated', segmentProperties);
100+
this.userIdentity = await this.getTelemetryUserIdentity();
101+
this._segmentAnalytics.identify(this.userIdentity);
102+
this.isBufferingEvents = false;
103+
log.info('Segment analytics activated', this.userIdentity);
104+
105+
// Process buffered events
106+
let event: TelemetryEvent | undefined;
107+
while ((event = this.eventBuffer.shift())) {
108+
this.track(event);
109+
}
89110
}
90111

91112
deactivate(): void {
113+
this.resolveDeviceId?.('unknown');
92114
// Flush on demand to make sure that nothing is left in the queue.
93115
void this._segmentAnalytics?.closeAndFlush();
94116
}
@@ -130,8 +152,13 @@ export class TelemetryService {
130152

131153
track(event: TelemetryEvent): void {
132154
try {
155+
if (this.isBufferingEvents) {
156+
this.eventBuffer.push(event);
157+
return;
158+
}
159+
133160
this._segmentAnalyticsTrack({
134-
...this.getTelemetryUserIdentity(),
161+
...this.userIdentity,
135162
event: event.type,
136163
properties: {
137164
...event.properties,
@@ -153,9 +180,17 @@ export class TelemetryService {
153180
this.track(new NewConnectionTelemetryEvent(connectionTelemetryProperties));
154181
}
155182

156-
getTelemetryUserIdentity(): { anonymousId: string } {
183+
private async getTelemetryUserIdentity(): Promise<typeof this.userIdentity> {
184+
const { value: deviceId, resolve: resolveDeviceId } = getDeviceId({
185+
getMachineId: (): Promise<string> => nodeMachineId.machineId(true),
186+
isNodeMachineId: true,
187+
});
188+
189+
this.resolveDeviceId = resolveDeviceId;
190+
157191
return {
158-
anonymousId: this._segmentAnonymousId,
192+
anonymousId: this.userIdentity.anonymousId,
193+
deviceId: await deviceId,
159194
};
160195
}
161196

src/test/suite/explorer/helpExplorer.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ suite('Help Explorer Test Suite', function () {
4242
assert.strictEqual(atlasHelpItem.label, 'Create Free Atlas Cluster');
4343
assert.strictEqual(atlasHelpItem.url.includes('mongodb.com'), true);
4444
const { anonymousId } =
45-
mdbTestExtension.testExtensionController._telemetryService.getTelemetryUserIdentity();
45+
mdbTestExtension.testExtensionController._telemetryService.userIdentity;
4646
assert.strictEqual(
4747
new URL(atlasHelpItem.url).searchParams.get('ajs_aid'),
4848
anonymousId

0 commit comments

Comments
 (0)