@webex/internal-plugin-metrics 3.12.0-llmrefactor.1 → 3.12.0-llmrefactor.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -0
- package/dist/config.js +3 -0
- package/dist/config.js.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/metrics.js +1 -1
- package/dist/metrics.types.js.map +1 -1
- 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/config.d.ts +3 -0
- package/dist/types/index.d.ts +2 -2
- package/dist/types/metrics.types.d.ts +3 -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 +330 -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/package.json +8 -8
- package/src/config.js +3 -0
- package/src/index.ts +2 -0
- package/src/metrics.types.ts +9 -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/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/unhandled-exception-telemetry/index.ts +403 -0
- package/src/unhandled-exception-telemetry/utils.ts +101 -0
- package/test/unit/spec/new-metrics.ts +168 -52
- 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 +688 -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,8 +76,11 @@ 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);
|
|
@@ -82,10 +94,12 @@ describe('internal-plugin-metrics', () => {
|
|
|
82
94
|
|
|
83
95
|
it('can call buildClientEventFetchRequestOptions before ready', async () => {
|
|
84
96
|
const webex = mockWebex();
|
|
85
|
-
const stub = sinon
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
97
|
+
const stub = sinon
|
|
98
|
+
.stub(
|
|
99
|
+
webex.internal.newMetrics.callDiagnosticMetrics,
|
|
100
|
+
'buildClientEventFetchRequestOptions'
|
|
101
|
+
)
|
|
102
|
+
.resolves({url: 'https://metrics.example.com'});
|
|
89
103
|
|
|
90
104
|
const result = await webex.internal.newMetrics.buildClientEventFetchRequestOptions({
|
|
91
105
|
name: 'client.alert.displayed',
|
|
@@ -99,6 +113,17 @@ describe('internal-plugin-metrics', () => {
|
|
|
99
113
|
});
|
|
100
114
|
assert.deepEqual(result, {url: 'https://metrics.example.com'});
|
|
101
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
|
+
});
|
|
102
127
|
});
|
|
103
128
|
|
|
104
129
|
describe('new-metrics', () => {
|
|
@@ -124,7 +149,7 @@ describe('internal-plugin-metrics', () => {
|
|
|
124
149
|
|
|
125
150
|
afterEach(() => {
|
|
126
151
|
sinon.restore();
|
|
127
|
-
})
|
|
152
|
+
});
|
|
128
153
|
|
|
129
154
|
it('lazy metrics backend initialization when checking if backend ready', () => {
|
|
130
155
|
assert.isUndefined(webex.internal.newMetrics.behavioralMetrics);
|
|
@@ -135,10 +160,10 @@ describe('internal-plugin-metrics', () => {
|
|
|
135
160
|
webex.internal.newMetrics.isReadyToSubmitOperationalEvents();
|
|
136
161
|
assert.isDefined(webex.internal.newMetrics.operationalMetrics);
|
|
137
162
|
|
|
138
|
-
assert.isUndefined(webex.internal.newMetrics.businessMetrics)
|
|
163
|
+
assert.isUndefined(webex.internal.newMetrics.businessMetrics);
|
|
139
164
|
webex.internal.newMetrics.isReadyToSubmitBusinessEvents();
|
|
140
165
|
assert.isDefined(webex.internal.newMetrics.businessMetrics);
|
|
141
|
-
})
|
|
166
|
+
});
|
|
142
167
|
|
|
143
168
|
it('returns the automated user classification', () => {
|
|
144
169
|
assert.strictEqual(
|
|
@@ -148,7 +173,7 @@ describe('internal-plugin-metrics', () => {
|
|
|
148
173
|
});
|
|
149
174
|
|
|
150
175
|
it('passes the table through to the business metrics', () => {
|
|
151
|
-
assert.isUndefined(webex.internal.newMetrics.businessMetrics)
|
|
176
|
+
assert.isUndefined(webex.internal.newMetrics.businessMetrics);
|
|
152
177
|
webex.internal.newMetrics.isReadyToSubmitBusinessEvents();
|
|
153
178
|
assert.isDefined(webex.internal.newMetrics.businessMetrics);
|
|
154
179
|
webex.internal.newMetrics.businessMetrics.submitBusinessEvent = sinon.stub();
|
|
@@ -156,14 +181,14 @@ describe('internal-plugin-metrics', () => {
|
|
|
156
181
|
name: 'foobar',
|
|
157
182
|
payload: {},
|
|
158
183
|
table: 'test',
|
|
159
|
-
metadata: {
|
|
184
|
+
metadata: {foo: 'bar'},
|
|
160
185
|
});
|
|
161
186
|
|
|
162
187
|
assert.calledWith(webex.internal.newMetrics.businessMetrics.submitBusinessEvent, {
|
|
163
188
|
name: 'foobar',
|
|
164
189
|
payload: {},
|
|
165
190
|
table: 'test',
|
|
166
|
-
metadata: {
|
|
191
|
+
metadata: {foo: 'bar'},
|
|
167
192
|
});
|
|
168
193
|
});
|
|
169
194
|
|
|
@@ -187,6 +212,87 @@ describe('internal-plugin-metrics', () => {
|
|
|
187
212
|
});
|
|
188
213
|
});
|
|
189
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
|
+
|
|
190
296
|
it('submits feature Event successfully', () => {
|
|
191
297
|
webex.internal.newMetrics.submitFeatureEvent({
|
|
192
298
|
name: 'client.feature.meeting.summary',
|
|
@@ -196,12 +302,14 @@ describe('internal-plugin-metrics', () => {
|
|
|
196
302
|
payload: {
|
|
197
303
|
meetingSummaryInfo: {
|
|
198
304
|
featureName: 'syncSystemMuteStatus',
|
|
199
|
-
featureActions: [
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
305
|
+
featureActions: [
|
|
306
|
+
{
|
|
307
|
+
actionName: 'syncMeetingMicUnmuteStatusToSystem',
|
|
308
|
+
actionId: '14200',
|
|
309
|
+
isInitialValue: false,
|
|
310
|
+
clickCount: '1',
|
|
311
|
+
},
|
|
312
|
+
],
|
|
205
313
|
},
|
|
206
314
|
},
|
|
207
315
|
});
|
|
@@ -215,12 +323,14 @@ describe('internal-plugin-metrics', () => {
|
|
|
215
323
|
payload: {
|
|
216
324
|
meetingSummaryInfo: {
|
|
217
325
|
featureName: 'syncSystemMuteStatus',
|
|
218
|
-
featureActions: [
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
326
|
+
featureActions: [
|
|
327
|
+
{
|
|
328
|
+
actionName: 'syncMeetingMicUnmuteStatusToSystem',
|
|
329
|
+
actionId: '14200',
|
|
330
|
+
isInitialValue: false,
|
|
331
|
+
clickCount: '1',
|
|
332
|
+
},
|
|
333
|
+
],
|
|
224
334
|
},
|
|
225
335
|
},
|
|
226
336
|
options: {meetingId: '123'},
|
|
@@ -281,9 +391,9 @@ describe('internal-plugin-metrics', () => {
|
|
|
281
391
|
method: 'POST',
|
|
282
392
|
api: 'metrics',
|
|
283
393
|
resource: 'clientmetrics',
|
|
284
|
-
headers: {
|
|
394
|
+
headers: {'x-prelogin-userid': 'my-id'},
|
|
285
395
|
body: {},
|
|
286
|
-
qs: {
|
|
396
|
+
qs: {alias: true},
|
|
287
397
|
});
|
|
288
398
|
assert.calledWith(
|
|
289
399
|
webex.logger.log,
|
|
@@ -292,8 +402,8 @@ describe('internal-plugin-metrics', () => {
|
|
|
292
402
|
});
|
|
293
403
|
|
|
294
404
|
it('handles failed request correctly', async () => {
|
|
295
|
-
webex.request.rejects(new Error(
|
|
296
|
-
sinon.stub(Utils, 'generateCommonErrorMetadata').returns('formattedError')
|
|
405
|
+
webex.request.rejects(new Error('test error'));
|
|
406
|
+
sinon.stub(Utils, 'generateCommonErrorMetadata').returns('formattedError');
|
|
297
407
|
try {
|
|
298
408
|
await webex.internal.newMetrics.clientMetricsAliasUser({event: 'test'}, 'my-id');
|
|
299
409
|
} catch (err) {
|
|
@@ -375,19 +485,26 @@ describe('internal-plugin-metrics', () => {
|
|
|
375
485
|
sinon.assert.match(webex.internal.newMetrics.delaySubmitClientEvents, true);
|
|
376
486
|
sinon.assert.match(webex.internal.newMetrics.delayedClientEventsOverrides, {});
|
|
377
487
|
|
|
378
|
-
webex.internal.newMetrics.setDelaySubmitClientEvents({
|
|
488
|
+
webex.internal.newMetrics.setDelaySubmitClientEvents({
|
|
489
|
+
shouldDelay: false,
|
|
490
|
+
overrides: {foo: 'bar'},
|
|
491
|
+
});
|
|
379
492
|
|
|
380
|
-
assert.calledOnce(
|
|
381
|
-
|
|
493
|
+
assert.calledOnce(
|
|
494
|
+
webex.internal.newMetrics.callDiagnosticMetrics.submitDelayedClientEvents
|
|
495
|
+
);
|
|
496
|
+
assert.calledWith(
|
|
497
|
+
webex.internal.newMetrics.callDiagnosticMetrics.submitDelayedClientEvents,
|
|
498
|
+
{foo: 'bar'}
|
|
499
|
+
);
|
|
382
500
|
|
|
383
501
|
sinon.assert.match(webex.internal.newMetrics.delaySubmitClientEvents, false);
|
|
384
502
|
sinon.assert.match(webex.internal.newMetrics.delayedClientEventsOverrides, {foo: 'bar'});
|
|
385
503
|
});
|
|
386
504
|
|
|
387
505
|
it('should not fail when called before webex is ready', () => {
|
|
388
|
-
|
|
389
506
|
// Create mock
|
|
390
|
-
webex = mockWebex()
|
|
507
|
+
webex = mockWebex();
|
|
391
508
|
|
|
392
509
|
webex.internal.newMetrics.callDiagnosticLatencies.saveTimestamp = sinon.stub();
|
|
393
510
|
webex.internal.newMetrics.callDiagnosticLatencies.clearTimestamps = sinon.stub();
|
|
@@ -402,7 +519,6 @@ describe('internal-plugin-metrics', () => {
|
|
|
402
519
|
webex.internal.newMetrics.setDelaySubmitClientEvents({shouldDelay: false});
|
|
403
520
|
// Webex is ready
|
|
404
521
|
webex.emit('ready');
|
|
405
|
-
|
|
406
522
|
});
|
|
407
523
|
});
|
|
408
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
|
+
});
|