Skip to content
Merged
Show file tree
Hide file tree
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
35 changes: 20 additions & 15 deletions packages/atlas-service/src/main.spec.ts
Original file line number Diff line number Diff line change
@@ -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) => {
Expand Down Expand Up @@ -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(),
Expand Down Expand Up @@ -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
Expand All @@ -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 () {
Expand Down Expand Up @@ -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({
Expand Down
17 changes: 6 additions & 11 deletions packages/atlas-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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', {
Expand Down
16 changes: 16 additions & 0 deletions packages/atlas-service/src/util.spec.ts
Original file line number Diff line number Diff line change
@@ -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',
});
});
});
9 changes: 9 additions & 0 deletions packages/atlas-service/src/util.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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'),
};
}
44 changes: 44 additions & 0 deletions packages/compass-e2e-tests/tests/atlas-login.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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');

Expand Down
7 changes: 4 additions & 3 deletions packages/compass-e2e-tests/tests/logging.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand All @@ -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');
Expand Down
12 changes: 12 additions & 0 deletions packages/compass-preferences-model/src/preferences-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type InternalUserPreferences = {
lastKnownVersion: string;
currentUserId?: string;
telemetryAnonymousId?: string;
telemetryAtlasUserId?: string;
userCreatedAt: number;
};

Expand Down Expand Up @@ -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
*/
Expand Down
17 changes: 16 additions & 1 deletion packages/compass/src/main/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof getOsInfo> extends Promise<infer T>
? Partial<T>
Expand Down Expand Up @@ -79,6 +80,7 @@ class CompassTelemetry {
}

this.analytics.track({
userId: this.telemetryAtlasUserId,
anonymousId: this.telemetryAnonymousId,
event: info.event,
properties: { ...info.properties, ...commonProperties },
Expand All @@ -105,6 +107,7 @@ class CompassTelemetry {
this.telemetryAnonymousId
) {
this.analytics.identify({
userId: this.telemetryAtlasUserId,
anonymousId: this.telemetryAnonymousId,
traits: {
...this._getCommonProperties(),
Expand All @@ -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();
Expand Down Expand Up @@ -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);
Expand Down