@webex/internal-plugin-metrics 3.11.0 → 3.12.0-llmrefactor.2

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 (74) hide show
  1. package/dist/automated-user.js +19 -0
  2. package/dist/automated-user.js.map +1 -0
  3. package/dist/batcher.js +3 -0
  4. package/dist/batcher.js.map +1 -1
  5. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +639 -65
  6. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
  7. package/dist/call-diagnostic/call-diagnostic-metrics.js +168 -76
  8. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
  9. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +18 -4
  10. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
  11. package/dist/call-diagnostic/config.js +17 -3
  12. package/dist/call-diagnostic/config.js.map +1 -1
  13. package/dist/config.js +1 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/generic-metrics.js +8 -6
  16. package/dist/generic-metrics.js.map +1 -1
  17. package/dist/index.js +22 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/metrics.js +1 -1
  20. package/dist/metrics.types.js +4 -0
  21. package/dist/metrics.types.js.map +1 -1
  22. package/dist/new-metrics.js +105 -26
  23. package/dist/new-metrics.js.map +1 -1
  24. package/dist/prelogin-metrics.js +106 -0
  25. package/dist/prelogin-metrics.js.map +1 -0
  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 +79 -15
  37. package/dist/types/call-diagnostic/call-diagnostic-metrics.util.d.ts +5 -0
  38. package/dist/types/call-diagnostic/config.d.ts +5 -0
  39. package/dist/types/config.d.ts +1 -0
  40. package/dist/types/index.d.ts +6 -3
  41. package/dist/types/metrics.types.d.ts +9 -4
  42. package/dist/types/new-metrics.d.ts +29 -1
  43. package/dist/types/prelogin-metrics.d.ts +47 -0
  44. package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
  45. package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
  46. package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
  47. package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
  48. package/package.json +12 -11
  49. package/src/automated-user.ts +16 -0
  50. package/src/batcher.js +4 -0
  51. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +800 -75
  52. package/src/call-diagnostic/call-diagnostic-metrics.ts +102 -15
  53. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +22 -2
  54. package/src/call-diagnostic/config.ts +15 -0
  55. package/src/config.js +1 -0
  56. package/src/generic-metrics.ts +6 -6
  57. package/src/index.ts +9 -0
  58. package/src/metrics.types.ts +27 -5
  59. package/src/new-metrics.ts +95 -6
  60. package/src/prelogin-metrics.ts +94 -0
  61. package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
  62. package/src/privacy-and-security-permission-enricher/index.ts +142 -0
  63. package/src/privacy-and-security-permission-enricher/types.ts +21 -0
  64. package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
  65. package/test/unit/spec/automated-user.ts +43 -0
  66. package/test/unit/spec/batcher.js +43 -0
  67. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +47 -11
  68. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1550 -416
  69. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +887 -159
  70. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +61 -3
  71. package/test/unit/spec/new-metrics.ts +183 -50
  72. package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
  73. package/test/unit/spec/prelogin-metrics.ts +132 -0
  74. package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
@@ -9,6 +9,8 @@ import CallDiagnosticMetrics from './call-diagnostic/call-diagnostic-metrics';
9
9
  import BehavioralMetrics from './behavioral-metrics';
10
10
  import OperationalMetrics from './operational-metrics';
11
11
  import BusinessMetrics from './business-metrics';
12
+ import PreLoginMetrics from './prelogin-metrics';
13
+ import PreLoginMetricsBatcher from './prelogin-metrics-batcher';
12
14
  import {
13
15
  RecursivePartial,
14
16
  MetricEventProduct,
@@ -17,17 +19,19 @@ import {
17
19
  ClientEvent,
18
20
  FeatureEvent,
19
21
  EventPayload,
20
- OperationalEvent,
21
22
  MediaQualityEvent,
22
23
  InternalEvent,
23
24
  SubmitClientEventOptions,
24
25
  Table,
25
26
  DelayedClientEvent,
26
27
  DelayedClientFeatureEvent,
28
+ PrivacyAndSecurityPermission,
27
29
  } from './metrics.types';
28
30
  import CallDiagnosticLatencies from './call-diagnostic/call-diagnostic-metrics-latencies';
29
31
  import {setMetricTimings} from './call-diagnostic/call-diagnostic-metrics.util';
30
32
  import {generateCommonErrorMetadata} from './utils';
33
+ import {isAutomatedUser as detectAutomatedUser} from './automated-user';
34
+ import PrivacyAndSecurityPermissionEnricher from './privacy-and-security-permission-enricher';
31
35
 
32
36
  /**
33
37
  * Metrics plugin to centralize all types of metrics.
@@ -45,6 +49,7 @@ class Metrics extends WebexPlugin {
45
49
  behavioralMetrics: BehavioralMetrics;
46
50
  operationalMetrics: OperationalMetrics;
47
51
  businessMetrics: BusinessMetrics;
52
+ preLoginMetrics: PreLoginMetrics;
48
53
  isReady = false;
49
54
 
50
55
  /**
@@ -64,6 +69,8 @@ class Metrics extends WebexPlugin {
64
69
 
65
70
  delayedClientFeatureEventsOverrides: Partial<DelayedClientFeatureEvent['options']> = {};
66
71
 
72
+ private privacyAndSecurityPermissionEnricher: PrivacyAndSecurityPermissionEnricher;
73
+
67
74
  /**
68
75
  * Constructor
69
76
  * @param args
@@ -74,8 +81,19 @@ class Metrics extends WebexPlugin {
74
81
  constructor(...args) {
75
82
  super(...args);
76
83
 
84
+ this.privacyAndSecurityPermissionEnricher = new PrivacyAndSecurityPermissionEnricher(
85
+ (error) => {
86
+ // @ts-ignore
87
+ this.webex.logger.error(
88
+ 'NewMetrics: @submitClientEvent. Privacy and security permission enrichment failed.',
89
+ error
90
+ );
91
+ }
92
+ );
77
93
  // @ts-ignore
78
94
  this.callDiagnosticLatencies = new CallDiagnosticLatencies({}, {parent: this.webex});
95
+ // @ts-ignore
96
+ this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
79
97
  this.onReady();
80
98
  }
81
99
 
@@ -85,8 +103,13 @@ class Metrics extends WebexPlugin {
85
103
  private onReady() {
86
104
  // @ts-ignore
87
105
  this.webex.once('ready', () => {
88
- // @ts-ignore
89
- this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
106
+ this.preLoginMetrics = new PreLoginMetrics(
107
+ // @ts-ignore
108
+ new PreLoginMetricsBatcher({}, {parent: this.webex}),
109
+ {},
110
+ // @ts-ignore
111
+ {parent: this.webex}
112
+ );
90
113
  this.isReady = true;
91
114
  this.setDelaySubmitClientEvents({
92
115
  shouldDelay: this.delaySubmitClientEvents,
@@ -172,6 +195,13 @@ class Metrics extends WebexPlugin {
172
195
  return this.businessMetrics?.isReadyToSubmitEvents() ?? false;
173
196
  }
174
197
 
198
+ /**
199
+ * @returns whether the current user agent belongs to an automated user
200
+ */
201
+ isAutomatedUser() {
202
+ return detectAutomatedUser();
203
+ }
204
+
175
205
  /**
176
206
  * Behavioral event
177
207
  * @param args
@@ -251,6 +281,38 @@ class Metrics extends WebexPlugin {
251
281
  return this.businessMetrics.submitBusinessEvent({name, payload, table, metadata});
252
282
  }
253
283
 
284
+ /**
285
+ * Call Analyzer: Pre-Login Event
286
+ * @param args
287
+ */
288
+ submitPreLoginEvent({
289
+ name,
290
+ preLoginId,
291
+ payload,
292
+ metadata,
293
+ }: {
294
+ name: string;
295
+ preLoginId: string;
296
+ payload: EventPayload;
297
+ metadata?: EventPayload;
298
+ }): Promise<void> {
299
+ if (!this.isReady) {
300
+ // @ts-ignore
301
+ this.webex.logger.log(
302
+ `NewMetrics: @submitPreLoginEvent. Attempted to submit before webex.ready: ${name}`
303
+ );
304
+
305
+ return Promise.resolve();
306
+ }
307
+
308
+ return this.preLoginMetrics.submitPreLoginEvent({
309
+ name,
310
+ preLoginId,
311
+ payload,
312
+ metadata,
313
+ });
314
+ }
315
+
254
316
  /**
255
317
  * Call Analyzer: Media Quality Event
256
318
  * @param args
@@ -283,7 +345,7 @@ class Metrics extends WebexPlugin {
283
345
  payload?: RecursivePartial<FeatureEvent['payload']>;
284
346
  options: any;
285
347
  }) {
286
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
348
+ if (!this.isReady) {
287
349
  // @ts-ignore
288
350
  this.webex.logger.log(
289
351
  `NewMetrics: @submitFeatureEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -318,7 +380,7 @@ class Metrics extends WebexPlugin {
318
380
  payload?: RecursivePartial<ClientEvent['payload']>;
319
381
  options?: SubmitClientEventOptions;
320
382
  }): Promise<any> {
321
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
383
+ if (!this.isReady) {
322
384
  // @ts-ignore
323
385
  this.webex.logger.log(
324
386
  `NewMetrics: @submitClientEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -331,14 +393,41 @@ class Metrics extends WebexPlugin {
331
393
  options: {meetingId: options?.meetingId},
332
394
  });
333
395
 
334
- return this.callDiagnosticMetrics.submitClientEvent({
396
+ const enrichedPayload = this.privacyAndSecurityPermissionEnricher.enrich({
335
397
  name,
336
398
  payload,
399
+ scope: this.getPermissionScope(options),
400
+ });
401
+
402
+ return this.callDiagnosticMetrics.submitClientEvent({
403
+ name,
404
+ payload: enrichedPayload,
337
405
  options,
338
406
  delaySubmitEvent: this.delaySubmitClientEvents,
339
407
  });
340
408
  }
341
409
 
410
+ /**
411
+ * Stores the latest browser permission state supplied by the client.
412
+ * @param permission latest normalized permission snapshot
413
+ */
414
+ public setPrivacyAndSecurityPermission(permission: PrivacyAndSecurityPermission): void {
415
+ this.privacyAndSecurityPermissionEnricher.setPermission(permission);
416
+ }
417
+
418
+ /**
419
+ * Resolves event options to the call identity used by Call Diagnostic where possible.
420
+ * @param options client event options
421
+ * @returns the permission history scope
422
+ */
423
+ private getPermissionScope(options?: SubmitClientEventOptions): string {
424
+ const meeting = options?.meetingId
425
+ ? (this as any).webex.meetings?.getBasicMeetingInformation?.(options.meetingId)
426
+ : undefined;
427
+
428
+ return options?.correlationId ?? meeting?.correlationId ?? 'default';
429
+ }
430
+
342
431
  /**
343
432
  * Issue request to alias a user's pre-login ID with their CI UUID
344
433
  * @param {string} preLoginId
@@ -0,0 +1,94 @@
1
+ import GenericMetrics from './generic-metrics';
2
+ import {BusinessEvent, EventPayload} from './metrics.types';
3
+ import PreLoginMetricsBatcher from './prelogin-metrics-batcher';
4
+
5
+ /**
6
+ * @description Util class to handle PreLogin Metrics
7
+ * @export
8
+ * @class PreLoginMetrics
9
+ */
10
+ export default class PreLoginMetrics extends GenericMetrics {
11
+ private preLoginMetricsBatcher: typeof PreLoginMetricsBatcher;
12
+
13
+ /**
14
+ * Constructor
15
+ * @param {PreLoginMetricsBatcher} preLoginMetricsBatcher - Pre-login metrics batcher
16
+ * @param {any} attrs - Attributes
17
+ * @param {any} options - Options
18
+ * @constructor
19
+ */
20
+ constructor(
21
+ preLoginMetricsBatcher: typeof PreLoginMetricsBatcher,
22
+ attrs: any = {},
23
+ options: {parent?: any} = {}
24
+ ) {
25
+ super(attrs, options);
26
+ this.preLoginMetricsBatcher = preLoginMetricsBatcher;
27
+ }
28
+
29
+ /**
30
+ * Submit a business metric to our metrics endpoint.
31
+ * Routes to the correct table with the correct schema payload by table.
32
+ * @see https://confluence-eng-gpk2.cisco.com/conf/display/WAP/Business+metrics++-%3E+ROMA
33
+ * @param {Object} options - The options object
34
+ * @param {string} options.name - Name of the metric
35
+ * @param {string} options.preLoginId - ID to identify pre-login user
36
+ * @param {EventPayload} options.payload - User payload of the metric
37
+ * @param {EventPayload} [options.metadata] - Optional metadata to include outside of eventPayload.value
38
+ * @returns {Promise<void>} Promise that resolves when the metric is submitted
39
+ */
40
+ public submitPreLoginEvent({
41
+ name,
42
+ preLoginId,
43
+ payload,
44
+ metadata,
45
+ }: {
46
+ name: string;
47
+ preLoginId: string;
48
+ payload: EventPayload;
49
+ metadata?: EventPayload;
50
+ }): Promise<void> {
51
+ if (!metadata) {
52
+ metadata = {};
53
+ }
54
+ if (!metadata.appType) {
55
+ metadata.appType = 'Web Client';
56
+ }
57
+
58
+ const finalEvent = this.buildEvent(name, preLoginId, payload, metadata);
59
+
60
+ this.preLoginMetricsBatcher.savePreLoginId(preLoginId);
61
+
62
+ return this.preLoginMetricsBatcher.request(finalEvent);
63
+ }
64
+
65
+ /**
66
+ * Builds a formatted event object for metrics submission.
67
+ * @param {string} metricName - Metric name
68
+ * @param {string} preLoginId - Pre-login user identifier
69
+ * @param {EventPayload} payload - Metric payload data
70
+ * @param {EventPayload} metadata - Additional metadata to include in the event
71
+ * @returns {object} Formatted metrics event object with type, eventPayload, and timestamp
72
+ */
73
+ private buildEvent(
74
+ metricName: string,
75
+ preLoginId: string,
76
+ payload: EventPayload,
77
+ metadata: EventPayload
78
+ ): BusinessEvent {
79
+ return {
80
+ type: ['business'],
81
+ eventPayload: {
82
+ metricName,
83
+ browserDetails: this.getBrowserDetails(),
84
+ context: this.getContext(),
85
+ timestamp: new Date().getTime(),
86
+ value: {
87
+ preLoginId,
88
+ ...metadata,
89
+ ...payload,
90
+ },
91
+ },
92
+ };
93
+ }
94
+ }
@@ -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
+ };
@@ -0,0 +1,43 @@
1
+ import {assert} from '@webex/test-helper-chai';
2
+
3
+ import {isAutomatedUser, isAutomatedUserAgent} from '../../../src/automated-user';
4
+
5
+ describe('automated user detection', () => {
6
+ [
7
+ {userAgent: 'SkypeUriPreview', expected: true},
8
+ {userAgent: 'skypeuripreview', expected: true},
9
+ {userAgent: 'Mozilla/5.0 (compatible; SkypeUriPreview/0.1)', expected: true},
10
+ {userAgent: 'Googlebot/2.1 (+http://www.google.com/bot.html)', expected: true},
11
+ {
12
+ userAgent:
13
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36',
14
+ expected: false,
15
+ },
16
+ {userAgent: undefined, expected: false},
17
+ ].forEach(({userAgent, expected}) => {
18
+ it(`returns ${expected} for ${userAgent || 'an undefined user agent'}`, () => {
19
+ assert.equal(isAutomatedUserAgent(userAgent), expected);
20
+ });
21
+ });
22
+
23
+ it('returns the cached classification when navigator changes', () => {
24
+ const cachedResult = isAutomatedUser();
25
+ const originalDescriptor = Object.getOwnPropertyDescriptor(global, 'navigator');
26
+
27
+ Object.defineProperty(global, 'navigator', {
28
+ value: cachedResult
29
+ ? {userAgent: 'Mozilla/5.0', webdriver: false}
30
+ : {userAgent: 'SkypeUriPreview', webdriver: true},
31
+ configurable: true,
32
+ writable: true,
33
+ });
34
+
35
+ assert.equal(isAutomatedUser(), cachedResult);
36
+
37
+ if (originalDescriptor) {
38
+ Object.defineProperty(global, 'navigator', originalDescriptor);
39
+ } else {
40
+ delete (global as any).navigator;
41
+ }
42
+ });
43
+ });
@@ -177,6 +177,49 @@ describe('plugin-metrics', () => {
177
177
  assert.lengthOf(webex.internal.metrics.batcher.queue, 0);
178
178
  });
179
179
  });
180
+
181
+ it('rejects the deferred without reenqueuing when batcherRetryOnNetworkError is false', () => {
182
+ webex.config.metrics = {...config.metrics, batcherRetryOnNetworkError: false};
183
+
184
+ webex.request = function () {
185
+ // noop
186
+ };
187
+
188
+ sinon.stub(webex, 'request').callsFake((options) => {
189
+ options.headers = {
190
+ trackingid: 'test-no-retry',
191
+ };
192
+
193
+ return Promise.reject(
194
+ new WebexHttpError.NetworkOrCORSError({
195
+ statusCode: 0,
196
+ options,
197
+ })
198
+ );
199
+ });
200
+
201
+ const promise = webex.internal.metrics.batcher.request({
202
+ key: 'testMetric',
203
+ });
204
+
205
+ return promiseTick(50)
206
+ .then(() => assert.lengthOf(webex.internal.metrics.batcher.queue, 1))
207
+ .then(() => clock.tick(config.metrics.batcherWait))
208
+ .then(() => assert.calledOnce(webex.request))
209
+ .then(() => promiseTick(50))
210
+ .then(() =>
211
+ promise.then(
212
+ () => {
213
+ assert.fail('promise should have been rejected');
214
+ },
215
+ (reason) => {
216
+ assert.instanceOf(reason, WebexHttpError.NetworkOrCORSError);
217
+ assert.lengthOf(webex.internal.metrics.batcher.queue, 0);
218
+ assert.calledOnce(webex.request);
219
+ }
220
+ )
221
+ );
222
+ });
180
223
  });
181
224
  });
182
225
  });