From f75efee4eeff8d9e2656b3382b3aea9f9352095a Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 14 Oct 2021 15:12:11 +0200 Subject: [PATCH 1/2] feat: track all API calls MONGOSH-977 Specifically: - Rename `mongosh:api-call` to `mongosh:api-call-with-arguments` for a bit more accuracy, and drop `mongosh:deprecated-api-call` in favor of a more generla `mongosh:api-call` event that provides information useful for telemetry. - Track top-level API calls of async APIs, avoiding tracking of nested calls and omitting synchronous APIs since they are used internally fairly extensively. --- packages/cli-repl/src/cli-repl.spec.ts | 13 ++++ packages/cli-repl/src/mongosh-repl.ts | 3 + .../src/setup-logger-and-telemetry.spec.ts | 64 ++++++++++++++--- .../logging/src/setup-logger-and-telemetry.ts | 71 ++++++++++++++++--- .../src/index.spec.ts | 2 +- packages/shell-api/src/bulk.ts | 2 +- packages/shell-api/src/collection.spec.ts | 4 +- packages/shell-api/src/collection.ts | 2 +- packages/shell-api/src/database.ts | 2 +- packages/shell-api/src/decorators.ts | 36 +++++++--- packages/shell-api/src/explainable.spec.ts | 2 +- packages/shell-api/src/explainable.ts | 2 +- packages/shell-api/src/integration.spec.ts | 35 +++++++-- packages/shell-api/src/mongo.ts | 2 +- packages/shell-api/src/replica-set.ts | 2 +- packages/shell-api/src/shard.ts | 2 +- .../shell-api/src/shell-instance-state.ts | 14 ++-- packages/types/src/index.ts | 30 +++++--- 18 files changed, 226 insertions(+), 62 deletions(-) diff --git a/packages/cli-repl/src/cli-repl.spec.ts b/packages/cli-repl/src/cli-repl.spec.ts index f96831818..8d54d0333 100644 --- a/packages/cli-repl/src/cli-repl.spec.ts +++ b/packages/cli-repl/src/cli-repl.spec.ts @@ -804,6 +804,19 @@ describe('CliRepl', () => { expect(loadEvents[1].properties.nested).to.equal(true); }); + it('posts analytics event for shell API calls', async() => { + await cliRepl.start(await testServer.connectionString(), {}); + input.write('db.printShardingStatus()\n'); + input.write('exit\n'); + await waitBus(cliRepl.bus, 'mongosh:closed'); + const apiEvents = requests.map( + req => JSON.parse(req.body).batch.filter(entry => entry.event === 'API Call')).flat(); + expect(apiEvents).to.have.lengthOf(1); + expect(apiEvents[0].properties.class).to.equal('Database'); + expect(apiEvents[0].properties.method).to.equal('printShardingStatus'); + expect(apiEvents[0].properties.count).to.equal(1); + }); + context('with a 5.0+ server', () => { skipIfServerVersion(testServer, '<= 4.4'); diff --git a/packages/cli-repl/src/mongosh-repl.ts b/packages/cli-repl/src/mongosh-repl.ts index 4a0290e8a..733365108 100644 --- a/packages/cli-repl/src/mongosh-repl.ts +++ b/packages/cli-repl/src/mongosh-repl.ts @@ -364,6 +364,9 @@ class MongoshNodeRepl implements EvaluationListener { this.output.write(this.writer(warn) + '\n'); } + (repl as any).on(asyncRepl.evalStart, () => { + this.bus.emit('mongosh:evaluate-started'); + }); (repl as any).on(asyncRepl.evalFinish, () => { this.bus.emit('mongosh:evaluate-finished'); }); diff --git a/packages/logging/src/setup-logger-and-telemetry.spec.ts b/packages/logging/src/setup-logger-and-telemetry.spec.ts index f4f32833c..9e3be14bd 100644 --- a/packages/logging/src/setup-logger-and-telemetry.spec.ts +++ b/packages/logging/src/setup-logger-and-telemetry.spec.ts @@ -55,13 +55,13 @@ describe('setupLoggerAndTelemetry', () => { } bus.emit('mongosh:setCtx', { method: 'setCtx' }); - bus.emit('mongosh:api-call', { method: 'auth', class: 'Database', db: 'test-1603986682000', arguments: { } }); - bus.emit('mongosh:api-call', { method: 'redactable', arguments: { filter: { email: 'mongosh@example.com' } } }); + bus.emit('mongosh:api-call-with-arguments', { method: 'auth', class: 'Database', db: 'test-1603986682000', arguments: { } }); + bus.emit('mongosh:api-call-with-arguments', { method: 'redactable', arguments: { filter: { email: 'mongosh@example.com' } } }); bus.emit('mongosh:evaluate-input', { input: '1+1' }); const circular: any = {}; circular.circular = circular; - bus.emit('mongosh:api-call', { method: 'circulararg', arguments: { options: { circular } } }); + bus.emit('mongosh:api-call-with-arguments', { method: 'circulararg', arguments: { options: { circular } } }); expect(circular.circular).to.equal(circular); // Make sure the argument is still intact afterwards bus.emit('mongosh:start-loading-cli-scripts', { usesShellOption: true }); @@ -374,22 +374,26 @@ describe('setupLoggerAndTelemetry', () => { logOutput = []; analyticsOutput = []; - bus.emit('mongosh:deprecated-api-call', { method: 'cloneDatabase', class: 'Database' }); - bus.emit('mongosh:deprecated-api-call', { method: 'cloneDatabase', class: 'Database' }); - bus.emit('mongosh:deprecated-api-call', { method: 'copyDatabase', class: 'Database' }); - bus.emit('mongosh:deprecated-api-call', { method: 'cloneDatabase', class: 'Database' }); + bus.emit('mongosh:api-call', { method: 'cloneDatabase', class: 'Database', deprecated: true, callDepth: 0, isAsync: true }); + bus.emit('mongosh:api-call', { method: 'cloneDatabase', class: 'Database', deprecated: true, callDepth: 0, isAsync: true }); + bus.emit('mongosh:api-call', { method: 'copyDatabase', class: 'Database', deprecated: true, callDepth: 0, isAsync: true }); + bus.emit('mongosh:api-call', { method: 'cloneDatabase', class: 'Database', deprecated: true, callDepth: 0, isAsync: true }); + bus.emit('mongosh:api-call', { method: 'mangleDatabase', class: 'Database', deprecated: true, callDepth: 1, isAsync: true }); + bus.emit('mongosh:api-call', { method: 'getName', class: 'Database', deprecated: false, callDepth: 0, isAsync: false }); expect(logOutput).to.be.empty; expect(analyticsOutput).to.be.empty; bus.emit('mongosh:evaluate-finished'); - expect(logOutput).to.have.length(2); - expect(analyticsOutput).to.have.length(2); + expect(logOutput).to.have.length(3); + expect(analyticsOutput).to.have.length(5); expect(logOutput[0].msg).to.equal('Deprecated API call'); expect(logOutput[0].attr).to.deep.equal({ class: 'Database', method: 'cloneDatabase' }); expect(logOutput[1].msg).to.equal('Deprecated API call'); expect(logOutput[1].attr).to.deep.equal({ class: 'Database', method: 'copyDatabase' }); + expect(logOutput[2].msg).to.equal('Deprecated API call'); + expect(logOutput[2].attr).to.deep.equal({ class: 'Database', method: 'mangleDatabase' }); expect(analyticsOutput).to.deep.equal([ [ 'track', @@ -414,14 +418,52 @@ describe('setupLoggerAndTelemetry', () => { method: 'copyDatabase', } } - ] + ], + [ + 'track', + { + userId: '53defe995fa47e6c13102d9d', + event: 'Deprecated Method', + properties: { + mongosh_version: '1.0.0', + class: 'Database', + method: 'mangleDatabase', + } + } + ], + [ + 'track', + { + userId: '53defe995fa47e6c13102d9d', + event: 'API Call', + properties: { + mongosh_version: '1.0.0', + class: 'Database', + method: 'cloneDatabase', + count: 3 + } + } + ], + [ + 'track', + { + userId: '53defe995fa47e6c13102d9d', + event: 'API Call', + properties: { + mongosh_version: '1.0.0', + class: 'Database', + method: 'copyDatabase', + count: 1 + } + } + ], ]); bus.emit('mongosh:new-user', userId, false); logOutput = []; analyticsOutput = []; - bus.emit('mongosh:deprecated-api-call', { method: 'cloneDatabase', class: 'Database' }); + bus.emit('mongosh:api-call', { method: 'cloneDatabase', class: 'Database', deprecated: true, callDepth: 0, isAsync: true }); expect(logOutput).to.be.empty; expect(analyticsOutput).to.be.empty; diff --git a/packages/logging/src/setup-logger-and-telemetry.ts b/packages/logging/src/setup-logger-and-telemetry.ts index 818f7072a..f2423d332 100644 --- a/packages/logging/src/setup-logger-and-telemetry.ts +++ b/packages/logging/src/setup-logger-and-telemetry.ts @@ -3,6 +3,7 @@ import redactInfo from 'mongodb-redact'; import { redactURICredentials } from '@mongosh/history'; import type { MongoshBus, + ApiEventWithArguments, ApiEvent, UseEvent, EvaluateInputEvent, @@ -67,6 +68,28 @@ class NoopAnalytics implements MongoshAnalytics { track(_info: any): void {} // eslint-disable-line @typescript-eslint/no-unused-vars } +/** + * A helper class for keeping track of how often specific events occurred. + */ +class MultiSet { + _entries: Map = new Map(); + + add(entry: T): void { + const key = JSON.stringify(Object.entries(entry).sort()); + this._entries.set(key, (this._entries.get(key) ?? 0) + 1); + } + + clear(): void { + this._entries.clear(); + } + + *[Symbol.iterator](): Iterator<[T, number]> { + for (const [key, count] of this._entries) { + yield [Object.fromEntries(JSON.parse(key)) as T, count]; + } + } +} + /** * Connect a MongoshBus instance that emits events to logging and analytics providers. * @@ -194,11 +217,11 @@ export function setupLoggerAndTelemetry( } }); - bus.on('mongosh:setCtx', function(args: ApiEvent) { + bus.on('mongosh:setCtx', function(args: ApiEventWithArguments) { log.info('MONGOSH', mongoLogId(1_000_000_010), 'shell-api', 'Initialized context', args); }); - bus.on('mongosh:api-call', function(args: ApiEvent) { + bus.on('mongosh:api-call-with-arguments', function(args: ApiEventWithArguments) { // TODO: redactInfo cannot handle circular or otherwise nontrivial input let arg; try { @@ -349,14 +372,27 @@ export function setupLoggerAndTelemetry( log.info('MONGOSH-SNIPPETS', mongoLogId(1_000_000_032), 'snippets', 'Rewrote error message', ev); }); - const deprecatedApiCalls = new Set(); - bus.on('mongosh:deprecated-api-call', function(ev: ApiEvent) { - deprecatedApiCalls.add(`${ev.class}#${ev.method}`); + const deprecatedApiCalls = new MultiSet>(); + const apiCalls = new MultiSet>(); + bus.on('mongosh:api-call', function(ev: ApiEvent) { + if (ev.deprecated) { + deprecatedApiCalls.add({ class: ev.class, method: ev.method }); + } + if (ev.callDepth === 0 && ev.isAsync) { + apiCalls.add({ class: ev.class, method: ev.method }); + } + }); + bus.on('mongosh:evaluate-started', function() { + // Clear API calls before evaluation starts. This is important because + // some API calls are also emitted by mongosh CLI repl internals, + // but we only care about those emitted from user code (i.e. during + // evaluation). + deprecatedApiCalls.clear(); + apiCalls.clear(); }); bus.on('mongosh:evaluate-finished', function() { - deprecatedApiCalls.forEach(e => { - const [clazz, method] = e.split('#'); - log.warn('MONGOSH', mongoLogId(1_000_000_033), 'shell-api', 'Deprecated API call', { class: clazz, method }); + for (const [entry] of deprecatedApiCalls) { + log.warn('MONGOSH', mongoLogId(1_000_000_033), 'shell-api', 'Deprecated API call', entry); if (telemetry) { analytics.track({ @@ -364,13 +400,26 @@ export function setupLoggerAndTelemetry( event: 'Deprecated Method', properties: { mongosh_version, - class: clazz, - method + ...entry } }); } - }); + } + for (const [entry, count] of apiCalls) { + if (telemetry) { + analytics.track({ + userId, + event: 'API Call', + properties: { + mongosh_version, + ...entry, + count + } + }); + } + } deprecatedApiCalls.clear(); + apiCalls.clear(); }); bus.on('mongosh-sp:connect-attempt-initialized', function(ev: SpConnectAttemptInitializedEvent) { diff --git a/packages/node-runtime-worker-thread/src/index.spec.ts b/packages/node-runtime-worker-thread/src/index.spec.ts index 15ce9d44e..7a414ab0a 100644 --- a/packages/node-runtime-worker-thread/src/index.spec.ts +++ b/packages/node-runtime-worker-thread/src/index.spec.ts @@ -175,7 +175,7 @@ describe('WorkerRuntime', () => { await runtime.evaluate('db.getCollectionNames()'); - expect(eventEmitter.emit).to.have.been.calledWith('mongosh:api-call', { + expect(eventEmitter.emit).to.have.been.calledWith('mongosh:api-call-with-arguments', { arguments: {}, class: 'Database', db: 'test', diff --git a/packages/shell-api/src/bulk.ts b/packages/shell-api/src/bulk.ts index 5a8766069..b4c635944 100644 --- a/packages/shell-api/src/bulk.ts +++ b/packages/shell-api/src/bulk.ts @@ -177,7 +177,7 @@ export default class Bulk extends ShellApiWithMongoClass { * @private */ private _emitBulkApiCall(methodName: string, methodArguments: Document = {}): void { - this._mongo._instanceState.emitApiCall({ + this._mongo._instanceState.emitApiCallWithArgs({ method: methodName, class: 'Bulk', db: this._collection._database._name, diff --git a/packages/shell-api/src/collection.spec.ts b/packages/shell-api/src/collection.spec.ts index 09d093e23..2e00f389c 100644 --- a/packages/shell-api/src/collection.spec.ts +++ b/packages/shell-api/src/collection.spec.ts @@ -65,14 +65,14 @@ describe('Collection', () => { }); describe('.collections', () => { it('allows to get a collection as property if is not one of the existing methods', () => { - const database = new Database({ _instanceState: { emitApiCall: (): void => {} } } as any, 'db1'); + const database = new Database({ _instanceState: { emitApiCallWithArgs: (): void => {} } } as any, 'db1'); const coll: any = new Collection({} as any, database, 'coll'); expect(coll.someCollection).to.have.instanceOf(Collection); expect(coll.someCollection._name).to.equal('coll.someCollection'); }); it('reuses collections', () => { - const database: any = new Database({ _instanceState: { emitApiCall: (): void => {} } } as any, 'db1'); + const database: any = new Database({ _instanceState: { emitApiCallWithArgs: (): void => {} } } as any, 'db1'); const coll: any = new Collection({} as any, database, 'coll'); expect(coll.someCollection).to.equal(database.getCollection('coll.someCollection')); expect(coll.someCollection).to.equal(database.coll.someCollection); diff --git a/packages/shell-api/src/collection.ts b/packages/shell-api/src/collection.ts index 63d2f4de3..2ee3da8e8 100644 --- a/packages/shell-api/src/collection.ts +++ b/packages/shell-api/src/collection.ts @@ -130,7 +130,7 @@ export default class Collection extends ShellApiWithMongoClass { * @private */ private _emitCollectionApiCall(methodName: string, methodArguments: Document = {}): void { - this._mongo._instanceState.emitApiCall({ + this._mongo._instanceState.emitApiCallWithArgs({ method: methodName, class: 'Collection', db: this._database._name, diff --git a/packages/shell-api/src/database.ts b/packages/shell-api/src/database.ts index 9011cadd4..67453c27b 100644 --- a/packages/shell-api/src/database.ts +++ b/packages/shell-api/src/database.ts @@ -126,7 +126,7 @@ export default class Database extends ShellApiWithMongoClass { * @private */ private _emitDatabaseApiCall(methodName: string, methodArguments: Document = {}): void { - this._mongo._instanceState.emitApiCall({ + this._mongo._instanceState.emitApiCallWithArgs({ method: methodName, class: 'Database', db: this._name, diff --git a/packages/shell-api/src/decorators.ts b/packages/shell-api/src/decorators.ts index e70c36a5a..b193013c5 100644 --- a/packages/shell-api/src/decorators.ts +++ b/packages/shell-api/src/decorators.ts @@ -230,13 +230,16 @@ function wrapWithApiChecks any>(fn: T, className: s const wrapper = (fn as any).returnsPromise ? markImplicitlyAwaited(async function(this: any, ...args: any[]): Promise { const instanceState = getShellInstanceState(this); - checkForDeprecation(instanceState, className, fn); + emitAPICallTelemetry(instanceState, className, fn, true); const interruptFlag = instanceState?.interrupted; interruptFlag?.checkpoint(); const interrupt = interruptFlag?.asPromise(); let result: any; try { + if (instanceState) { + instanceState.apiCallDepth++; + } result = await Promise.race([ interrupt?.promise ?? new Promise(() => {}), fn.call(this, ...args) @@ -244,6 +247,9 @@ function wrapWithApiChecks any>(fn: T, className: s } catch (e) { throw instanceState?.transformError(e) ?? e; } finally { + if (instanceState) { + instanceState.apiCallDepth--; + } if (interrupt) { interrupt.destroy(); } @@ -252,14 +258,21 @@ function wrapWithApiChecks any>(fn: T, className: s return result; }) : function(this: any, ...args: any[]): any { const instanceState = getShellInstanceState(this); - checkForDeprecation(instanceState, className, fn); + emitAPICallTelemetry(instanceState, className, fn, false); const interruptFlag = instanceState?.interrupted; interruptFlag?.checkpoint(); let result: any; try { + if (instanceState) { + instanceState.apiCallDepth++; + } result = fn.call(this, ...args); } catch (e) { throw instanceState?.transformError(e) ?? e; + } finally { + if (instanceState) { + instanceState.apiCallDepth--; + } } interruptFlag?.checkpoint(); return result; @@ -270,20 +283,21 @@ function wrapWithApiChecks any>(fn: T, className: s } /** - * Emit a 'mongosh:deprecated-api-call' event on the instance state's message bus - * if the function was marked with the {@link deprecated} decorator. + * Emit a 'mongosh:api-call' event on the instance state's message bus, + * with the 'deprecated' property set if the function was marked with + * the {@link deprecated} decorator. * * @param instanceState A ShellInstanceState object. * @param className The name of the class in question. * @param fn The class method in question. */ -function checkForDeprecation(instanceState: ShellInstanceState | undefined, className: string, fn: Function) { - if (instanceState && typeof instanceState.emitDeprecatedApiCall === 'function' && typeof fn === 'function' && (fn as any).deprecated) { - instanceState.emitDeprecatedApiCall({ - method: fn.name, - class: className - }); - } +function emitAPICallTelemetry(instanceState: ShellInstanceState | undefined, className: string, fn: Function, isAsync: boolean) { + instanceState?.emitApiCall?.({ + method: fn.name, + class: className, + deprecated: !!(fn as any).deprecated, + isAsync + }); } /** diff --git a/packages/shell-api/src/explainable.spec.ts b/packages/shell-api/src/explainable.spec.ts index 1a8da2e91..6b3d7f1be 100644 --- a/packages/shell-api/src/explainable.spec.ts +++ b/packages/shell-api/src/explainable.spec.ts @@ -40,7 +40,7 @@ describe('Explainable', () => { }); }); describe('metadata', () => { - const mongo: any = { _instanceState: { emitApiCall: sinon.spy() } }; + const mongo: any = { _instanceState: { emitApiCallWithArgs: sinon.spy() } }; const db = new Database(mongo, 'myDB'); const coll = new Collection(mongo, db, 'myCollection'); const explainable = new Explainable(mongo, coll, 'queryPlannerExtended'); diff --git a/packages/shell-api/src/explainable.ts b/packages/shell-api/src/explainable.ts index c45b9df11..db4ce7894 100644 --- a/packages/shell-api/src/explainable.ts +++ b/packages/shell-api/src/explainable.ts @@ -58,7 +58,7 @@ export default class Explainable extends ShellApiWithMongoClass { * @private */ private _emitExplainableApiCall(methodName: string, methodArguments: Document = {}): void { - this._mongo._instanceState.emitApiCall({ + this._mongo._instanceState.emitApiCallWithArgs({ method: methodName, class: 'Explainable', db: this._collection._database._name, diff --git a/packages/shell-api/src/integration.spec.ts b/packages/shell-api/src/integration.spec.ts index 16415ce5f..d64f0f223 100644 --- a/packages/shell-api/src/integration.spec.ts +++ b/packages/shell-api/src/integration.spec.ts @@ -9,7 +9,7 @@ import type AggregationCursor from './aggregation-cursor'; import { startTestServer, skipIfServerVersion, skipIfApiStrict } from '../../../testing/integration-testing-hooks'; import { toShellResult, Topologies } from './index'; import type { Document } from '@mongosh/service-provider-core'; -import { ShellUserConfig } from '@mongosh/types'; +import { ShellUserConfig, ApiEvent } from '@mongosh/types'; import { EventEmitter, once } from 'events'; // Compile JS code as an expression. We use this to generate some JS functions @@ -2304,9 +2304,9 @@ describe('Shell API (integration)', function() { }); }); - describe('deprecations', () => { - it('emit an event when a deprecated method is called', async() => { - const deprecatedCall = once(instanceState.messageBus, 'mongosh:deprecated-api-call'); + describe('method tracking', () => { + it('emits an event when a deprecated method is called', async() => { + const deprecatedCall = once(instanceState.messageBus, 'mongosh:api-call'); try { mongo.setSlaveOk(); expect.fail('Expected error'); @@ -2317,9 +2317,34 @@ describe('Shell API (integration)', function() { expect(events).to.have.length(1); expect(events[0]).to.deep.equal({ method: 'setSlaveOk', - class: 'Mongo' + class: 'Mongo', + deprecated: true, + callDepth: 0, + isAsync: false }); }); + + it('keeps track of whether a call is a top-level call or not', async() => { + const events: ApiEvent[] = []; + instanceState.messageBus.on('mongosh:api-call', (ev: ApiEvent) => events.push(ev)); + try { + await database.printShardingStatus(); + expect.fail('Expected error'); + } catch (e) { + expect(e.message).to.contain('This db does not have sharding enabled'); + } + expect(events.length).to.be.greaterThan(1); + expect(events[0]).to.deep.equal({ + method: 'printShardingStatus', + class: 'Database', + deprecated: false, + callDepth: 0, + isAsync: true + }); + expect(events.filter(ev => ev.method === 'find').length).to.be.greaterThan(1); + expect(events.filter(ev => ev.method === 'print').length).to.equal(1); + expect(events.filter(ev => ev.callDepth === 0).length).to.equal(1); + }); }); describe('interruption', () => { diff --git a/packages/shell-api/src/mongo.ts b/packages/shell-api/src/mongo.ts index 575bb6038..ae2775ff9 100644 --- a/packages/shell-api/src/mongo.ts +++ b/packages/shell-api/src/mongo.ts @@ -149,7 +149,7 @@ export default class Mongo extends ShellApiClass { * @private */ private _emitMongoApiCall(methodName: string, methodArguments: Document = {}): void { - this._instanceState.emitApiCall({ + this._instanceState.emitApiCallWithArgs({ method: methodName, class: 'Mongo', uri: this._uri, diff --git a/packages/shell-api/src/replica-set.ts b/packages/shell-api/src/replica-set.ts index 442e407c1..87ac6b5da 100644 --- a/packages/shell-api/src/replica-set.ts +++ b/packages/shell-api/src/replica-set.ts @@ -392,7 +392,7 @@ export default class ReplicaSet extends ShellApiWithMongoClass { * @private */ private _emitReplicaSetApiCall(methodName: string, methodArguments: Document = {}): void { - this._database._mongo._instanceState.emitApiCall({ + this._database._mongo._instanceState.emitApiCallWithArgs({ method: methodName, class: 'ReplicaSet', arguments: methodArguments diff --git a/packages/shell-api/src/shard.ts b/packages/shell-api/src/shard.ts index 5cbf31bfb..51d59fe23 100644 --- a/packages/shell-api/src/shard.ts +++ b/packages/shell-api/src/shard.ts @@ -39,7 +39,7 @@ export default class Shard extends ShellApiWithMongoClass { * @private */ private _emitShardApiCall(methodName: string, methodArguments: Document = {}): void { - this._database._mongo._instanceState.emitApiCall({ + this._database._mongo._instanceState.emitApiCallWithArgs({ method: methodName, class: 'Shard', arguments: methodArguments diff --git a/packages/shell-api/src/shell-instance-state.ts b/packages/shell-api/src/shell-instance-state.ts index 9b7292030..ad381236e 100644 --- a/packages/shell-api/src/shell-instance-state.ts +++ b/packages/shell-api/src/shell-instance-state.ts @@ -6,7 +6,7 @@ import { ReplPlatform, ServerApi, ServiceProvider, TopologyDescription } from '@mongosh/service-provider-core'; -import type { ApiEvent, ConfigProvider, MongoshBus, ShellUserConfig } from '@mongosh/types'; +import type { ApiEvent, ApiEventWithArguments, ConfigProvider, MongoshBus, ShellUserConfig } from '@mongosh/types'; import { EventEmitter } from 'events'; import redactInfo from 'mongodb-redact'; import ChangeStreamCursor from './change-stream-cursor'; @@ -133,6 +133,7 @@ export default class ShellInstanceState { public evaluationListener: EvaluationListener; public displayBatchSizeFromDBQuery: number | undefined = undefined; public isInteractive = false; + public apiCallDepth = 0; private warningsShown: Set = new Set(); public readonly interrupted = new InterruptFlag(); @@ -272,12 +273,15 @@ export default class ShellInstanceState { } } - public emitApiCall(event: ApiEvent): void { - this.messageBus.emit('mongosh:api-call', event); + public emitApiCallWithArgs(event: ApiEventWithArguments): void { + this.messageBus.emit('mongosh:api-call-with-arguments', event); } - public emitDeprecatedApiCall(event: ApiEvent): void { - this.messageBus.emit('mongosh:deprecated-api-call', event); + public emitApiCall(event: Omit): void { + this.messageBus.emit('mongosh:api-call', { + ...event, + callDepth: this.apiCallDepth + }); } public setEvaluationListener(listener: EvaluationListener): void { diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 0dc3da5da..eb75e1c91 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -6,8 +6,8 @@ export interface ApiEventArguments { filter?: object; } -export interface ApiEvent { - method?: string; +export interface ApiEventWithArguments { + method: string; class?: string; db?: string; coll?: string; @@ -15,7 +15,17 @@ export interface ApiEvent { arguments?: ApiEventArguments; } -export interface ApiWarning extends ApiEvent { +export interface ApiEvent { + method: string; + class: string; + deprecated: boolean; + isAsync: boolean; + callDepth: number; +} + +export interface ApiWarning { + method: string; + class: string; message: string; } @@ -206,6 +216,10 @@ export interface MongoshBusEventsMap { * Signals the start of the evaluation of user code inside Shellevaluator. */ 'mongosh:evaluate-input': (ev: EvaluateInputEvent) => void; + /** + * Signals the initiation of the evaluation of user code in AsyncRepl (final step of the evaluation). + */ + 'mongosh:evaluate-started': () => void; /** * Signals the completion of the evaluation of user code in AsyncRepl (final step of the evaluation) * regardless of success, error, or being interrupted. @@ -226,15 +240,15 @@ export interface MongoshBusEventsMap { /** * Signals the global context for the shell evaluation has been initialized. */ - 'mongosh:setCtx': (ev: ApiEvent) => void; + 'mongosh:setCtx': (ev: ApiEventWithArguments) => void; /** - * Signals usage of a shell API method. + * Signals usage of a shell API method. This includes arguments and is not suitable for telemetry. */ - 'mongosh:api-call': (ev: ApiEvent) => void; + 'mongosh:api-call-with-arguments': (ev: ApiEventWithArguments) => void; /** - * Signals usage of a deprecated shell API method. + * Signals usage of a shell API method as an API entry point, suitable for telemetry. */ - 'mongosh:deprecated-api-call': (ev: ApiEvent) => void; + 'mongosh:api-call': (ev: ApiEvent) => void; /** * Signals an error for an operation that we can silently ignore but still warn about. */ From ba4a5bf0172ea758b9557faa071d1266c2df2e2a Mon Sep 17 00:00:00 2001 From: Anna Henningsen Date: Thu, 21 Oct 2021 20:01:27 +0200 Subject: [PATCH 2/2] fixup: camel case --- packages/shell-api/src/decorators.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/shell-api/src/decorators.ts b/packages/shell-api/src/decorators.ts index b193013c5..e936eb75f 100644 --- a/packages/shell-api/src/decorators.ts +++ b/packages/shell-api/src/decorators.ts @@ -230,7 +230,7 @@ function wrapWithApiChecks any>(fn: T, className: s const wrapper = (fn as any).returnsPromise ? markImplicitlyAwaited(async function(this: any, ...args: any[]): Promise { const instanceState = getShellInstanceState(this); - emitAPICallTelemetry(instanceState, className, fn, true); + emitApiCallTelemetry(instanceState, className, fn, true); const interruptFlag = instanceState?.interrupted; interruptFlag?.checkpoint(); const interrupt = interruptFlag?.asPromise(); @@ -258,7 +258,7 @@ function wrapWithApiChecks any>(fn: T, className: s return result; }) : function(this: any, ...args: any[]): any { const instanceState = getShellInstanceState(this); - emitAPICallTelemetry(instanceState, className, fn, false); + emitApiCallTelemetry(instanceState, className, fn, false); const interruptFlag = instanceState?.interrupted; interruptFlag?.checkpoint(); let result: any; @@ -291,7 +291,7 @@ function wrapWithApiChecks any>(fn: T, className: s * @param className The name of the class in question. * @param fn The class method in question. */ -function emitAPICallTelemetry(instanceState: ShellInstanceState | undefined, className: string, fn: Function, isAsync: boolean) { +function emitApiCallTelemetry(instanceState: ShellInstanceState | undefined, className: string, fn: Function, isAsync: boolean) { instanceState?.emitApiCall?.({ method: fn.name, class: className,