@webex/internal-plugin-metrics 3.0.0-beta.31 → 3.0.0-beta.310

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 +66 -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 +830 -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 +411 -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 +83 -0
  43. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +414 -0
  44. package/src/call-diagnostic/call-diagnostic-metrics.ts +896 -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 +465 -0
  56. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +477 -0
  57. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +2012 -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 +269 -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,896 @@
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
+ payloadOverrides,
455
+ }: {
456
+ clientErrorCode: number;
457
+ serviceErrorCode: any;
458
+ serviceErrorName?: any;
459
+ payloadOverrides?: any;
460
+ }): ClientEventError {
461
+ let error: ClientEventError;
462
+
463
+ if (clientErrorCode) {
464
+ const partialParsedError = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
465
+
466
+ if (partialParsedError) {
467
+ error = merge(
468
+ {fatal: true, shownToUser: false, name: 'other', category: 'other'}, // default values
469
+ {errorCode: clientErrorCode},
470
+ serviceErrorName ? {errorData: {errorName: serviceErrorName}} : {},
471
+ {serviceErrorCode},
472
+ partialParsedError,
473
+ payloadOverrides || {}
474
+ );
475
+
476
+ return error;
477
+ }
478
+ }
479
+
480
+ return undefined;
481
+ }
482
+
483
+ /**
484
+ * Generate error payload for Client Event
485
+ * @param rawError
486
+ */
487
+ generateClientEventErrorPayload(rawError: any) {
488
+ if (rawError.name) {
489
+ if (isBrowserMediaErrorName(rawError.name)) {
490
+ return this.getErrorPayloadForClientErrorCode({
491
+ serviceErrorCode: undefined,
492
+ clientErrorCode: BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[rawError.name],
493
+ serviceErrorName: rawError.name,
494
+ });
495
+ }
496
+ }
497
+
498
+ const serviceErrorCode =
499
+ rawError?.error?.body?.errorCode ||
500
+ rawError?.body?.errorCode ||
501
+ rawError?.body?.code ||
502
+ rawError?.body?.reason?.reasonCode;
503
+
504
+ if (serviceErrorCode) {
505
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
506
+ if (clientErrorCode) {
507
+ return this.getErrorPayloadForClientErrorCode({clientErrorCode, serviceErrorCode});
508
+ }
509
+
510
+ // by default, if it is locus error, return new locus err
511
+ if (isLocusServiceErrorCode(serviceErrorCode)) {
512
+ return this.getErrorPayloadForClientErrorCode({
513
+ clientErrorCode: NEW_LOCUS_ERROR_CLIENT_CODE,
514
+ serviceErrorCode,
515
+ });
516
+ }
517
+ }
518
+
519
+ if (isMeetingInfoServiceError(rawError)) {
520
+ return this.getErrorPayloadForClientErrorCode({
521
+ clientErrorCode: MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
522
+ serviceErrorCode,
523
+ });
524
+ }
525
+
526
+ if (isNetworkError(rawError)) {
527
+ const payload = this.getErrorPayloadForClientErrorCode({
528
+ clientErrorCode: NETWORK_ERROR,
529
+ serviceErrorCode,
530
+ payloadOverrides: rawError.payloadOverrides,
531
+ });
532
+ payload.errorDescription = rawError.message;
533
+
534
+ return payload;
535
+ }
536
+
537
+ if (isUnauthorizedError(rawError)) {
538
+ const payload = this.getErrorPayloadForClientErrorCode({
539
+ clientErrorCode: AUTHENTICATION_FAILED_CODE,
540
+ serviceErrorCode,
541
+ payloadOverrides: rawError.payloadOverrides,
542
+ });
543
+ payload.errorDescription = rawError.message;
544
+
545
+ return payload;
546
+ }
547
+
548
+ // otherwise return unkown error
549
+ return this.getErrorPayloadForClientErrorCode({
550
+ clientErrorCode: UNKNOWN_ERROR,
551
+ serviceErrorCode: UNKNOWN_ERROR,
552
+ });
553
+ }
554
+
555
+ /**
556
+ * Create client event object for in meeting events
557
+ * @param arg - create args
558
+ * @param arg.event - event key
559
+ * @param arg.options - options
560
+ * @returns object
561
+ */
562
+ private createClientEventObjectInMeeting({
563
+ name,
564
+ options,
565
+ errors,
566
+ }: {
567
+ name: ClientEvent['name'];
568
+ options?: SubmitClientEventOptions;
569
+ errors?: ClientEventPayloadError;
570
+ }) {
571
+ this.logger.log(
572
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
573
+ 'CallDiagnosticMetrics: @createClientEventObjectInMeeting. Creating in meeting event object.',
574
+ `name: ${name}`
575
+ );
576
+ const {meetingId, mediaConnections, globalMeetingId, webexConferenceIdStr} = options;
577
+
578
+ // @ts-ignore
579
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
580
+
581
+ if (!meeting) {
582
+ console.warn(
583
+ 'Attempt to send client event but no meeting was found...',
584
+ `name: ${name}, meetingId: ${meetingId}`
585
+ );
586
+ // @ts-ignore
587
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
588
+ fields: {
589
+ meetingId,
590
+ name,
591
+ },
592
+ });
593
+
594
+ return undefined;
595
+ }
596
+
597
+ // grab identifiers
598
+ const identifiers = this.getIdentifiers({
599
+ meeting,
600
+ mediaConnections: meeting?.mediaConnections || mediaConnections,
601
+ webexConferenceIdStr,
602
+ globalMeetingId,
603
+ });
604
+
605
+ // create client event object
606
+ const clientEventObject: ClientEvent['payload'] = {
607
+ name,
608
+ canProceed: true,
609
+ identifiers,
610
+ errors,
611
+ eventData: {
612
+ webClientDomain: window.location.hostname,
613
+ },
614
+ userType: meeting.getCurUserType(),
615
+ loginType: this.getCurLoginType(),
616
+ isConvergedArchitectureEnabled: this.getIsConvergedArchitectureEnabled({
617
+ meetingId,
618
+ }),
619
+ webexSubServiceType: this.getSubServiceType(meeting),
620
+ };
621
+
622
+ if (options?.rawError?.message) {
623
+ // @ts-ignore
624
+ clientEventObject.eventData.rawErrorMessage = options?.rawError?.message;
625
+ }
626
+
627
+ return clientEventObject;
628
+ }
629
+
630
+ /**
631
+ * Create client event object for pre meeting events
632
+ * @param arg - create args
633
+ * @param arg.event - event key
634
+ * @param arg.options - payload
635
+ * @returns object
636
+ */
637
+ private createClientEventObjectPreMeeting({
638
+ name,
639
+ options,
640
+ errors,
641
+ }: {
642
+ name: ClientEvent['name'];
643
+ options?: SubmitClientEventOptions;
644
+ errors?: ClientEventPayloadError;
645
+ }) {
646
+ this.logger.log(
647
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
648
+ 'CallDiagnosticMetrics: @createClientEventObjectPreMeeting. Creating pre meeting event object.',
649
+ `name: ${name}`
650
+ );
651
+ const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId} = options;
652
+
653
+ // grab identifiers
654
+ const identifiers = this.getIdentifiers({
655
+ correlationId,
656
+ preLoginId,
657
+ globalMeetingId,
658
+ webexConferenceIdStr,
659
+ });
660
+
661
+ // create client event object
662
+ const clientEventObject: ClientEvent['payload'] = {
663
+ name,
664
+ errors,
665
+ canProceed: true,
666
+ identifiers,
667
+ eventData: {
668
+ webClientDomain: window.location.hostname,
669
+ },
670
+ loginType: this.getCurLoginType(),
671
+ };
672
+
673
+ if (options?.rawError?.message) {
674
+ // @ts-ignore
675
+ clientEventObject.eventData.rawErrorMessage = options?.rawError?.message;
676
+ }
677
+
678
+ return clientEventObject;
679
+ }
680
+
681
+ /**
682
+ * Prepare Client Event CA event.
683
+ * @param arg - submit params
684
+ * @param arg.event - event key
685
+ * @param arg.payload - additional payload to be merged with default payload
686
+ * @param arg.options - payload
687
+ * @returns {any} options to be with fetch
688
+ * @throws
689
+ */
690
+ private prepareClientEvent({
691
+ name,
692
+ payload,
693
+ options,
694
+ }: {
695
+ name: ClientEvent['name'];
696
+ payload?: ClientEventPayload;
697
+ options?: SubmitClientEventOptions;
698
+ }) {
699
+ const {meetingId, correlationId, rawError} = options;
700
+ let clientEventObject: ClientEvent['payload'];
701
+
702
+ // check if we need to generate errors
703
+ const errors: ClientEventPayloadError = [];
704
+
705
+ if (rawError) {
706
+ this.logger.log(
707
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
708
+ 'CallDiagnosticMetrics: @prepareClientEvent. Error detected, attempting to map and attach it to the event...',
709
+ `name: ${name}`,
710
+ `rawError: ${generateCommonErrorMetadata(rawError)}`
711
+ );
712
+
713
+ const generatedError = this.generateClientEventErrorPayload(rawError);
714
+ if (generatedError) {
715
+ errors.push(generatedError);
716
+ }
717
+ this.logger.log(
718
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
719
+ 'CallDiagnosticMetrics: @prepareClientEvent. Generated errors:',
720
+ `generatedError: ${JSON.stringify(generatedError)}`
721
+ );
722
+ }
723
+
724
+ // events that will most likely happen in join phase
725
+ if (meetingId) {
726
+ clientEventObject = this.createClientEventObjectInMeeting({name, options, errors});
727
+ } else if (correlationId) {
728
+ // any pre join events or events that are outside the meeting.
729
+ clientEventObject = this.createClientEventObjectPreMeeting({name, options, errors});
730
+ } else {
731
+ throw new Error('Not implemented');
732
+ }
733
+
734
+ // merge any new properties, or override existing ones
735
+ clientEventObject = merge(clientEventObject, payload);
736
+
737
+ // append client event data to the call diagnostic event
738
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
739
+
740
+ return diagnosticEvent;
741
+ }
742
+
743
+ /**
744
+ * Submit Client Event CA event.
745
+ * @param arg - submit params
746
+ * @param arg.event - event key
747
+ * @param arg.payload - additional payload to be merged with default payload
748
+ * @param arg.options - payload
749
+ * @throws
750
+ */
751
+ public submitClientEvent({
752
+ name,
753
+ payload,
754
+ options,
755
+ }: {
756
+ name: ClientEvent['name'];
757
+ payload?: ClientEventPayload;
758
+ options?: SubmitClientEventOptions;
759
+ }) {
760
+ this.logger.log(
761
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
762
+ 'CallDiagnosticMetrics: @submitClientEvent. Submit Client Event CA event.',
763
+ `name: ${name}`,
764
+ `payload: ${JSON.stringify(payload)}`,
765
+ `options: ${JSON.stringify(options)}`
766
+ );
767
+ const diagnosticEvent = this.prepareClientEvent({name, payload, options});
768
+
769
+ if (options?.preLoginId) {
770
+ return this.submitToCallDiagnosticsPreLogin(diagnosticEvent, options?.preLoginId);
771
+ }
772
+
773
+ this.validator({type: 'ce', event: diagnosticEvent});
774
+
775
+ return this.submitToCallDiagnostics(diagnosticEvent);
776
+ }
777
+
778
+ /**
779
+ * Prepare the event and send the request to metrics-a service.
780
+ * @param event
781
+ * @returns promise
782
+ */
783
+ submitToCallDiagnostics(event: Event): Promise<any> {
784
+ // build metrics-a event type
785
+ const finalEvent = {
786
+ eventPayload: event,
787
+ type: ['diagnostic-event'],
788
+ };
789
+
790
+ this.logger.log(
791
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
792
+ 'CallDiagnosticMetrics: @submitToCallDiagnostics. Preparing to send the request',
793
+ `finalEvent: ${JSON.stringify(finalEvent)}`
794
+ );
795
+
796
+ return this.callDiagnosticEventsBatcher.request(finalEvent);
797
+ }
798
+
799
+ /**
800
+ * Pre login events are not batched. We make the request directly.
801
+ * @param event
802
+ * @param preLoginId
803
+ * @returns
804
+ */
805
+ public submitToCallDiagnosticsPreLogin = (event: Event, preLoginId?: string): Promise<any> => {
806
+ // build metrics-a event type
807
+ // @ts-ignore
808
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
809
+ eventPayload: event,
810
+ type: ['diagnostic-event'],
811
+ });
812
+
813
+ // append sent timestamp
814
+ diagnosticEvent.eventPayload.originTime.sent = new Date().toISOString();
815
+
816
+ this.logger.log(
817
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
818
+ `CallDiagnosticMetrics: @submitToCallDiagnosticsPreLogin. Sending the request:`,
819
+ `diagnosticEvent: ${JSON.stringify(diagnosticEvent)}`
820
+ );
821
+
822
+ // @ts-ignore
823
+ return this.webex.internal.newMetrics.postPreLoginMetric(diagnosticEvent, preLoginId);
824
+ };
825
+
826
+ /**
827
+ * Builds a request options object to later be passed to fetch().
828
+ * @param arg - submit params
829
+ * @param arg.event - event key
830
+ * @param arg.payload - additional payload to be merged with default payload
831
+ * @param arg.options - client event options
832
+ * @returns {Promise<any>}
833
+ * @throws
834
+ */
835
+ public async buildClientEventFetchRequestOptions({
836
+ name,
837
+ payload,
838
+ options,
839
+ }: {
840
+ name: ClientEvent['name'];
841
+ payload?: ClientEventPayload;
842
+ options?: SubmitClientEventOptions;
843
+ }): Promise<any> {
844
+ this.logger.log(
845
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
846
+ 'CallDiagnosticMetrics: @buildClientEventFetchRequestOptions. Building request options object for fetch()...',
847
+ `name: ${name}`,
848
+ `payload: ${JSON.stringify(payload)}`,
849
+ `options: ${JSON.stringify(options)}`
850
+ );
851
+
852
+ const clientEvent = this.prepareClientEvent({name, payload, options});
853
+
854
+ // build metrics-a event type
855
+ // @ts-ignore
856
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
857
+ eventPayload: clientEvent,
858
+ type: ['diagnostic-event'],
859
+ });
860
+
861
+ const request = {
862
+ method: 'POST',
863
+ service: 'metrics',
864
+ resource: 'clientmetrics',
865
+ body: {
866
+ metrics: [diagnosticEvent],
867
+ },
868
+ headers: {},
869
+ // @ts-ignore
870
+ waitForServiceTimeout: this.webex.internal.metrics.config.waitForServiceTimeout,
871
+ };
872
+
873
+ if (options.preLoginId) {
874
+ request.headers = {
875
+ authorization: false,
876
+ 'x-prelogin-userid': options.preLoginId,
877
+ };
878
+ request.resource = 'clientmetrics-prelogin';
879
+ }
880
+
881
+ // @ts-ignore
882
+ return this.webex.prepareFetchOptions(request);
883
+ }
884
+
885
+ /**
886
+ * Returns true if the specified serviceErrorCode maps to an expected error.
887
+ * @param {number} serviceErrorCode the service error code
888
+ * @returns {boolean}
889
+ */
890
+ public isServiceErrorExpected(serviceErrorCode: number): boolean {
891
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
892
+ const clientErrorPayload = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
893
+
894
+ return clientErrorPayload?.category === 'expected';
895
+ }
896
+ }