@webex/internal-plugin-metrics 3.0.0-beta.32 → 3.0.0-beta.320

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