@webex/internal-plugin-metrics 3.0.0-beta.218 → 3.0.0-beta.219

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.
@@ -14,6 +14,8 @@ import {
14
14
  prepareDiagnosticMetricItem,
15
15
  userAgentToString,
16
16
  extractVersionMetadata,
17
+ isMeetingInfoServiceError,
18
+ isBrowserMediaErrorName,
17
19
  } from './call-diagnostic-metrics.util';
18
20
  import {CLIENT_NAME} from '../config';
19
21
  import {
@@ -30,14 +32,17 @@ import {
30
32
  ClientEventError,
31
33
  ClientEventPayload,
32
34
  ClientInfo,
35
+ ClientEventPayloadError,
33
36
  } from '../metrics.types';
34
37
  import CallDiagnosticEventsBatcher from './call-diagnostic-metrics-batcher';
35
38
  import {
36
39
  CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD,
37
40
  CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND,
38
- MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
39
41
  NEW_LOCUS_ERROR_CLIENT_CODE,
40
42
  SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP,
43
+ UNKNOWN_ERROR,
44
+ BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP,
45
+ MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
41
46
  } from './config';
42
47
 
43
48
  const {getOSVersion, getBrowserName, getBrowserVersion} = BrowserDetection();
@@ -346,9 +351,11 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
346
351
  public getErrorPayloadForClientErrorCode({
347
352
  clientErrorCode,
348
353
  serviceErrorCode,
354
+ serviceErrorName,
349
355
  }: {
350
356
  clientErrorCode: number;
351
357
  serviceErrorCode: any;
358
+ serviceErrorName?: any;
352
359
  }): ClientEventError {
353
360
  let error: ClientEventError;
354
361
 
@@ -359,6 +366,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
359
366
  error = merge(
360
367
  {fatal: true, shownToUser: false, name: 'other', category: 'other'}, // default values
361
368
  {errorCode: clientErrorCode},
369
+ serviceErrorName ? {errorData: {errorName: serviceErrorName}} : {},
362
370
  {serviceErrorCode},
363
371
  partialParsedError
364
372
  );
@@ -375,30 +383,49 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
375
383
  * @param rawError
376
384
  */
377
385
  generateClientEventErrorPayload(rawError: any) {
386
+ if (rawError.name) {
387
+ if (isBrowserMediaErrorName(rawError.name)) {
388
+ return this.getErrorPayloadForClientErrorCode({
389
+ serviceErrorCode: undefined,
390
+ clientErrorCode: BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[rawError.name],
391
+ serviceErrorName: rawError.name,
392
+ });
393
+ }
394
+ }
395
+
378
396
  const serviceErrorCode =
379
- rawError?.error?.body?.errorCode || rawError?.body?.errorCode || rawError?.body?.code;
397
+ rawError?.error?.body?.errorCode ||
398
+ rawError?.body?.errorCode ||
399
+ rawError?.body?.code ||
400
+ rawError?.body?.reason?.reasonCode;
401
+
380
402
  if (serviceErrorCode) {
381
403
  const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
382
404
  if (clientErrorCode) {
383
405
  return this.getErrorPayloadForClientErrorCode({clientErrorCode, serviceErrorCode});
384
406
  }
385
407
 
386
- // by default, if it is locus error, return nre locus err
408
+ // by default, if it is locus error, return new locus err
387
409
  if (isLocusServiceErrorCode(serviceErrorCode)) {
388
410
  return this.getErrorPayloadForClientErrorCode({
389
411
  clientErrorCode: NEW_LOCUS_ERROR_CLIENT_CODE,
390
412
  serviceErrorCode,
391
413
  });
392
414
  }
415
+ }
393
416
 
394
- // otherwise return meeting info
417
+ if (isMeetingInfoServiceError(rawError)) {
395
418
  return this.getErrorPayloadForClientErrorCode({
396
419
  clientErrorCode: MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
397
420
  serviceErrorCode,
398
421
  });
399
422
  }
400
423
 
401
- return undefined;
424
+ // otherwise return unkown error
425
+ return this.getErrorPayloadForClientErrorCode({
426
+ clientErrorCode: UNKNOWN_ERROR,
427
+ serviceErrorCode: UNKNOWN_ERROR,
428
+ });
402
429
  }
403
430
 
404
431
  /**
@@ -411,11 +438,13 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
411
438
  private createClientEventObjectInMeeting({
412
439
  name,
413
440
  options,
441
+ errors,
414
442
  }: {
415
443
  name: ClientEvent['name'];
416
444
  options?: SubmitClientEventOptions;
445
+ errors?: ClientEventPayloadError;
417
446
  }) {
418
- const {meetingId, mediaConnections, rawError} = options;
447
+ const {meetingId, mediaConnections} = options;
419
448
 
420
449
  // @ts-ignore
421
450
  const meeting = this.webex.meetings.meetingCollection.get(meetingId);
@@ -442,16 +471,6 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
442
471
  mediaConnections: meeting?.mediaConnections || mediaConnections,
443
472
  });
444
473
 
445
- // check if we need to generate errors
446
- const errors: ClientEvent['payload']['errors'] = [];
447
-
448
- if (rawError) {
449
- const generatedError = this.generateClientEventErrorPayload(rawError);
450
- if (generatedError) {
451
- errors.push(generatedError);
452
- }
453
- }
454
-
455
474
  // create client event object
456
475
  const clientEventObject: ClientEvent['payload'] = {
457
476
  name,
@@ -481,9 +500,11 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
481
500
  private createClientEventObjectPreMeeting({
482
501
  name,
483
502
  options,
503
+ errors,
484
504
  }: {
485
505
  name: ClientEvent['name'];
486
506
  options?: SubmitClientEventOptions;
507
+ errors?: ClientEventPayloadError;
487
508
  }) {
488
509
  const {correlationId} = options;
489
510
 
@@ -495,6 +516,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
495
516
  // create client event object
496
517
  const clientEventObject: ClientEvent['payload'] = {
497
518
  name,
519
+ errors,
498
520
  canProceed: true,
499
521
  identifiers,
500
522
  eventData: {
@@ -524,15 +546,25 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
524
546
  payload?: ClientEventPayload;
525
547
  options?: SubmitClientEventOptions;
526
548
  }) {
527
- const {meetingId, correlationId} = options;
549
+ const {meetingId, correlationId, rawError} = options;
528
550
  let clientEventObject: ClientEvent['payload'];
529
551
 
552
+ // check if we need to generate errors
553
+ const errors: ClientEventPayloadError = [];
554
+
555
+ if (rawError) {
556
+ const generatedError = this.generateClientEventErrorPayload(rawError);
557
+ if (generatedError) {
558
+ errors.push(generatedError);
559
+ }
560
+ }
561
+
530
562
  // events that will most likely happen in join phase
531
563
  if (meetingId) {
532
- clientEventObject = this.createClientEventObjectInMeeting({name, options});
564
+ clientEventObject = this.createClientEventObjectInMeeting({name, options, errors});
533
565
  } else if (correlationId) {
534
566
  // any pre join events or events that are outside the meeting.
535
- clientEventObject = this.createClientEventObjectPreMeeting({name, options});
567
+ clientEventObject = this.createClientEventObjectPreMeeting({name, options, errors});
536
568
  } else {
537
569
  throw new Error('Not implemented');
538
570
  }
@@ -11,6 +11,7 @@ import {
11
11
  MediaQualityEventVideoSetupDelayPayload,
12
12
  MetricEventNames,
13
13
  } from '../metrics.types';
14
+ import {BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP, WBX_APP_API_URL} from './config';
14
15
 
15
16
  const {getOSName, getOSVersion, getBrowserName, getBrowserVersion} = BrowserDetection();
16
17
 
@@ -96,7 +97,7 @@ export const clearEmptyKeysRecursively = (obj: any) => {
96
97
  * If it is 7 digits and starts with a 2, it is locus.
97
98
  *
98
99
  * @param errorCode
99
- * @returns
100
+ * @returns {boolean}
100
101
  */
101
102
  export const isLocusServiceErrorCode = (errorCode: string | number) => {
102
103
  const code = `${errorCode}`;
@@ -108,6 +109,36 @@ export const isLocusServiceErrorCode = (errorCode: string | number) => {
108
109
  return false;
109
110
  };
110
111
 
112
+ /**
113
+ * MeetingInfo errors sometimes has body.data.meetingInfo object
114
+ * MeetingInfo errors come with a wbxappapi url
115
+ *
116
+ * @param {Object} rawError
117
+ * @returns {boolean}
118
+ */
119
+ export const isMeetingInfoServiceError = (rawError: any) => {
120
+ if (rawError.body?.data?.meetingInfo || rawError.body?.url?.includes(WBX_APP_API_URL)) {
121
+ return true;
122
+ }
123
+
124
+ return false;
125
+ };
126
+
127
+ /**
128
+ * MDN Media Devices getUserMedia() method returns a name if it errs
129
+ * Documentation can be found here: https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
130
+ *
131
+ * @param errorCode
132
+ * @returns
133
+ */
134
+ export const isBrowserMediaErrorName = (errorName: any) => {
135
+ if (BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[errorName]) {
136
+ return true;
137
+ }
138
+
139
+ return false;
140
+ };
141
+
111
142
  /**
112
143
  * @param webClientDomain
113
144
  * @returns
@@ -6,7 +6,32 @@ import {ClientEventError} from '../metrics.types';
6
6
 
7
7
  export const NEW_LOCUS_ERROR_CLIENT_CODE = 4008;
8
8
  export const MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE = 4100;
9
+ export const UNKNOWN_ERROR = 9999; // Unexpected error that is not a meetingInfo error, locus error or browser media error.
9
10
  export const ICE_FAILURE_CLIENT_CODE = 2004;
11
+ export const WBX_APP_API_URL = 'wbxappapi'; // MeetingInfo WebexAppApi response object normally contains a body.url that includes the string 'wbxappapi'
12
+
13
+ // Found in https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
14
+ const BROWSER_MEDIA_ERROR_NAMES = {
15
+ PERMISSION_DENIED_ERROR: 'PermissionDeniedError',
16
+ NOT_ALLOWED_ERROR: 'NotAllowedError',
17
+ NOT_READABLE_ERROR: 'NotReadableError',
18
+ ABORT_ERROR: 'AbortError',
19
+ NOT_FOUND_ERROR: 'NotFoundError',
20
+ OVERCONSTRAINED_ERROR: 'OverconstrainedError',
21
+ SECURITY_ERROR: 'SecurityError',
22
+ TYPE_ERROR: 'TypeError',
23
+ };
24
+
25
+ export const BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP = {
26
+ [BROWSER_MEDIA_ERROR_NAMES.PERMISSION_DENIED_ERROR]: 4032, // User did not grant permission
27
+ [BROWSER_MEDIA_ERROR_NAMES.NOT_ALLOWED_ERROR]: 4032, // User did not grant permission
28
+ [BROWSER_MEDIA_ERROR_NAMES.NOT_READABLE_ERROR]: 2729, // Although the user granted permission to use the matching devices, a hardware error occurred at the operating system, browser, or Web page level which prevented access to the device.
29
+ [BROWSER_MEDIA_ERROR_NAMES.ABORT_ERROR]: 2729, // Although the user and operating system both granted access to the hardware device, and no hardware issues occurred that would cause a NotReadableError DOMException, throw if some problem occurred which prevented the device from being used.
30
+ [BROWSER_MEDIA_ERROR_NAMES.NOT_FOUND_ERROR]: 2729, // User did not grant permission
31
+ [BROWSER_MEDIA_ERROR_NAMES.OVERCONSTRAINED_ERROR]: 2729, // Thrown if the specified constraints resulted in no candidate devices which met the criteria requested.
32
+ [BROWSER_MEDIA_ERROR_NAMES.SECURITY_ERROR]: 2729, // Thrown if user media support is disabled on the Document on which getUserMedia() was called. The mechanism by which user media support is enabled and disabled is left up to the individual user agent.
33
+ [BROWSER_MEDIA_ERROR_NAMES.TYPE_ERROR]: 2729, // Thrown if the list of constraints specified is empty, or has all constraints set to false. This can also happen if you try to call getUserMedia() in an insecure context, since navigator.mediaDevices is undefined in an insecure context.
34
+ };
10
35
 
11
36
  const ERROR_DESCRIPTIONS = {
12
37
  UNKNOWN_CALL_FAILURE: 'UnknownCallFailure',
@@ -63,41 +88,81 @@ const ERROR_DESCRIPTIONS = {
63
88
  RECORDING_IN_PROGRESS_FAILED: 'RecordingInProgressFailed',
64
89
  MEETING_INFO_LOOKUP_ERROR: 'MeetingInfoLookupError',
65
90
  CALL_FULL_ADD_GUEST: 'CallFullAddGuest',
91
+ REQUIRE_WEBEX_LOGIN: 'RequireWebexLogin',
92
+ USER_NOT_ALLOWED_ACCESS_MEETING: 'UserNotAllowedAccessMeeting',
93
+ USER_NEEDS_ACTIVATION: 'UserNeedsActivation',
94
+ SIGN_UP_INVALID_EMAIL: 'SignUpInvalidEmail',
95
+ UNKNOWN_ERROR: 'UnknownError',
96
+ NO_MEDIA_FOUND: 'NoMediaFound',
97
+ STREAM_ERROR_NO_MEDIA: 'StreamErrorNoMedia',
98
+ CAMERA_PERMISSION_DENIED: 'CameraPermissionDenied',
66
99
  };
67
100
 
68
101
  export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = {
69
102
  // ---- Webex API ----
103
+ // Taken from https://wiki.cisco.com/display/HFWEB/MeetingInfo+API and https://sqbu-github.cisco.com/WebExSquared/spark-client-framework/blob/master/spark-client-framework/Services/WebexMeetingService/WebexMeetingModel.h
70
104
  // Site not support the URL's domain
71
105
  58400: 4100,
72
106
  99002: 4100,
73
- // Cannot find the data
107
+ // Cannot find the data. Unkown meeting.
74
108
  99009: 4100,
109
+ // Meeting is not allow to cross env
110
+ 58500: 4100,
111
+ // Input parameters contain invalit item
112
+ 400001: 4100,
113
+ // Empty password or token. Meeting is not allow to access since require password
114
+ 403004: 4005,
115
+ // Wrong password. Meeting is not allow to access since password error
116
+ 403028: 4005,
117
+ // Wrong or expired permission. Meeting is not allow to access since permissionToken error or expire
118
+ 403032: 4005,
119
+ // Meeting is required login for current user
120
+ 403034: 4036,
121
+ // Meeting is not allow to access since require password or hostKey
122
+ // Empty password or host key
123
+ 403036: 4005,
124
+ // Meeting is not allow to access since password or hostKey error
125
+ // Wrong password or host key
126
+ 403038: 4005,
75
127
  // CMR Meeting Not Supported (meeting exists, but not CMR meeting)
76
128
  403040: 4100,
77
129
  // Requires Moderator Pin or Guest Pin
78
130
  403041: 4005,
79
- // Meeting is not allow to access since password or hostKey error
80
- 403038: 4005,
81
- // Meeting is not allow to access since require password or hostKey
82
- 403036: 4005,
131
+ // Email blocked
132
+ 403047: 4101,
133
+ // Device not authenticated for your organization
134
+ 403408: 4101,
83
135
  // Invalid panelist Pin
84
- 403043: 4100,
85
- // Device not registered in org
86
- 403048: 4100,
87
- // Not allowed to join external meetings
88
- 403049: 4100,
89
- 403100: 4100,
90
- // Enforce sign in: need login before access when policy enforce sign in
91
- 403101: 4100,
136
+ 403043: 4005,
137
+ // Device not registered in org. Device not authenticated.
138
+ 403048: 4101,
139
+ // Not allowed to join external meetings. Violate meeting join policy. Your organization settings don't allow you to join this meeting.
140
+ 403049: 4101,
141
+ // Invalid email. Requires sign in meeting's current site.
142
+ 403100: 4101,
143
+ // Enforce sign in: need login before access when policy enforce sign in. GuestForceUserSignInPolicy
144
+ 403101: 4036,
92
145
  // Enforce sign in: sign in with your email address that is approved by your organization
93
- 403102: 4100,
94
- // Join internal Meeting: need login before access when policy enforce sign in
95
- 403103: 4100,
146
+ 403102: 4036,
147
+ // Join internal Meeting: need login before access when policy enforce sign in. Guest force user sign in internal meeting policy.
148
+ 403103: 4036,
96
149
  // Join internal Meeting: The host's organization policy doesn't allow your account to join this meeting. Try switching to another account
97
- 403104: 4100,
98
- 404001: 4100,
99
- // Site data not found
150
+ 403104: 4101,
151
+ 404001: 4101,
152
+ // Site data not found. Unkonwn meeting. Site data not found(or null).
100
153
  404006: 4100,
154
+ // Invalid input with too many requests. Too many requests access, please input captcha code
155
+ 423001: 4005,
156
+ // Wrong password with too many requests. PasswordError too many time, please input captcha code
157
+ 423005: 4005,
158
+ // Wrong password or host key with too many requests
159
+ 423006: 4005,
160
+ // PasswordError with right captcha, please input captcha code
161
+ 423010: 4005,
162
+ // PasswordOrHostKeyError with right captcha, please input captcha code
163
+ 423012: 4005,
164
+ // Unverified or invalid input. Force show captcha. Please input captcha code"
165
+ 423013: 4005,
101
166
  // Too many requests access
102
167
  429005: 4100,
103
168
 
@@ -120,6 +185,10 @@ export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = {
120
185
  2423004: 4003,
121
186
  // LOCUS_REQUIRES_MODERATOR_PIN_OR_GUEST
122
187
  2423005: 4005,
188
+ 2423006: 4005,
189
+ 2423016: 4005,
190
+ 2423017: 4005,
191
+ 2423018: 4005,
123
192
  // LOCUS_REQUIRES_MODERATOR_ROLE
124
193
  2423007: 4006,
125
194
  // LOCUS_JOIN_RESTRICTED_USER_NOT_IN_ROOM
@@ -164,6 +233,20 @@ export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = {
164
233
  2405001: 4029,
165
234
  // LOCUS_RECORDING_NOT_ENABLED
166
235
  2409005: 4029,
236
+
237
+ // ---- U2C Sign in catalog ------
238
+ // The user exists, but hasn't completed activation. Needs to visit Atlas for more processing.
239
+ 100002: 4102,
240
+ // The user exists, had completed activation earlier, but requires re-activation because of change in login strategy.
241
+ // Common example is: user signed up using an OAuth provider, but that OAuth provider was removed by org's admin. Now the user needs to re-activate using alternate login strategies: password-pin, new OAuth provider, SSO, etc.
242
+ 100007: 4102,
243
+ // The user does not exist
244
+ 100001: 4103,
245
+ // The user wasn't found, and the organization used for search is a domain-claimed organization.
246
+ 100006: 4103,
247
+ 100005: 4103, // Depracated because of an issue in the UCF Clients
248
+ // If both email-hash and domain-hash are null or undefined.
249
+ 100004: 4103,
167
250
  };
168
251
 
169
252
  export const CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD: Record<number, Partial<ClientEventError>> = {
@@ -269,10 +352,15 @@ export const CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD: Record<number, Partial<ClientEv
269
352
  fatal: true,
270
353
  },
271
354
  3007: {
272
- errorDescription: ERROR_DESCRIPTIONS.ROOM_TOO_LARGE_FREE_ACCOUNT,
355
+ errorDescription: ERROR_DESCRIPTIONS.STREAM_ERROR_NO_MEDIA,
273
356
  category: 'expected',
274
357
  fatal: true,
275
358
  },
359
+ 3013: {
360
+ errorDescription: ERROR_DESCRIPTIONS.ROOM_TOO_LARGE_FREE_ACCOUNT,
361
+ category: 'expected',
362
+ fatal: false,
363
+ },
276
364
  4001: {
277
365
  errorDescription: ERROR_DESCRIPTIONS.MEETING_INACTIVE,
278
366
  category: 'expected',
@@ -428,6 +516,16 @@ export const CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD: Record<number, Partial<ClientEv
428
516
  category: 'expected',
429
517
  fatal: true,
430
518
  },
519
+ 4032: {
520
+ errorDescription: ERROR_DESCRIPTIONS.CAMERA_PERMISSION_DENIED,
521
+ category: 'expected',
522
+ fatal: true,
523
+ },
524
+ 4036: {
525
+ errorDescription: ERROR_DESCRIPTIONS.REQUIRE_WEBEX_LOGIN,
526
+ category: 'expected',
527
+ fatal: true,
528
+ },
431
529
  5000: {
432
530
  errorDescription: ERROR_DESCRIPTIONS.SIP_CALLEE_BUSY,
433
531
  category: 'expected',
@@ -450,6 +548,31 @@ export const CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD: Record<number, Partial<ClientEv
450
548
  category: 'expected',
451
549
  fatal: false,
452
550
  },
551
+ 4101: {
552
+ errorDescription: ERROR_DESCRIPTIONS.USER_NOT_ALLOWED_ACCESS_MEETING,
553
+ category: 'expected',
554
+ fatal: true,
555
+ },
556
+ 4102: {
557
+ errorDescription: ERROR_DESCRIPTIONS.USER_NEEDS_ACTIVATION,
558
+ category: 'expected',
559
+ fatal: true,
560
+ },
561
+ 4103: {
562
+ errorDescription: ERROR_DESCRIPTIONS.SIGN_UP_INVALID_EMAIL,
563
+ category: 'expected',
564
+ fatal: true,
565
+ },
566
+ 2729: {
567
+ errorDescription: ERROR_DESCRIPTIONS.NO_MEDIA_FOUND,
568
+ category: 'expected',
569
+ fatal: false,
570
+ },
571
+ 9999: {
572
+ errorDescription: ERROR_DESCRIPTIONS.UNKNOWN_ERROR,
573
+ category: 'other',
574
+ fatal: true,
575
+ },
453
576
  };
454
577
 
455
578
  export const CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND = 'js_sdk_call_diagnostic_event_failed_to_send';
@@ -90,6 +90,7 @@ export type NetworkType = NonNullable<RawEvent['origin']>['networkType'];
90
90
 
91
91
  export type ClientEventPayload = RecursivePartial<ClientEvent['payload']>;
92
92
  export type ClientEventLeaveReason = ClientEvent['payload']['leaveReason'];
93
+ export type ClientEventPayloadError = ClientEvent['payload']['errors'];
93
94
 
94
95
  export type MediaQualityEventAudioSetupDelayPayload = NonNullable<
95
96
  MediaQualityEvent['payload']