@webex/internal-plugin-metrics 3.0.0-beta.37 → 3.0.0-beta.370

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