diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e6067c2cb29..95ed1587da8 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,7 @@ updates: - dependency-name: "@mongosh/*" - dependency-name: "@mongodb-js/*" - dependency-name: os-dns-native + - dependency-name: native-machine-id - dependency-name: system-ca # All the electron and its related deps @@ -36,6 +37,7 @@ updates: - "mongodb-client-encryption" - "kerberos" - "os-dns-native" + - "native-machine-id" - "system-ca" - "saslprep" diff --git a/configs/webpack-config-compass/src/externals.ts b/configs/webpack-config-compass/src/externals.ts index 99246639311..9d31944914c 100644 --- a/configs/webpack-config-compass/src/externals.ts +++ b/configs/webpack-config-compass/src/externals.ts @@ -12,6 +12,7 @@ export const sharedExternals: string[] = [ 'keytar', 'kerberos', 'interruptor', + 'native-machine-id', 'os-dns-native', 'system-ca', 'win-export-certificate-and-key', diff --git a/docs/tracking-plan.md b/docs/tracking-plan.md index 79e17c13376..c12b3a71e8a 100644 --- a/docs/tracking-plan.md +++ b/docs/tracking-plan.md @@ -230,6 +230,8 @@ Traits sent along with the Segment identify call - **arch** (required): `string` - The architecture of the system's processor, derived from Node.js `os.arch()`. 'x64' for 64-bit processors and 'arm' for ARM processors. +- **device_id** (required): `string` + - A unique identifier for the device running Compass. Set to `"unknown"` if not available. - **os_type** (optional): `string | undefined` - The type of operating system, including specific operating system names or types (e.g., 'Linux', 'Windows_NT', 'Darwin'). diff --git a/package-lock.json b/package-lock.json index f0b1aedda62..f02922d44a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8017,6 +8017,12 @@ "resolved": "packages/databases-collections-list", "link": true }, + "node_modules/@mongodb-js/device-id": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/device-id/-/device-id-0.2.0.tgz", + "integrity": "sha512-auEMkQc6hpSQSQziK5AbeuJeVnI7OQvWmaoMIWcXrMm+RA6pF0ADXZPS6kBtBIrRhWElV6PVYiq+Gfzsss2RYQ==", + "license": "Apache-2.0" + }, "node_modules/@mongodb-js/devtools-connect": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.7.2.tgz", @@ -43346,11 +43352,13 @@ "hasInstallScript": true, "license": "SSPL", "dependencies": { + "@mongodb-js/device-id": "^0.2.0", "@mongosh/node-runtime-worker-thread": "^3.3.8", "clipboard": "^2.0.6", "kerberos": "^2.2.1", "keytar": "^7.9.0", "mongodb-client-encryption": "^6.3.0", + "native-machine-id": "^0.1.1", "os-dns-native": "^1.2.1", "system-ca": "^2.0.0" }, @@ -59446,6 +59454,11 @@ } } }, + "@mongodb-js/device-id": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@mongodb-js/device-id/-/device-id-0.2.0.tgz", + "integrity": "sha512-auEMkQc6hpSQSQziK5AbeuJeVnI7OQvWmaoMIWcXrMm+RA6pF0ADXZPS6kBtBIrRhWElV6PVYiq+Gfzsss2RYQ==" + }, "@mongodb-js/devtools-connect": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/@mongodb-js/devtools-connect/-/devtools-connect-3.7.2.tgz", @@ -79461,6 +79474,7 @@ "@mongodb-js/compass-workspaces": "^0.35.0", "@mongodb-js/connection-info": "^0.12.0", "@mongodb-js/connection-storage": "^0.29.0", + "@mongodb-js/device-id": "^0.2.0", "@mongodb-js/devtools-proxy-support": "^0.4.4", "@mongodb-js/eslint-config-compass": "^1.3.8", "@mongodb-js/get-os-info": "^0.4.0", @@ -79511,6 +79525,7 @@ "mongodb-instance-model": "^12.28.0", "mongodb-log-writer": "^2.3.4", "mongodb-ns": "^2.4.2", + "native-machine-id": "^0.1.1", "os-dns-native": "^1.2.1", "react": "^17.0.2", "react-dom": "^17.0.2", diff --git a/packages/compass-e2e-tests/helpers/telemetry.ts b/packages/compass-e2e-tests/helpers/telemetry.ts index b6b9198fdad..5c0c6207cbc 100644 --- a/packages/compass-e2e-tests/helpers/telemetry.ts +++ b/packages/compass-e2e-tests/helpers/telemetry.ts @@ -5,6 +5,7 @@ import { EJSON } from 'bson'; import type { MongoLogEntry } from 'mongodb-log-writer'; import { TEST_COMPASS_WEB } from './compass'; import type { CompassBrowser } from './compass-browser'; +import { expect } from 'chai'; export type Telemetry = { requests: any[]; @@ -109,3 +110,11 @@ export async function startTelemetryServer(): Promise { screens, }; } + +export function deleteCommonVariedProperties(entry: unknown): void { + expect(entry).to.have.property('connection_id'); + delete (entry as { connection_id: unknown }).connection_id; + + // Device ID is not set in all track events so we delete without asserting. + delete (entry as { device_id: unknown }).device_id; +} diff --git a/packages/compass-e2e-tests/tests/collection-bulk-delete.test.ts b/packages/compass-e2e-tests/tests/collection-bulk-delete.test.ts index eb93e94a822..385098ae8c5 100644 --- a/packages/compass-e2e-tests/tests/collection-bulk-delete.test.ts +++ b/packages/compass-e2e-tests/tests/collection-bulk-delete.test.ts @@ -1,6 +1,9 @@ import { expect } from 'chai'; import type { CompassBrowser } from '../helpers/compass-browser'; -import { startTelemetryServer } from '../helpers/telemetry'; +import { + deleteCommonVariedProperties, + startTelemetryServer, +} from '../helpers/telemetry'; import type { Telemetry } from '../helpers/telemetry'; import { init, @@ -59,8 +62,7 @@ describe('Bulk Delete', function () { // Check the telemetry const openedEvent = await telemetryEntry('Bulk Delete Opened'); - expect(openedEvent.connection_id).to.exist; - delete openedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(openedEvent); expect(openedEvent).to.deep.equal({}); @@ -95,8 +97,7 @@ describe('Bulk Delete', function () { // this id is always different, because the connection is not a saved one // so we just check it exists for simplicity - expect(executedEvent.connection_id).to.exist; - delete executedEvent.connection_id; + deleteCommonVariedProperties(executedEvent); expect(executedEvent).to.deep.equal({}); @@ -177,8 +178,7 @@ describe('Bulk Delete', function () { // Check the telemetry const openedEvent = await telemetryEntry('Delete Export Opened'); - expect(openedEvent.connection_id).to.exist; - delete openedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(openedEvent); expect(openedEvent).to.deep.equal({}); diff --git a/packages/compass-e2e-tests/tests/collection-bulk-update.test.ts b/packages/compass-e2e-tests/tests/collection-bulk-update.test.ts index 9340a0982cd..370bdf20fa0 100644 --- a/packages/compass-e2e-tests/tests/collection-bulk-update.test.ts +++ b/packages/compass-e2e-tests/tests/collection-bulk-update.test.ts @@ -1,6 +1,9 @@ import { expect } from 'chai'; import type { CompassBrowser } from '../helpers/compass-browser'; -import { startTelemetryServer } from '../helpers/telemetry'; +import { + deleteCommonVariedProperties, + startTelemetryServer, +} from '../helpers/telemetry'; import type { Telemetry } from '../helpers/telemetry'; import { init, @@ -59,8 +62,7 @@ describe('Bulk Update', () => { // Check the telemetry const openedEvent = await telemetryEntry('Bulk Update Opened'); - expect(openedEvent.connection_id).to.exist; - delete openedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(openedEvent); expect(openedEvent).to.deep.equal({ isUpdatePreviewSupported: true, @@ -125,8 +127,7 @@ describe('Bulk Update', () => { // Check the telemetry const executedEvent = await telemetryEntry('Bulk Update Executed'); - expect(executedEvent.connection_id).to.exist; - delete executedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(executedEvent); expect(executedEvent).to.deep.equal({ isUpdatePreviewSupported: true, @@ -173,8 +174,7 @@ describe('Bulk Update', () => { // Check the telemetry const favoritedEvent = await telemetryEntry('Bulk Update Favorited'); - expect(favoritedEvent.connection_id).to.exist; - delete favoritedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(favoritedEvent); expect(favoritedEvent).to.deep.equal({ isUpdatePreviewSupported: true, diff --git a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts index 2174ae2dae3..cd2db036837 100644 --- a/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts +++ b/packages/compass-e2e-tests/tests/collection-documents-tab.test.ts @@ -1,7 +1,10 @@ import chai from 'chai'; import clipboard from 'clipboardy'; import type { CompassBrowser } from '../helpers/compass-browser'; -import { startTelemetryServer } from '../helpers/telemetry'; +import { + deleteCommonVariedProperties, + startTelemetryServer, +} from '../helpers/telemetry'; import type { Telemetry } from '../helpers/telemetry'; import { init, @@ -168,8 +171,7 @@ describe('Collection documents tab', function () { // Check the telemetry const queryExecutedEvent = await telemetryEntry('Query Executed'); - expect(queryExecutedEvent.connection_id).to.exist; - delete queryExecutedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(queryExecutedEvent); expect(queryExecutedEvent).to.deep.equal({ changed_maxtimems: false, @@ -208,8 +210,7 @@ describe('Collection documents tab', function () { // Check the telemetry const queryExecutedEvent = await telemetryEntry('Query Executed'); - expect(queryExecutedEvent.connection_id).to.exist; - delete queryExecutedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(queryExecutedEvent); expect(queryExecutedEvent).to.deep.equal({ changed_maxtimems: false, diff --git a/packages/compass-e2e-tests/tests/collection-export.test.ts b/packages/compass-e2e-tests/tests/collection-export.test.ts index db202674e4e..dd6c90ac463 100644 --- a/packages/compass-e2e-tests/tests/collection-export.test.ts +++ b/packages/compass-e2e-tests/tests/collection-export.test.ts @@ -1,7 +1,10 @@ import { promises as fs } from 'fs'; import { expect } from 'chai'; import type { CompassBrowser } from '../helpers/compass-browser'; -import { startTelemetryServer } from '../helpers/telemetry'; +import { + deleteCommonVariedProperties, + startTelemetryServer, +} from '../helpers/telemetry'; import type { Telemetry } from '../helpers/telemetry'; import { init, @@ -143,8 +146,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, has_projection: false, @@ -214,8 +216,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, has_projection: false, @@ -283,8 +284,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, has_projection: true, @@ -337,8 +337,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: true, file_type: 'csv', @@ -411,8 +410,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, file_type: 'json', @@ -480,8 +478,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, file_type: 'json', @@ -534,8 +531,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: true, file_type: 'json', @@ -590,8 +586,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: true, file_type: 'json', @@ -681,8 +676,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, file_type: 'csv', @@ -768,8 +762,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, file_type: 'json', @@ -863,8 +856,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, file_type: 'csv', @@ -978,8 +970,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, file_type: 'csv', @@ -1081,8 +1072,7 @@ describe('Collection export', function () { const exportCompletedEvent = await telemetryEntry('Export Completed'); delete exportCompletedEvent.duration; // Duration varies. - expect(exportCompletedEvent.connection_id).to.exist; - delete exportCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(exportCompletedEvent); expect(exportCompletedEvent).to.deep.equal({ all_docs: false, file_type: 'json', diff --git a/packages/compass-e2e-tests/tests/collection-import.test.ts b/packages/compass-e2e-tests/tests/collection-import.test.ts index 91734a7d0fb..dc0da1fc277 100644 --- a/packages/compass-e2e-tests/tests/collection-import.test.ts +++ b/packages/compass-e2e-tests/tests/collection-import.test.ts @@ -14,7 +14,10 @@ import { } from '../helpers/compass'; import type { Compass } from '../helpers/compass'; import * as Selectors from '../helpers/selectors'; -import { startTelemetryServer } from '../helpers/telemetry'; +import { + deleteCommonVariedProperties, + startTelemetryServer, +} from '../helpers/telemetry'; import type { Telemetry } from '../helpers/telemetry'; import { createDummyCollections, @@ -752,8 +755,7 @@ describe('Collection import', function () { const importCompletedEvent = await telemetryEntry('Import Completed'); delete importCompletedEvent.duration; // Duration varies. - expect(importCompletedEvent.connection_id).to.exist; - delete importCompletedEvent.connection_id; // connection_id varies + deleteCommonVariedProperties(importCompletedEvent); expect(importCompletedEvent).to.deep.equal({ delimiter: ',', newline: '\n', diff --git a/packages/compass-e2e-tests/tests/logging.test.ts b/packages/compass-e2e-tests/tests/logging.test.ts index 6738a6985d2..7506a0e7655 100644 --- a/packages/compass-e2e-tests/tests/logging.test.ts +++ b/packages/compass-e2e-tests/tests/logging.test.ts @@ -59,6 +59,15 @@ describe('Logging and Telemetry integration', function () { ); }); + it('tracks device_id in telemetry events', function () { + const events = telemetry.events(); + const event = events.find((e) => e.properties?.device_id); + + expect(event).to.exist; + expect(event.properties.device_id).to.not.equal('unknown'); + expect(event.properties.device_id).to.match(/^[a-f0-9]{64}$/); + }); + it('tracks an event for identify call', function () { const identify = telemetry .events() diff --git a/packages/compass/.depcheckrc b/packages/compass/.depcheckrc index 4456ebdc20d..48e032f6546 100644 --- a/packages/compass/.depcheckrc +++ b/packages/compass/.depcheckrc @@ -7,6 +7,7 @@ ignores: [ 'kerberos', 'keytar', 'os-dns-native', + 'native-machine-id', 'system-ca', 'win-export-certificate-and-key', 'macos-export-certificate-and-key', diff --git a/packages/compass/package.json b/packages/compass/package.json index f462599556a..052f1e30403 100644 --- a/packages/compass/package.json +++ b/packages/compass/package.json @@ -131,6 +131,7 @@ "keytar", "kerberos", "os-dns-native", + "native-machine-id", "win-export-certificate-and-key", "macos-export-certificate-and-key" ] @@ -180,11 +181,13 @@ "email": "compass@mongodb.com" }, "dependencies": { + "@mongodb-js/device-id": "^0.2.0", "@mongosh/node-runtime-worker-thread": "^3.3.8", "clipboard": "^2.0.6", "kerberos": "^2.2.1", "keytar": "^7.9.0", "mongodb-client-encryption": "^6.3.0", + "native-machine-id": "^0.1.1", "os-dns-native": "^1.2.1", "system-ca": "^2.0.0" }, diff --git a/packages/compass/src/main/optional-deps.ts b/packages/compass/src/main/optional-deps.ts index 9f90a82ab5c..3c29a419e27 100644 --- a/packages/compass/src/main/optional-deps.ts +++ b/packages/compass/src/main/optional-deps.ts @@ -4,6 +4,7 @@ const attempts = [ [() => require('keytar'), 'keytar'], [() => require('kerberos'), 'kerberos'], [() => require('os-dns-native'), 'os-dns-native'], + [() => require('native-machine-id'), 'native-machine-id'], [ () => process.platform === 'win32' && require('win-export-certificate-and-key'), diff --git a/packages/compass/src/main/telemetry.ts b/packages/compass/src/main/telemetry.ts index 42733f7f2ee..e56b26a5f5c 100644 --- a/packages/compass/src/main/telemetry.ts +++ b/packages/compass/src/main/telemetry.ts @@ -6,6 +6,8 @@ import type { CompassApplication } from './application'; import type { EventEmitter } from 'events'; import { getOsInfo } from '@mongodb-js/get-os-info'; import type { IdentifyTraits } from '@mongodb-js/compass-telemetry'; +import { getDeviceId } from '@mongodb-js/device-id'; +import { getMachineId } from 'native-machine-id'; const { log, mongoLogId } = createLogger('COMPASS-TELEMETRY'); @@ -38,6 +40,7 @@ class CompassTelemetry { private static queuedEvents: EventInfo[] = []; // Events that happen before we fetch user preferences private static telemetryAnonymousId = ''; // The randomly generated anonymous user id. private static telemetryAtlasUserId?: string; + private static telemetryDeviceId = 'unknown'; private static lastReportedScreen = ''; private static osInfo: ReturnType extends Promise ? Partial @@ -53,6 +56,7 @@ class CompassTelemetry { // Used in both track and identify to add common traits // to any event that we send to segment return { + device_id: this.telemetryDeviceId, compass_version: app.getVersion().split('.').slice(0, 2).join('.'), compass_full_version: app.getVersion(), compass_distribution: process.env.HADRON_DISTRIBUTION, @@ -138,6 +142,17 @@ class CompassTelemetry { preferences.getPreferences(); this.telemetryAnonymousId = telemetryAnonymousId ?? ''; this.telemetryAtlasUserId = telemetryAtlasUserId; + this.telemetryDeviceId = await getDeviceId({ + getMachineId: () => getMachineId({ raw: true }), + isNodeMachineId: false, + onError: (err) => + log.error( + mongoLogId(1_001_000_352), + 'Telemetry', + 'Failed to get device ID', + { err: err.message } + ), + }).value; try { this.osInfo = await getOsInfo();