diff --git a/packages/atlas-service/src/main.spec.ts b/packages/atlas-service/src/main.spec.ts index eb8ab2dd166..d763dd0073a 100644 --- a/packages/atlas-service/src/main.spec.ts +++ b/packages/atlas-service/src/main.spec.ts @@ -1,11 +1,11 @@ import Sinon from 'sinon'; import { expect } from 'chai'; -import { AtlasService, getTrackingUserInfo, throwIfNotOk } from './main'; +import { AtlasService, throwIfNotOk } from './main'; +import * as util from './util'; import { EventEmitter } from 'events'; import { createSandboxFromDefaultPreferences } from 'compass-preferences-model'; import type { PreferencesAccess } from 'compass-preferences-model'; import type { AtlasUserConfigStore } from './user-config-store'; -import type { AtlasUserInfo } from './util'; function getListenerCount(emitter: EventEmitter) { return emitter.eventNames().reduce((acc, name) => { @@ -74,6 +74,14 @@ describe('AtlasServiceMain', function () { let preferences: PreferencesAccess; + let getTrackingUserInfoStub: Sinon.SinonStubbedMember< + typeof util.getTrackingUserInfo + >; + + before(function () { + getTrackingUserInfoStub = sandbox.stub(util, 'getTrackingUserInfo'); + }); + beforeEach(async function () { AtlasService['ipcMain'] = { handle: sandbox.stub(), @@ -114,8 +122,15 @@ describe('AtlasServiceMain', function () { sandbox.resetHistory(); }); + after(function () { + sandbox.restore(); + }); + describe('signIn', function () { it('should sign in using oidc plugin', async function () { + const atlasUid = 'abcdefgh'; + getTrackingUserInfoStub.returns({ auid: atlasUid }); + const userInfo = await AtlasService.signIn(); expect( mockOidcPlugin.mongoClientOptions.authMechanismProperties @@ -124,6 +139,9 @@ describe('AtlasServiceMain', function () { // proper error message from oidc plugin in case of failed sign in ).to.have.been.calledTwice; expect(userInfo).to.have.property('sub', '1234'); + expect(preferences.getPreferences().telemetryAtlasUserId).to.equal( + atlasUid + ); }); it('should debounce inflight sign in requests', async function () { @@ -522,19 +540,6 @@ describe('AtlasServiceMain', function () { }); }); - describe('getTrackingUserInfo', function () { - it('should return required tracking info from user info', function () { - expect( - getTrackingUserInfo({ - sub: '1234', - primaryEmail: 'test@example.com', - } as AtlasUserInfo) - ).to.deep.eq({ - auid: '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4', - }); - }); - }); - describe('setupAIAccess', function () { beforeEach(async function () { await preferences.savePreferences({ diff --git a/packages/atlas-service/src/main.ts b/packages/atlas-service/src/main.ts index 6b0fb073104..eccb414ab78 100644 --- a/packages/atlas-service/src/main.ts +++ b/packages/atlas-service/src/main.ts @@ -1,8 +1,7 @@ import { shell, app } from 'electron'; import { URL, URLSearchParams } from 'url'; -import { createHash } from 'crypto'; import type { AuthFlowType, MongoDBOIDCPlugin } from '@mongodb-js/oidc-plugin'; -import { AtlasServiceError } from './util'; +import { AtlasServiceError, getTrackingUserInfo } from './util'; import { createMongoDBOIDCPlugin, hookLoggerToMongoLogWriter as oidcPluginHookLoggerToMongoLogWriter, @@ -114,14 +113,6 @@ const TOKEN_TYPE_TO_HINT = { refreshToken: 'refresh_token', } as const; -export function getTrackingUserInfo(userInfo: AtlasUserInfo) { - return { - // AUID is shared Cloud user identificator that can be tracked through - // various MongoDB properties - auid: createHash('sha256').update(userInfo.sub, 'utf8').digest('hex'), - }; -} - export type AtlasServiceConfig = { atlasApiBaseUrl: string; atlasApiUnauthBaseUrl: string; @@ -374,7 +365,11 @@ export class AtlasService { 'AtlasService', 'Signed in successfully' ); - track('Atlas Sign In Success', getTrackingUserInfo(userInfo)); + const { auid } = getTrackingUserInfo(userInfo); + track('Atlas Sign In Success', { auid }); + await this.preferences.savePreferences({ + telemetryAtlasUserId: auid, + }); return userInfo; } catch (err) { track('Atlas Sign In Error', { diff --git a/packages/atlas-service/src/util.spec.ts b/packages/atlas-service/src/util.spec.ts new file mode 100644 index 00000000000..65ae6a9667f --- /dev/null +++ b/packages/atlas-service/src/util.spec.ts @@ -0,0 +1,16 @@ +import { getTrackingUserInfo } from './util'; +import type { AtlasUserInfo } from './util'; +import { expect } from 'chai'; + +describe('getTrackingUserInfo', function () { + it('should return required tracking info from user info', function () { + expect( + getTrackingUserInfo({ + sub: '1234', + primaryEmail: 'test@example.com', + } as AtlasUserInfo) + ).to.deep.eq({ + auid: '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4', + }); + }); +}); diff --git a/packages/atlas-service/src/util.ts b/packages/atlas-service/src/util.ts index 83802330ef4..6adfd3b3c41 100644 --- a/packages/atlas-service/src/util.ts +++ b/packages/atlas-service/src/util.ts @@ -1,6 +1,7 @@ import type * as plugin from '@mongodb-js/oidc-plugin'; import util from 'util'; import type { AtlasUserConfig } from './user-config-store'; +import { createHash } from 'crypto'; export type AtlasUserInfo = { sub: string; @@ -160,3 +161,11 @@ export class AtlasServiceError extends Error { this.detail = detail; } } + +export function getTrackingUserInfo(userInfo: AtlasUserInfo) { + return { + // AUID is shared Cloud user identificator that can be tracked through + // various MongoDB properties + auid: createHash('sha256').update(userInfo.sub, 'utf8').digest('hex'), + }; +} diff --git a/packages/compass-e2e-tests/tests/atlas-login.test.ts b/packages/compass-e2e-tests/tests/atlas-login.test.ts index e733a0b3494..7989d1ef817 100644 --- a/packages/compass-e2e-tests/tests/atlas-login.test.ts +++ b/packages/compass-e2e-tests/tests/atlas-login.test.ts @@ -13,6 +13,8 @@ import { expect } from 'chai'; import { createNumbersCollection } from '../helpers/insert-data'; import { AcceptTOSToggle } from '../helpers/selectors'; import { startMockAtlasServiceServer } from '../helpers/atlas-service'; +import type { Telemetry } from '../helpers/telemetry'; +import { startTelemetryServer } from '../helpers/telemetry'; const DEFAULT_TOKEN_PAYLOAD = { expires_in: 3600, @@ -145,6 +147,48 @@ describe('Atlas Login', function () { }); }); + describe('telemetry', () => { + let telemetry: Telemetry; + + before(async function () { + telemetry = await startTelemetryServer(); + }); + + after(async function () { + await telemetry.stop(); + }); + + it('should send identify after the user has logged in', async function () { + const atlasUserIdBefore = await browser.getFeature( + 'telemetryAtlasUserId' + ); + expect(atlasUserIdBefore).to.not.exist; + + await browser.openSettingsModal('Feature Preview'); + + await browser.clickVisible(Selectors.LogInWithAtlasButton); + + const loginStatus = browser.$(Selectors.AtlasLoginStatus); + await browser.waitUntil(async () => { + return ( + (await loginStatus.getText()).trim() === + 'Logged in with Atlas account test@example.com' + ); + }); + + const atlasUserIdAfter = await browser.getFeature( + 'telemetryAtlasUserId' + ); + expect(atlasUserIdAfter).to.be.a('string'); + + const identify = telemetry + .events() + .find((entry) => entry.type === 'identify'); + expect(identify.traits.platform).to.equal(process.platform); + expect(identify.traits.arch).to.match(/^(x64|arm64)$/); + }); + }); + it('should allow to accept TOS when signed in', async function () { await browser.openSettingsModal('Feature Preview'); diff --git a/packages/compass-e2e-tests/tests/logging.test.ts b/packages/compass-e2e-tests/tests/logging.test.ts index 395c6bdb51f..1df5547242a 100644 --- a/packages/compass-e2e-tests/tests/logging.test.ts +++ b/packages/compass-e2e-tests/tests/logging.test.ts @@ -379,13 +379,16 @@ describe('Logging and Telemetry integration', function () { }); }); - describe('on subsequent run', function () { + describe('on subsequent run - with atlas user id', function () { let compass: Compass; let telemetry: Telemetry; + const auid = 'abcdef'; before(async function () { telemetry = await startTelemetryServer(); compass = await init(this.test?.fullTitle()); + + await compass.browser.setFeature('telemetryAtlasUserId', auid); }); afterEach(async function () { @@ -398,8 +401,6 @@ describe('Logging and Telemetry integration', function () { }); it('tracks an event for identify call', function () { - console.log(telemetry.events()); - const identify = telemetry .events() .find((entry) => entry.type === 'identify'); diff --git a/packages/compass-preferences-model/src/preferences-schema.ts b/packages/compass-preferences-model/src/preferences-schema.ts index 0cfc28b6a37..a186b489e2a 100644 --- a/packages/compass-preferences-model/src/preferences-schema.ts +++ b/packages/compass-preferences-model/src/preferences-schema.ts @@ -67,6 +67,7 @@ export type InternalUserPreferences = { lastKnownVersion: string; currentUserId?: string; telemetryAnonymousId?: string; + telemetryAtlasUserId?: string; userCreatedAt: number; }; @@ -313,6 +314,17 @@ export const storedUserPreferencesProps: Required<{ validator: z.string().uuid().optional(), type: 'string', }, + /** + * Stores a unique telemetry atlas ID for the current user. + */ + telemetryAtlasUserId: { + ui: false, + cli: false, + global: false, + description: null, + validator: z.string().optional(), + type: 'string', + }, /** * Stores the timestamp for when the user was created */ diff --git a/packages/compass/src/main/telemetry.ts b/packages/compass/src/main/telemetry.ts index e5276aa85a5..d2422f73139 100644 --- a/packages/compass/src/main/telemetry.ts +++ b/packages/compass/src/main/telemetry.ts @@ -36,6 +36,7 @@ class CompassTelemetry { private static state: 'enabled' | 'disabled' = 'disabled'; 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 lastReportedScreen = ''; private static osInfo: ReturnType extends Promise ? Partial @@ -79,6 +80,7 @@ class CompassTelemetry { } this.analytics.track({ + userId: this.telemetryAtlasUserId, anonymousId: this.telemetryAnonymousId, event: info.event, properties: { ...info.properties, ...commonProperties }, @@ -105,6 +107,7 @@ class CompassTelemetry { this.telemetryAnonymousId ) { this.analytics.identify({ + userId: this.telemetryAtlasUserId, anonymousId: this.telemetryAnonymousId, traits: { ...this._getCommonProperties(), @@ -127,9 +130,10 @@ class CompassTelemetry { private static async _init(app: typeof CompassApplication) { const { preferences } = app; - const { trackUsageStatistics, telemetryAnonymousId } = + const { trackUsageStatistics, telemetryAnonymousId, telemetryAtlasUserId } = preferences.getPreferences(); this.telemetryAnonymousId = telemetryAnonymousId ?? ''; + this.telemetryAtlasUserId = telemetryAtlasUserId; try { this.osInfo = await getOsInfo(); @@ -176,11 +180,22 @@ class CompassTelemetry { this.state = 'disabled'; } }; + const onAtlasUserIdChanged = (value?: string) => { + if (value) { + this.telemetryAtlasUserId = value; + this.identify(); + } + }; + onTrackUsageStatisticsChanged(trackUsageStatistics); // initial setup with current value preferences.onPreferenceValueChanged( 'trackUsageStatistics', onTrackUsageStatisticsChanged ); + preferences.onPreferenceValueChanged( + 'telemetryAtlasUserId', + onAtlasUserIdChanged + ); process.on('compass:track', (meta: EventInfo) => { this._track(meta);