@webex/internal-plugin-metrics 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.3

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 (45) hide show
  1. package/README.md +36 -0
  2. package/dist/config.js +3 -0
  3. package/dist/config.js.map +1 -1
  4. package/dist/index.js.map +1 -1
  5. package/dist/metrics.js +1 -1
  6. package/dist/metrics.types.js.map +1 -1
  7. package/dist/new-metrics.js +47 -11
  8. package/dist/new-metrics.js.map +1 -1
  9. package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
  10. package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
  11. package/dist/privacy-and-security-permission-enricher/index.js +143 -0
  12. package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
  13. package/dist/privacy-and-security-permission-enricher/types.js +7 -0
  14. package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
  15. package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
  16. package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
  17. package/dist/types/config.d.ts +3 -0
  18. package/dist/types/index.d.ts +2 -2
  19. package/dist/types/metrics.types.d.ts +3 -0
  20. package/dist/types/new-metrics.d.ts +13 -1
  21. package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
  22. package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
  23. package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
  24. package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
  25. package/dist/types/unhandled-exception-telemetry/index.d.ts +60 -0
  26. package/dist/types/unhandled-exception-telemetry/utils.d.ts +6 -0
  27. package/dist/unhandled-exception-telemetry/index.js +330 -0
  28. package/dist/unhandled-exception-telemetry/index.js.map +1 -0
  29. package/dist/unhandled-exception-telemetry/utils.js +105 -0
  30. package/dist/unhandled-exception-telemetry/utils.js.map +1 -0
  31. package/package.json +8 -8
  32. package/src/config.js +3 -0
  33. package/src/index.ts +2 -0
  34. package/src/metrics.types.ts +9 -0
  35. package/src/new-metrics.ts +44 -2
  36. package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
  37. package/src/privacy-and-security-permission-enricher/index.ts +142 -0
  38. package/src/privacy-and-security-permission-enricher/types.ts +21 -0
  39. package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
  40. package/src/unhandled-exception-telemetry/index.ts +403 -0
  41. package/src/unhandled-exception-telemetry/utils.ts +101 -0
  42. package/test/unit/spec/new-metrics.ts +168 -52
  43. package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
  44. package/test/unit/spec/unhandled-exception-telemetry/utils.ts +109 -0
  45. package/test/unit/spec/unhandled-exception-telemetry.ts +688 -0
@@ -23,6 +23,15 @@ export type ClientUserNameInput = NonNullable<RawClientEvent['userNameInput']>;
23
23
 
24
24
  export type ClientEmailInput = NonNullable<RawClientEvent['emailInput']>;
25
25
 
26
+ export type PrivacyAndSecurityPermission = NonNullable<
27
+ RawClientEvent['privacyAndSecurityPermission']
28
+ >;
29
+
30
+ export type PrivacyAndSecurityPermissionResource = keyof PrivacyAndSecurityPermission;
31
+
32
+ export type PrivacyAndSecurityPermissionState =
33
+ PrivacyAndSecurityPermission[PrivacyAndSecurityPermissionResource];
34
+
26
35
  export type BrowserLaunchMethodType = NonNullable<
27
36
  RawEvent['origin']['clientInfo']
28
37
  >['browserLaunchMethod'];
@@ -19,18 +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
33
  import {isAutomatedUser as detectAutomatedUser} from './automated-user';
34
+ import PrivacyAndSecurityPermissionEnricher from './privacy-and-security-permission-enricher';
35
+ import {startUnhandledExceptionTelemetry} from './unhandled-exception-telemetry';
34
36
 
35
37
  /**
36
38
  * Metrics plugin to centralize all types of metrics.
@@ -68,6 +70,8 @@ class Metrics extends WebexPlugin {
68
70
 
69
71
  delayedClientFeatureEventsOverrides: Partial<DelayedClientFeatureEvent['options']> = {};
70
72
 
73
+ private privacyAndSecurityPermissionEnricher: PrivacyAndSecurityPermissionEnricher;
74
+
71
75
  /**
72
76
  * Constructor
73
77
  * @param args
@@ -78,6 +82,15 @@ class Metrics extends WebexPlugin {
78
82
  constructor(...args) {
79
83
  super(...args);
80
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
+ );
81
94
  // @ts-ignore
82
95
  this.callDiagnosticLatencies = new CallDiagnosticLatencies({}, {parent: this.webex});
83
96
  // @ts-ignore
@@ -103,6 +116,8 @@ class Metrics extends WebexPlugin {
103
116
  shouldDelay: this.delaySubmitClientEvents,
104
117
  overrides: this.delayedClientEventsOverrides,
105
118
  });
119
+ // @ts-ignore
120
+ startUnhandledExceptionTelemetry(this.webex);
106
121
  });
107
122
  }
108
123
 
@@ -381,14 +396,41 @@ class Metrics extends WebexPlugin {
381
396
  options: {meetingId: options?.meetingId},
382
397
  });
383
398
 
384
- return this.callDiagnosticMetrics.submitClientEvent({
399
+ const enrichedPayload = this.privacyAndSecurityPermissionEnricher.enrich({
385
400
  name,
386
401
  payload,
402
+ scope: this.getPermissionScope(options),
403
+ });
404
+
405
+ return this.callDiagnosticMetrics.submitClientEvent({
406
+ name,
407
+ payload: enrichedPayload,
387
408
  options,
388
409
  delaySubmitEvent: this.delaySubmitClientEvents,
389
410
  });
390
411
  }
391
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
+
392
434
  /**
393
435
  * Issue request to alias a user's pre-login ID with their CI UUID
394
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
+ };