@webex/internal-plugin-metrics 3.0.0-beta.34 → 3.0.0-beta.341

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 (68) hide show
  1. package/dist/batcher.js +2 -1
  2. package/dist/batcher.js.map +1 -1
  3. package/dist/call-diagnostic/call-diagnostic-metrics-batcher.js +65 -0
  4. package/dist/call-diagnostic/call-diagnostic-metrics-batcher.js.map +1 -0
  5. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +456 -0
  6. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -0
  7. package/dist/call-diagnostic/call-diagnostic-metrics.js +827 -0
  8. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -0
  9. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +337 -0
  10. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -0
  11. package/dist/call-diagnostic/config.js +610 -0
  12. package/dist/call-diagnostic/config.js.map +1 -0
  13. package/dist/client-metrics-batcher.js +2 -1
  14. package/dist/client-metrics-batcher.js.map +1 -1
  15. package/dist/config.js +2 -1
  16. package/dist/config.js.map +1 -1
  17. package/dist/index.js +18 -0
  18. package/dist/index.js.map +1 -1
  19. package/dist/metrics.js +23 -28
  20. package/dist/metrics.js.map +1 -1
  21. package/dist/metrics.types.js +7 -0
  22. package/dist/metrics.types.js.map +1 -0
  23. package/dist/new-metrics.js +333 -0
  24. package/dist/new-metrics.js.map +1 -0
  25. package/dist/types/batcher.d.ts +2 -0
  26. package/dist/types/call-diagnostic/call-diagnostic-metrics-batcher.d.ts +2 -0
  27. package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +194 -0
  28. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +418 -0
  29. package/dist/types/call-diagnostic/call-diagnostic-metrics.util.d.ts +96 -0
  30. package/dist/types/call-diagnostic/config.d.ts +172 -0
  31. package/dist/types/client-metrics-batcher.d.ts +2 -0
  32. package/dist/types/config.d.ts +36 -0
  33. package/dist/types/index.d.ts +13 -0
  34. package/dist/types/metrics.d.ts +3 -0
  35. package/dist/types/metrics.types.d.ts +104 -0
  36. package/dist/types/new-metrics.d.ts +139 -0
  37. package/dist/types/utils.d.ts +6 -0
  38. package/dist/utils.js +27 -0
  39. package/dist/utils.js.map +1 -0
  40. package/package.json +13 -8
  41. package/src/batcher.js +1 -0
  42. package/src/call-diagnostic/call-diagnostic-metrics-batcher.ts +75 -0
  43. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +419 -0
  44. package/src/call-diagnostic/call-diagnostic-metrics.ts +882 -0
  45. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +362 -0
  46. package/src/call-diagnostic/config.ts +666 -0
  47. package/src/client-metrics-batcher.js +1 -0
  48. package/src/config.js +1 -0
  49. package/src/index.ts +43 -0
  50. package/src/metrics.js +18 -24
  51. package/src/metrics.types.ts +160 -0
  52. package/src/new-metrics.ts +317 -0
  53. package/src/utils.ts +17 -0
  54. package/test/unit/spec/batcher.js +2 -0
  55. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +452 -0
  56. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +506 -0
  57. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +2109 -0
  58. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +565 -0
  59. package/test/unit/spec/client-metrics-batcher.js +2 -0
  60. package/test/unit/spec/metrics.js +66 -97
  61. package/test/unit/spec/new-metrics.ts +267 -0
  62. package/test/unit/spec/utils.ts +22 -0
  63. package/tsconfig.json +6 -0
  64. package/dist/call-diagnostic-events-batcher.js +0 -60
  65. package/dist/call-diagnostic-events-batcher.js.map +0 -1
  66. package/src/call-diagnostic-events-batcher.js +0 -62
  67. package/src/index.js +0 -17
  68. package/test/unit/spec/call-diagnostic-events-batcher.js +0 -195
@@ -0,0 +1,882 @@
1
+ /* eslint-disable @typescript-eslint/no-unused-vars */
2
+ /* eslint-disable class-methods-use-this */
3
+ /* eslint-disable valid-jsdoc */
4
+ import {getOSNameInternal} from '@webex/internal-plugin-metrics';
5
+ import {BrowserDetection, getBrowserSerial} from '@webex/common';
6
+ import uuid from 'uuid';
7
+ import {merge} from 'lodash';
8
+ import {StatelessWebexPlugin} from '@webex/webex-core';
9
+
10
+ import {
11
+ anonymizeIPAddress,
12
+ clearEmptyKeysRecursively,
13
+ isLocusServiceErrorCode,
14
+ prepareDiagnosticMetricItem,
15
+ userAgentToString,
16
+ extractVersionMetadata,
17
+ isMeetingInfoServiceError,
18
+ isBrowserMediaErrorName,
19
+ isNetworkError,
20
+ isUnauthorizedError,
21
+ } from './call-diagnostic-metrics.util';
22
+ import {CLIENT_NAME} from '../config';
23
+ import {
24
+ Event,
25
+ ClientType,
26
+ SubClientType,
27
+ NetworkType,
28
+ EnvironmentType,
29
+ NewEnvironmentType,
30
+ ClientEvent,
31
+ SubmitClientEventOptions,
32
+ MediaQualityEvent,
33
+ SubmitMQEOptions,
34
+ SubmitMQEPayload,
35
+ ClientLaunchMethodType,
36
+ ClientEventError,
37
+ ClientEventPayload,
38
+ ClientInfo,
39
+ ClientEventPayloadError,
40
+ ClientSubServiceType,
41
+ } from '../metrics.types';
42
+ import CallDiagnosticEventsBatcher from './call-diagnostic-metrics-batcher';
43
+ import {
44
+ CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD,
45
+ CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND,
46
+ NEW_LOCUS_ERROR_CLIENT_CODE,
47
+ SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP,
48
+ UNKNOWN_ERROR,
49
+ BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP,
50
+ MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
51
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
52
+ NETWORK_ERROR,
53
+ AUTHENTICATION_FAILED_CODE,
54
+ WEBEX_SUB_SERVICE_TYPES,
55
+ } from './config';
56
+ import {generateCommonErrorMetadata} from '../utils';
57
+
58
+ const {getOSVersion, getBrowserName, getBrowserVersion} = BrowserDetection();
59
+
60
+ type GetOriginOptions = {
61
+ clientType: ClientType;
62
+ subClientType: SubClientType;
63
+ networkType?: NetworkType;
64
+ clientLaunchMethod?: ClientLaunchMethodType;
65
+ environment?: EnvironmentType;
66
+ newEnvironment?: NewEnvironmentType;
67
+ };
68
+
69
+ type GetIdentifiersOptions = {
70
+ meeting?: any;
71
+ mediaConnections?: any[];
72
+ correlationId?: string;
73
+ preLoginId?: string;
74
+ globalMeetingId?: string;
75
+ webexConferenceIdStr?: string;
76
+ };
77
+
78
+ /**
79
+ * @description Util class to handle Call Analyzer Metrics
80
+ * @export
81
+ * @class CallDiagnosticMetrics
82
+ */
83
+ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
84
+ // @ts-ignore
85
+ private callDiagnosticEventsBatcher: CallDiagnosticEventsBatcher;
86
+ private logger: any; // to avoid adding @ts-ignore everywhere
87
+ private hasLoggedBrowserSerial: boolean;
88
+ // the default validator before piping an event to the batcher
89
+ // this function can be overridden by the user
90
+ public validator: (options: {
91
+ type: 'mqe' | 'ce';
92
+ event: Event;
93
+ }) => Promise<{event: Event; valid: boolean}> = (options: {type: 'mqe' | 'ce'; event: Event}) =>
94
+ Promise.resolve({event: options?.event, valid: true});
95
+
96
+ /**
97
+ * Constructor
98
+ * @param args
99
+ */
100
+ constructor(...args) {
101
+ super(...args);
102
+ // @ts-ignore
103
+ this.logger = this.webex.logger;
104
+ // @ts-ignore
105
+ this.callDiagnosticEventsBatcher = new CallDiagnosticEventsBatcher({}, {parent: this.webex});
106
+ }
107
+
108
+ /**
109
+ * Returns the login type of the current user
110
+ * @returns one of 'login-ci','unverified-guest', null
111
+ */
112
+ getCurLoginType() {
113
+ // @ts-ignore
114
+ if (this.webex.canAuthorize) {
115
+ // @ts-ignore
116
+ return this.webex.credentials.isUnverifiedGuest ? 'unverified-guest' : 'login-ci';
117
+ }
118
+
119
+ return null;
120
+ }
121
+
122
+ /**
123
+ * Returns if the meeting has converged architecture enabled
124
+ * @param options.meetingId
125
+ */
126
+ getIsConvergedArchitectureEnabled({meetingId}: {meetingId?: string}): boolean {
127
+ if (meetingId) {
128
+ // @ts-ignore
129
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
130
+
131
+ return meeting?.meetingInfo?.enableConvergedArchitecture;
132
+ }
133
+
134
+ return undefined;
135
+ }
136
+
137
+ /**
138
+ * Returns meeting's subServiceType
139
+ * @param meeting
140
+ * @returns
141
+ */
142
+ getSubServiceType(meeting?: any): ClientSubServiceType {
143
+ if (meeting) {
144
+ // @ts-ignore
145
+ const meetingInfo = meeting?.meetingInfo;
146
+ // if not Scheduled, not Webinar, pmr - then pmr
147
+ if (!meetingInfo?.webexScheduled && !meetingInfo?.enableEvent && meetingInfo?.pmr) {
148
+ return WEBEX_SUB_SERVICE_TYPES.PMR;
149
+ }
150
+ // if Scheduled, not Webinar, not pmr - then ScheduledMeeting
151
+ if (meetingInfo?.webexScheduled && !meetingInfo?.enableEvent && !meetingInfo?.pmr) {
152
+ return WEBEX_SUB_SERVICE_TYPES.SCHEDULED_MEETING;
153
+ }
154
+ // if Scheduled, Webinar, not pmr - then Webinar
155
+ if (meetingInfo?.webexScheduled && meetingInfo?.enableEvent && !meetingInfo?.pmr) {
156
+ return WEBEX_SUB_SERVICE_TYPES.WEBINAR;
157
+ }
158
+ }
159
+
160
+ return undefined;
161
+ }
162
+
163
+ /**
164
+ * Get origin object for Call Diagnostic Event payload.
165
+ * @param options
166
+ * @param meetingId
167
+ * @returns
168
+ */
169
+ getOrigin(options: GetOriginOptions, meetingId?: string) {
170
+ const defaultClientType: ClientType =
171
+ // @ts-ignore
172
+ this.webex.meetings.config?.metrics?.clientType;
173
+ const defaultSubClientType: SubClientType =
174
+ // @ts-ignore
175
+ this.webex.meetings.config?.metrics?.subClientType;
176
+ // @ts-ignore
177
+ const providedClientVersion: string = this.webex.meetings.config?.metrics?.clientVersion;
178
+ // @ts-ignore
179
+ const defaultSDKClientVersion = `${CLIENT_NAME}/${this.webex.version}`;
180
+
181
+ let versionMetadata: Pick<ClientInfo, 'majorVersion' | 'minorVersion'> = {};
182
+
183
+ // sdk version split doesn't really make sense for now...
184
+ if (providedClientVersion) {
185
+ versionMetadata = extractVersionMetadata(providedClientVersion);
186
+ }
187
+
188
+ if (!this.hasLoggedBrowserSerial) {
189
+ this.logger.log(
190
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
191
+ `CallDiagnosticMetrics: @createClientEventObjectInMeeting => collected browser data`,
192
+ JSON.stringify(getBrowserSerial())
193
+ );
194
+
195
+ this.hasLoggedBrowserSerial = true;
196
+ }
197
+
198
+ if (
199
+ (defaultClientType && defaultSubClientType) ||
200
+ (options.clientType && options.subClientType)
201
+ ) {
202
+ const origin: Event['origin'] = {
203
+ name: 'endpoint',
204
+ networkType: options?.networkType || 'unknown',
205
+ userAgent: userAgentToString({
206
+ // @ts-ignore
207
+ clientName: this.webex.meetings?.config?.metrics?.clientName,
208
+ // @ts-ignore
209
+ webexVersion: this.webex.version,
210
+ }),
211
+ clientInfo: {
212
+ clientType: options?.clientType || defaultClientType,
213
+ clientVersion: providedClientVersion || defaultSDKClientVersion,
214
+ ...versionMetadata,
215
+ publicNetworkPrefix:
216
+ // @ts-ignore
217
+ anonymizeIPAddress(this.webex.meetings.geoHintInfo?.clientAddress) || undefined,
218
+ localNetworkPrefix:
219
+ anonymizeIPAddress(
220
+ // @ts-ignore
221
+ this.webex.meetings.meetingCollection
222
+ .get(meetingId)
223
+ ?.statsAnalyzer?.getLocalIpAddress()
224
+ ) || undefined,
225
+ osVersion: getOSVersion() || 'unknown',
226
+ subClientType: options?.subClientType || defaultSubClientType,
227
+ os: getOSNameInternal(),
228
+ browser: getBrowserName(),
229
+ browserVersion: getBrowserVersion(),
230
+ },
231
+ };
232
+
233
+ if (meetingId) {
234
+ // @ts-ignore
235
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
236
+ if (meeting?.environment) {
237
+ origin.environment = meeting.environment;
238
+ }
239
+ }
240
+
241
+ if (options?.environment) {
242
+ origin.environment = options.environment;
243
+ }
244
+
245
+ if (options?.newEnvironment) {
246
+ origin.newEnvironment = options.newEnvironment;
247
+ }
248
+
249
+ if (options?.clientLaunchMethod) {
250
+ origin.clientInfo.clientLaunchMethod = options.clientLaunchMethod;
251
+ }
252
+
253
+ return origin;
254
+ }
255
+
256
+ throw new Error("ClientType and SubClientType can't be undefined");
257
+ }
258
+
259
+ /**
260
+ * Gather identifier details for call diagnostic payload.
261
+ * @throws Error if initialization fails.
262
+ * @param options
263
+ */
264
+ getIdentifiers(options: GetIdentifiersOptions) {
265
+ const {
266
+ meeting,
267
+ mediaConnections,
268
+ correlationId,
269
+ webexConferenceIdStr,
270
+ globalMeetingId,
271
+ preLoginId,
272
+ } = options;
273
+ const identifiers: Event['event']['identifiers'] = {
274
+ correlationId: 'unknown',
275
+ };
276
+
277
+ if (meeting) {
278
+ identifiers.correlationId = meeting.correlationId;
279
+ }
280
+
281
+ if (correlationId) {
282
+ identifiers.correlationId = correlationId;
283
+ }
284
+ // @ts-ignore
285
+ if (this.webex.internal) {
286
+ // @ts-ignore
287
+ const {device} = this.webex.internal;
288
+ identifiers.userId = device.userId || preLoginId;
289
+ identifiers.deviceId = device.url;
290
+ identifiers.orgId = device.orgId;
291
+ // @ts-ignore
292
+ identifiers.locusUrl = this.webex.internal.services.get('locus');
293
+ }
294
+
295
+ if (meeting?.locusInfo?.fullState) {
296
+ identifiers.locusUrl = meeting.locusUrl;
297
+ identifiers.locusId = meeting.locusUrl && meeting.locusUrl.split('/').pop();
298
+ identifiers.locusStartTime =
299
+ meeting.locusInfo.fullState && meeting.locusInfo.fullState.lastActive;
300
+ }
301
+
302
+ if (meeting?.meetingInfo?.confIdStr || meeting?.meetingInfo?.confID) {
303
+ identifiers.webexConferenceIdStr = `${
304
+ meeting.meetingInfo?.confIdStr || meeting.meetingInfo?.confID
305
+ }`;
306
+ }
307
+
308
+ if (meeting?.meetingInfo?.meetingId) {
309
+ identifiers.globalMeetingId = meeting.meetingInfo?.meetingId;
310
+ }
311
+
312
+ if (meeting?.meetingInfo?.siteName) {
313
+ identifiers.webexSiteName = meeting.meetingInfo?.siteName;
314
+ }
315
+
316
+ if (mediaConnections) {
317
+ identifiers.mediaAgentAlias = mediaConnections?.[0]?.mediaAgentAlias;
318
+ identifiers.mediaAgentGroupId = mediaConnections?.[0]?.mediaAgentGroupId;
319
+ }
320
+
321
+ if (!identifiers?.webexConferenceIdStr && webexConferenceIdStr) {
322
+ identifiers.webexConferenceIdStr = `${webexConferenceIdStr}`;
323
+ }
324
+
325
+ if (!identifiers?.globalMeetingId && globalMeetingId) {
326
+ identifiers.globalMeetingId = globalMeetingId;
327
+ }
328
+
329
+ if (identifiers.correlationId === undefined) {
330
+ throw new Error('Identifiers initialization failed.');
331
+ }
332
+
333
+ return identifiers;
334
+ }
335
+
336
+ /**
337
+ * Create diagnostic event, which can hold client event, feature event or MQE event data.
338
+ * This just initiates the shared properties that are required for all the 3 event categories.
339
+ * @param eventData
340
+ * @param options
341
+ * @returns
342
+ */
343
+ prepareDiagnosticEvent(eventData: Event['event'], options: any) {
344
+ const {meetingId} = options;
345
+ const origin = this.getOrigin(options, meetingId);
346
+
347
+ const event: Event = {
348
+ eventId: uuid.v4(),
349
+ version: 1,
350
+ origin,
351
+ originTime: {
352
+ triggered: new Date().toISOString(),
353
+ // is overridden in prepareRequest batcher
354
+ sent: 'not_defined_yet',
355
+ },
356
+ // @ts-ignore
357
+ senderCountryCode: this.webex.meetings.geoHintInfo?.countryCode,
358
+ event: eventData,
359
+ };
360
+
361
+ // sanitize (remove empty properties, CA requires it)
362
+ // but we don't want to sanitize MQE as most of the times
363
+ // values will be 0, [] etc, and they are required.
364
+ if (eventData.name !== 'client.mediaquality.event') {
365
+ clearEmptyKeysRecursively(event);
366
+ }
367
+
368
+ return event;
369
+ }
370
+
371
+ /**
372
+ * TODO: NOT IMPLEMENTED
373
+ * Submit Feature Event
374
+ * @returns
375
+ */
376
+ public submitFeatureEvent() {
377
+ throw Error('Not implemented');
378
+ }
379
+
380
+ /**
381
+ * Submit Media Quality Event
382
+ * @param args - submit params
383
+ * @param arg.name - event key
384
+ * @param arg.payload - additional payload to be merge with the default payload
385
+ * @param arg.options - options
386
+ */
387
+ submitMQE({
388
+ name,
389
+ payload,
390
+ options,
391
+ }: {
392
+ name: MediaQualityEvent['name'];
393
+ payload: SubmitMQEPayload;
394
+ options: SubmitMQEOptions;
395
+ }) {
396
+ const {meetingId, mediaConnections, webexConferenceIdStr, globalMeetingId} = options;
397
+
398
+ // events that will most likely happen in join phase
399
+ if (meetingId) {
400
+ // @ts-ignore
401
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
402
+
403
+ if (!meeting) {
404
+ console.warn(
405
+ 'Attempt to send MQE but no meeting was found...',
406
+ `event: ${name}, meetingId: ${meetingId}`
407
+ );
408
+ // @ts-ignore
409
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
410
+ fields: {
411
+ meetingId,
412
+ name,
413
+ },
414
+ });
415
+
416
+ return;
417
+ }
418
+
419
+ // merge identifiers
420
+ const identifiers = this.getIdentifiers({
421
+ meeting,
422
+ mediaConnections: meeting.mediaConnections || mediaConnections,
423
+ webexConferenceIdStr,
424
+ globalMeetingId,
425
+ });
426
+
427
+ // create media quality event object
428
+ let clientEventObject: MediaQualityEvent['payload'] = {
429
+ name,
430
+ canProceed: true,
431
+ identifiers,
432
+ eventData: {
433
+ webClientDomain: window.location.hostname,
434
+ },
435
+ intervals: payload.intervals,
436
+ sourceMetadata: {
437
+ applicationSoftwareType: CLIENT_NAME,
438
+ // @ts-ignore
439
+ applicationSoftwareVersion: this.webex.version,
440
+ mediaEngineSoftwareType: getBrowserName() || 'browser',
441
+ mediaEngineSoftwareVersion: getOSVersion() || 'unknown',
442
+ startTime: new Date().toISOString(),
443
+ },
444
+ };
445
+
446
+ // merge any new properties, or override existing ones
447
+ clientEventObject = merge(clientEventObject, payload);
448
+
449
+ // append media quality event data to the call diagnostic event
450
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
451
+ this.validator({type: 'mqe', event: diagnosticEvent});
452
+ this.submitToCallDiagnostics(diagnosticEvent);
453
+ } else {
454
+ throw new Error(
455
+ 'Media quality events cant be sent outside the context of a meeting. Meeting id is required.'
456
+ );
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Return Client Event payload by client error code
462
+ * @param arg - get error arg
463
+ * @param arg.clientErrorCode
464
+ * @param arg.serviceErrorCode
465
+ * @param arg.payloadOverrides
466
+ * @returns
467
+ */
468
+ public getErrorPayloadForClientErrorCode({
469
+ clientErrorCode,
470
+ serviceErrorCode,
471
+ serviceErrorName,
472
+ rawErrorMessage,
473
+ payloadOverrides,
474
+ }: {
475
+ clientErrorCode: number;
476
+ serviceErrorCode: any;
477
+ serviceErrorName?: any;
478
+ rawErrorMessage?: string;
479
+ payloadOverrides?: any;
480
+ }): ClientEventError {
481
+ let error: ClientEventError;
482
+
483
+ if (clientErrorCode) {
484
+ const partialParsedError = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
485
+
486
+ if (partialParsedError) {
487
+ error = merge(
488
+ {fatal: true, shownToUser: false, name: 'other', category: 'other'}, // default values
489
+ {errorCode: clientErrorCode},
490
+ serviceErrorName ? {errorData: {errorName: serviceErrorName}} : {},
491
+ {serviceErrorCode},
492
+ {rawErrorMessage},
493
+ partialParsedError,
494
+ payloadOverrides || {}
495
+ );
496
+
497
+ return error;
498
+ }
499
+ }
500
+
501
+ return undefined;
502
+ }
503
+
504
+ /**
505
+ * Generate error payload for Client Event
506
+ * @param rawError
507
+ */
508
+ generateClientEventErrorPayload(rawError: any) {
509
+ const rawErrorMessage = rawError.message;
510
+ if (rawError.name) {
511
+ if (isBrowserMediaErrorName(rawError.name)) {
512
+ return this.getErrorPayloadForClientErrorCode({
513
+ serviceErrorCode: undefined,
514
+ clientErrorCode: BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[rawError.name],
515
+ serviceErrorName: rawError.name,
516
+ rawErrorMessage,
517
+ });
518
+ }
519
+ }
520
+
521
+ const serviceErrorCode =
522
+ rawError?.error?.body?.errorCode ||
523
+ rawError?.body?.errorCode ||
524
+ rawError?.body?.code ||
525
+ rawError?.body?.reason?.reasonCode;
526
+
527
+ if (serviceErrorCode) {
528
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
529
+ if (clientErrorCode) {
530
+ return this.getErrorPayloadForClientErrorCode({
531
+ clientErrorCode,
532
+ serviceErrorCode,
533
+ rawErrorMessage,
534
+ });
535
+ }
536
+
537
+ // by default, if it is locus error, return new locus err
538
+ if (isLocusServiceErrorCode(serviceErrorCode)) {
539
+ return this.getErrorPayloadForClientErrorCode({
540
+ clientErrorCode: NEW_LOCUS_ERROR_CLIENT_CODE,
541
+ serviceErrorCode,
542
+ rawErrorMessage,
543
+ });
544
+ }
545
+ }
546
+
547
+ if (isMeetingInfoServiceError(rawError)) {
548
+ return this.getErrorPayloadForClientErrorCode({
549
+ clientErrorCode: MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
550
+ serviceErrorCode,
551
+ rawErrorMessage,
552
+ });
553
+ }
554
+
555
+ if (isNetworkError(rawError)) {
556
+ return this.getErrorPayloadForClientErrorCode({
557
+ clientErrorCode: NETWORK_ERROR,
558
+ serviceErrorCode,
559
+ payloadOverrides: rawError.payloadOverrides,
560
+ rawErrorMessage,
561
+ });
562
+ }
563
+
564
+ if (isUnauthorizedError(rawError)) {
565
+ return this.getErrorPayloadForClientErrorCode({
566
+ clientErrorCode: AUTHENTICATION_FAILED_CODE,
567
+ serviceErrorCode,
568
+ payloadOverrides: rawError.payloadOverrides,
569
+ rawErrorMessage,
570
+ });
571
+ }
572
+
573
+ // otherwise return unkown error
574
+ return this.getErrorPayloadForClientErrorCode({
575
+ clientErrorCode: UNKNOWN_ERROR,
576
+ serviceErrorCode: UNKNOWN_ERROR,
577
+ rawErrorMessage,
578
+ });
579
+ }
580
+
581
+ /**
582
+ * Create client event object for in meeting events
583
+ * @param arg - create args
584
+ * @param arg.event - event key
585
+ * @param arg.options - options
586
+ * @returns object
587
+ */
588
+ private createClientEventObjectInMeeting({
589
+ name,
590
+ options,
591
+ errors,
592
+ }: {
593
+ name: ClientEvent['name'];
594
+ options?: SubmitClientEventOptions;
595
+ errors?: ClientEventPayloadError;
596
+ }) {
597
+ const {meetingId, mediaConnections, globalMeetingId, webexConferenceIdStr} = options;
598
+
599
+ // @ts-ignore
600
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
601
+
602
+ if (!meeting) {
603
+ console.warn(
604
+ 'Attempt to send client event but no meeting was found...',
605
+ `name: ${name}, meetingId: ${meetingId}`
606
+ );
607
+ // @ts-ignore
608
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
609
+ fields: {
610
+ meetingId,
611
+ name,
612
+ },
613
+ });
614
+
615
+ return undefined;
616
+ }
617
+
618
+ // grab identifiers
619
+ const identifiers = this.getIdentifiers({
620
+ meeting,
621
+ mediaConnections: meeting?.mediaConnections || mediaConnections,
622
+ webexConferenceIdStr,
623
+ globalMeetingId,
624
+ });
625
+
626
+ // create client event object
627
+ const clientEventObject: ClientEvent['payload'] = {
628
+ name,
629
+ canProceed: true,
630
+ identifiers,
631
+ errors,
632
+ eventData: {
633
+ webClientDomain: window.location.hostname,
634
+ },
635
+ userType: meeting.getCurUserType(),
636
+ loginType:
637
+ 'loginType' in meeting.callStateForMetrics
638
+ ? meeting.callStateForMetrics.loginType
639
+ : this.getCurLoginType(),
640
+ isConvergedArchitectureEnabled: this.getIsConvergedArchitectureEnabled({
641
+ meetingId,
642
+ }),
643
+ webexSubServiceType: this.getSubServiceType(meeting),
644
+ };
645
+
646
+ return clientEventObject;
647
+ }
648
+
649
+ /**
650
+ * Create client event object for pre meeting events
651
+ * @param arg - create args
652
+ * @param arg.event - event key
653
+ * @param arg.options - payload
654
+ * @returns object
655
+ */
656
+ private createClientEventObjectPreMeeting({
657
+ name,
658
+ options,
659
+ errors,
660
+ }: {
661
+ name: ClientEvent['name'];
662
+ options?: SubmitClientEventOptions;
663
+ errors?: ClientEventPayloadError;
664
+ }) {
665
+ const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId} = options;
666
+
667
+ // grab identifiers
668
+ const identifiers = this.getIdentifiers({
669
+ correlationId,
670
+ preLoginId,
671
+ globalMeetingId,
672
+ webexConferenceIdStr,
673
+ });
674
+
675
+ // create client event object
676
+ const clientEventObject: ClientEvent['payload'] = {
677
+ name,
678
+ errors,
679
+ canProceed: true,
680
+ identifiers,
681
+ eventData: {
682
+ webClientDomain: window.location.hostname,
683
+ },
684
+ loginType: this.getCurLoginType(),
685
+ };
686
+
687
+ return clientEventObject;
688
+ }
689
+
690
+ /**
691
+ * Prepare Client Event CA event.
692
+ * @param arg - submit params
693
+ * @param arg.event - event key
694
+ * @param arg.payload - additional payload to be merged with default payload
695
+ * @param arg.options - payload
696
+ * @returns {any} options to be with fetch
697
+ * @throws
698
+ */
699
+ private prepareClientEvent({
700
+ name,
701
+ payload,
702
+ options,
703
+ }: {
704
+ name: ClientEvent['name'];
705
+ payload?: ClientEventPayload;
706
+ options?: SubmitClientEventOptions;
707
+ }) {
708
+ const {meetingId, correlationId, rawError} = options;
709
+ let clientEventObject: ClientEvent['payload'];
710
+
711
+ // check if we need to generate errors
712
+ const errors: ClientEventPayloadError = [];
713
+
714
+ if (rawError) {
715
+ const generatedError = this.generateClientEventErrorPayload(rawError);
716
+ if (generatedError) {
717
+ errors.push(generatedError);
718
+ }
719
+ this.logger.log(
720
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
721
+ 'CallDiagnosticMetrics: @prepareClientEvent. Generated errors:',
722
+ `generatedError: ${JSON.stringify(generatedError)}`
723
+ );
724
+ }
725
+
726
+ // events that will most likely happen in join phase
727
+ if (meetingId) {
728
+ clientEventObject = this.createClientEventObjectInMeeting({name, options, errors});
729
+ } else if (correlationId) {
730
+ // any pre join events or events that are outside the meeting.
731
+ clientEventObject = this.createClientEventObjectPreMeeting({name, options, errors});
732
+ } else {
733
+ throw new Error('Not implemented');
734
+ }
735
+
736
+ // merge any new properties, or override existing ones
737
+ clientEventObject = merge(clientEventObject, payload);
738
+
739
+ // append client event data to the call diagnostic event
740
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
741
+
742
+ return diagnosticEvent;
743
+ }
744
+
745
+ /**
746
+ * Submit Client Event CA event.
747
+ * @param arg - submit params
748
+ * @param arg.event - event key
749
+ * @param arg.payload - additional payload to be merged with default payload
750
+ * @param arg.options - payload
751
+ * @throws
752
+ */
753
+ public submitClientEvent({
754
+ name,
755
+ payload,
756
+ options,
757
+ }: {
758
+ name: ClientEvent['name'];
759
+ payload?: ClientEventPayload;
760
+ options?: SubmitClientEventOptions;
761
+ }) {
762
+ this.logger.log(
763
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
764
+ 'CallDiagnosticMetrics: @submitClientEvent. Submit Client Event CA event.',
765
+ `name: ${name}`
766
+ );
767
+ const diagnosticEvent = this.prepareClientEvent({name, payload, options});
768
+
769
+ if (options?.preLoginId) {
770
+ return this.submitToCallDiagnosticsPreLogin(diagnosticEvent, options?.preLoginId);
771
+ }
772
+
773
+ this.validator({type: 'ce', event: diagnosticEvent});
774
+
775
+ return this.submitToCallDiagnostics(diagnosticEvent);
776
+ }
777
+
778
+ /**
779
+ * Prepare the event and send the request to metrics-a service.
780
+ * @param event
781
+ * @returns promise
782
+ */
783
+ submitToCallDiagnostics(event: Event): Promise<any> {
784
+ // build metrics-a event type
785
+ const finalEvent = {
786
+ eventPayload: event,
787
+ type: ['diagnostic-event'],
788
+ };
789
+
790
+ return this.callDiagnosticEventsBatcher.request(finalEvent);
791
+ }
792
+
793
+ /**
794
+ * Pre login events are not batched. We make the request directly.
795
+ * @param event
796
+ * @param preLoginId
797
+ * @returns
798
+ */
799
+ public submitToCallDiagnosticsPreLogin = (event: Event, preLoginId?: string): Promise<any> => {
800
+ // build metrics-a event type
801
+ // @ts-ignore
802
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
803
+ eventPayload: event,
804
+ type: ['diagnostic-event'],
805
+ });
806
+
807
+ // append sent timestamp
808
+ diagnosticEvent.eventPayload.originTime.sent = new Date().toISOString();
809
+
810
+ // @ts-ignore
811
+ return this.webex.internal.newMetrics.postPreLoginMetric(diagnosticEvent, preLoginId);
812
+ };
813
+
814
+ /**
815
+ * Builds a request options object to later be passed to fetch().
816
+ * @param arg - submit params
817
+ * @param arg.event - event key
818
+ * @param arg.payload - additional payload to be merged with default payload
819
+ * @param arg.options - client event options
820
+ * @returns {Promise<any>}
821
+ * @throws
822
+ */
823
+ public async buildClientEventFetchRequestOptions({
824
+ name,
825
+ payload,
826
+ options,
827
+ }: {
828
+ name: ClientEvent['name'];
829
+ payload?: ClientEventPayload;
830
+ options?: SubmitClientEventOptions;
831
+ }): Promise<any> {
832
+ this.logger.log(
833
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
834
+ 'CallDiagnosticMetrics: @buildClientEventFetchRequestOptions. Building request options object for fetch()...',
835
+ `name: ${name}`
836
+ );
837
+
838
+ const clientEvent = this.prepareClientEvent({name, payload, options});
839
+
840
+ // build metrics-a event type
841
+ // @ts-ignore
842
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
843
+ eventPayload: clientEvent,
844
+ type: ['diagnostic-event'],
845
+ });
846
+
847
+ const request = {
848
+ method: 'POST',
849
+ service: 'metrics',
850
+ resource: 'clientmetrics',
851
+ body: {
852
+ metrics: [diagnosticEvent],
853
+ },
854
+ headers: {},
855
+ // @ts-ignore
856
+ waitForServiceTimeout: this.webex.internal.metrics.config.waitForServiceTimeout,
857
+ };
858
+
859
+ if (options.preLoginId) {
860
+ request.headers = {
861
+ authorization: false,
862
+ 'x-prelogin-userid': options.preLoginId,
863
+ };
864
+ request.resource = 'clientmetrics-prelogin';
865
+ }
866
+
867
+ // @ts-ignore
868
+ return this.webex.prepareFetchOptions(request);
869
+ }
870
+
871
+ /**
872
+ * Returns true if the specified serviceErrorCode maps to an expected error.
873
+ * @param {number} serviceErrorCode the service error code
874
+ * @returns {boolean}
875
+ */
876
+ public isServiceErrorExpected(serviceErrorCode: number): boolean {
877
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
878
+ const clientErrorPayload = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
879
+
880
+ return clientErrorPayload?.category === 'expected';
881
+ }
882
+ }