@webex/internal-plugin-metrics 3.0.0-beta.38 → 3.0.0-beta.380

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