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

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 +850 -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 +28 -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 +913 -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 +20 -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 +2297 -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 +76 -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,913 @@
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
+ payloadOverrides: rawError.payloadOverrides,
611
+ rawErrorMessage,
612
+ httpStatusCode,
613
+ });
614
+ }
615
+
616
+ /**
617
+ * Create client event object for in meeting events
618
+ * @param arg - create args
619
+ * @param arg.event - event key
620
+ * @param arg.options - options
621
+ * @returns object
622
+ */
623
+ private createClientEventObjectInMeeting({
624
+ name,
625
+ options,
626
+ errors,
627
+ }: {
628
+ name: ClientEvent['name'];
629
+ options?: SubmitClientEventOptions;
630
+ errors?: ClientEventPayloadError;
631
+ }) {
632
+ const {meetingId, mediaConnections, globalMeetingId, webexConferenceIdStr} = options;
633
+
634
+ // @ts-ignore
635
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
636
+
637
+ if (!meeting) {
638
+ console.warn(
639
+ 'Attempt to send client event but no meeting was found...',
640
+ `name: ${name}, meetingId: ${meetingId}`
641
+ );
642
+ // @ts-ignore
643
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
644
+ fields: {
645
+ meetingId,
646
+ name,
647
+ },
648
+ });
649
+
650
+ return undefined;
651
+ }
652
+
653
+ // grab identifiers
654
+ const identifiers = this.getIdentifiers({
655
+ meeting,
656
+ mediaConnections: meeting?.mediaConnections || mediaConnections,
657
+ webexConferenceIdStr,
658
+ globalMeetingId,
659
+ });
660
+
661
+ // create client event object
662
+ const clientEventObject: ClientEvent['payload'] = {
663
+ name,
664
+ canProceed: true,
665
+ identifiers,
666
+ errors,
667
+ eventData: {
668
+ webClientDomain: window.location.hostname,
669
+ },
670
+ userType: meeting.getCurUserType(),
671
+ loginType:
672
+ 'loginType' in meeting.callStateForMetrics
673
+ ? meeting.callStateForMetrics.loginType
674
+ : this.getCurLoginType(),
675
+ isConvergedArchitectureEnabled: this.getIsConvergedArchitectureEnabled({
676
+ meetingId,
677
+ }),
678
+ webexSubServiceType: this.getSubServiceType(meeting),
679
+ };
680
+
681
+ return clientEventObject;
682
+ }
683
+
684
+ /**
685
+ * Create client event object for pre meeting events
686
+ * @param arg - create args
687
+ * @param arg.event - event key
688
+ * @param arg.options - payload
689
+ * @returns object
690
+ */
691
+ private createClientEventObjectPreMeeting({
692
+ name,
693
+ options,
694
+ errors,
695
+ }: {
696
+ name: ClientEvent['name'];
697
+ options?: SubmitClientEventOptions;
698
+ errors?: ClientEventPayloadError;
699
+ }) {
700
+ const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId} = options;
701
+
702
+ // grab identifiers
703
+ const identifiers = this.getIdentifiers({
704
+ correlationId,
705
+ preLoginId,
706
+ globalMeetingId,
707
+ webexConferenceIdStr,
708
+ });
709
+
710
+ // create client event object
711
+ const clientEventObject: ClientEvent['payload'] = {
712
+ name,
713
+ errors,
714
+ canProceed: true,
715
+ identifiers,
716
+ eventData: {
717
+ webClientDomain: window.location.hostname,
718
+ },
719
+ loginType: this.getCurLoginType(),
720
+ };
721
+
722
+ return clientEventObject;
723
+ }
724
+
725
+ /**
726
+ * Prepare Client Event CA event.
727
+ * @param arg - submit params
728
+ * @param arg.event - event key
729
+ * @param arg.payload - additional payload to be merged with default payload
730
+ * @param arg.options - payload
731
+ * @returns {any} options to be with fetch
732
+ * @throws
733
+ */
734
+ private prepareClientEvent({
735
+ name,
736
+ payload,
737
+ options,
738
+ }: {
739
+ name: ClientEvent['name'];
740
+ payload?: ClientEventPayload;
741
+ options?: SubmitClientEventOptions;
742
+ }) {
743
+ const {meetingId, correlationId, rawError} = options;
744
+ let clientEventObject: ClientEvent['payload'];
745
+
746
+ // check if we need to generate errors
747
+ const errors: ClientEventPayloadError = [];
748
+
749
+ if (rawError) {
750
+ const generatedError = this.generateClientEventErrorPayload(rawError);
751
+ if (generatedError) {
752
+ errors.push(generatedError);
753
+ }
754
+ this.logger.log(
755
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
756
+ 'CallDiagnosticMetrics: @prepareClientEvent. Generated errors:',
757
+ `generatedError: ${JSON.stringify(generatedError)}`
758
+ );
759
+ }
760
+
761
+ // events that will most likely happen in join phase
762
+ if (meetingId) {
763
+ clientEventObject = this.createClientEventObjectInMeeting({name, options, errors});
764
+ } else if (correlationId) {
765
+ // any pre join events or events that are outside the meeting.
766
+ clientEventObject = this.createClientEventObjectPreMeeting({name, options, errors});
767
+ } else {
768
+ throw new Error('Not implemented');
769
+ }
770
+
771
+ // merge any new properties, or override existing ones
772
+ clientEventObject = merge(clientEventObject, payload);
773
+
774
+ // append client event data to the call diagnostic event
775
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
776
+
777
+ return diagnosticEvent;
778
+ }
779
+
780
+ /**
781
+ * Submit Client Event CA event.
782
+ * @param arg - submit params
783
+ * @param arg.event - event key
784
+ * @param arg.payload - additional payload to be merged with default payload
785
+ * @param arg.options - payload
786
+ * @throws
787
+ */
788
+ public submitClientEvent({
789
+ name,
790
+ payload,
791
+ options,
792
+ }: {
793
+ name: ClientEvent['name'];
794
+ payload?: ClientEventPayload;
795
+ options?: SubmitClientEventOptions;
796
+ }) {
797
+ this.logger.log(
798
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
799
+ 'CallDiagnosticMetrics: @submitClientEvent. Submit Client Event CA event.',
800
+ `name: ${name}`
801
+ );
802
+ const diagnosticEvent = this.prepareClientEvent({name, payload, options});
803
+
804
+ if (options?.preLoginId) {
805
+ return this.submitToCallDiagnosticsPreLogin(diagnosticEvent, options?.preLoginId);
806
+ }
807
+
808
+ this.validator({type: 'ce', event: diagnosticEvent});
809
+
810
+ return this.submitToCallDiagnostics(diagnosticEvent);
811
+ }
812
+
813
+ /**
814
+ * Prepare the event and send the request to metrics-a service.
815
+ * @param event
816
+ * @returns promise
817
+ */
818
+ submitToCallDiagnostics(event: Event): Promise<any> {
819
+ // build metrics-a event type
820
+ const finalEvent = {
821
+ eventPayload: event,
822
+ type: ['diagnostic-event'],
823
+ };
824
+
825
+ return this.callDiagnosticEventsBatcher.request(finalEvent);
826
+ }
827
+
828
+ /**
829
+ * Prepare the event and send the request to metrics-a service, pre login.
830
+ * @param event
831
+ * @param preLoginId
832
+ * @returns
833
+ */
834
+ submitToCallDiagnosticsPreLogin = (event: Event, preLoginId?: string): Promise<any> => {
835
+ // build metrics-a event type
836
+ const finalEvent = {
837
+ eventPayload: event,
838
+ type: ['diagnostic-event'],
839
+ };
840
+ this.preLoginMetricsBatcher.savePreLoginId(preLoginId);
841
+
842
+ return this.preLoginMetricsBatcher.request(finalEvent);
843
+ };
844
+
845
+ /**
846
+ * Builds a request options object to later be passed to fetch().
847
+ * @param arg - submit params
848
+ * @param arg.event - event key
849
+ * @param arg.payload - additional payload to be merged with default payload
850
+ * @param arg.options - client event options
851
+ * @returns {Promise<any>}
852
+ * @throws
853
+ */
854
+ public async buildClientEventFetchRequestOptions({
855
+ name,
856
+ payload,
857
+ options,
858
+ }: {
859
+ name: ClientEvent['name'];
860
+ payload?: ClientEventPayload;
861
+ options?: SubmitClientEventOptions;
862
+ }): Promise<any> {
863
+ this.logger.log(
864
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
865
+ 'CallDiagnosticMetrics: @buildClientEventFetchRequestOptions. Building request options object for fetch()...',
866
+ `name: ${name}`
867
+ );
868
+
869
+ const clientEvent = this.prepareClientEvent({name, payload, options});
870
+
871
+ // build metrics-a event type
872
+ // @ts-ignore
873
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
874
+ eventPayload: clientEvent,
875
+ type: ['diagnostic-event'],
876
+ });
877
+
878
+ const request = {
879
+ method: 'POST',
880
+ service: 'metrics',
881
+ resource: 'clientmetrics',
882
+ body: {
883
+ metrics: [diagnosticEvent],
884
+ },
885
+ headers: {},
886
+ // @ts-ignore
887
+ waitForServiceTimeout: this.webex.internal.metrics.config.waitForServiceTimeout,
888
+ };
889
+
890
+ if (options.preLoginId) {
891
+ request.headers = {
892
+ authorization: false,
893
+ 'x-prelogin-userid': options.preLoginId,
894
+ };
895
+ request.resource = 'clientmetrics-prelogin';
896
+ }
897
+
898
+ // @ts-ignore
899
+ return this.webex.prepareFetchOptions(request);
900
+ }
901
+
902
+ /**
903
+ * Returns true if the specified serviceErrorCode maps to an expected error.
904
+ * @param {number} serviceErrorCode the service error code
905
+ * @returns {boolean}
906
+ */
907
+ public isServiceErrorExpected(serviceErrorCode: number): boolean {
908
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
909
+ const clientErrorPayload = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
910
+
911
+ return clientErrorPayload?.category === 'expected';
912
+ }
913
+ }