Skip to content

Commit

Permalink
chore(atlas-service): add sign in tracking for atlas-service COMPASS-…
Browse files Browse the repository at this point in the history
…7112 (#4774)

* chore(atlas-service): add tracking for sign in and sign out

* chore(atlas-service): add comment explaining what AUID is
  • Loading branch information
gribnoysup authored Aug 30, 2023
1 parent 1513126 commit 0b36060
Show file tree
Hide file tree
Showing 2 changed files with 56 additions and 14 deletions.
45 changes: 33 additions & 12 deletions packages/atlas-service/src/main.spec.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import Sinon from 'sinon';
import { expect } from 'chai';
import { AtlasService, throwIfNotOk } from './main';
import { AtlasService, getTrackingUserInfo, throwIfNotOk } from './main';
import { EventEmitter } from 'events';
import 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 All @@ -22,6 +23,9 @@ describe('AtlasServiceMain', function () {
return { sub: '1234' };
},
},
'http://example.com/v1/revoke?client_id=1234abcd': {
ok: true,
},
}[url];
});

Expand Down Expand Up @@ -468,30 +472,47 @@ describe('AtlasServiceMain', function () {
describe('signOut', function () {
it('should reset service state, revoke tokens, and destroy plugin', async function () {
const logger = new EventEmitter();
const plugin = {
destroy: sandbox.stub().resolves(),
};
AtlasService['openExternal'] = sandbox.stub().resolves();
AtlasService['createMongoDBOIDCPlugin'] = (() => {
return plugin;
}) as any;
AtlasService['fetch'] = sandbox.stub().resolves({ ok: true }) as any;
AtlasService['oidcPluginLogger'] = logger;

await AtlasService.init();
expect(getListenerCount(logger)).to.eq(25);
// We did all preparations, reset sinon history for easier assertions
sandbox.resetHistory();

expect(getListenerCount(logger)).to.eq(25);

await AtlasService.signOut();
expect(getListenerCount(logger)).to.eq(0);
expect(logger).to.not.eq(AtlasService['oidcPluginLogger']);
expect(plugin.destroy).to.have.been.calledOnce;
expect(mockOidcPlugin.destroy).to.have.been.calledOnce;
expect(AtlasService['fetch']).to.have.been.calledOnceWith(
'http://example.com/v1/revoke?client_id=1234abcd'
);
expect(AtlasService['openExternal']).to.have.been.calledOnce;
});

it('should throw when called before sign in', async function () {
try {
await AtlasService.signOut();
expect.fail('Expected signOut to throw');
} catch (err) {
expect(err).to.have.property(
'message',
"Can't sign out if not signed in yet"
);
}
});
});

describe('getTrackingUserInfo', function () {
it('should return required tracking info from user info', function () {
expect(
getTrackingUserInfo({
sub: '1234',
primaryEmail: '[email protected]',
} as AtlasUserInfo)
).to.deep.eq({
auid: '03ac674216f3e15c761ee1a5e255f067953623c8b388b4459e13f978d7c846f4',
email: '[email protected]',
});
});
});
});
25 changes: 23 additions & 2 deletions packages/atlas-service/src/main.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { ipcMain, shell } from 'electron';
import { URL, URLSearchParams } from 'url';
import { createHash } from 'crypto';
import type { AuthFlowType, MongoDBOIDCPlugin } from '@mongodb-js/oidc-plugin';
import {
createMongoDBOIDCPlugin,
Expand Down Expand Up @@ -35,7 +36,7 @@ import { SecretStore, SECRET_STORE_KEY } from './secret-store';
import { AtlasUserConfigStore } from './user-config-store';
import { OidcPluginLogger } from './oidc-plugin-logger';

const { log } = createLoggerAndTelemetry('COMPASS-ATLAS-SERVICE');
const { log, track } = createLoggerAndTelemetry('COMPASS-ATLAS-SERVICE');

const redirectRequestHandler = oidcServerRequestHandler.bind(null, {
productName: 'Compass',
Expand Down Expand Up @@ -106,6 +107,15 @@ 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'),
email: userInfo.primaryEmail,
};
}

export class AtlasService {
private constructor() {
// singleton
Expand Down Expand Up @@ -326,8 +336,13 @@ export class AtlasService {
'AtlasService',
'Signed in successfully'
);
return this.getUserInfo({ signal });
const userInfo = await this.getUserInfo({ signal });
track('Atlas Sign In Success', getTrackingUserInfo(userInfo));
return userInfo;
} catch (err) {
track('Atlas Sign In Error', {
error: (err as Error).message,
});
log.error(
mongoLogId(1_001_000_220),
'AtlasService',
Expand All @@ -344,6 +359,9 @@ export class AtlasService {
}

static async signOut(): Promise<void> {
if (!this.currentUser) {
throw new Error("Can't sign out if not signed in yet");
}
// Reset and recreate event emitter first so that we don't accidentally
// react on any old plugin instance events
this.oidcPluginLogger.removeAllListeners();
Expand All @@ -365,13 +383,16 @@ export class AtlasService {
// this is not a failed state for the app, we already cleaned up token
// from everywhere, so we just ignore this
}
// Keep a copy of current user info for tracking
const userInfo = this.currentUser;
// Reset service state
this.currentUser = null;
this.oidcPluginLogger.emit('atlas-service-signed-out');
// Open Atlas sign out page to end the browser session created for sign in
void this.openExternal(
'https://account.mongodb.com/account/login?signedOut=true'
);
track('Atlas Sign Out', getTrackingUserInfo(userInfo));
}

// For every case where we request token, if requesting token fails we still
Expand Down

0 comments on commit 0b36060

Please sign in to comment.