@webex/internal-plugin-metrics 3.12.0-next.4 → 3.12.0-next.41

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 (65) 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 +638 -65
  6. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
  7. package/dist/call-diagnostic/call-diagnostic-metrics.js +167 -76
  8. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
  9. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +5 -0
  10. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
  11. package/dist/call-diagnostic/config.js +16 -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/index.js +15 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/metrics.js +1 -1
  18. package/dist/metrics.types.js +4 -0
  19. package/dist/metrics.types.js.map +1 -1
  20. package/dist/new-metrics.js +60 -17
  21. package/dist/new-metrics.js.map +1 -1
  22. package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
  23. package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
  24. package/dist/privacy-and-security-permission-enricher/index.js +143 -0
  25. package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
  26. package/dist/privacy-and-security-permission-enricher/types.js +7 -0
  27. package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
  28. package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
  29. package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
  30. package/dist/types/automated-user.d.ts +2 -0
  31. package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +208 -5
  32. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +67 -14
  33. package/dist/types/call-diagnostic/config.d.ts +4 -0
  34. package/dist/types/config.d.ts +1 -0
  35. package/dist/types/index.d.ts +5 -3
  36. package/dist/types/metrics.types.d.ts +7 -2
  37. package/dist/types/new-metrics.d.ts +17 -1
  38. package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
  39. package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
  40. package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
  41. package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
  42. package/package.json +12 -11
  43. package/src/automated-user.ts +16 -0
  44. package/src/batcher.js +4 -0
  45. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +800 -73
  46. package/src/call-diagnostic/call-diagnostic-metrics.ts +101 -15
  47. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +5 -0
  48. package/src/call-diagnostic/config.ts +14 -0
  49. package/src/config.js +1 -0
  50. package/src/index.ts +7 -0
  51. package/src/metrics.types.ts +25 -3
  52. package/src/new-metrics.ts +53 -6
  53. package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
  54. package/src/privacy-and-security-permission-enricher/index.ts +142 -0
  55. package/src/privacy-and-security-permission-enricher/types.ts +21 -0
  56. package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
  57. package/test/unit/spec/automated-user.ts +43 -0
  58. package/test/unit/spec/batcher.js +43 -0
  59. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +14 -2
  60. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1434 -303
  61. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +852 -159
  62. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +27 -0
  63. package/test/unit/spec/new-metrics.ts +183 -50
  64. package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
  65. package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
@@ -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
  });
@@ -192,6 +192,12 @@ describe('plugin-metrics', () => {
192
192
  webex.internal.newMetrics.callDiagnosticLatencies.getTotalJMTWithUserDelay = sinon
193
193
  .stub()
194
194
  .returns(64);
195
+ webex.internal.newMetrics.callDiagnosticLatencies.getInterstitialToJoinOK = sinon
196
+ .stub()
197
+ .returns(10);
198
+ webex.internal.newMetrics.callDiagnosticLatencies.getTotalJMT = sinon
199
+ .stub()
200
+ .returns(20);
195
201
  const promise = webex.internal.newMetrics.callDiagnosticMetrics.submitToCallDiagnostics(
196
202
  //@ts-ignore
197
203
  {event: {name: 'client.locus.join.response'}}
@@ -346,6 +352,12 @@ describe('plugin-metrics', () => {
346
352
  webex.internal.newMetrics.callDiagnosticLatencies.getStayLobbyTime = sinon
347
353
  .stub()
348
354
  .returns(1);
355
+ webex.internal.newMetrics.callDiagnosticLatencies.getStayLobbyTimeCappedBy = sinon
356
+ .stub()
357
+ .returns(1);
358
+ webex.internal.newMetrics.callDiagnosticLatencies.getTotalMediaJMT = sinon
359
+ .stub()
360
+ .returns(44);
349
361
  webex.internal.newMetrics.callDiagnosticLatencies.getTotalMediaJMTWithUserDelay = sinon
350
362
  .stub()
351
363
  .returns(43);
@@ -366,9 +378,9 @@ describe('plugin-metrics', () => {
366
378
  assert.deepEqual(webex.request.getCalls()[0].args[0].body.metrics[0].eventPayload.event, {
367
379
  name: 'client.media-engine.ready',
368
380
  joinTimes: {
369
- totalMediaJMT: 61,
381
+ totalMediaJMT: 44,
370
382
  interstitialToMediaOKJMT: 22,
371
- callInitMediaEngineReady: 10,
383
+ callInitMediaEngineReady: 22,
372
384
  totalMediaJMTWithUserDelay: 43,
373
385
  totalJMTWithUserDelay: 64,
374
386
  },