@mongosh/logging 3.5.0 → 3.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -39,6 +39,7 @@ import { MongoLogWriter } from 'mongodb-log-writer';
39
39
  import { mongoLogId } from 'mongodb-log-writer';
40
40
  import type {
41
41
  AnalyticsIdentifyMessage,
42
+ AnalyticsTrackMessage,
42
43
  MongoshAnalytics,
43
44
  MongoshAnalyticsIdentity,
44
45
  } from './analytics-helpers';
@@ -52,6 +53,40 @@ import type {
52
53
  MongoshLoggingAndTelemetryArguments,
53
54
  MongoshTrackingProperties,
54
55
  } from './types';
56
+ import { createHmac } from 'crypto';
57
+
58
+ /**
59
+ * @returns A hashed, unique identifier for the running device or `"unknown"` if not known.
60
+ */
61
+ export async function getDeviceId({
62
+ onError,
63
+ }: {
64
+ onError?: (error: Error) => void;
65
+ } = {}): Promise<string | 'unknown'> {
66
+ try {
67
+ // Create a hashed format from the all uppercase version of the machine ID
68
+ // to match it exactly with the denisbrodbeck/machineid library that Atlas CLI uses.
69
+ const originalId: string =
70
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
71
+ await require('native-machine-id').getMachineId({
72
+ raw: true,
73
+ });
74
+
75
+ if (!originalId) {
76
+ return 'unknown';
77
+ }
78
+ const hmac = createHmac('sha256', originalId);
79
+
80
+ /** This matches the message used to create the hashes in Atlas CLI */
81
+ const DEVICE_ID_HASH_MESSAGE = 'atlascli';
82
+
83
+ hmac.update(DEVICE_ID_HASH_MESSAGE);
84
+ return hmac.digest('hex');
85
+ } catch (error) {
86
+ onError?.(error as Error);
87
+ return 'unknown';
88
+ }
89
+ }
55
90
 
56
91
  export function setupLoggingAndTelemetry(
57
92
  props: MongoshLoggingAndTelemetryArguments
@@ -62,7 +97,8 @@ export function setupLoggingAndTelemetry(
62
97
  return loggingAndTelemetry;
63
98
  }
64
99
 
65
- class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
100
+ /** @internal */
101
+ export class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
66
102
  private static dummyLogger = new MongoLogWriter(
67
103
  '',
68
104
  null,
@@ -82,30 +118,71 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
82
118
  private readonly mongoshVersion: string;
83
119
 
84
120
  private log: MongoLogWriter;
85
- private pendingLogEvents: CallableFunction[] = [];
121
+ private pendingBusEvents: CallableFunction[] = [];
122
+ private pendingTelemetryEvents: CallableFunction[] = [];
86
123
  private isSetup = false;
87
- private isBufferingEvents = false;
124
+ private isBufferingBusEvents = false;
125
+ private isBufferingTelemetryEvents = false;
126
+
127
+ private deviceId: string | undefined;
128
+ /** @internal */
129
+ public setupTelemetryPromise: Promise<void> = Promise.resolve();
130
+
131
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
132
+ private resolveDeviceId: (value: string) => void = () => {};
88
133
 
89
134
  constructor({
90
135
  bus,
91
136
  analytics,
92
137
  userTraits,
93
138
  mongoshVersion,
139
+ deviceId,
94
140
  }: MongoshLoggingAndTelemetryArguments) {
95
141
  this.bus = bus;
96
142
  this.analytics = analytics;
97
143
  this.log = LoggingAndTelemetry.dummyLogger;
98
144
  this.userTraits = userTraits;
99
145
  this.mongoshVersion = mongoshVersion;
146
+ this.deviceId = deviceId;
100
147
  }
101
148
 
102
149
  public setup(): void {
103
150
  if (this.isSetup) {
104
151
  throw new Error('Setup can only be called once.');
105
152
  }
153
+ this.isBufferingTelemetryEvents = true;
154
+ this.isBufferingBusEvents = true;
155
+
156
+ this.setupTelemetryPromise = this.setupTelemetry();
106
157
  this.setupBusEventListeners();
158
+
107
159
  this.isSetup = true;
108
- this.isBufferingEvents = true;
160
+ }
161
+
162
+ public flush(): void {
163
+ // Run any telemetry events even if device ID hasn't been resolved yet
164
+ this.runAndClearPendingTelemetryEvents();
165
+
166
+ // Run any other pending events with the set or dummy log for telemetry purposes.
167
+ this.runAndClearPendingBusEvents();
168
+
169
+ this.resolveDeviceId('unknown');
170
+ }
171
+
172
+ private async setupTelemetry(): Promise<void> {
173
+ if (!this.deviceId) {
174
+ this.deviceId = await Promise.race([
175
+ getDeviceId({
176
+ onError: (error) =>
177
+ this.bus.emit('mongosh:error', error, 'telemetry'),
178
+ }),
179
+ new Promise<string>((resolve) => {
180
+ this.resolveDeviceId = resolve;
181
+ }),
182
+ ]);
183
+ }
184
+
185
+ this.runAndClearPendingTelemetryEvents();
109
186
  }
110
187
 
111
188
  public attachLogger(logger: MongoLogWriter): void {
@@ -119,24 +196,32 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
119
196
  );
120
197
  }
121
198
  this.log = logger;
122
- this.isBufferingEvents = false;
123
199
 
124
- this.runAndClearPendingEvents();
200
+ this.runAndClearPendingBusEvents();
125
201
 
126
202
  this.bus.emit('mongosh:log-initialized');
127
203
  }
128
204
 
129
205
  public detachLogger() {
130
206
  this.log = LoggingAndTelemetry.dummyLogger;
131
- // Still run any remaining pending events with the dummy log for telemetry purposes.
132
- this.runAndClearPendingEvents();
207
+
208
+ this.flush();
209
+ }
210
+
211
+ private runAndClearPendingBusEvents() {
212
+ let pendingEvent: CallableFunction | undefined;
213
+ while ((pendingEvent = this.pendingBusEvents.shift())) {
214
+ pendingEvent();
215
+ }
216
+ this.isBufferingBusEvents = false;
133
217
  }
134
218
 
135
- private runAndClearPendingEvents() {
219
+ private runAndClearPendingTelemetryEvents() {
136
220
  let pendingEvent: CallableFunction | undefined;
137
- while ((pendingEvent = this.pendingLogEvents.shift())) {
221
+ while ((pendingEvent = this.pendingTelemetryEvents.shift())) {
138
222
  pendingEvent();
139
223
  }
224
+ this.isBufferingTelemetryEvents = false;
140
225
  }
141
226
 
142
227
  /** Information used and set by different bus events. */
@@ -171,11 +256,10 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
171
256
  listener: (...args: Parameters<EventsMap[K]>) => void
172
257
  ) => {
173
258
  this.bus.on(event, ((...args: Parameters<EventsMap[K]>) => {
174
- if (this.isBufferingEvents) {
175
- this.pendingLogEvents.push(() => listener(...args));
259
+ if (this.isBufferingBusEvents) {
260
+ this.pendingBusEvents.push(() => listener(...args));
176
261
  return;
177
262
  }
178
-
179
263
  listener(...args);
180
264
  }) as MongoshBusEventsMap[K]);
181
265
  return this.bus;
@@ -193,12 +277,52 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
193
277
 
194
278
  const getTelemetryUserIdentity = (): MongoshAnalyticsIdentity => {
195
279
  return {
280
+ deviceId: this.deviceId,
196
281
  anonymousId:
197
282
  this.busEventState.telemetryAnonymousId ??
198
283
  (this.busEventState.userId as string),
199
284
  };
200
285
  };
201
286
 
287
+ const track = (
288
+ message: Pick<AnalyticsTrackMessage, 'event' | 'timestamp'> & {
289
+ properties?: Omit<
290
+ AnalyticsTrackMessage['properties'],
291
+ keyof MongoshTrackingProperties
292
+ >;
293
+ }
294
+ ): void => {
295
+ const callback = () =>
296
+ this.analytics.track({
297
+ ...getTelemetryUserIdentity(),
298
+ ...message,
299
+ properties: {
300
+ ...getTrackingProperties(),
301
+ ...message.properties,
302
+ },
303
+ });
304
+
305
+ if (this.isBufferingTelemetryEvents) {
306
+ this.pendingTelemetryEvents.push(callback);
307
+ } else {
308
+ callback();
309
+ }
310
+ };
311
+
312
+ const identify = (): void => {
313
+ const callback = () =>
314
+ this.analytics.identify({
315
+ ...getTelemetryUserIdentity(),
316
+ traits: getUserTraits(),
317
+ });
318
+
319
+ if (this.isBufferingTelemetryEvents) {
320
+ this.pendingTelemetryEvents.push(callback);
321
+ } else {
322
+ callback();
323
+ }
324
+ };
325
+
202
326
  onBus('mongosh:start-mongosh-repl', (ev: StartMongoshReplEvent) => {
203
327
  this.log.info(
204
328
  'MONGOSH',
@@ -230,7 +354,6 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
230
354
  atlas_hostname: args.is_atlas ? resolved_hostname : null,
231
355
  };
232
356
  const properties = {
233
- ...getTrackingProperties(),
234
357
  ...argsWithoutUriAndHostname,
235
358
  ...atlasHostname,
236
359
  };
@@ -248,8 +371,7 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
248
371
  }
249
372
  );
250
373
 
251
- this.analytics.track({
252
- ...getTelemetryUserIdentity(),
374
+ track({
253
375
  event: 'New Connection',
254
376
  properties,
255
377
  });
@@ -264,11 +386,9 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
264
386
  );
265
387
 
266
388
  const normalizedTimings = Object.fromEntries(normalizedTimingsArray);
267
- this.analytics.track({
268
- ...getTelemetryUserIdentity(),
389
+ track({
269
390
  event: 'Startup Time',
270
391
  properties: {
271
- ...getTrackingProperties(),
272
392
  is_interactive: args.isInteractive,
273
393
  js_context: args.jsContext,
274
394
  ...normalizedTimings,
@@ -284,10 +404,8 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
284
404
  }
285
405
  this.busEventState.telemetryAnonymousId =
286
406
  newTelemetryUserIdentity.anonymousId;
287
- this.analytics.identify({
288
- anonymousId: newTelemetryUserIdentity.anonymousId,
289
- traits: getUserTraits(),
290
- });
407
+
408
+ identify();
291
409
  }
292
410
  );
293
411
 
@@ -303,10 +421,7 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
303
421
  } else {
304
422
  this.busEventState.userId = updatedTelemetryUserIdentity.userId;
305
423
  }
306
- this.analytics.identify({
307
- ...getTelemetryUserIdentity(),
308
- traits: getUserTraits(),
309
- });
424
+ identify();
310
425
  this.log.info(
311
426
  'MONGOSH',
312
427
  mongoLogId(1_000_000_005),
@@ -334,11 +449,9 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
334
449
  );
335
450
 
336
451
  if (error.name.includes('Mongosh')) {
337
- this.analytics.track({
338
- ...getTelemetryUserIdentity(),
452
+ track({
339
453
  event: 'Error',
340
454
  properties: {
341
- ...getTrackingProperties(),
342
455
  name: mongoshError.name,
343
456
  code: mongoshError.code,
344
457
  scope: mongoshError.scope,
@@ -388,12 +501,8 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
388
501
  args
389
502
  );
390
503
 
391
- this.analytics.track({
392
- ...getTelemetryUserIdentity(),
504
+ track({
393
505
  event: 'Use',
394
- properties: {
395
- ...getTrackingProperties(),
396
- },
397
506
  });
398
507
  });
399
508
 
@@ -406,11 +515,9 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
406
515
  args
407
516
  );
408
517
 
409
- this.analytics.track({
410
- ...getTelemetryUserIdentity(),
518
+ track({
411
519
  event: 'Show',
412
520
  properties: {
413
- ...getTrackingProperties(),
414
521
  method: args.method,
415
522
  },
416
523
  });
@@ -452,13 +559,11 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
452
559
  args
453
560
  );
454
561
 
455
- this.analytics.track({
456
- ...getTelemetryUserIdentity(),
562
+ track({
457
563
  event: this.busEventState.hasStartedMongoshRepl
458
564
  ? 'Script Loaded'
459
565
  : 'Script Loaded CLI',
460
566
  properties: {
461
- ...getTrackingProperties(),
462
567
  nested: args.nested,
463
568
  ...(this.busEventState.hasStartedMongoshRepl
464
569
  ? {}
@@ -475,11 +580,9 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
475
580
  'Evaluating script passed on the command line'
476
581
  );
477
582
 
478
- this.analytics.track({
479
- ...getTelemetryUserIdentity(),
583
+ track({
480
584
  event: 'Script Evaluated',
481
585
  properties: {
482
- ...getTrackingProperties(),
483
586
  shell: this.busEventState.usesShellOption,
484
587
  },
485
588
  });
@@ -493,12 +596,8 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
493
596
  'Loading .mongoshrc.js'
494
597
  );
495
598
 
496
- this.analytics.track({
497
- ...getTelemetryUserIdentity(),
599
+ track({
498
600
  event: 'Mongoshrc Loaded',
499
- properties: {
500
- ...getTrackingProperties(),
501
- },
502
601
  });
503
602
  });
504
603
 
@@ -510,12 +609,8 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
510
609
  'Warning about .mongorc.js/.mongoshrc.js mismatch'
511
610
  );
512
611
 
513
- this.analytics.track({
514
- ...getTelemetryUserIdentity(),
612
+ track({
515
613
  event: 'Mongorc Warning',
516
- properties: {
517
- ...getTrackingProperties(),
518
- },
519
614
  });
520
615
  });
521
616
 
@@ -680,12 +775,8 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
680
775
  );
681
776
 
682
777
  if (ev.args[0] === 'install') {
683
- this.analytics.track({
684
- ...getTelemetryUserIdentity(),
778
+ track({
685
779
  event: 'Snippet Install',
686
- properties: {
687
- ...getTrackingProperties(),
688
- },
689
780
  });
690
781
  }
691
782
  });
@@ -738,21 +829,17 @@ class LoggingAndTelemetry implements MongoshLoggingAndTelemetry {
738
829
  entry
739
830
  );
740
831
 
741
- this.analytics.track({
742
- ...getTelemetryUserIdentity(),
832
+ track({
743
833
  event: 'Deprecated Method',
744
834
  properties: {
745
- ...getTrackingProperties(),
746
835
  ...entry,
747
836
  },
748
837
  });
749
838
  }
750
839
  for (const [entry, count] of apiCalls) {
751
- this.analytics.track({
752
- ...getTelemetryUserIdentity(),
840
+ track({
753
841
  event: 'API Call',
754
842
  properties: {
755
- ...getTrackingProperties(),
756
843
  ...entry,
757
844
  count,
758
845
  },
package/src/types.ts CHANGED
@@ -6,6 +6,8 @@ import type { MultiSet } from './helpers';
6
6
  export interface MongoshLoggingAndTelemetry {
7
7
  attachLogger(logger: MongoLogWriter): void;
8
8
  detachLogger(): void;
9
+ /** Flush any remaining log or telemetry events. */
10
+ flush(): void;
9
11
  }
10
12
 
11
13
  export type MongoshLoggingAndTelemetryArguments = {
@@ -15,6 +17,8 @@ export type MongoshLoggingAndTelemetryArguments = {
15
17
  [key: string]: unknown;
16
18
  };
17
19
  mongoshVersion: string;
20
+ /** Machine-specific ID; gets set automatically when omitted */
21
+ deviceId?: string | undefined;
18
22
  };
19
23
 
20
24
  export type MongoshTrackingProperties = {