diff --git a/packages/cli-repl/README.md b/packages/cli-repl/README.md index 8900f5ff7e..6c6ceb22ef 100644 --- a/packages/cli-repl/README.md +++ b/packages/cli-repl/README.md @@ -99,25 +99,24 @@ bus.emit('mongosh:connect', { }) ``` -### bus.on('mongosh:new-user', telemetryAnonymousId, enableTelemetry) -Where `telemetryAnonymousId` is a [BSON ObjectID][object-id] and `enableTelemetry` is a boolean flag. -This is used for telemetry tracking when the user initially uses mongosh. +### bus.on('mongosh:new-user', telemetryUserIdentity, enableTelemetry) +Where `telemetryUserIdentity` is `userId` and `anonymousId` which are both a [BSON ObjectID][object-id]. +And `enableTelemetry` is a boolean flag. +This is used internally to update telemetry preferences. Example: ```js -bus.emit('mongosh:new-user', '12394dfjvnaw3uw3erdf', true) +bus.emit('mongosh:new-user', { userId: '12394dfjvnaw3uw3erdf', anonymousId: '12394dfjvnaw3uw3erdf' }, true) ``` ### bus.on('mongosh:update-user', telemetryUserIdentity, enableTelemetry) -Initially, we used `userId` as Segment user identifier, but this usage is being deprecated. -The `anonymousId` should be used instead. We keep sending `userId` to Segment for old users though to preserve their analytics. -Where `userID`/`anonymousId` is a [BSON ObjectID][object-id] and `enableTelemetry` is a boolean flag. -This is used internally to update telemetry preferences and `userID`/`anonymousId` in the -logger. +Where `telemetryUserIdentity` is `userId` and `anonymousId` which are both a [BSON ObjectID][object-id]. +And `enableTelemetry` is a boolean flag. +This is used internally to update telemetry preferences. Example: ```js -bus.emit('mongosh:update-user', { userId: undefined, anonymousId: '12394dfjvnaw3uw3erdf' } , false) +bus.emit('mongosh:update-user', { userId: '12394dfjvnaw3uw3erdf', anonymousId: null } , false) ``` ### bus.on('mongosh:error', error) diff --git a/packages/cli-repl/src/cli-repl.spec.ts b/packages/cli-repl/src/cli-repl.spec.ts index 48967f43c9..a745b2c30b 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -113,7 +113,7 @@ describe('CliRepl', () => { it('does not store config options on disk that have not been changed', async() => { let content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); expect(Object.keys(EJSON.parse(content))).to.deep.equal([ - 'telemetryAnonymousId', 'enableTelemetry', 'disableGreetingMessage' + 'userId', 'telemetryAnonymousId', 'enableTelemetry', 'disableGreetingMessage' ]); input.write('config.set("inspectDepth", config.get("inspectDepth"))\n'); @@ -121,7 +121,7 @@ describe('CliRepl', () => { await waitEval(cliRepl.bus); content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); expect(Object.keys(EJSON.parse(content))).to.deep.equal([ - 'telemetryAnonymousId', 'enableTelemetry', 'disableGreetingMessage', 'inspectDepth' + 'userId', 'telemetryAnonymousId', 'enableTelemetry', 'disableGreetingMessage', 'inspectDepth' ]); // When a new REPL is created: @@ -129,7 +129,7 @@ describe('CliRepl', () => { await cliRepl.start('', {}); content = await fs.readFile(path.join(tmpdir.path, 'config'), { encoding: 'utf8' }); expect(Object.keys(EJSON.parse(content))).to.deep.equal([ - 'telemetryAnonymousId', 'enableTelemetry', 'disableGreetingMessage', 'inspectDepth' + 'userId', 'telemetryAnonymousId', 'enableTelemetry', 'disableGreetingMessage', 'inspectDepth' ]); }); @@ -286,16 +286,16 @@ describe('CliRepl', () => { }); context('during startup', () => { - it('persists telemetryAnonymousId', async() => { - const telemetryAnonymousIds: string[] = []; + it('persists userId and telemetryAnonymousId', async() => { + const telemetryUserIdentitys: { userId?: string; anonymousId?: string }[] = []; for (let i = 0; i < 2; i++) { cliRepl = new CliRepl(cliReplOptions); - cliRepl.bus.on('mongosh:new-user', telemetryAnonymousId => telemetryAnonymousIds.push(telemetryAnonymousId)); - cliRepl.bus.on('mongosh:update-user', telemetryUserIdentity => telemetryAnonymousIds.push(telemetryUserIdentity.anonymousId)); + cliRepl.bus.on('mongosh:new-user', telemetryUserIdentity => telemetryUserIdentitys.push(telemetryUserIdentity)); + cliRepl.bus.on('mongosh:update-user', telemetryUserIdentity => telemetryUserIdentitys.push(telemetryUserIdentity)); await cliRepl.start('', {}); } - expect(telemetryAnonymousIds).to.have.lengthOf(2); - expect([...new Set(telemetryAnonymousIds)]).to.have.lengthOf(1); + expect(telemetryUserIdentitys).to.have.lengthOf(2); + expect(telemetryUserIdentitys[0]).to.deep.equal(telemetryUserIdentitys[1]) }); it('emits error for invalid config', async() => { diff --git a/packages/cli-repl/src/cli-repl.ts b/packages/cli-repl/src/cli-repl.ts index ad8f337623..684b3fe5fa 100644 --- a/packages/cli-repl/src/cli-repl.ts +++ b/packages/cli-repl/src/cli-repl.ts @@ -66,7 +66,7 @@ export type CliReplOptions = { } & Pick; /** The set of config options that is *always* available in config files stored on the file system. */ -type CliUserConfigOnDisk = Partial & Pick; +type CliUserConfigOnDisk = Partial & Pick; /** * The REPL used from the terminal. @@ -104,8 +104,11 @@ class CliRepl implements MongoshIOProvider { this.output = options.output; this.analyticsOptions = options.analyticsOptions; this.onExit = options.onExit; + + const id = new bson.ObjectId().toString(); this.config = { - telemetryAnonymousId: new bson.ObjectId().toString(), + userId: id, + telemetryAnonymousId: id, enableTelemetry: true }; @@ -118,11 +121,11 @@ class CliRepl implements MongoshIOProvider { }) .on('new-config', (config: CliUserConfigOnDisk) => { this.setTelemetryEnabled(config.enableTelemetry); - this.bus.emit('mongosh:new-user', config.telemetryAnonymousId); + this.bus.emit('mongosh:new-user', { userId: config.userId, anonymousId: config.telemetryAnonymousId }); }) .on('update-config', (config: CliUserConfigOnDisk) => { this.setTelemetryEnabled(config.enableTelemetry); - this.bus.emit('mongosh:update-user', { userId: config.userId, anonymousId: config.telemetryAnonymousId ?? config.userId }); + this.bus.emit('mongosh:update-user', { userId: config.userId, anonymousId: config.telemetryAnonymousId }); }); this.mongocryptdManager = new MongocryptdManager( diff --git a/packages/cli-repl/test/e2e.spec.ts b/packages/cli-repl/test/e2e.spec.ts index 04a147738d..5ce3df71b6 100644 --- a/packages/cli-repl/test/e2e.spec.ts +++ b/packages/cli-repl/test/e2e.spec.ts @@ -884,6 +884,7 @@ describe('e2e', function() { describe('config file', () => { it('sets up a config file', async() => { const config = await readConfig(); + expect(config.userId).to.match(/^[a-f0-9]{24}$/); expect(config.telemetryAnonymousId).to.match(/^[a-f0-9]{24}$/); expect(config.enableTelemetry).to.be.true; expect(config.disableGreetingMessage).to.be.true; diff --git a/packages/logging/src/analytics-helpers.spec.ts b/packages/logging/src/analytics-helpers.spec.ts index f26ebb8fca..864d3e9e17 100644 --- a/packages/logging/src/analytics-helpers.spec.ts +++ b/packages/logging/src/analytics-helpers.spec.ts @@ -17,18 +17,18 @@ describe('ToggleableAnalytics', () => { const toggleable = new ToggleableAnalytics(target); expect(events).to.have.lengthOf(0); - toggleable.identify({ anonymousId: 'me', traits: { platform: '1234' } }); - toggleable.track({ anonymousId: 'me', event: 'something', properties: { mongosh_version: '1.2.3' } }); + toggleable.identify({ userId: 'me', traits: { platform: '1234' } }); + toggleable.track({ userId: 'me', event: 'something', properties: { mongosh_version: '1.2.3' } }); expect(events).to.have.lengthOf(0); toggleable.enable(); expect(events).to.have.lengthOf(2); - toggleable.track({ anonymousId: 'me', event: 'something2', properties: { mongosh_version: '1.2.3' } }); + toggleable.track({ userId: 'me', event: 'something2', properties: { mongosh_version: '1.2.3' } }); expect(events).to.have.lengthOf(3); toggleable.pause(); - toggleable.track({ anonymousId: 'me', event: 'something3', properties: { mongosh_version: '1.2.3' } }); + toggleable.track({ userId: 'me', event: 'something3', properties: { mongosh_version: '1.2.3' } }); expect(events).to.have.lengthOf(3); toggleable.disable(); @@ -36,9 +36,9 @@ describe('ToggleableAnalytics', () => { toggleable.enable(); expect(events).to.deep.equal([ - [ 'identify', { anonymousId: 'me', traits: { platform: '1234' } } ], - [ 'track', { anonymousId: 'me', event: 'something', properties: { mongosh_version: '1.2.3' } } ], - [ 'track', { anonymousId: 'me', event: 'something2', properties: { mongosh_version: '1.2.3' } } ] + [ 'identify', { userId: 'me', traits: { platform: '1234' } } ], + [ 'track', { userId: 'me', event: 'something', properties: { mongosh_version: '1.2.3' } } ], + [ 'track', { userId: 'me', event: 'something2', properties: { mongosh_version: '1.2.3' } } ] ]); }); }); diff --git a/packages/logging/src/analytics-helpers.ts b/packages/logging/src/analytics-helpers.ts index 3d7b5ae1a3..a4bd3c7d50 100644 --- a/packages/logging/src/analytics-helpers.ts +++ b/packages/logging/src/analytics-helpers.ts @@ -1,16 +1,18 @@ +export type MongoshAnalyticsIdentity = { + userId: string; +} | { + anonymousId: string; +} + /** * General interface for an Analytics provider that mongosh can use. */ export interface MongoshAnalytics { - identify(message: { - userId?: string, - anonymousId: string, + identify(message: MongoshAnalyticsIdentity & { traits: { platform: string } }): void; - track(message: { - userId?: string, - anonymousId: string, + track(message: MongoshAnalyticsIdentity & { event: string, properties: { // eslint-disable-next-line camelcase diff --git a/packages/logging/src/setup-logger-and-telemetry.spec.ts b/packages/logging/src/setup-logger-and-telemetry.spec.ts index 56c1caa129..c0806eeb0a 100644 --- a/packages/logging/src/setup-logger-and-telemetry.spec.ts +++ b/packages/logging/src/setup-logger-and-telemetry.spec.ts @@ -10,7 +10,7 @@ describe('setupLoggerAndTelemetry', () => { let analyticsOutput: ['identify'|'track'|'log', any][]; let bus: MongoshBus; - const telemetryAnonymousId = '53defe995fa47e6c13102d9d'; + const userId = '53defe995fa47e6c13102d9d'; const logId = '5fb3c20ee1507e894e5340f3'; const logger = new MongoLogWriter(logId, `/tmp/${logId}_log`, { @@ -36,8 +36,8 @@ describe('setupLoggerAndTelemetry', () => { expect(logOutput).to.have.lengthOf(0); expect(analyticsOutput).to.be.empty; - bus.emit('mongosh:new-user', telemetryAnonymousId); - bus.emit('mongosh:update-user', { anonymousId: telemetryAnonymousId }); + bus.emit('mongosh:new-user', { userId, anonymousId: userId }); + bus.emit('mongosh:update-user', { userId, anonymousId: userId }); bus.emit('mongosh:connect', { uri: 'mongodb://localhost/', is_localhost: true, @@ -364,7 +364,7 @@ describe('setupLoggerAndTelemetry', () => { expect(logOutput).to.have.lengthOf(0); expect(analyticsOutput).to.be.empty; - bus.emit('mongosh:new-user', telemetryAnonymousId); + bus.emit('mongosh:new-user', { userId, anonymousId: userId }); logOutput = []; analyticsOutput = []; @@ -454,7 +454,7 @@ describe('setupLoggerAndTelemetry', () => { ], ]); - bus.emit('mongosh:new-user', telemetryAnonymousId); + bus.emit('mongosh:new-user', { userId, anonymousId: userId }); logOutput = []; analyticsOutput = []; diff --git a/packages/logging/src/setup-logger-and-telemetry.ts b/packages/logging/src/setup-logger-and-telemetry.ts index afb9586855..881d3b0dc6 100644 --- a/packages/logging/src/setup-logger-and-telemetry.ts +++ b/packages/logging/src/setup-logger-and-telemetry.ts @@ -29,8 +29,7 @@ import type { SnippetsTransformErrorEvent, EditorRunEditCommandEvent, EditorReadVscodeExtensionsDoneEvent, - EditorReadVscodeExtensionsFailedEvent, - TelemetryUserIdentity + EditorReadVscodeExtensionsFailedEvent } from '@mongosh/types'; import { inspect } from 'util'; import { MongoLogWriter, mongoLogId } from 'mongodb-log-writer'; @@ -73,7 +72,18 @@ export function setupLoggerAndTelemetry( userTraits: any, mongosh_version: string): void { const { logId } = log; - let telemetryUserIdentity: TelemetryUserIdentity; + let userId: string; + let telemetryAnonymousId: string; + + const getTelemetryUserIdentity = () => { + if (telemetryAnonymousId) { + return { + anonymousId: telemetryAnonymousId + }; + } + + return { userId }; + }; // We emit different analytics events for loading files and evaluating scripts // depending on whether we're already in the REPL or not yet. We store the @@ -96,15 +106,15 @@ export function setupLoggerAndTelemetry( const { uri: _uri, ...argsWithoutUri } = args; // eslint-disable-line @typescript-eslint/no-unused-vars const params = { session_id: logId, - userId: telemetryUserIdentity?.userId, - telemetryAnonymousId: telemetryUserIdentity?.anonymousId, + userId, + telemetryAnonymousId, connectionUri, ...argsWithoutUri }; log.info('MONGOSH', mongoLogId(1_000_000_004), 'connect', 'Connecting to server', params); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'New Connection', properties: { mongosh_version, @@ -114,24 +124,31 @@ export function setupLoggerAndTelemetry( }); }); - bus.on('mongosh:new-user', function(anonymousId: string) { - telemetryUserIdentity = { anonymousId }; - analytics.identify({ anonymousId, traits: userTraits }); - }); - - bus.on('mongosh:update-user', function(updatedTelemetryUserIdentity: TelemetryUserIdentity) { - telemetryUserIdentity = updatedTelemetryUserIdentity; - - const telemetryIdentifyArg: TelemetryUserIdentity & { traits: any } = { - anonymousId: telemetryUserIdentity.anonymousId, + bus.on('mongosh:new-user', function(newTelemetryUserIdentity: { userId: string; anonymousId: string }) { + if (!newTelemetryUserIdentity.anonymousId) { + userId = newTelemetryUserIdentity.userId; + } + telemetryAnonymousId = newTelemetryUserIdentity.anonymousId; + analytics.identify({ + anonymousId: newTelemetryUserIdentity.anonymousId, traits: userTraits - }; + }); + }); - if (telemetryUserIdentity?.userId) { - telemetryIdentifyArg.userId = telemetryUserIdentity.userId; + bus.on('mongosh:update-user', function(updatedTelemetryUserIdentity: { userId: string; anonymousId?: string }) { + if (updatedTelemetryUserIdentity.anonymousId) { + telemetryAnonymousId = updatedTelemetryUserIdentity.anonymousId; + analytics.identify({ + anonymousId: updatedTelemetryUserIdentity.anonymousId, + traits: userTraits + }); + } else { + userId = updatedTelemetryUserIdentity.userId; + analytics.identify({ + userId: updatedTelemetryUserIdentity.userId, + traits: userTraits + }); } - - analytics.identify(telemetryIdentifyArg); log.info('MONGOSH', mongoLogId(1_000_000_005), 'config', 'User updated'); }); @@ -144,7 +161,7 @@ export function setupLoggerAndTelemetry( if (error.name.includes('Mongosh')) { analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Error', properties: { mongosh_version, @@ -169,7 +186,7 @@ export function setupLoggerAndTelemetry( log.info('MONGOSH', mongoLogId(1_000_000_008), 'shell-api', 'Used "use" command', args); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Use', properties: { mongosh_version @@ -181,7 +198,7 @@ export function setupLoggerAndTelemetry( log.info('MONGOSH', mongoLogId(1_000_000_009), 'shell-api', 'Used "show" command', args); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Show', properties: { mongosh_version, @@ -209,7 +226,7 @@ export function setupLoggerAndTelemetry( log.info('MONGOSH', mongoLogId(1_000_000_012), 'shell-api', 'Loading file via load()', args); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: hasStartedMongoshRepl ? 'Script Loaded' : 'Script Loaded CLI', properties: { mongosh_version, @@ -223,7 +240,7 @@ export function setupLoggerAndTelemetry( log.info('MONGOSH', mongoLogId(1_000_000_013), 'repl', 'Evaluating script passed on the command line'); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Script Evaluated', properties: { mongosh_version, @@ -236,7 +253,7 @@ export function setupLoggerAndTelemetry( log.info('MONGOSH', mongoLogId(1_000_000_014), 'repl', 'Loading .mongoshrc.js'); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Mongoshrc Loaded', properties: { mongosh_version @@ -248,7 +265,7 @@ export function setupLoggerAndTelemetry( log.info('MONGOSH', mongoLogId(1_000_000_015), 'repl', 'Warning about .mongorc.js/.mongoshrc.js mismatch'); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Mongorc Warning', properties: { mongosh_version @@ -324,7 +341,7 @@ export function setupLoggerAndTelemetry( if (ev.args[0] === 'install') { analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Snippet Install', properties: { mongosh_version @@ -360,7 +377,7 @@ export function setupLoggerAndTelemetry( log.warn('MONGOSH', mongoLogId(1_000_000_033), 'shell-api', 'Deprecated API call', entry); analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'Deprecated Method', properties: { mongosh_version, @@ -370,7 +387,7 @@ export function setupLoggerAndTelemetry( } for (const [entry, count] of apiCalls) { analytics.track({ - ...telemetryUserIdentity, + ...getTelemetryUserIdentity(), event: 'API Call', properties: { mongosh_version, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0eef4f9b5e..81ea094c7c 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -167,11 +167,6 @@ export interface EditorReadVscodeExtensionsFailedEvent { error: Error; } -export interface TelemetryUserIdentity { - userId?: string; - anonymousId: string; -} - export interface MongoshBusEventsMap extends ConnectEventMap { /** * Signals a connection to a MongoDB instance has been established @@ -181,11 +176,11 @@ export interface MongoshBusEventsMap extends ConnectEventMap { /** * Signals that the shell is started by a new user. */ - 'mongosh:new-user': (id: string) => void; + 'mongosh:new-user': (identity: { userId: string; anonymousId: string }) => void; /** * Signals a change of the user telemetry settings. */ - 'mongosh:update-user': (identity: TelemetryUserIdentity) => void; + 'mongosh:update-user': (identity: { userId: string; anonymousId?: string }) => void; /** * Signals an error that should be logged or potentially tracked by analytics. */ @@ -417,7 +412,7 @@ export class SnippetShellUserConfigValidator extends ShellUserConfigValidator { } export class CliUserConfig extends SnippetShellUserConfig { - userId?: string; + userId = ''; telemetryAnonymousId = ''; disableGreetingMessage = false; forceDisableTelemetry = false;