@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.
- package/dist/automated-user.js +19 -0
- package/dist/automated-user.js.map +1 -0
- package/dist/batcher.js +3 -0
- package/dist/batcher.js.map +1 -1
- package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +639 -65
- package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
- package/dist/call-diagnostic/call-diagnostic-metrics.js +168 -76
- package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
- package/dist/call-diagnostic/call-diagnostic-metrics.util.js +18 -4
- package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
- package/dist/call-diagnostic/config.js +17 -3
- package/dist/call-diagnostic/config.js.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/generic-metrics.js +8 -6
- package/dist/generic-metrics.js.map +1 -1
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/metrics.js +1 -1
- package/dist/metrics.types.js +4 -0
- package/dist/metrics.types.js.map +1 -1
- package/dist/new-metrics.js +105 -26
- package/dist/new-metrics.js.map +1 -1
- package/dist/prelogin-metrics.js +106 -0
- package/dist/prelogin-metrics.js.map +1 -0
- package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
- package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
- package/dist/privacy-and-security-permission-enricher/index.js +143 -0
- package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
- package/dist/privacy-and-security-permission-enricher/types.js +7 -0
- package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
- package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
- package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
- package/dist/types/automated-user.d.ts +2 -0
- package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +208 -5
- package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +79 -15
- package/dist/types/call-diagnostic/call-diagnostic-metrics.util.d.ts +5 -0
- package/dist/types/call-diagnostic/config.d.ts +5 -0
- package/dist/types/config.d.ts +1 -0
- package/dist/types/index.d.ts +6 -3
- package/dist/types/metrics.types.d.ts +9 -4
- package/dist/types/new-metrics.d.ts +29 -1
- package/dist/types/prelogin-metrics.d.ts +47 -0
- package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
- package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
- package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
- package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
- package/package.json +12 -11
- package/src/automated-user.ts +16 -0
- package/src/batcher.js +4 -0
- package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +800 -75
- package/src/call-diagnostic/call-diagnostic-metrics.ts +102 -15
- package/src/call-diagnostic/call-diagnostic-metrics.util.ts +22 -2
- package/src/call-diagnostic/config.ts +15 -0
- package/src/config.js +1 -0
- package/src/generic-metrics.ts +6 -6
- package/src/index.ts +9 -0
- package/src/metrics.types.ts +27 -5
- package/src/new-metrics.ts +95 -6
- package/src/prelogin-metrics.ts +94 -0
- package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
- package/src/privacy-and-security-permission-enricher/index.ts +142 -0
- package/src/privacy-and-security-permission-enricher/types.ts +21 -0
- package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
- package/test/unit/spec/automated-user.ts +43 -0
- package/test/unit/spec/batcher.js +43 -0
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +47 -11
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1550 -416
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +887 -159
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +61 -3
- package/test/unit/spec/new-metrics.ts +183 -50
- package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
- package/test/unit/spec/prelogin-metrics.ts +132 -0
- package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
package/src/new-metrics.ts
CHANGED
|
@@ -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
|
-
|
|
89
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
});
|