@webex/internal-plugin-metrics 3.12.0-webex-services-ready.4 → 3.13.0-next.1
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/.repo-context.json +466 -0
- package/.sdd/manifest.json +626 -0
- package/AGENTS.md +208 -0
- package/README.md +274 -0
- package/dist/call-diagnostic/call-diagnostic-metrics.js +155 -94
- package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
- package/dist/call-diagnostic/config.js +3 -1
- package/dist/call-diagnostic/config.js.map +1 -1
- package/dist/config.js +7 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics.js +118 -5
- package/dist/metrics.js.map +1 -1
- package/dist/metrics.types.js.map +1 -1
- package/dist/network-telemetry.js +610 -0
- package/dist/network-telemetry.js.map +1 -0
- package/dist/new-metrics.js +47 -11
- package/dist/new-metrics.js.map +1 -1
- 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/call-diagnostic/call-diagnostic-metrics.d.ts +12 -3
- package/dist/types/call-diagnostic/config.d.ts +2 -0
- package/dist/types/config.d.ts +8 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/metrics.types.d.ts +7 -3
- package/dist/types/network-telemetry.d.ts +108 -0
- package/dist/types/new-metrics.d.ts +13 -1
- 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/dist/types/unhandled-exception-telemetry/index.d.ts +60 -0
- package/dist/types/unhandled-exception-telemetry/utils.d.ts +6 -0
- package/dist/unhandled-exception-telemetry/index.js +331 -0
- package/dist/unhandled-exception-telemetry/index.js.map +1 -0
- package/dist/unhandled-exception-telemetry/utils.js +105 -0
- package/dist/unhandled-exception-telemetry/utils.js.map +1 -0
- package/docs/adr/index.md +27 -0
- package/docs/architecture.md +437 -0
- package/docs/getting-started.md +131 -0
- package/docs/index.md +52 -0
- package/docs/specs/README.md +88 -0
- package/package.json +11 -11
- package/schemas/repo-context.schema.json +796 -0
- package/src/call-diagnostic/call-diagnostic-metrics.ts +100 -30
- package/src/call-diagnostic/config.ts +2 -0
- package/src/call-diagnostic/docs/README.md +535 -0
- package/src/config.js +7 -0
- package/src/docs/README.md +703 -0
- package/src/index.ts +11 -0
- package/src/metrics.js +123 -5
- package/src/metrics.types.ts +13 -3
- package/src/network-telemetry.ts +757 -0
- package/src/new-metrics.ts +44 -2
- package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
- package/src/privacy-and-security-permission-enricher/docs/README.md +356 -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/src/rtcMetrics/docs/README.md +447 -0
- package/src/unhandled-exception-telemetry/docs/README.md +536 -0
- package/src/unhandled-exception-telemetry/index.ts +404 -0
- package/src/unhandled-exception-telemetry/utils.ts +101 -0
- package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +485 -21
- package/test/unit/spec/metrics.js +14 -5
- package/test/unit/spec/network-telemetry.ts +610 -0
- package/test/unit/spec/new-metrics.ts +190 -48
- package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
- package/test/unit/spec/unhandled-exception-telemetry/utils.ts +109 -0
- package/test/unit/spec/unhandled-exception-telemetry.ts +689 -0
|
@@ -8,20 +8,27 @@ import {
|
|
|
8
8
|
import MockWebex from '@webex/test-helper-mock-webex';
|
|
9
9
|
import sinon from 'sinon';
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
import SourceNewMetrics from '@webex/internal-plugin-metrics/src/new-metrics';
|
|
12
|
+
import * as Telemetry from '@webex/internal-plugin-metrics/src/unhandled-exception-telemetry';
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
describe('internal-plugin-metrics', () => {
|
|
15
|
+
const mockWebex = (NewMetricsPlugin = NewMetrics) =>
|
|
16
|
+
new MockWebex({
|
|
17
|
+
children: {
|
|
18
|
+
newMetrics: NewMetricsPlugin,
|
|
19
|
+
},
|
|
20
|
+
meetings: {
|
|
21
|
+
getBasicMeetingInformation: sinon.stub().callsFake((meetingId) => ({
|
|
22
|
+
id: meetingId,
|
|
23
|
+
correlationId: `correlation-${meetingId}`,
|
|
24
|
+
})),
|
|
25
|
+
},
|
|
26
|
+
request: sinon.stub().resolves({}),
|
|
27
|
+
logger: {
|
|
28
|
+
log: sinon.stub(),
|
|
29
|
+
error: sinon.stub(),
|
|
30
|
+
},
|
|
31
|
+
});
|
|
25
32
|
|
|
26
33
|
describe('check submitClientEvent, submitFeatureEvent when webex is not ready', () => {
|
|
27
34
|
let webex;
|
|
@@ -50,12 +57,14 @@ describe('internal-plugin-metrics', () => {
|
|
|
50
57
|
payload: {
|
|
51
58
|
meetingSummaryInfo: {
|
|
52
59
|
featureName: 'syncSystemMuteStatus',
|
|
53
|
-
featureActions: [
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
60
|
+
featureActions: [
|
|
61
|
+
{
|
|
62
|
+
actionName: 'syncMeetingMicUnmuteStatusToSystem',
|
|
63
|
+
actionId: '14200',
|
|
64
|
+
isInitialValue: false,
|
|
65
|
+
clickCount: '1',
|
|
66
|
+
},
|
|
67
|
+
],
|
|
59
68
|
},
|
|
60
69
|
},
|
|
61
70
|
});
|
|
@@ -67,12 +76,54 @@ describe('internal-plugin-metrics', () => {
|
|
|
67
76
|
});
|
|
68
77
|
|
|
69
78
|
describe('new-metrics contstructor', () => {
|
|
70
|
-
|
|
79
|
+
afterEach(() => {
|
|
80
|
+
sinon.restore();
|
|
81
|
+
});
|
|
71
82
|
|
|
83
|
+
it('checks callDiagnosticLatencies is defined before ready emit', () => {
|
|
72
84
|
const webex = mockWebex();
|
|
73
85
|
|
|
74
86
|
assert.instanceOf(webex.internal.newMetrics.callDiagnosticLatencies, CallDiagnosticLatencies);
|
|
75
87
|
});
|
|
88
|
+
|
|
89
|
+
it('checks callDiagnosticMetrics is defined before ready emit', () => {
|
|
90
|
+
const webex = mockWebex();
|
|
91
|
+
|
|
92
|
+
assert.isDefined(webex.internal.newMetrics.callDiagnosticMetrics);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('can call buildClientEventFetchRequestOptions before ready', async () => {
|
|
96
|
+
const webex = mockWebex();
|
|
97
|
+
const stub = sinon
|
|
98
|
+
.stub(
|
|
99
|
+
webex.internal.newMetrics.callDiagnosticMetrics,
|
|
100
|
+
'buildClientEventFetchRequestOptions'
|
|
101
|
+
)
|
|
102
|
+
.resolves({url: 'https://metrics.example.com'});
|
|
103
|
+
|
|
104
|
+
const result = await webex.internal.newMetrics.buildClientEventFetchRequestOptions({
|
|
105
|
+
name: 'client.alert.displayed',
|
|
106
|
+
options: {correlationId: 'test-id'},
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
assert.calledOnceWithExactly(stub, {
|
|
110
|
+
name: 'client.alert.displayed',
|
|
111
|
+
payload: undefined,
|
|
112
|
+
options: {correlationId: 'test-id'},
|
|
113
|
+
});
|
|
114
|
+
assert.deepEqual(result, {url: 'https://metrics.example.com'});
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('starts unhandled exception telemetry when webex is ready', () => {
|
|
118
|
+
const start = sinon.stub(Telemetry, 'startUnhandledExceptionTelemetry');
|
|
119
|
+
const webex = mockWebex(SourceNewMetrics);
|
|
120
|
+
|
|
121
|
+
assert.notCalled(start);
|
|
122
|
+
|
|
123
|
+
webex.emit('ready');
|
|
124
|
+
|
|
125
|
+
assert.calledOnceWithExactly(start, webex);
|
|
126
|
+
});
|
|
76
127
|
});
|
|
77
128
|
|
|
78
129
|
describe('new-metrics', () => {
|
|
@@ -98,7 +149,7 @@ describe('internal-plugin-metrics', () => {
|
|
|
98
149
|
|
|
99
150
|
afterEach(() => {
|
|
100
151
|
sinon.restore();
|
|
101
|
-
})
|
|
152
|
+
});
|
|
102
153
|
|
|
103
154
|
it('lazy metrics backend initialization when checking if backend ready', () => {
|
|
104
155
|
assert.isUndefined(webex.internal.newMetrics.behavioralMetrics);
|
|
@@ -109,10 +160,10 @@ describe('internal-plugin-metrics', () => {
|
|
|
109
160
|
webex.internal.newMetrics.isReadyToSubmitOperationalEvents();
|
|
110
161
|
assert.isDefined(webex.internal.newMetrics.operationalMetrics);
|
|
111
162
|
|
|
112
|
-
assert.isUndefined(webex.internal.newMetrics.businessMetrics)
|
|
163
|
+
assert.isUndefined(webex.internal.newMetrics.businessMetrics);
|
|
113
164
|
webex.internal.newMetrics.isReadyToSubmitBusinessEvents();
|
|
114
165
|
assert.isDefined(webex.internal.newMetrics.businessMetrics);
|
|
115
|
-
})
|
|
166
|
+
});
|
|
116
167
|
|
|
117
168
|
it('returns the automated user classification', () => {
|
|
118
169
|
assert.strictEqual(
|
|
@@ -122,7 +173,7 @@ describe('internal-plugin-metrics', () => {
|
|
|
122
173
|
});
|
|
123
174
|
|
|
124
175
|
it('passes the table through to the business metrics', () => {
|
|
125
|
-
assert.isUndefined(webex.internal.newMetrics.businessMetrics)
|
|
176
|
+
assert.isUndefined(webex.internal.newMetrics.businessMetrics);
|
|
126
177
|
webex.internal.newMetrics.isReadyToSubmitBusinessEvents();
|
|
127
178
|
assert.isDefined(webex.internal.newMetrics.businessMetrics);
|
|
128
179
|
webex.internal.newMetrics.businessMetrics.submitBusinessEvent = sinon.stub();
|
|
@@ -130,14 +181,14 @@ describe('internal-plugin-metrics', () => {
|
|
|
130
181
|
name: 'foobar',
|
|
131
182
|
payload: {},
|
|
132
183
|
table: 'test',
|
|
133
|
-
metadata: {
|
|
184
|
+
metadata: {foo: 'bar'},
|
|
134
185
|
});
|
|
135
186
|
|
|
136
187
|
assert.calledWith(webex.internal.newMetrics.businessMetrics.submitBusinessEvent, {
|
|
137
188
|
name: 'foobar',
|
|
138
189
|
payload: {},
|
|
139
190
|
table: 'test',
|
|
140
|
-
metadata: {
|
|
191
|
+
metadata: {foo: 'bar'},
|
|
141
192
|
});
|
|
142
193
|
});
|
|
143
194
|
|
|
@@ -161,6 +212,87 @@ describe('internal-plugin-metrics', () => {
|
|
|
161
212
|
});
|
|
162
213
|
});
|
|
163
214
|
|
|
215
|
+
describe('privacy and security permission enrichment', () => {
|
|
216
|
+
const permission = {
|
|
217
|
+
camera: {status: 'GRANTED' as const},
|
|
218
|
+
microphone: {status: 'DENIED' as const, reason: 'DENIED_BY_USER' as const},
|
|
219
|
+
contentShare: {status: 'REQUESTING' as const},
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
it('enriches an eligible client event through the public metrics API', () => {
|
|
223
|
+
webex.internal.newMetrics.setPrivacyAndSecurityPermission(permission);
|
|
224
|
+
webex.internal.newMetrics.submitClientEvent({name: 'client.call.initiated'});
|
|
225
|
+
|
|
226
|
+
const submittedPayload =
|
|
227
|
+
webex.internal.newMetrics.callDiagnosticMetrics.submitClientEvent.firstCall.args[0]
|
|
228
|
+
.payload;
|
|
229
|
+
|
|
230
|
+
assert.deepEqual(submittedPayload.privacyAndSecurityPermission, {
|
|
231
|
+
camera: permission.camera,
|
|
232
|
+
microphone: permission.microphone,
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('uses the meeting correlation id to preserve history across identifier transitions', () => {
|
|
237
|
+
webex.meetings.getBasicMeetingInformation
|
|
238
|
+
.withArgs('meeting-1')
|
|
239
|
+
.returns({id: 'meeting-1', correlationId: 'correlation-1'});
|
|
240
|
+
webex.internal.newMetrics.setPrivacyAndSecurityPermission(permission);
|
|
241
|
+
|
|
242
|
+
webex.internal.newMetrics.submitClientEvent({
|
|
243
|
+
name: 'client.call.initiated',
|
|
244
|
+
options: {correlationId: 'correlation-1'},
|
|
245
|
+
});
|
|
246
|
+
webex.internal.newMetrics.submitClientEvent({
|
|
247
|
+
name: 'client.ice.end',
|
|
248
|
+
payload: {},
|
|
249
|
+
options: {meetingId: 'meeting-1'},
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
const submissions = webex.internal.newMetrics.callDiagnosticMetrics.submitClientEvent.args;
|
|
253
|
+
|
|
254
|
+
assert.property(submissions[0][0].payload, 'privacyAndSecurityPermission');
|
|
255
|
+
assert.notProperty(submissions[1][0].payload, 'privacyAndSecurityPermission');
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('uses the default scope when no correlation id can be resolved', () => {
|
|
259
|
+
webex.internal.newMetrics.setPrivacyAndSecurityPermission(permission);
|
|
260
|
+
|
|
261
|
+
webex.internal.newMetrics.submitClientEvent({
|
|
262
|
+
name: 'client.call.initiated',
|
|
263
|
+
options: {sessionCorrelationId: 'session-1'},
|
|
264
|
+
});
|
|
265
|
+
webex.internal.newMetrics.submitClientEvent({
|
|
266
|
+
name: 'client.ice.end',
|
|
267
|
+
payload: {},
|
|
268
|
+
options: {sessionCorrelationId: 'session-2'},
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
const submissions = webex.internal.newMetrics.callDiagnosticMetrics.submitClientEvent.args;
|
|
272
|
+
|
|
273
|
+
assert.property(submissions[0][0].payload, 'privacyAndSecurityPermission');
|
|
274
|
+
assert.notProperty(submissions[1][0].payload, 'privacyAndSecurityPermission');
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('captures the permission snapshot before a delayed event is queued', () => {
|
|
278
|
+
webex.internal.newMetrics.setPrivacyAndSecurityPermission(permission);
|
|
279
|
+
webex.internal.newMetrics.setDelaySubmitClientEvents({shouldDelay: true});
|
|
280
|
+
webex.internal.newMetrics.submitClientEvent({name: 'client.call.initiated'});
|
|
281
|
+
webex.internal.newMetrics.setPrivacyAndSecurityPermission({
|
|
282
|
+
camera: {status: 'DENIED' as const},
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const submission =
|
|
286
|
+
webex.internal.newMetrics.callDiagnosticMetrics.submitClientEvent.firstCall.args[0];
|
|
287
|
+
|
|
288
|
+
assert.isTrue(submission.delaySubmitEvent);
|
|
289
|
+
assert.deepEqual(submission.payload.privacyAndSecurityPermission, {
|
|
290
|
+
camera: permission.camera,
|
|
291
|
+
microphone: permission.microphone,
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
});
|
|
295
|
+
|
|
164
296
|
it('submits feature Event successfully', () => {
|
|
165
297
|
webex.internal.newMetrics.submitFeatureEvent({
|
|
166
298
|
name: 'client.feature.meeting.summary',
|
|
@@ -170,12 +302,14 @@ describe('internal-plugin-metrics', () => {
|
|
|
170
302
|
payload: {
|
|
171
303
|
meetingSummaryInfo: {
|
|
172
304
|
featureName: 'syncSystemMuteStatus',
|
|
173
|
-
featureActions: [
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
305
|
+
featureActions: [
|
|
306
|
+
{
|
|
307
|
+
actionName: 'syncMeetingMicUnmuteStatusToSystem',
|
|
308
|
+
actionId: '14200',
|
|
309
|
+
isInitialValue: false,
|
|
310
|
+
clickCount: '1',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
179
313
|
},
|
|
180
314
|
},
|
|
181
315
|
});
|
|
@@ -189,12 +323,14 @@ describe('internal-plugin-metrics', () => {
|
|
|
189
323
|
payload: {
|
|
190
324
|
meetingSummaryInfo: {
|
|
191
325
|
featureName: 'syncSystemMuteStatus',
|
|
192
|
-
featureActions: [
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
326
|
+
featureActions: [
|
|
327
|
+
{
|
|
328
|
+
actionName: 'syncMeetingMicUnmuteStatusToSystem',
|
|
329
|
+
actionId: '14200',
|
|
330
|
+
isInitialValue: false,
|
|
331
|
+
clickCount: '1',
|
|
332
|
+
},
|
|
333
|
+
],
|
|
198
334
|
},
|
|
199
335
|
},
|
|
200
336
|
options: {meetingId: '123'},
|
|
@@ -255,9 +391,9 @@ describe('internal-plugin-metrics', () => {
|
|
|
255
391
|
method: 'POST',
|
|
256
392
|
api: 'metrics',
|
|
257
393
|
resource: 'clientmetrics',
|
|
258
|
-
headers: {
|
|
394
|
+
headers: {'x-prelogin-userid': 'my-id'},
|
|
259
395
|
body: {},
|
|
260
|
-
qs: {
|
|
396
|
+
qs: {alias: true},
|
|
261
397
|
});
|
|
262
398
|
assert.calledWith(
|
|
263
399
|
webex.logger.log,
|
|
@@ -266,8 +402,8 @@ describe('internal-plugin-metrics', () => {
|
|
|
266
402
|
});
|
|
267
403
|
|
|
268
404
|
it('handles failed request correctly', async () => {
|
|
269
|
-
webex.request.rejects(new Error(
|
|
270
|
-
sinon.stub(Utils, 'generateCommonErrorMetadata').returns('formattedError')
|
|
405
|
+
webex.request.rejects(new Error('test error'));
|
|
406
|
+
sinon.stub(Utils, 'generateCommonErrorMetadata').returns('formattedError');
|
|
271
407
|
try {
|
|
272
408
|
await webex.internal.newMetrics.clientMetricsAliasUser({event: 'test'}, 'my-id');
|
|
273
409
|
} catch (err) {
|
|
@@ -349,19 +485,26 @@ describe('internal-plugin-metrics', () => {
|
|
|
349
485
|
sinon.assert.match(webex.internal.newMetrics.delaySubmitClientEvents, true);
|
|
350
486
|
sinon.assert.match(webex.internal.newMetrics.delayedClientEventsOverrides, {});
|
|
351
487
|
|
|
352
|
-
webex.internal.newMetrics.setDelaySubmitClientEvents({
|
|
488
|
+
webex.internal.newMetrics.setDelaySubmitClientEvents({
|
|
489
|
+
shouldDelay: false,
|
|
490
|
+
overrides: {foo: 'bar'},
|
|
491
|
+
});
|
|
353
492
|
|
|
354
|
-
assert.calledOnce(
|
|
355
|
-
|
|
493
|
+
assert.calledOnce(
|
|
494
|
+
webex.internal.newMetrics.callDiagnosticMetrics.submitDelayedClientEvents
|
|
495
|
+
);
|
|
496
|
+
assert.calledWith(
|
|
497
|
+
webex.internal.newMetrics.callDiagnosticMetrics.submitDelayedClientEvents,
|
|
498
|
+
{foo: 'bar'}
|
|
499
|
+
);
|
|
356
500
|
|
|
357
501
|
sinon.assert.match(webex.internal.newMetrics.delaySubmitClientEvents, false);
|
|
358
502
|
sinon.assert.match(webex.internal.newMetrics.delayedClientEventsOverrides, {foo: 'bar'});
|
|
359
503
|
});
|
|
360
504
|
|
|
361
505
|
it('should not fail when called before webex is ready', () => {
|
|
362
|
-
|
|
363
506
|
// Create mock
|
|
364
|
-
webex = mockWebex()
|
|
507
|
+
webex = mockWebex();
|
|
365
508
|
|
|
366
509
|
webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
|
|
367
510
|
webex.internal.newMetrics.callDiagnosticLatencies.clearTimestamps = sinon.stub();
|
|
@@ -376,7 +519,6 @@ describe('internal-plugin-metrics', () => {
|
|
|
376
519
|
webex.internal.newMetrics.setDelaySubmitClientEvents({shouldDelay: false});
|
|
377
520
|
// Webex is ready
|
|
378
521
|
webex.emit('ready');
|
|
379
|
-
|
|
380
522
|
});
|
|
381
523
|
});
|
|
382
524
|
});
|
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import {assert} from '@webex/test-helper-chai';
|
|
2
|
+
import sinon from 'sinon';
|
|
3
|
+
|
|
4
|
+
import type {ClientEvent, PrivacyAndSecurityPermission} from '../../../src/metrics.types';
|
|
5
|
+
import PrivacyAndSecurityPermissionEnricher, {
|
|
6
|
+
PERMISSION_ENRICHMENT_RULES,
|
|
7
|
+
} from '../../../src/privacy-and-security-permission-enricher';
|
|
8
|
+
|
|
9
|
+
describe('PrivacyAndSecurityPermissionEnricher', () => {
|
|
10
|
+
const permission: PrivacyAndSecurityPermission = {
|
|
11
|
+
camera: {status: 'GRANTED'},
|
|
12
|
+
microphone: {status: 'DENIED', reason: 'DENIED_BY_USER'},
|
|
13
|
+
contentShare: {status: 'REQUESTING'},
|
|
14
|
+
};
|
|
15
|
+
let onEnrichmentError: sinon.SinonStub;
|
|
16
|
+
let enricher: PrivacyAndSecurityPermissionEnricher;
|
|
17
|
+
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
onEnrichmentError = sinon.stub();
|
|
20
|
+
enricher = new PrivacyAndSecurityPermissionEnricher(onEnrichmentError);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
sinon.restore();
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe('permission enrichment rules', () => {
|
|
28
|
+
it('registers each production event in only one rule', () => {
|
|
29
|
+
const registeredEvents = PERMISSION_ENRICHMENT_RULES.flatMap(({events}) => [...events]);
|
|
30
|
+
|
|
31
|
+
assert.lengthOf(new Set(registeredEvents), registeredEvents.length);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
[
|
|
35
|
+
'client.call.initiated',
|
|
36
|
+
'client.media.capabilities',
|
|
37
|
+
'client.ice.end',
|
|
38
|
+
'client.locus.join.request',
|
|
39
|
+
'client.locus.join.response',
|
|
40
|
+
'client.media-engine.ready',
|
|
41
|
+
].forEach((name) => {
|
|
42
|
+
it(`projects camera and microphone for ${name}`, () => {
|
|
43
|
+
enricher.setPermission(permission);
|
|
44
|
+
|
|
45
|
+
const payload = enricher.enrich({name: name as ClientEvent['name'], scope: 'meeting'});
|
|
46
|
+
|
|
47
|
+
assert.deepEqual(payload?.privacyAndSecurityPermission, {
|
|
48
|
+
camera: permission.camera,
|
|
49
|
+
microphone: permission.microphone,
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
['client.media.tx.start', 'client.media.tx.stop'].forEach((name) => {
|
|
55
|
+
[
|
|
56
|
+
{mediaType: 'audio' as const, expected: {microphone: permission.microphone}},
|
|
57
|
+
{mediaType: 'video' as const, expected: {camera: permission.camera}},
|
|
58
|
+
{mediaType: 'share' as const, expected: {contentShare: permission.contentShare}},
|
|
59
|
+
].forEach(({mediaType, expected}) => {
|
|
60
|
+
it(`projects ${mediaType} permission for ${name}`, () => {
|
|
61
|
+
enricher.setPermission(permission);
|
|
62
|
+
|
|
63
|
+
const payload = enricher.enrich({
|
|
64
|
+
name: name as ClientEvent['name'],
|
|
65
|
+
payload: {mediaType},
|
|
66
|
+
scope: 'meeting',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
assert.deepEqual(payload?.privacyAndSecurityPermission, expected);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
[
|
|
75
|
+
'client.share.initiated',
|
|
76
|
+
'client.share.floor-grant.request',
|
|
77
|
+
'client.share.floor-granted.local',
|
|
78
|
+
].forEach((name) => {
|
|
79
|
+
it(`projects content-share permission for ${name}`, () => {
|
|
80
|
+
enricher.setPermission(permission);
|
|
81
|
+
|
|
82
|
+
const payload = enricher.enrich({
|
|
83
|
+
name: name as ClientEvent['name'],
|
|
84
|
+
payload: {mediaType: 'share'},
|
|
85
|
+
scope: 'meeting',
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert.deepEqual(payload?.privacyAndSecurityPermission, {
|
|
89
|
+
contentShare: permission.contentShare,
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
['client.call.leave', 'client.call.remote-ended', 'client.call.aborted'].forEach((name) => {
|
|
95
|
+
it(`projects every available permission for ${name}`, () => {
|
|
96
|
+
enricher.setPermission(permission);
|
|
97
|
+
|
|
98
|
+
const payload = enricher.enrich({name: name as ClientEvent['name'], scope: 'meeting'});
|
|
99
|
+
|
|
100
|
+
assert.deepEqual(payload?.privacyAndSecurityPermission, permission);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('setPermission', () => {
|
|
106
|
+
it('stores a defensive copy of the permission snapshot', () => {
|
|
107
|
+
const mutablePermission: PrivacyAndSecurityPermission = {
|
|
108
|
+
camera: {status: 'GRANTED'},
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
enricher.setPermission(mutablePermission);
|
|
112
|
+
mutablePermission.camera = {status: 'DENIED', reason: 'DENIED_BY_USER'};
|
|
113
|
+
|
|
114
|
+
const payload = enricher.enrich({name: 'client.call.initiated', scope: 'meeting'});
|
|
115
|
+
|
|
116
|
+
assert.deepEqual(payload?.privacyAndSecurityPermission, {
|
|
117
|
+
camera: {status: 'GRANTED'},
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe('enrich', () => {
|
|
123
|
+
it('does not mutate the input payload or stored permission state', () => {
|
|
124
|
+
const inputPayload = {mediaType: 'audio' as const};
|
|
125
|
+
|
|
126
|
+
enricher.setPermission(permission);
|
|
127
|
+
const payload = enricher.enrich({
|
|
128
|
+
name: 'client.call.initiated',
|
|
129
|
+
payload: inputPayload,
|
|
130
|
+
scope: 'meeting',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
assert.notStrictEqual(payload, inputPayload);
|
|
134
|
+
assert.notProperty(inputPayload, 'privacyAndSecurityPermission');
|
|
135
|
+
assert.notStrictEqual(payload?.privacyAndSecurityPermission?.camera, permission.camera);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it('returns the original payload when the requested permission is unavailable', () => {
|
|
139
|
+
const inputPayload = {mediaType: 'audio' as const};
|
|
140
|
+
|
|
141
|
+
enricher.setPermission({camera: permission.camera});
|
|
142
|
+
const payload = enricher.enrich({
|
|
143
|
+
name: 'client.media.tx.start',
|
|
144
|
+
payload: inputPayload,
|
|
145
|
+
scope: 'meeting',
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
assert.strictEqual(payload, inputPayload);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('returns the original payload for unsupported share media types', () => {
|
|
152
|
+
const inputPayload = {mediaType: 'whiteboard' as const};
|
|
153
|
+
|
|
154
|
+
enricher.setPermission(permission);
|
|
155
|
+
const payload = enricher.enrich({
|
|
156
|
+
name: 'client.share.initiated',
|
|
157
|
+
payload: inputPayload,
|
|
158
|
+
scope: 'meeting',
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
assert.strictEqual(payload, inputPayload);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('reports initial permission once and suppresses an unchanged later event', () => {
|
|
165
|
+
enricher.setPermission(permission);
|
|
166
|
+
|
|
167
|
+
const initialPayload = enricher.enrich({name: 'client.call.initiated', scope: 'meeting'});
|
|
168
|
+
const unchangedPayload = enricher.enrich({name: 'client.ice.end', scope: 'meeting'});
|
|
169
|
+
|
|
170
|
+
assert.property(initialPayload, 'privacyAndSecurityPermission');
|
|
171
|
+
assert.isUndefined(unchangedPayload);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('reports only permission resources that changed', () => {
|
|
175
|
+
enricher.setPermission(permission);
|
|
176
|
+
enricher.enrich({name: 'client.call.initiated', scope: 'meeting'});
|
|
177
|
+
enricher.setPermission({
|
|
178
|
+
...permission,
|
|
179
|
+
camera: {status: 'DENIED', reason: 'DENIED_BY_SYSTEM'},
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
const payload = enricher.enrich({name: 'client.ice.end', scope: 'meeting'});
|
|
183
|
+
|
|
184
|
+
assert.deepEqual(payload?.privacyAndSecurityPermission, {
|
|
185
|
+
camera: {status: 'DENIED', reason: 'DENIED_BY_SYSTEM'},
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('reports a changed reason when status is unchanged', () => {
|
|
190
|
+
enricher.setPermission({microphone: {status: 'DENIED', reason: 'DENIED_BY_USER'}});
|
|
191
|
+
enricher.enrich({
|
|
192
|
+
name: 'client.media.tx.start',
|
|
193
|
+
payload: {mediaType: 'audio'},
|
|
194
|
+
scope: 'meeting',
|
|
195
|
+
});
|
|
196
|
+
enricher.setPermission({microphone: {status: 'DENIED', reason: 'DENIED_BY_SYSTEM'}});
|
|
197
|
+
|
|
198
|
+
const payload = enricher.enrich({
|
|
199
|
+
name: 'client.media.tx.stop',
|
|
200
|
+
payload: {mediaType: 'audio'},
|
|
201
|
+
scope: 'meeting',
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
assert.deepEqual(payload?.privacyAndSecurityPermission, {
|
|
205
|
+
microphone: {status: 'DENIED', reason: 'DENIED_BY_SYSTEM'},
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('shares resource history across enrichment rules', () => {
|
|
210
|
+
enricher.setPermission(permission);
|
|
211
|
+
enricher.enrich({name: 'client.call.initiated', scope: 'meeting'});
|
|
212
|
+
|
|
213
|
+
const payload = enricher.enrich({
|
|
214
|
+
name: 'client.media.tx.start',
|
|
215
|
+
payload: {mediaType: 'audio'},
|
|
216
|
+
scope: 'meeting',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
assert.notProperty(payload, 'privacyAndSecurityPermission');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('tracks permission history independently by scope', () => {
|
|
223
|
+
enricher.setPermission(permission);
|
|
224
|
+
|
|
225
|
+
const firstMeeting = enricher.enrich({name: 'client.call.initiated', scope: 'meeting-1'});
|
|
226
|
+
const secondMeeting = enricher.enrich({name: 'client.call.initiated', scope: 'meeting-2'});
|
|
227
|
+
|
|
228
|
+
assert.property(firstMeeting, 'privacyAndSecurityPermission');
|
|
229
|
+
assert.property(secondMeeting, 'privacyAndSecurityPermission');
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('clears only the terminal event scope', () => {
|
|
233
|
+
enricher.setPermission(permission);
|
|
234
|
+
enricher.enrich({name: 'client.call.initiated', scope: 'meeting-1'});
|
|
235
|
+
enricher.enrich({name: 'client.call.initiated', scope: 'meeting-2'});
|
|
236
|
+
enricher.enrich({name: 'client.call.leave', scope: 'meeting-1'});
|
|
237
|
+
|
|
238
|
+
const reusedScope = enricher.enrich({name: 'client.call.initiated', scope: 'meeting-1'});
|
|
239
|
+
const activeScope = enricher.enrich({name: 'client.ice.end', scope: 'meeting-2'});
|
|
240
|
+
|
|
241
|
+
assert.property(reusedScope, 'privacyAndSecurityPermission');
|
|
242
|
+
assert.isUndefined(activeScope);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('preserves an explicit permission payload and uses it as the baseline', () => {
|
|
246
|
+
const explicitPayload = {
|
|
247
|
+
privacyAndSecurityPermission: {
|
|
248
|
+
camera: permission.camera,
|
|
249
|
+
microphone: permission.microphone,
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
enricher.setPermission(permission);
|
|
254
|
+
const returnedPayload = enricher.enrich({
|
|
255
|
+
name: 'client.call.initiated',
|
|
256
|
+
payload: explicitPayload,
|
|
257
|
+
scope: 'meeting',
|
|
258
|
+
});
|
|
259
|
+
const laterPayload = enricher.enrich({name: 'client.ice.end', scope: 'meeting'});
|
|
260
|
+
|
|
261
|
+
assert.strictEqual(returnedPayload, explicitPayload);
|
|
262
|
+
assert.isUndefined(laterPayload);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('clears history after an explicit terminal payload', () => {
|
|
266
|
+
enricher.setPermission(permission);
|
|
267
|
+
enricher.enrich({name: 'client.call.initiated', scope: 'meeting'});
|
|
268
|
+
enricher.enrich({
|
|
269
|
+
name: 'client.call.leave',
|
|
270
|
+
payload: {privacyAndSecurityPermission: permission},
|
|
271
|
+
scope: 'meeting',
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const payload = enricher.enrich({name: 'client.call.initiated', scope: 'meeting'});
|
|
275
|
+
|
|
276
|
+
assert.property(payload, 'privacyAndSecurityPermission');
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
it('returns unchanged before permission state is supplied', () => {
|
|
280
|
+
const inputPayload = {mediaType: 'audio' as const};
|
|
281
|
+
|
|
282
|
+
const payload = enricher.enrich({
|
|
283
|
+
name: 'client.call.initiated',
|
|
284
|
+
payload: inputPayload,
|
|
285
|
+
scope: 'meeting',
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
assert.strictEqual(payload, inputPayload);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('does not enrich unrelated events', () => {
|
|
292
|
+
enricher.setPermission(permission);
|
|
293
|
+
|
|
294
|
+
const payload = enricher.enrich({name: 'client.alert.displayed', scope: 'meeting'});
|
|
295
|
+
|
|
296
|
+
assert.isUndefined(payload);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('reports enrichment errors and returns the original payload', () => {
|
|
300
|
+
const inputPayload = {};
|
|
301
|
+
|
|
302
|
+
Object.defineProperty(inputPayload, 'privacyAndSecurityPermission', {
|
|
303
|
+
get: () => {
|
|
304
|
+
throw new Error('permission read failed');
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
const payload = enricher.enrich({
|
|
309
|
+
name: 'client.call.initiated',
|
|
310
|
+
payload: inputPayload,
|
|
311
|
+
scope: 'meeting',
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
assert.strictEqual(payload, inputPayload);
|
|
315
|
+
assert.calledOnce(onEnrichmentError);
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|