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

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 +824 -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 +75 -0
  43. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +414 -0
  44. package/src/call-diagnostic/call-diagnostic-metrics.ts +863 -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 +477 -0
  57. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +1884 -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,863 @@
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
+ const {meetingId, mediaConnections, globalMeetingId, webexConferenceIdStr} = options;
572
+
573
+ // @ts-ignore
574
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
575
+
576
+ if (!meeting) {
577
+ console.warn(
578
+ 'Attempt to send client event but no meeting was found...',
579
+ `name: ${name}, meetingId: ${meetingId}`
580
+ );
581
+ // @ts-ignore
582
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
583
+ fields: {
584
+ meetingId,
585
+ name,
586
+ },
587
+ });
588
+
589
+ return undefined;
590
+ }
591
+
592
+ // grab identifiers
593
+ const identifiers = this.getIdentifiers({
594
+ meeting,
595
+ mediaConnections: meeting?.mediaConnections || mediaConnections,
596
+ webexConferenceIdStr,
597
+ globalMeetingId,
598
+ });
599
+
600
+ // create client event object
601
+ const clientEventObject: ClientEvent['payload'] = {
602
+ name,
603
+ canProceed: true,
604
+ identifiers,
605
+ errors,
606
+ eventData: {
607
+ webClientDomain: window.location.hostname,
608
+ },
609
+ userType: meeting.getCurUserType(),
610
+ loginType: this.getCurLoginType(),
611
+ isConvergedArchitectureEnabled: this.getIsConvergedArchitectureEnabled({
612
+ meetingId,
613
+ }),
614
+ webexSubServiceType: this.getSubServiceType(meeting),
615
+ };
616
+
617
+ if (options?.rawError?.message) {
618
+ // @ts-ignore
619
+ clientEventObject.eventData.rawErrorMessage = options?.rawError?.message;
620
+ }
621
+
622
+ return clientEventObject;
623
+ }
624
+
625
+ /**
626
+ * Create client event object for pre meeting events
627
+ * @param arg - create args
628
+ * @param arg.event - event key
629
+ * @param arg.options - payload
630
+ * @returns object
631
+ */
632
+ private createClientEventObjectPreMeeting({
633
+ name,
634
+ options,
635
+ errors,
636
+ }: {
637
+ name: ClientEvent['name'];
638
+ options?: SubmitClientEventOptions;
639
+ errors?: ClientEventPayloadError;
640
+ }) {
641
+ const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId} = options;
642
+
643
+ // grab identifiers
644
+ const identifiers = this.getIdentifiers({
645
+ correlationId,
646
+ preLoginId,
647
+ globalMeetingId,
648
+ webexConferenceIdStr,
649
+ });
650
+
651
+ // create client event object
652
+ const clientEventObject: ClientEvent['payload'] = {
653
+ name,
654
+ errors,
655
+ canProceed: true,
656
+ identifiers,
657
+ eventData: {
658
+ webClientDomain: window.location.hostname,
659
+ },
660
+ loginType: this.getCurLoginType(),
661
+ };
662
+
663
+ if (options?.rawError?.message) {
664
+ // @ts-ignore
665
+ clientEventObject.eventData.rawErrorMessage = options?.rawError?.message;
666
+ }
667
+
668
+ return clientEventObject;
669
+ }
670
+
671
+ /**
672
+ * Prepare Client Event CA event.
673
+ * @param arg - submit params
674
+ * @param arg.event - event key
675
+ * @param arg.payload - additional payload to be merged with default payload
676
+ * @param arg.options - payload
677
+ * @returns {any} options to be with fetch
678
+ * @throws
679
+ */
680
+ private prepareClientEvent({
681
+ name,
682
+ payload,
683
+ options,
684
+ }: {
685
+ name: ClientEvent['name'];
686
+ payload?: ClientEventPayload;
687
+ options?: SubmitClientEventOptions;
688
+ }) {
689
+ const {meetingId, correlationId, rawError} = options;
690
+ let clientEventObject: ClientEvent['payload'];
691
+
692
+ // check if we need to generate errors
693
+ const errors: ClientEventPayloadError = [];
694
+
695
+ if (rawError) {
696
+ const generatedError = this.generateClientEventErrorPayload(rawError);
697
+ if (generatedError) {
698
+ errors.push(generatedError);
699
+ }
700
+ this.logger.log(
701
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
702
+ 'CallDiagnosticMetrics: @prepareClientEvent. Generated errors:',
703
+ `generatedError: ${JSON.stringify(generatedError)}`
704
+ );
705
+ }
706
+
707
+ // events that will most likely happen in join phase
708
+ if (meetingId) {
709
+ clientEventObject = this.createClientEventObjectInMeeting({name, options, errors});
710
+ } else if (correlationId) {
711
+ // any pre join events or events that are outside the meeting.
712
+ clientEventObject = this.createClientEventObjectPreMeeting({name, options, errors});
713
+ } else {
714
+ throw new Error('Not implemented');
715
+ }
716
+
717
+ // merge any new properties, or override existing ones
718
+ clientEventObject = merge(clientEventObject, payload);
719
+
720
+ // append client event data to the call diagnostic event
721
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
722
+
723
+ return diagnosticEvent;
724
+ }
725
+
726
+ /**
727
+ * Submit Client Event CA event.
728
+ * @param arg - submit params
729
+ * @param arg.event - event key
730
+ * @param arg.payload - additional payload to be merged with default payload
731
+ * @param arg.options - payload
732
+ * @throws
733
+ */
734
+ public submitClientEvent({
735
+ name,
736
+ payload,
737
+ options,
738
+ }: {
739
+ name: ClientEvent['name'];
740
+ payload?: ClientEventPayload;
741
+ options?: SubmitClientEventOptions;
742
+ }) {
743
+ this.logger.log(
744
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
745
+ 'CallDiagnosticMetrics: @submitClientEvent. Submit Client Event CA event.',
746
+ `name: ${name}`
747
+ );
748
+ const diagnosticEvent = this.prepareClientEvent({name, payload, options});
749
+
750
+ if (options?.preLoginId) {
751
+ return this.submitToCallDiagnosticsPreLogin(diagnosticEvent, options?.preLoginId);
752
+ }
753
+
754
+ this.validator({type: 'ce', event: diagnosticEvent});
755
+
756
+ return this.submitToCallDiagnostics(diagnosticEvent);
757
+ }
758
+
759
+ /**
760
+ * Prepare the event and send the request to metrics-a service.
761
+ * @param event
762
+ * @returns promise
763
+ */
764
+ submitToCallDiagnostics(event: Event): Promise<any> {
765
+ // build metrics-a event type
766
+ const finalEvent = {
767
+ eventPayload: event,
768
+ type: ['diagnostic-event'],
769
+ };
770
+
771
+ return this.callDiagnosticEventsBatcher.request(finalEvent);
772
+ }
773
+
774
+ /**
775
+ * Pre login events are not batched. We make the request directly.
776
+ * @param event
777
+ * @param preLoginId
778
+ * @returns
779
+ */
780
+ public submitToCallDiagnosticsPreLogin = (event: Event, preLoginId?: string): Promise<any> => {
781
+ // build metrics-a event type
782
+ // @ts-ignore
783
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
784
+ eventPayload: event,
785
+ type: ['diagnostic-event'],
786
+ });
787
+
788
+ // append sent timestamp
789
+ diagnosticEvent.eventPayload.originTime.sent = new Date().toISOString();
790
+
791
+ // @ts-ignore
792
+ return this.webex.internal.newMetrics.postPreLoginMetric(diagnosticEvent, preLoginId);
793
+ };
794
+
795
+ /**
796
+ * Builds a request options object to later be passed to fetch().
797
+ * @param arg - submit params
798
+ * @param arg.event - event key
799
+ * @param arg.payload - additional payload to be merged with default payload
800
+ * @param arg.options - client event options
801
+ * @returns {Promise<any>}
802
+ * @throws
803
+ */
804
+ public async buildClientEventFetchRequestOptions({
805
+ name,
806
+ payload,
807
+ options,
808
+ }: {
809
+ name: ClientEvent['name'];
810
+ payload?: ClientEventPayload;
811
+ options?: SubmitClientEventOptions;
812
+ }): Promise<any> {
813
+ this.logger.log(
814
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
815
+ 'CallDiagnosticMetrics: @buildClientEventFetchRequestOptions. Building request options object for fetch()...',
816
+ `name: ${name}`
817
+ );
818
+
819
+ const clientEvent = this.prepareClientEvent({name, payload, options});
820
+
821
+ // build metrics-a event type
822
+ // @ts-ignore
823
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
824
+ eventPayload: clientEvent,
825
+ type: ['diagnostic-event'],
826
+ });
827
+
828
+ const request = {
829
+ method: 'POST',
830
+ service: 'metrics',
831
+ resource: 'clientmetrics',
832
+ body: {
833
+ metrics: [diagnosticEvent],
834
+ },
835
+ headers: {},
836
+ // @ts-ignore
837
+ waitForServiceTimeout: this.webex.internal.metrics.config.waitForServiceTimeout,
838
+ };
839
+
840
+ if (options.preLoginId) {
841
+ request.headers = {
842
+ authorization: false,
843
+ 'x-prelogin-userid': options.preLoginId,
844
+ };
845
+ request.resource = 'clientmetrics-prelogin';
846
+ }
847
+
848
+ // @ts-ignore
849
+ return this.webex.prepareFetchOptions(request);
850
+ }
851
+
852
+ /**
853
+ * Returns true if the specified serviceErrorCode maps to an expected error.
854
+ * @param {number} serviceErrorCode the service error code
855
+ * @returns {boolean}
856
+ */
857
+ public isServiceErrorExpected(serviceErrorCode: number): boolean {
858
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
859
+ const clientErrorPayload = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
860
+
861
+ return clientErrorPayload?.category === 'expected';
862
+ }
863
+ }