Skip to content

chore(telemetry): add device ID resolution COMPASS-8443 #1034

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
May 5, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -1318,6 +1318,7 @@
"@mongodb-js/compass-components": "^1.34.8",
"@mongodb-js/connection-form": "1.47.8",
"@mongodb-js/connection-info": "^0.11.9",
"@mongodb-js/device-id": "^0.2.0",
"@mongodb-js/mongodb-constants": "^0.11.1",
"@mongosh/browser-runtime-electron": "^3.10.0",
"@mongosh/i18n": "^2.9.1",
@@ -1338,6 +1339,7 @@
"mongodb-log-writer": "^2.4.1",
"mongodb-query-parser": "^4.3.2",
"mongodb-schema": "^12.6.2",
"node-machine-id": "1.1.12",
"numeral": "^2.0.6",
"query-string": "^7.1.3",
"react": "^18.3.1",
3 changes: 1 addition & 2 deletions src/explorer/helpTree.ts
Original file line number Diff line number Diff line change
@@ -116,8 +116,7 @@ export default class HelpTree
iconName: 'report',
});

const telemetryUserIdentity =
this._telemetryService?.getTelemetryUserIdentity();
const telemetryUserIdentity = this._telemetryService?.userIdentity;

const atlas = new HelpLinkTreeItem({
title: 'Create Free Atlas Cluster',
2 changes: 1 addition & 1 deletion src/mdbExtensionController.ts
Original file line number Diff line number Diff line change
@@ -180,7 +180,7 @@ export default class MDBExtensionController implements vscode.Disposable {
this._explorerController.activateConnectionsTreeView();
this._helpExplorer.activateHelpTreeView();
this._playgroundsExplorer.activatePlaygroundsTreeView();
this._telemetryService.activateSegmentAnalytics();
void this._telemetryService.activateSegmentAnalytics();
this._participantController.createParticipant(this._context);

await this._connectionController.loadSavedConnections();
73 changes: 54 additions & 19 deletions src/telemetry/telemetryService.ts
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ import path from 'path';
import * as vscode from 'vscode';
import { config } from 'dotenv';
import type { DataService } from 'mongodb-data-service';
import fs from 'fs';
import fs from 'fs/promises';
import { Analytics as SegmentAnalytics } from '@segment/analytics-node';
import { throttle } from 'lodash';

@@ -18,6 +18,10 @@ import {
SidePanelOpenedTelemetryEvent,
ParticipantResponseFailedTelemetryEvent,
} from './telemetryEvents';
import { getDeviceId } from '@mongodb-js/device-id';

// eslint-disable-next-line @typescript-eslint/no-var-requires
const nodeMachineId = require('node-machine-id');

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

/**
* This controller manages telemetry.
*/
export class TelemetryService {
_segmentAnalytics?: SegmentAnalytics;
_segmentAnonymousId: string;
_segmentKey?: string; // The segment API write key.

private _context: vscode.ExtensionContext;
private _shouldTrackTelemetry: boolean; // When tests run the extension, we don't want to track telemetry.
private _segmentAnalytics?: SegmentAnalytics;
public _segmentKey?: string; // The segment API write key.
private eventBuffer: TelemetryEvent[] = [];
private isBufferingEvents = true;
public userIdentity: {
anonymousId: string;
deviceId?: string;
};
private resolveDeviceId: ((value: string) => void) | undefined;
private readonly _context: vscode.ExtensionContext;
private readonly _shouldTrackTelemetry: boolean; // When tests run the extension, we don't want to track telemetry.

constructor(
storageController: StorageController,
@@ -48,11 +58,12 @@ export class TelemetryService {
const { anonymousId } = storageController.getUserIdentity();
this._context = context;
this._shouldTrackTelemetry = shouldTrackTelemetry || false;
this._segmentAnonymousId = anonymousId;
this._segmentKey = this._readSegmentKey();
this.userIdentity = {
anonymousId,
};
}

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

try {
@@ -61,18 +72,21 @@ export class TelemetryService {
'./constants.json'
);
// eslint-disable-next-line no-sync
const constantsFile = fs.readFileSync(segmentKeyFileLocation, 'utf8');
const constantsFile = await fs.readFile(segmentKeyFileLocation, {
encoding: 'utf8',
});
const { segmentKey } = JSON.parse(constantsFile) as {
segmentKey?: string;
};
return segmentKey;
} catch (error) {
log.error('Failed to read segmentKey from the constants file', error);
return;
return undefined;
}
}

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

const segmentProperties = this.getTelemetryUserIdentity();
this._segmentAnalytics.identify(segmentProperties);
log.info('Segment analytics activated', segmentProperties);
this.userIdentity = await this.getTelemetryUserIdentity();
this._segmentAnalytics.identify(this.userIdentity);
this.isBufferingEvents = false;
log.info('Segment analytics activated', this.userIdentity);

// Process buffered events
let event: TelemetryEvent | undefined;
while ((event = this.eventBuffer.shift())) {
this.track(event);
}
}

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

track(event: TelemetryEvent): void {
try {
if (this.isBufferingEvents) {
this.eventBuffer.push(event);
return;
}

this._segmentAnalyticsTrack({
...this.getTelemetryUserIdentity(),
...this.userIdentity,
event: event.type,
properties: {
...event.properties,
@@ -153,9 +180,17 @@ export class TelemetryService {
this.track(new NewConnectionTelemetryEvent(connectionTelemetryProperties));
}

getTelemetryUserIdentity(): { anonymousId: string } {
private async getTelemetryUserIdentity(): Promise<typeof this.userIdentity> {
const { value: deviceId, resolve: resolveDeviceId } = getDeviceId({
getMachineId: (): Promise<string> => nodeMachineId.machineId(true),
isNodeMachineId: true,
});

this.resolveDeviceId = resolveDeviceId;

return {
anonymousId: this._segmentAnonymousId,
anonymousId: this.userIdentity.anonymousId,
deviceId: await deviceId,
};
}

2 changes: 1 addition & 1 deletion src/test/suite/explorer/helpExplorer.test.ts
Original file line number Diff line number Diff line change
@@ -42,7 +42,7 @@ suite('Help Explorer Test Suite', function () {
assert.strictEqual(atlasHelpItem.label, 'Create Free Atlas Cluster');
assert.strictEqual(atlasHelpItem.url.includes('mongodb.com'), true);
const { anonymousId } =
mdbTestExtension.testExtensionController._telemetryService.getTelemetryUserIdentity();
mdbTestExtension.testExtensionController._telemetryService.userIdentity;
assert.strictEqual(
new URL(atlasHelpItem.url).searchParams.get('ajs_aid'),
anonymousId
Loading