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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/.eslintrc.js +6 -0
  2. package/babel.config.js +3 -0
  3. package/dist/batcher.js +41 -3
  4. package/dist/batcher.js.map +1 -1
  5. package/dist/call-diagnostic/call-diagnostic-metrics-batcher.js +64 -0
  6. package/dist/call-diagnostic/call-diagnostic-metrics-batcher.js.map +1 -0
  7. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +474 -0
  8. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -0
  9. package/dist/call-diagnostic/call-diagnostic-metrics.js +849 -0
  10. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -0
  11. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +349 -0
  12. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -0
  13. package/dist/call-diagnostic/config.js +609 -0
  14. package/dist/call-diagnostic/config.js.map +1 -0
  15. package/dist/client-metrics-batcher.js +3 -3
  16. package/dist/client-metrics-batcher.js.map +1 -1
  17. package/dist/config.js +6 -9
  18. package/dist/config.js.map +1 -1
  19. package/dist/index.js +35 -2
  20. package/dist/index.js.map +1 -1
  21. package/dist/metrics.js +24 -22
  22. package/dist/metrics.js.map +1 -1
  23. package/dist/metrics.types.js +7 -0
  24. package/dist/metrics.types.js.map +1 -0
  25. package/dist/new-metrics.js +302 -0
  26. package/dist/new-metrics.js.map +1 -0
  27. package/dist/prelogin-metrics-batcher.js +81 -0
  28. package/dist/prelogin-metrics-batcher.js.map +1 -0
  29. package/dist/types/batcher.d.ts +5 -0
  30. package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +204 -0
  31. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +427 -0
  32. package/dist/types/call-diagnostic/call-diagnostic-metrics.util.d.ts +103 -0
  33. package/dist/types/call-diagnostic/config.d.ts +178 -0
  34. package/dist/types/config.d.ts +18 -0
  35. package/dist/types/index.d.ts +15 -3
  36. package/dist/types/metrics.d.ts +1 -0
  37. package/dist/types/metrics.types.d.ts +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 +26 -0
  42. package/dist/utils.js.map +1 -0
  43. package/jest.config.js +3 -0
  44. package/package.json +34 -10
  45. package/process +1 -0
  46. package/src/batcher.js +38 -0
  47. package/src/call-diagnostic/call-diagnostic-metrics-batcher.ts +72 -0
  48. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +435 -0
  49. package/src/call-diagnostic/call-diagnostic-metrics.ts +912 -0
  50. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +392 -0
  51. package/src/call-diagnostic/config.ts +685 -0
  52. package/src/client-metrics-batcher.js +1 -0
  53. package/src/config.js +1 -0
  54. package/src/index.ts +54 -0
  55. package/src/metrics.js +17 -16
  56. package/src/metrics.types.ts +168 -0
  57. package/src/new-metrics.ts +278 -0
  58. package/src/prelogin-metrics-batcher.ts +95 -0
  59. package/src/utils.ts +17 -0
  60. package/test/unit/spec/batcher.js +2 -0
  61. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +458 -0
  62. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +520 -0
  63. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +2276 -0
  64. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +628 -0
  65. package/test/unit/spec/client-metrics-batcher.js +2 -0
  66. package/test/unit/spec/metrics.js +68 -95
  67. package/test/unit/spec/new-metrics.ts +233 -0
  68. package/test/unit/spec/prelogin-metrics-batcher.ts +250 -0
  69. package/test/unit/spec/utils.ts +22 -0
  70. package/tsconfig.json +6 -0
  71. package/dist/call-diagnostic-events-batcher.js +0 -60
  72. package/dist/call-diagnostic-events-batcher.js.map +0 -1
  73. package/dist/internal-plugin-metrics.d.ts +0 -21
  74. package/dist/tsdoc-metadata.json +0 -11
  75. package/src/call-diagnostic-events-batcher.js +0 -62
  76. package/src/index.js +0 -17
  77. package/test/unit/spec/call-diagnostic-events-batcher.js +0 -195
  78. package/dist/types/{call-diagnostic-events-batcher.d.ts → call-diagnostic/call-diagnostic-metrics-batcher.d.ts} +1 -1
@@ -0,0 +1,912 @@
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
+ * @param arg.httpStatusCode
475
+ * @returns
476
+ */
477
+ public getErrorPayloadForClientErrorCode({
478
+ clientErrorCode,
479
+ serviceErrorCode,
480
+ serviceErrorName,
481
+ rawErrorMessage,
482
+ payloadOverrides,
483
+ httpStatusCode,
484
+ }: {
485
+ clientErrorCode: number;
486
+ serviceErrorCode: any;
487
+ serviceErrorName?: any;
488
+ rawErrorMessage?: string;
489
+ payloadOverrides?: any;
490
+ httpStatusCode?: number;
491
+ }): ClientEventError {
492
+ let error: ClientEventError;
493
+
494
+ if (clientErrorCode) {
495
+ const partialParsedError = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
496
+
497
+ if (partialParsedError) {
498
+ error = merge(
499
+ {fatal: true, shownToUser: false, name: 'other', category: 'other'}, // default values
500
+ {errorCode: clientErrorCode},
501
+ serviceErrorName ? {errorData: {errorName: serviceErrorName}} : {},
502
+ {serviceErrorCode},
503
+ {rawErrorMessage},
504
+ httpStatusCode === undefined ? {} : {httpStatusCode},
505
+ partialParsedError,
506
+ payloadOverrides || {}
507
+ );
508
+
509
+ return error;
510
+ }
511
+ }
512
+
513
+ return undefined;
514
+ }
515
+
516
+ /**
517
+ * Generate error payload for Client Event
518
+ * @param rawError
519
+ */
520
+ generateClientEventErrorPayload(rawError: any) {
521
+ const rawErrorMessage = rawError.message;
522
+ const httpStatusCode = rawError.statusCode;
523
+ if (rawError.name) {
524
+ if (isBrowserMediaErrorName(rawError.name)) {
525
+ return this.getErrorPayloadForClientErrorCode({
526
+ serviceErrorCode: undefined,
527
+ clientErrorCode: BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[rawError.name],
528
+ serviceErrorName: rawError.name,
529
+ rawErrorMessage,
530
+ httpStatusCode,
531
+ });
532
+ }
533
+ }
534
+
535
+ if (isSdpOfferCreationError(rawError)) {
536
+ // error code is 30005, but that's not specific enough. we also need to check error.cause.type
537
+ const causeType = rawError.cause?.type;
538
+
539
+ return this.getErrorPayloadForClientErrorCode({
540
+ serviceErrorCode: undefined,
541
+ clientErrorCode:
542
+ SDP_OFFER_CREATION_ERROR_MAP[causeType] || SDP_OFFER_CREATION_ERROR_MAP.GENERAL,
543
+ serviceErrorName: rawError.name,
544
+ rawErrorMessage,
545
+ httpStatusCode,
546
+ });
547
+ }
548
+
549
+ const serviceErrorCode =
550
+ rawError?.error?.body?.errorCode ||
551
+ rawError?.body?.errorCode ||
552
+ rawError?.body?.code ||
553
+ rawError?.body?.reason?.reasonCode;
554
+
555
+ if (serviceErrorCode) {
556
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
557
+ if (clientErrorCode) {
558
+ return this.getErrorPayloadForClientErrorCode({
559
+ clientErrorCode,
560
+ serviceErrorCode,
561
+ rawErrorMessage,
562
+ httpStatusCode,
563
+ });
564
+ }
565
+
566
+ // by default, if it is locus error, return new locus err
567
+ if (isLocusServiceErrorCode(serviceErrorCode)) {
568
+ return this.getErrorPayloadForClientErrorCode({
569
+ clientErrorCode: NEW_LOCUS_ERROR_CLIENT_CODE,
570
+ serviceErrorCode,
571
+ rawErrorMessage,
572
+ httpStatusCode,
573
+ });
574
+ }
575
+ }
576
+
577
+ if (isMeetingInfoServiceError(rawError)) {
578
+ return this.getErrorPayloadForClientErrorCode({
579
+ clientErrorCode: MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
580
+ serviceErrorCode,
581
+ rawErrorMessage,
582
+ httpStatusCode,
583
+ });
584
+ }
585
+
586
+ if (isNetworkError(rawError)) {
587
+ return this.getErrorPayloadForClientErrorCode({
588
+ clientErrorCode: NETWORK_ERROR,
589
+ serviceErrorCode,
590
+ payloadOverrides: rawError.payloadOverrides,
591
+ rawErrorMessage,
592
+ httpStatusCode,
593
+ });
594
+ }
595
+
596
+ if (isUnauthorizedError(rawError)) {
597
+ return this.getErrorPayloadForClientErrorCode({
598
+ clientErrorCode: AUTHENTICATION_FAILED_CODE,
599
+ serviceErrorCode,
600
+ payloadOverrides: rawError.payloadOverrides,
601
+ rawErrorMessage,
602
+ httpStatusCode,
603
+ });
604
+ }
605
+
606
+ // otherwise return unkown error
607
+ return this.getErrorPayloadForClientErrorCode({
608
+ clientErrorCode: UNKNOWN_ERROR,
609
+ serviceErrorCode: UNKNOWN_ERROR,
610
+ rawErrorMessage,
611
+ httpStatusCode,
612
+ });
613
+ }
614
+
615
+ /**
616
+ * Create client event object for in meeting events
617
+ * @param arg - create args
618
+ * @param arg.event - event key
619
+ * @param arg.options - options
620
+ * @returns object
621
+ */
622
+ private createClientEventObjectInMeeting({
623
+ name,
624
+ options,
625
+ errors,
626
+ }: {
627
+ name: ClientEvent['name'];
628
+ options?: SubmitClientEventOptions;
629
+ errors?: ClientEventPayloadError;
630
+ }) {
631
+ const {meetingId, mediaConnections, globalMeetingId, webexConferenceIdStr} = options;
632
+
633
+ // @ts-ignore
634
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
635
+
636
+ if (!meeting) {
637
+ console.warn(
638
+ 'Attempt to send client event but no meeting was found...',
639
+ `name: ${name}, meetingId: ${meetingId}`
640
+ );
641
+ // @ts-ignore
642
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
643
+ fields: {
644
+ meetingId,
645
+ name,
646
+ },
647
+ });
648
+
649
+ return undefined;
650
+ }
651
+
652
+ // grab identifiers
653
+ const identifiers = this.getIdentifiers({
654
+ meeting,
655
+ mediaConnections: meeting?.mediaConnections || mediaConnections,
656
+ webexConferenceIdStr,
657
+ globalMeetingId,
658
+ });
659
+
660
+ // create client event object
661
+ const clientEventObject: ClientEvent['payload'] = {
662
+ name,
663
+ canProceed: true,
664
+ identifiers,
665
+ errors,
666
+ eventData: {
667
+ webClientDomain: window.location.hostname,
668
+ },
669
+ userType: meeting.getCurUserType(),
670
+ loginType:
671
+ 'loginType' in meeting.callStateForMetrics
672
+ ? meeting.callStateForMetrics.loginType
673
+ : this.getCurLoginType(),
674
+ isConvergedArchitectureEnabled: this.getIsConvergedArchitectureEnabled({
675
+ meetingId,
676
+ }),
677
+ webexSubServiceType: this.getSubServiceType(meeting),
678
+ };
679
+
680
+ return clientEventObject;
681
+ }
682
+
683
+ /**
684
+ * Create client event object for pre meeting events
685
+ * @param arg - create args
686
+ * @param arg.event - event key
687
+ * @param arg.options - payload
688
+ * @returns object
689
+ */
690
+ private createClientEventObjectPreMeeting({
691
+ name,
692
+ options,
693
+ errors,
694
+ }: {
695
+ name: ClientEvent['name'];
696
+ options?: SubmitClientEventOptions;
697
+ errors?: ClientEventPayloadError;
698
+ }) {
699
+ const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId} = options;
700
+
701
+ // grab identifiers
702
+ const identifiers = this.getIdentifiers({
703
+ correlationId,
704
+ preLoginId,
705
+ globalMeetingId,
706
+ webexConferenceIdStr,
707
+ });
708
+
709
+ // create client event object
710
+ const clientEventObject: ClientEvent['payload'] = {
711
+ name,
712
+ errors,
713
+ canProceed: true,
714
+ identifiers,
715
+ eventData: {
716
+ webClientDomain: window.location.hostname,
717
+ },
718
+ loginType: this.getCurLoginType(),
719
+ };
720
+
721
+ return clientEventObject;
722
+ }
723
+
724
+ /**
725
+ * Prepare Client Event CA event.
726
+ * @param arg - submit params
727
+ * @param arg.event - event key
728
+ * @param arg.payload - additional payload to be merged with default payload
729
+ * @param arg.options - payload
730
+ * @returns {any} options to be with fetch
731
+ * @throws
732
+ */
733
+ private prepareClientEvent({
734
+ name,
735
+ payload,
736
+ options,
737
+ }: {
738
+ name: ClientEvent['name'];
739
+ payload?: ClientEventPayload;
740
+ options?: SubmitClientEventOptions;
741
+ }) {
742
+ const {meetingId, correlationId, rawError} = options;
743
+ let clientEventObject: ClientEvent['payload'];
744
+
745
+ // check if we need to generate errors
746
+ const errors: ClientEventPayloadError = [];
747
+
748
+ if (rawError) {
749
+ const generatedError = this.generateClientEventErrorPayload(rawError);
750
+ if (generatedError) {
751
+ errors.push(generatedError);
752
+ }
753
+ this.logger.log(
754
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
755
+ 'CallDiagnosticMetrics: @prepareClientEvent. Generated errors:',
756
+ `generatedError: ${JSON.stringify(generatedError)}`
757
+ );
758
+ }
759
+
760
+ // events that will most likely happen in join phase
761
+ if (meetingId) {
762
+ clientEventObject = this.createClientEventObjectInMeeting({name, options, errors});
763
+ } else if (correlationId) {
764
+ // any pre join events or events that are outside the meeting.
765
+ clientEventObject = this.createClientEventObjectPreMeeting({name, options, errors});
766
+ } else {
767
+ throw new Error('Not implemented');
768
+ }
769
+
770
+ // merge any new properties, or override existing ones
771
+ clientEventObject = merge(clientEventObject, payload);
772
+
773
+ // append client event data to the call diagnostic event
774
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
775
+
776
+ return diagnosticEvent;
777
+ }
778
+
779
+ /**
780
+ * Submit Client Event CA event.
781
+ * @param arg - submit params
782
+ * @param arg.event - event key
783
+ * @param arg.payload - additional payload to be merged with default payload
784
+ * @param arg.options - payload
785
+ * @throws
786
+ */
787
+ public submitClientEvent({
788
+ name,
789
+ payload,
790
+ options,
791
+ }: {
792
+ name: ClientEvent['name'];
793
+ payload?: ClientEventPayload;
794
+ options?: SubmitClientEventOptions;
795
+ }) {
796
+ this.logger.log(
797
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
798
+ 'CallDiagnosticMetrics: @submitClientEvent. Submit Client Event CA event.',
799
+ `name: ${name}`
800
+ );
801
+ const diagnosticEvent = this.prepareClientEvent({name, payload, options});
802
+
803
+ if (options?.preLoginId) {
804
+ return this.submitToCallDiagnosticsPreLogin(diagnosticEvent, options?.preLoginId);
805
+ }
806
+
807
+ this.validator({type: 'ce', event: diagnosticEvent});
808
+
809
+ return this.submitToCallDiagnostics(diagnosticEvent);
810
+ }
811
+
812
+ /**
813
+ * Prepare the event and send the request to metrics-a service.
814
+ * @param event
815
+ * @returns promise
816
+ */
817
+ submitToCallDiagnostics(event: Event): Promise<any> {
818
+ // build metrics-a event type
819
+ const finalEvent = {
820
+ eventPayload: event,
821
+ type: ['diagnostic-event'],
822
+ };
823
+
824
+ return this.callDiagnosticEventsBatcher.request(finalEvent);
825
+ }
826
+
827
+ /**
828
+ * Prepare the event and send the request to metrics-a service, pre login.
829
+ * @param event
830
+ * @param preLoginId
831
+ * @returns
832
+ */
833
+ submitToCallDiagnosticsPreLogin = (event: Event, preLoginId?: string): Promise<any> => {
834
+ // build metrics-a event type
835
+ const finalEvent = {
836
+ eventPayload: event,
837
+ type: ['diagnostic-event'],
838
+ };
839
+ this.preLoginMetricsBatcher.savePreLoginId(preLoginId);
840
+
841
+ return this.preLoginMetricsBatcher.request(finalEvent);
842
+ };
843
+
844
+ /**
845
+ * Builds a request options object to later be passed to fetch().
846
+ * @param arg - submit params
847
+ * @param arg.event - event key
848
+ * @param arg.payload - additional payload to be merged with default payload
849
+ * @param arg.options - client event options
850
+ * @returns {Promise<any>}
851
+ * @throws
852
+ */
853
+ public async buildClientEventFetchRequestOptions({
854
+ name,
855
+ payload,
856
+ options,
857
+ }: {
858
+ name: ClientEvent['name'];
859
+ payload?: ClientEventPayload;
860
+ options?: SubmitClientEventOptions;
861
+ }): Promise<any> {
862
+ this.logger.log(
863
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
864
+ 'CallDiagnosticMetrics: @buildClientEventFetchRequestOptions. Building request options object for fetch()...',
865
+ `name: ${name}`
866
+ );
867
+
868
+ const clientEvent = this.prepareClientEvent({name, payload, options});
869
+
870
+ // build metrics-a event type
871
+ // @ts-ignore
872
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
873
+ eventPayload: clientEvent,
874
+ type: ['diagnostic-event'],
875
+ });
876
+
877
+ const request = {
878
+ method: 'POST',
879
+ service: 'metrics',
880
+ resource: 'clientmetrics',
881
+ body: {
882
+ metrics: [diagnosticEvent],
883
+ },
884
+ headers: {},
885
+ // @ts-ignore
886
+ waitForServiceTimeout: this.webex.internal.metrics.config.waitForServiceTimeout,
887
+ };
888
+
889
+ if (options.preLoginId) {
890
+ request.headers = {
891
+ authorization: false,
892
+ 'x-prelogin-userid': options.preLoginId,
893
+ };
894
+ request.resource = 'clientmetrics-prelogin';
895
+ }
896
+
897
+ // @ts-ignore
898
+ return this.webex.prepareFetchOptions(request);
899
+ }
900
+
901
+ /**
902
+ * Returns true if the specified serviceErrorCode maps to an expected error.
903
+ * @param {number} serviceErrorCode the service error code
904
+ * @returns {boolean}
905
+ */
906
+ public isServiceErrorExpected(serviceErrorCode: number): boolean {
907
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
908
+ const clientErrorPayload = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
909
+
910
+ return clientErrorPayload?.category === 'expected';
911
+ }
912
+ }