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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/dist/automated-user.js +19 -0
  2. package/dist/automated-user.js.map +1 -0
  3. package/dist/batcher.js +3 -0
  4. package/dist/batcher.js.map +1 -1
  5. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +639 -65
  6. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
  7. package/dist/call-diagnostic/call-diagnostic-metrics.js +168 -76
  8. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
  9. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +18 -4
  10. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
  11. package/dist/call-diagnostic/config.js +17 -3
  12. package/dist/call-diagnostic/config.js.map +1 -1
  13. package/dist/config.js +1 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/generic-metrics.js +8 -6
  16. package/dist/generic-metrics.js.map +1 -1
  17. package/dist/index.js +22 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/metrics.js +1 -1
  20. package/dist/metrics.types.js +4 -0
  21. package/dist/metrics.types.js.map +1 -1
  22. package/dist/new-metrics.js +105 -26
  23. package/dist/new-metrics.js.map +1 -1
  24. package/dist/prelogin-metrics.js +106 -0
  25. package/dist/prelogin-metrics.js.map +1 -0
  26. package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
  27. package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
  28. package/dist/privacy-and-security-permission-enricher/index.js +143 -0
  29. package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
  30. package/dist/privacy-and-security-permission-enricher/types.js +7 -0
  31. package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
  32. package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
  33. package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
  34. package/dist/types/automated-user.d.ts +2 -0
  35. package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +208 -5
  36. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +79 -15
  37. package/dist/types/call-diagnostic/call-diagnostic-metrics.util.d.ts +5 -0
  38. package/dist/types/call-diagnostic/config.d.ts +5 -0
  39. package/dist/types/config.d.ts +1 -0
  40. package/dist/types/index.d.ts +6 -3
  41. package/dist/types/metrics.types.d.ts +9 -4
  42. package/dist/types/new-metrics.d.ts +29 -1
  43. package/dist/types/prelogin-metrics.d.ts +47 -0
  44. package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
  45. package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
  46. package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
  47. package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
  48. package/package.json +12 -11
  49. package/src/automated-user.ts +16 -0
  50. package/src/batcher.js +4 -0
  51. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +800 -75
  52. package/src/call-diagnostic/call-diagnostic-metrics.ts +102 -15
  53. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +22 -2
  54. package/src/call-diagnostic/config.ts +15 -0
  55. package/src/config.js +1 -0
  56. package/src/generic-metrics.ts +6 -6
  57. package/src/index.ts +9 -0
  58. package/src/metrics.types.ts +27 -5
  59. package/src/new-metrics.ts +95 -6
  60. package/src/prelogin-metrics.ts +94 -0
  61. package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
  62. package/src/privacy-and-security-permission-enricher/index.ts +142 -0
  63. package/src/privacy-and-security-permission-enricher/types.ts +21 -0
  64. package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
  65. package/test/unit/spec/automated-user.ts +43 -0
  66. package/test/unit/spec/batcher.js +43 -0
  67. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +47 -11
  68. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1550 -416
  69. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +887 -159
  70. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +61 -3
  71. package/test/unit/spec/new-metrics.ts +183 -50
  72. package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
  73. package/test/unit/spec/prelogin-metrics.ts +132 -0
  74. package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
@@ -0,0 +1,132 @@
1
+ import PreLoginMetricsBatcher from '../../../src/prelogin-metrics-batcher';
2
+ import {PreLoginMetrics} from '@webex/internal-plugin-metrics';
3
+ import {assert} from '@webex/test-helper-chai';
4
+ import MockWebex from '@webex/test-helper-mock-webex';
5
+ import sinon from 'sinon';
6
+
7
+ describe('internal-plugin-metrics', () => {
8
+ const mockedWebex: MockWebex = new MockWebex();
9
+ const fakedPreLoginMetricsBatcher: typeof PreLoginMetricsBatcher = new PreLoginMetricsBatcher({}, {parent: mockedWebex});
10
+
11
+ describe('prelogin-metrics', () => {
12
+ afterEach(() => {
13
+ sinon.restore();
14
+ });
15
+
16
+ it('Should send metrics to preloginMetricsBatcher', async () => {
17
+ const testEvent = 'test';
18
+ const testId = 'abc123';
19
+ const preLoginMetrics = new PreLoginMetrics(fakedPreLoginMetricsBatcher, {}, {parent: mockedWebex});
20
+
21
+ sinon.stub(fakedPreLoginMetricsBatcher, 'savePreLoginId');
22
+ const requestSpy = sinon.stub(fakedPreLoginMetricsBatcher, 'request');
23
+
24
+ await preLoginMetrics.submitPreLoginEvent({name: testEvent, preLoginId: testId, payload: {}});
25
+
26
+ assert.calledOnceWithMatch(requestSpy, {
27
+ type: ['business'],
28
+ eventPayload: {metricName: testEvent, value: {preLoginId: testId}},
29
+ });
30
+ });
31
+
32
+ it('Should send metadata to preloginMetricsBatcher', async () => {
33
+ const testEvent = 'test';
34
+ const testId = 'abc123';
35
+ const testMetadata = { 'testKey': 'test-value' };
36
+ const preLoginMetrics = new PreLoginMetrics(fakedPreLoginMetricsBatcher, {}, {parent: mockedWebex});
37
+
38
+ sinon.stub(fakedPreLoginMetricsBatcher, 'savePreLoginId');
39
+ const requestSpy = sinon.stub(fakedPreLoginMetricsBatcher, 'request');
40
+
41
+ await preLoginMetrics.submitPreLoginEvent({name: testEvent, preLoginId: testId, payload: {}, metadata: testMetadata});
42
+
43
+ assert.calledOnceWithMatch(requestSpy, {
44
+ type: ['business'],
45
+ eventPayload: {metricName: testEvent, value: {preLoginId: testId, ...testMetadata}},
46
+ });
47
+ });
48
+
49
+ it('Should send payload to preloginMetricsBatcher', async () => {
50
+ const testEvent = 'test';
51
+ const testId = 'abc123';
52
+ const testPayload = { 'testKey': 'test-value' };
53
+ const preLoginMetrics = new PreLoginMetrics(fakedPreLoginMetricsBatcher, {}, {parent: mockedWebex});
54
+
55
+ sinon.stub(fakedPreLoginMetricsBatcher, 'savePreLoginId');
56
+ const requestSpy = sinon.stub(fakedPreLoginMetricsBatcher, 'request');
57
+
58
+ await preLoginMetrics.submitPreLoginEvent({name: testEvent, preLoginId: testId, payload: testPayload});
59
+
60
+ assert.calledOnceWithMatch(requestSpy, {
61
+ type: ['business'],
62
+ eventPayload: {metricName: testEvent, value: {preLoginId: testId, ...testPayload}},
63
+ });
64
+ });
65
+
66
+ it('Should fill appType if not defined', async () => {
67
+ const testEvent = 'test';
68
+ const preLoginMetrics = new PreLoginMetrics(fakedPreLoginMetricsBatcher, {}, {parent: mockedWebex});
69
+
70
+ sinon.stub(fakedPreLoginMetricsBatcher, 'savePreLoginId');
71
+ const requestSpy = sinon.stub(fakedPreLoginMetricsBatcher, 'request');
72
+
73
+ await preLoginMetrics.submitPreLoginEvent({name: testEvent, preLoginId: 'abc123', payload: {}});
74
+
75
+ assert.calledOnceWithMatch(requestSpy, {
76
+ type: ['business'],
77
+ eventPayload: {metricName: testEvent, value: {appType: 'Web Client'}},
78
+ });
79
+ });
80
+
81
+ it('Should add browser details', async () => {
82
+ const testEvent = 'test';
83
+ const testBrowserDetails = { browser: 'Firefox', domain: 'test.example.com' };
84
+ const preLoginMetrics = new PreLoginMetrics(fakedPreLoginMetricsBatcher, {}, {parent: mockedWebex});
85
+
86
+ sinon.stub(preLoginMetrics, 'getBrowserDetails').returns(testBrowserDetails);
87
+ sinon.stub(fakedPreLoginMetricsBatcher, 'savePreLoginId');
88
+ const requestSpy = sinon.stub(fakedPreLoginMetricsBatcher, 'request');
89
+
90
+ await preLoginMetrics.submitPreLoginEvent({name: testEvent, preLoginId: 'abc123', payload: {}});
91
+
92
+ assert.calledOnceWithMatch(requestSpy, {
93
+ type: ['business'],
94
+ eventPayload: {browserDetails: testBrowserDetails, metricName: testEvent},
95
+ });
96
+ });
97
+
98
+ it('Should add context', async () => {
99
+ const testEvent = 'test';
100
+ const testContext = { device: { id: 'abc123' }};
101
+ const preLoginMetrics = new PreLoginMetrics(fakedPreLoginMetricsBatcher, {}, {parent: mockedWebex});
102
+
103
+ sinon.stub(preLoginMetrics, 'getContext').returns(testContext);
104
+ sinon.stub(fakedPreLoginMetricsBatcher, 'savePreLoginId');
105
+ const requestSpy = sinon.stub(fakedPreLoginMetricsBatcher, 'request');
106
+
107
+ await preLoginMetrics.submitPreLoginEvent({name: testEvent, preLoginId: 'abc123', payload: {}});
108
+
109
+ assert.calledOnceWithMatch(requestSpy, {
110
+ type: ['business'],
111
+ eventPayload: {context: testContext, metricName: testEvent},
112
+ });
113
+ });
114
+
115
+ it('Should add timestamp', async () => {
116
+ const testEvent = 'test';
117
+ const testTime = 1234;
118
+ const preLoginMetrics = new PreLoginMetrics(fakedPreLoginMetricsBatcher, {}, {parent: mockedWebex});
119
+
120
+ sinon.useFakeTimers(testTime);
121
+ sinon.stub(fakedPreLoginMetricsBatcher, 'savePreLoginId');
122
+ const requestSpy = sinon.stub(fakedPreLoginMetricsBatcher, 'request');
123
+
124
+ await preLoginMetrics.submitPreLoginEvent({name: testEvent, preLoginId: 'abc123', payload: {}});
125
+
126
+ assert.calledOnceWithMatch(requestSpy, {
127
+ type: ['business'],
128
+ eventPayload: {metricName: testEvent, timestamp: testTime},
129
+ });
130
+ });
131
+ });
132
+ });
@@ -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
+ });