@webex/internal-plugin-metrics 3.12.0-next.5 → 3.12.0-next.50

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.
Files changed (84) hide show
  1. package/README.md +274 -0
  2. package/dist/automated-user.js +19 -0
  3. package/dist/automated-user.js.map +1 -0
  4. package/dist/batcher.js +3 -0
  5. package/dist/batcher.js.map +1 -1
  6. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +635 -62
  7. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
  8. package/dist/call-diagnostic/call-diagnostic-metrics.js +188 -78
  9. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
  10. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +5 -0
  11. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
  12. package/dist/call-diagnostic/config.js +16 -3
  13. package/dist/call-diagnostic/config.js.map +1 -1
  14. package/dist/config.js +8 -0
  15. package/dist/config.js.map +1 -1
  16. package/dist/index.js +25 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/metrics.js +118 -5
  19. package/dist/metrics.js.map +1 -1
  20. package/dist/metrics.types.js +4 -0
  21. package/dist/metrics.types.js.map +1 -1
  22. package/dist/network-telemetry.js +610 -0
  23. package/dist/network-telemetry.js.map +1 -0
  24. package/dist/new-metrics.js +63 -17
  25. package/dist/new-metrics.js.map +1 -1
  26. package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
  27. package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
  28. package/dist/privacy-and-security-permission-enricher/index.js +143 -0
  29. package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
  30. package/dist/privacy-and-security-permission-enricher/types.js +7 -0
  31. package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
  32. package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
  33. package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
  34. package/dist/types/automated-user.d.ts +2 -0
  35. package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +208 -5
  36. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +73 -14
  37. package/dist/types/call-diagnostic/config.d.ts +4 -0
  38. package/dist/types/config.d.ts +9 -0
  39. package/dist/types/index.d.ts +5 -3
  40. package/dist/types/metrics.types.d.ts +7 -2
  41. package/dist/types/network-telemetry.d.ts +108 -0
  42. package/dist/types/new-metrics.d.ts +17 -1
  43. package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
  44. package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
  45. package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
  46. package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
  47. package/dist/types/unhandled-exception-telemetry/index.d.ts +60 -0
  48. package/dist/types/unhandled-exception-telemetry/utils.d.ts +6 -0
  49. package/dist/unhandled-exception-telemetry/index.js +330 -0
  50. package/dist/unhandled-exception-telemetry/index.js.map +1 -0
  51. package/dist/unhandled-exception-telemetry/utils.js +105 -0
  52. package/dist/unhandled-exception-telemetry/utils.js.map +1 -0
  53. package/package.json +12 -11
  54. package/src/automated-user.ts +16 -0
  55. package/src/batcher.js +4 -0
  56. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +781 -71
  57. package/src/call-diagnostic/call-diagnostic-metrics.ts +119 -16
  58. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +5 -0
  59. package/src/call-diagnostic/config.ts +14 -0
  60. package/src/config.js +8 -0
  61. package/src/index.ts +16 -0
  62. package/src/metrics.js +123 -5
  63. package/src/metrics.types.ts +25 -3
  64. package/src/network-telemetry.ts +757 -0
  65. package/src/new-metrics.ts +56 -6
  66. package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
  67. package/src/privacy-and-security-permission-enricher/index.ts +142 -0
  68. package/src/privacy-and-security-permission-enricher/types.ts +21 -0
  69. package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
  70. package/src/unhandled-exception-telemetry/index.ts +403 -0
  71. package/src/unhandled-exception-telemetry/utils.ts +101 -0
  72. package/test/unit/spec/automated-user.ts +43 -0
  73. package/test/unit/spec/batcher.js +43 -0
  74. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +14 -2
  75. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1424 -293
  76. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +906 -159
  77. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +27 -0
  78. package/test/unit/spec/metrics.js +14 -5
  79. package/test/unit/spec/network-telemetry.ts +610 -0
  80. package/test/unit/spec/new-metrics.ts +203 -50
  81. package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
  82. package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
  83. package/test/unit/spec/unhandled-exception-telemetry/utils.ts +109 -0
  84. package/test/unit/spec/unhandled-exception-telemetry.ts +688 -0
@@ -19,17 +19,20 @@ import {
19
19
  ClientEvent,
20
20
  FeatureEvent,
21
21
  EventPayload,
22
- OperationalEvent,
23
22
  MediaQualityEvent,
24
23
  InternalEvent,
25
24
  SubmitClientEventOptions,
26
25
  Table,
27
26
  DelayedClientEvent,
28
27
  DelayedClientFeatureEvent,
28
+ PrivacyAndSecurityPermission,
29
29
  } from './metrics.types';
30
30
  import CallDiagnosticLatencies from './call-diagnostic/call-diagnostic-metrics-latencies';
31
31
  import {setMetricTimings} from './call-diagnostic/call-diagnostic-metrics.util';
32
32
  import {generateCommonErrorMetadata} from './utils';
33
+ import {isAutomatedUser as detectAutomatedUser} from './automated-user';
34
+ import PrivacyAndSecurityPermissionEnricher from './privacy-and-security-permission-enricher';
35
+ import {startUnhandledExceptionTelemetry} from './unhandled-exception-telemetry';
33
36
 
34
37
  /**
35
38
  * Metrics plugin to centralize all types of metrics.
@@ -67,6 +70,8 @@ class Metrics extends WebexPlugin {
67
70
 
68
71
  delayedClientFeatureEventsOverrides: Partial<DelayedClientFeatureEvent['options']> = {};
69
72
 
73
+ private privacyAndSecurityPermissionEnricher: PrivacyAndSecurityPermissionEnricher;
74
+
70
75
  /**
71
76
  * Constructor
72
77
  * @param args
@@ -77,8 +82,19 @@ class Metrics extends WebexPlugin {
77
82
  constructor(...args) {
78
83
  super(...args);
79
84
 
85
+ this.privacyAndSecurityPermissionEnricher = new PrivacyAndSecurityPermissionEnricher(
86
+ (error) => {
87
+ // @ts-ignore
88
+ this.webex.logger.error(
89
+ 'NewMetrics: @submitClientEvent. Privacy and security permission enrichment failed.',
90
+ error
91
+ );
92
+ }
93
+ );
80
94
  // @ts-ignore
81
95
  this.callDiagnosticLatencies = new CallDiagnosticLatencies({}, {parent: this.webex});
96
+ // @ts-ignore
97
+ this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
82
98
  this.onReady();
83
99
  }
84
100
 
@@ -88,8 +104,6 @@ class Metrics extends WebexPlugin {
88
104
  private onReady() {
89
105
  // @ts-ignore
90
106
  this.webex.once('ready', () => {
91
- // @ts-ignore
92
- this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
93
107
  this.preLoginMetrics = new PreLoginMetrics(
94
108
  // @ts-ignore
95
109
  new PreLoginMetricsBatcher({}, {parent: this.webex}),
@@ -102,6 +116,8 @@ class Metrics extends WebexPlugin {
102
116
  shouldDelay: this.delaySubmitClientEvents,
103
117
  overrides: this.delayedClientEventsOverrides,
104
118
  });
119
+ // @ts-ignore
120
+ startUnhandledExceptionTelemetry(this.webex);
105
121
  });
106
122
  }
107
123
 
@@ -182,6 +198,13 @@ class Metrics extends WebexPlugin {
182
198
  return this.businessMetrics?.isReadyToSubmitEvents() ?? false;
183
199
  }
184
200
 
201
+ /**
202
+ * @returns whether the current user agent belongs to an automated user
203
+ */
204
+ isAutomatedUser() {
205
+ return detectAutomatedUser();
206
+ }
207
+
185
208
  /**
186
209
  * Behavioral event
187
210
  * @param args
@@ -325,7 +348,7 @@ class Metrics extends WebexPlugin {
325
348
  payload?: RecursivePartial<FeatureEvent['payload']>;
326
349
  options: any;
327
350
  }) {
328
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
351
+ if (!this.isReady) {
329
352
  // @ts-ignore
330
353
  this.webex.logger.log(
331
354
  `NewMetrics: @submitFeatureEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -360,7 +383,7 @@ class Metrics extends WebexPlugin {
360
383
  payload?: RecursivePartial<ClientEvent['payload']>;
361
384
  options?: SubmitClientEventOptions;
362
385
  }): Promise<any> {
363
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
386
+ if (!this.isReady) {
364
387
  // @ts-ignore
365
388
  this.webex.logger.log(
366
389
  `NewMetrics: @submitClientEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -373,14 +396,41 @@ class Metrics extends WebexPlugin {
373
396
  options: {meetingId: options?.meetingId},
374
397
  });
375
398
 
376
- return this.callDiagnosticMetrics.submitClientEvent({
399
+ const enrichedPayload = this.privacyAndSecurityPermissionEnricher.enrich({
377
400
  name,
378
401
  payload,
402
+ scope: this.getPermissionScope(options),
403
+ });
404
+
405
+ return this.callDiagnosticMetrics.submitClientEvent({
406
+ name,
407
+ payload: enrichedPayload,
379
408
  options,
380
409
  delaySubmitEvent: this.delaySubmitClientEvents,
381
410
  });
382
411
  }
383
412
 
413
+ /**
414
+ * Stores the latest browser permission state supplied by the client.
415
+ * @param permission latest normalized permission snapshot
416
+ */
417
+ public setPrivacyAndSecurityPermission(permission: PrivacyAndSecurityPermission): void {
418
+ this.privacyAndSecurityPermissionEnricher.setPermission(permission);
419
+ }
420
+
421
+ /**
422
+ * Resolves event options to the call identity used by Call Diagnostic where possible.
423
+ * @param options client event options
424
+ * @returns the permission history scope
425
+ */
426
+ private getPermissionScope(options?: SubmitClientEventOptions): string {
427
+ const meeting = options?.meetingId
428
+ ? (this as any).webex.meetings?.getBasicMeetingInformation?.(options.meetingId)
429
+ : undefined;
430
+
431
+ return options?.correlationId ?? meeting?.correlationId ?? 'default';
432
+ }
433
+
384
434
  /**
385
435
  * Issue request to alias a user's pre-login ID with their CI UUID
386
436
  * @param {string} preLoginId
@@ -0,0 +1,33 @@
1
+ import type {ClientEvent} from '../metrics.types';
2
+ import type {PermissionEnrichmentPolicy} from './types';
3
+
4
+ export const CAMERA_AND_MICROPHONE_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
5
+ 'client.call.initiated',
6
+ 'client.media.capabilities',
7
+ 'client.ice.end',
8
+ 'client.locus.join.request',
9
+ 'client.locus.join.response',
10
+ 'client.media-engine.ready',
11
+ ]);
12
+
13
+ export const MEDIA_TX_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
14
+ 'client.media.tx.start',
15
+ 'client.media.tx.stop',
16
+ ]);
17
+
18
+ export const CONTENT_SHARE_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
19
+ 'client.share.initiated',
20
+ 'client.share.floor-grant.request',
21
+ 'client.share.floor-granted.local',
22
+ ]);
23
+
24
+ export const FINAL_PERMISSION_EVENTS = new Set<ClientEvent['name']>([
25
+ 'client.call.leave',
26
+ 'client.call.remote-ended',
27
+ 'client.call.aborted',
28
+ ]);
29
+
30
+ export const NO_PERMISSION_ENRICHMENT: PermissionEnrichmentPolicy = {
31
+ resources: [],
32
+ terminal: false,
33
+ };
@@ -0,0 +1,142 @@
1
+ import type {ClientEventPayload, PrivacyAndSecurityPermission} from '../metrics.types';
2
+ import {
3
+ CAMERA_AND_MICROPHONE_PERMISSION_EVENTS,
4
+ CONTENT_SHARE_PERMISSION_EVENTS,
5
+ FINAL_PERMISSION_EVENTS,
6
+ MEDIA_TX_PERMISSION_EVENTS,
7
+ NO_PERMISSION_ENRICHMENT,
8
+ } from './constants';
9
+ import type {
10
+ PermissionEnrichmentContext,
11
+ PermissionEnrichmentPolicy,
12
+ PermissionEnrichmentRule,
13
+ } from './types';
14
+ import {
15
+ getChangedPermission,
16
+ projectPrivacyAndSecurityPermission,
17
+ resolveContentShareResources,
18
+ resolveMediaResources,
19
+ } from './utils';
20
+
21
+ export const PERMISSION_ENRICHMENT_RULES = [
22
+ {
23
+ events: CAMERA_AND_MICROPHONE_PERMISSION_EVENTS,
24
+ resolve: () => ({resources: ['camera', 'microphone'], terminal: false}),
25
+ },
26
+ {
27
+ events: MEDIA_TX_PERMISSION_EVENTS,
28
+ resolve: (payload) => ({resources: resolveMediaResources(payload), terminal: false}),
29
+ },
30
+ {
31
+ events: CONTENT_SHARE_PERMISSION_EVENTS,
32
+ resolve: (payload) => ({
33
+ resources: resolveContentShareResources(payload),
34
+ terminal: false,
35
+ }),
36
+ },
37
+ {
38
+ events: FINAL_PERMISSION_EVENTS,
39
+ resolve: () => ({resources: ['camera', 'microphone', 'contentShare'], terminal: true}),
40
+ },
41
+ ] satisfies readonly PermissionEnrichmentRule[];
42
+
43
+ const resolvePermissionEnrichmentPolicy = (
44
+ name: PermissionEnrichmentContext['name'],
45
+ payload?: ClientEventPayload
46
+ ): PermissionEnrichmentPolicy =>
47
+ PERMISSION_ENRICHMENT_RULES.find(({events}) => events.has(name))?.resolve(payload) ??
48
+ NO_PERMISSION_ENRICHMENT;
49
+
50
+ /**
51
+ * Enriches eligible client events with relevant browser permission changes.
52
+ */
53
+ export default class PrivacyAndSecurityPermissionEnricher {
54
+ private permission?: PrivacyAndSecurityPermission;
55
+
56
+ private lastReported = new Map<string, PrivacyAndSecurityPermission>();
57
+
58
+ private readonly onEnrichmentError: (error: unknown) => void;
59
+
60
+ /**
61
+ * Creates a permission enricher.
62
+ * @param {Function} onEnrichmentError permission enrichment error handler
63
+ */
64
+ constructor(onEnrichmentError: (error: unknown) => void) {
65
+ this.onEnrichmentError = onEnrichmentError;
66
+ }
67
+
68
+ /**
69
+ * Stores the latest normalized browser permission state.
70
+ * @param {PrivacyAndSecurityPermission} permission permission snapshot
71
+ * @returns {void}
72
+ */
73
+ public setPermission(permission: PrivacyAndSecurityPermission): void {
74
+ this.permission = projectPrivacyAndSecurityPermission(permission, [
75
+ 'camera',
76
+ 'microphone',
77
+ 'contentShare',
78
+ ]);
79
+ }
80
+
81
+ /**
82
+ * Returns the original payload or a copy enriched with relevant permission changes.
83
+ * @param {PermissionEnrichmentContext} context event context and permission history scope
84
+ * @returns {ClientEventPayload | undefined}
85
+ */
86
+ public enrich({
87
+ name,
88
+ payload,
89
+ scope,
90
+ }: PermissionEnrichmentContext): ClientEventPayload | undefined {
91
+ const policy = resolvePermissionEnrichmentPolicy(name, payload);
92
+
93
+ try {
94
+ if (payload?.privacyAndSecurityPermission !== undefined) {
95
+ // An explicitly supplied permission payload is authoritative for this event.
96
+ if (!policy.terminal) {
97
+ this.lastReported.set(scope, {
98
+ ...this.lastReported.get(scope),
99
+ ...(payload.privacyAndSecurityPermission as PrivacyAndSecurityPermission),
100
+ });
101
+ }
102
+
103
+ return payload;
104
+ }
105
+
106
+ if (policy.resources.length === 0) {
107
+ return payload;
108
+ }
109
+
110
+ const projectedPermission = this.permission
111
+ ? projectPrivacyAndSecurityPermission(this.permission, policy.resources)
112
+ : undefined;
113
+
114
+ if (!projectedPermission) {
115
+ return payload;
116
+ }
117
+
118
+ if (policy.terminal) {
119
+ return {...payload, privacyAndSecurityPermission: projectedPermission};
120
+ }
121
+
122
+ const lastReported = this.lastReported.get(scope) ?? {};
123
+ const changedPermission = getChangedPermission(projectedPermission, lastReported);
124
+
125
+ if (!changedPermission) {
126
+ return payload;
127
+ }
128
+
129
+ this.lastReported.set(scope, {...lastReported, ...changedPermission});
130
+
131
+ return {...payload, privacyAndSecurityPermission: changedPermission};
132
+ } catch (error) {
133
+ this.onEnrichmentError(error);
134
+
135
+ return payload;
136
+ } finally {
137
+ if (policy.terminal) {
138
+ this.lastReported.delete(scope);
139
+ }
140
+ }
141
+ }
142
+ }
@@ -0,0 +1,21 @@
1
+ import type {
2
+ ClientEvent,
3
+ ClientEventPayload,
4
+ PrivacyAndSecurityPermissionResource,
5
+ } from '../metrics.types';
6
+
7
+ export type PermissionEnrichmentPolicy = {
8
+ resources: readonly PrivacyAndSecurityPermissionResource[];
9
+ terminal: boolean;
10
+ };
11
+
12
+ export type PermissionEnrichmentRule = {
13
+ events: ReadonlySet<ClientEvent['name']>;
14
+ resolve: (payload?: ClientEventPayload) => PermissionEnrichmentPolicy;
15
+ };
16
+
17
+ export type PermissionEnrichmentContext = {
18
+ name: ClientEvent['name'];
19
+ payload?: ClientEventPayload;
20
+ scope: string;
21
+ };
@@ -0,0 +1,82 @@
1
+ import {isEqual} from 'lodash';
2
+
3
+ import type {
4
+ ClientEventPayload,
5
+ PrivacyAndSecurityPermission,
6
+ PrivacyAndSecurityPermissionResource,
7
+ } from '../metrics.types';
8
+
9
+ export const resolveMediaResources = (
10
+ payload?: ClientEventPayload
11
+ ): PrivacyAndSecurityPermissionResource[] => {
12
+ switch (payload?.mediaType) {
13
+ case 'audio':
14
+ return ['microphone'];
15
+ case 'video':
16
+ return ['camera'];
17
+ case 'share':
18
+ return ['contentShare'];
19
+ default:
20
+ return [];
21
+ }
22
+ };
23
+
24
+ export const resolveContentShareResources = (
25
+ payload?: ClientEventPayload
26
+ ): PrivacyAndSecurityPermissionResource[] => {
27
+ if (payload?.mediaType !== 'share') {
28
+ return [];
29
+ }
30
+
31
+ return ['contentShare'];
32
+ };
33
+
34
+ const copyPermissionState = (
35
+ source: PrivacyAndSecurityPermission,
36
+ target: PrivacyAndSecurityPermission,
37
+ resource: PrivacyAndSecurityPermissionResource
38
+ ): void => {
39
+ switch (resource) {
40
+ case 'camera':
41
+ if (source.camera) {
42
+ target.camera = {...source.camera};
43
+ }
44
+ break;
45
+ case 'microphone':
46
+ if (source.microphone) {
47
+ target.microphone = {...source.microphone};
48
+ }
49
+ break;
50
+ case 'contentShare':
51
+ if (source.contentShare) {
52
+ target.contentShare = {...source.contentShare};
53
+ }
54
+ break;
55
+ default:
56
+ break;
57
+ }
58
+ };
59
+
60
+ export const projectPrivacyAndSecurityPermission = (
61
+ permission: PrivacyAndSecurityPermission,
62
+ resources: readonly PrivacyAndSecurityPermissionResource[]
63
+ ): PrivacyAndSecurityPermission | undefined => {
64
+ const projectedPermission: PrivacyAndSecurityPermission = {};
65
+
66
+ resources.forEach((resource) => {
67
+ copyPermissionState(permission, projectedPermission, resource);
68
+ });
69
+
70
+ return Object.keys(projectedPermission).length > 0 ? projectedPermission : undefined;
71
+ };
72
+
73
+ export const getChangedPermission = (
74
+ current: PrivacyAndSecurityPermission,
75
+ previous: PrivacyAndSecurityPermission
76
+ ): PrivacyAndSecurityPermission | undefined => {
77
+ const changedResources = (Object.keys(current) as PrivacyAndSecurityPermissionResource[]).filter(
78
+ (resource) => !isEqual(current[resource], previous[resource])
79
+ );
80
+
81
+ return projectPrivacyAndSecurityPermission(current, changedResources);
82
+ };