@webex/internal-plugin-metrics 3.0.0-bnr.5 → 3.0.0-next.10

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