@webex/internal-plugin-metrics 3.0.0-beta.39 → 3.0.0-beta.391

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 +486 -0
  6. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -0
  7. package/dist/call-diagnostic/call-diagnostic-metrics.js +860 -0
  8. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -0
  9. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +363 -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 +27 -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 +300 -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 +208 -0
  30. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +427 -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 +447 -0
  47. package/src/call-diagnostic/call-diagnostic-metrics.ts +919 -0
  48. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +391 -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 +21 -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 +453 -0
  60. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +615 -0
  61. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +2303 -0
  62. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +628 -0
  63. package/test/unit/spec/client-metrics-batcher.js +2 -0
  64. package/test/unit/spec/metrics.js +74 -97
  65. package/test/unit/spec/new-metrics.ts +231 -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,919 @@
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
+ const {installationId} = device.config || {};
297
+
298
+ identifiers.userId = device.userId || preLoginId;
299
+ identifiers.deviceId = device.url;
300
+ identifiers.orgId = device.orgId;
301
+ // @ts-ignore
302
+ identifiers.locusUrl = this.webex.internal.services.get('locus');
303
+
304
+ if (installationId) {
305
+ identifiers.machineId = installationId;
306
+ }
307
+ }
308
+
309
+ if (meeting?.locusInfo?.fullState) {
310
+ identifiers.locusUrl = meeting.locusUrl;
311
+ identifiers.locusId = meeting.locusUrl && meeting.locusUrl.split('/').pop();
312
+ identifiers.locusStartTime =
313
+ meeting.locusInfo.fullState && meeting.locusInfo.fullState.lastActive;
314
+ }
315
+
316
+ if (meeting?.meetingInfo?.confIdStr || meeting?.meetingInfo?.confID) {
317
+ identifiers.webexConferenceIdStr = `${
318
+ meeting.meetingInfo?.confIdStr || meeting.meetingInfo?.confID
319
+ }`;
320
+ }
321
+
322
+ if (meeting?.meetingInfo?.meetingId) {
323
+ identifiers.globalMeetingId = meeting.meetingInfo?.meetingId;
324
+ }
325
+
326
+ if (meeting?.meetingInfo?.siteName) {
327
+ identifiers.webexSiteName = meeting.meetingInfo?.siteName;
328
+ }
329
+
330
+ if (mediaConnections) {
331
+ identifiers.mediaAgentAlias = mediaConnections?.[0]?.mediaAgentAlias;
332
+ identifiers.mediaAgentGroupId = mediaConnections?.[0]?.mediaAgentGroupId;
333
+ }
334
+
335
+ if (!identifiers?.webexConferenceIdStr && webexConferenceIdStr) {
336
+ identifiers.webexConferenceIdStr = `${webexConferenceIdStr}`;
337
+ }
338
+
339
+ if (!identifiers?.globalMeetingId && globalMeetingId) {
340
+ identifiers.globalMeetingId = globalMeetingId;
341
+ }
342
+
343
+ if (identifiers.correlationId === undefined) {
344
+ throw new Error('Identifiers initialization failed.');
345
+ }
346
+
347
+ return identifiers;
348
+ }
349
+
350
+ /**
351
+ * Create diagnostic event, which can hold client event, feature event or MQE event data.
352
+ * This just initiates the shared properties that are required for all the 3 event categories.
353
+ * @param eventData
354
+ * @param options
355
+ * @returns
356
+ */
357
+ prepareDiagnosticEvent(eventData: Event['event'], options: any) {
358
+ const {meetingId} = options;
359
+ const origin = this.getOrigin(options, meetingId);
360
+
361
+ const event: Event = {
362
+ eventId: uuid.v4(),
363
+ version: 1,
364
+ origin,
365
+ originTime: {
366
+ triggered: new Date().toISOString(),
367
+ // is overridden in prepareRequest batcher
368
+ sent: 'not_defined_yet',
369
+ },
370
+ // @ts-ignore
371
+ senderCountryCode: this.webex.meetings.geoHintInfo?.countryCode,
372
+ event: eventData,
373
+ };
374
+
375
+ // sanitize (remove empty properties, CA requires it)
376
+ // but we don't want to sanitize MQE as most of the times
377
+ // values will be 0, [] etc, and they are required.
378
+ if (eventData.name !== 'client.mediaquality.event') {
379
+ clearEmptyKeysRecursively(event);
380
+ }
381
+
382
+ return event;
383
+ }
384
+
385
+ /**
386
+ * TODO: NOT IMPLEMENTED
387
+ * Submit Feature Event
388
+ * @returns
389
+ */
390
+ public submitFeatureEvent() {
391
+ throw Error('Not implemented');
392
+ }
393
+
394
+ /**
395
+ * Submit Media Quality Event
396
+ * @param args - submit params
397
+ * @param arg.name - event key
398
+ * @param arg.payload - additional payload to be merge with the default payload
399
+ * @param arg.options - options
400
+ */
401
+ submitMQE({
402
+ name,
403
+ payload,
404
+ options,
405
+ }: {
406
+ name: MediaQualityEvent['name'];
407
+ payload: SubmitMQEPayload;
408
+ options: SubmitMQEOptions;
409
+ }) {
410
+ const {meetingId, mediaConnections, webexConferenceIdStr, globalMeetingId} = options;
411
+
412
+ // events that will most likely happen in join phase
413
+ if (meetingId) {
414
+ // @ts-ignore
415
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
416
+
417
+ if (!meeting) {
418
+ console.warn(
419
+ 'Attempt to send MQE but no meeting was found...',
420
+ `event: ${name}, meetingId: ${meetingId}`
421
+ );
422
+ // @ts-ignore
423
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
424
+ fields: {
425
+ meetingId,
426
+ name,
427
+ },
428
+ });
429
+
430
+ return;
431
+ }
432
+
433
+ // merge identifiers
434
+ const identifiers = this.getIdentifiers({
435
+ meeting,
436
+ mediaConnections: meeting.mediaConnections || mediaConnections,
437
+ webexConferenceIdStr,
438
+ globalMeetingId,
439
+ });
440
+
441
+ // create media quality event object
442
+ let clientEventObject: MediaQualityEvent['payload'] = {
443
+ name,
444
+ canProceed: true,
445
+ identifiers,
446
+ eventData: {
447
+ webClientDomain: window.location.hostname,
448
+ },
449
+ intervals: payload.intervals,
450
+ sourceMetadata: {
451
+ applicationSoftwareType: CLIENT_NAME,
452
+ // @ts-ignore
453
+ applicationSoftwareVersion: this.webex.version,
454
+ mediaEngineSoftwareType: getBrowserName() || 'browser',
455
+ mediaEngineSoftwareVersion: getOSVersion() || 'unknown',
456
+ startTime: new Date().toISOString(),
457
+ },
458
+ };
459
+
460
+ // merge any new properties, or override existing ones
461
+ clientEventObject = merge(clientEventObject, payload);
462
+
463
+ // append media quality event data to the call diagnostic event
464
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
465
+ this.validator({type: 'mqe', event: diagnosticEvent});
466
+ this.submitToCallDiagnostics(diagnosticEvent);
467
+ } else {
468
+ throw new Error(
469
+ 'Media quality events cant be sent outside the context of a meeting. Meeting id is required.'
470
+ );
471
+ }
472
+ }
473
+
474
+ /**
475
+ * Return Client Event payload by client error code
476
+ * @param arg - get error arg
477
+ * @param arg.clientErrorCode
478
+ * @param arg.serviceErrorCode
479
+ * @param arg.payloadOverrides
480
+ * @param arg.httpStatusCode
481
+ * @returns
482
+ */
483
+ public getErrorPayloadForClientErrorCode({
484
+ clientErrorCode,
485
+ serviceErrorCode,
486
+ serviceErrorName,
487
+ rawErrorMessage,
488
+ payloadOverrides,
489
+ httpStatusCode,
490
+ }: {
491
+ clientErrorCode: number;
492
+ serviceErrorCode: any;
493
+ serviceErrorName?: any;
494
+ rawErrorMessage?: string;
495
+ payloadOverrides?: any;
496
+ httpStatusCode?: number;
497
+ }): ClientEventError {
498
+ let error: ClientEventError;
499
+
500
+ if (clientErrorCode) {
501
+ const partialParsedError = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
502
+
503
+ if (partialParsedError) {
504
+ error = merge(
505
+ {fatal: true, shownToUser: false, name: 'other', category: 'other'}, // default values
506
+ {errorCode: clientErrorCode},
507
+ serviceErrorName ? {errorData: {errorName: serviceErrorName}} : {},
508
+ {serviceErrorCode},
509
+ {rawErrorMessage},
510
+ httpStatusCode === undefined ? {} : {httpStatusCode},
511
+ partialParsedError,
512
+ payloadOverrides || {}
513
+ );
514
+
515
+ return error;
516
+ }
517
+ }
518
+
519
+ return undefined;
520
+ }
521
+
522
+ /**
523
+ * Generate error payload for Client Event
524
+ * @param rawError
525
+ */
526
+ generateClientEventErrorPayload(rawError: any) {
527
+ const rawErrorMessage = rawError.message;
528
+ const httpStatusCode = rawError.statusCode;
529
+ if (rawError.name) {
530
+ if (isBrowserMediaErrorName(rawError.name)) {
531
+ return this.getErrorPayloadForClientErrorCode({
532
+ serviceErrorCode: undefined,
533
+ clientErrorCode: BROWSER_MEDIA_ERROR_NAME_TO_CLIENT_ERROR_CODES_MAP[rawError.name],
534
+ serviceErrorName: rawError.name,
535
+ rawErrorMessage,
536
+ httpStatusCode,
537
+ });
538
+ }
539
+ }
540
+
541
+ if (isSdpOfferCreationError(rawError)) {
542
+ // error code is 30005, but that's not specific enough. we also need to check error.cause.type
543
+ const causeType = rawError.cause?.type;
544
+
545
+ return this.getErrorPayloadForClientErrorCode({
546
+ serviceErrorCode: undefined,
547
+ clientErrorCode:
548
+ SDP_OFFER_CREATION_ERROR_MAP[causeType] || SDP_OFFER_CREATION_ERROR_MAP.GENERAL,
549
+ serviceErrorName: rawError.name,
550
+ rawErrorMessage,
551
+ httpStatusCode,
552
+ });
553
+ }
554
+
555
+ const serviceErrorCode =
556
+ rawError?.error?.body?.errorCode ||
557
+ rawError?.body?.errorCode ||
558
+ rawError?.body?.code ||
559
+ rawError?.body?.reason?.reasonCode;
560
+
561
+ if (serviceErrorCode) {
562
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
563
+ if (clientErrorCode) {
564
+ return this.getErrorPayloadForClientErrorCode({
565
+ clientErrorCode,
566
+ serviceErrorCode,
567
+ rawErrorMessage,
568
+ httpStatusCode,
569
+ });
570
+ }
571
+
572
+ // by default, if it is locus error, return new locus err
573
+ if (isLocusServiceErrorCode(serviceErrorCode)) {
574
+ return this.getErrorPayloadForClientErrorCode({
575
+ clientErrorCode: NEW_LOCUS_ERROR_CLIENT_CODE,
576
+ serviceErrorCode,
577
+ rawErrorMessage,
578
+ httpStatusCode,
579
+ });
580
+ }
581
+ }
582
+
583
+ if (isMeetingInfoServiceError(rawError)) {
584
+ return this.getErrorPayloadForClientErrorCode({
585
+ clientErrorCode: MEETING_INFO_LOOKUP_ERROR_CLIENT_CODE,
586
+ serviceErrorCode,
587
+ rawErrorMessage,
588
+ httpStatusCode,
589
+ });
590
+ }
591
+
592
+ if (isNetworkError(rawError)) {
593
+ return this.getErrorPayloadForClientErrorCode({
594
+ clientErrorCode: NETWORK_ERROR,
595
+ serviceErrorCode,
596
+ payloadOverrides: rawError.payloadOverrides,
597
+ rawErrorMessage,
598
+ httpStatusCode,
599
+ });
600
+ }
601
+
602
+ if (isUnauthorizedError(rawError)) {
603
+ return this.getErrorPayloadForClientErrorCode({
604
+ clientErrorCode: AUTHENTICATION_FAILED_CODE,
605
+ serviceErrorCode,
606
+ payloadOverrides: rawError.payloadOverrides,
607
+ rawErrorMessage,
608
+ httpStatusCode,
609
+ });
610
+ }
611
+
612
+ // otherwise return unkown error
613
+ return this.getErrorPayloadForClientErrorCode({
614
+ clientErrorCode: UNKNOWN_ERROR,
615
+ serviceErrorCode: UNKNOWN_ERROR,
616
+ payloadOverrides: rawError.payloadOverrides,
617
+ rawErrorMessage,
618
+ httpStatusCode,
619
+ });
620
+ }
621
+
622
+ /**
623
+ * Create client event object for in meeting events
624
+ * @param arg - create args
625
+ * @param arg.event - event key
626
+ * @param arg.options - options
627
+ * @returns object
628
+ */
629
+ private createClientEventObjectInMeeting({
630
+ name,
631
+ options,
632
+ errors,
633
+ }: {
634
+ name: ClientEvent['name'];
635
+ options?: SubmitClientEventOptions;
636
+ errors?: ClientEventPayloadError;
637
+ }) {
638
+ const {meetingId, mediaConnections, globalMeetingId, webexConferenceIdStr} = options;
639
+
640
+ // @ts-ignore
641
+ const meeting = this.webex.meetings.meetingCollection.get(meetingId);
642
+
643
+ if (!meeting) {
644
+ console.warn(
645
+ 'Attempt to send client event but no meeting was found...',
646
+ `name: ${name}, meetingId: ${meetingId}`
647
+ );
648
+ // @ts-ignore
649
+ this.webex.internal.metrics.submitClientMetrics(CALL_DIAGNOSTIC_EVENT_FAILED_TO_SEND, {
650
+ fields: {
651
+ meetingId,
652
+ name,
653
+ },
654
+ });
655
+
656
+ return undefined;
657
+ }
658
+
659
+ // grab identifiers
660
+ const identifiers = this.getIdentifiers({
661
+ meeting,
662
+ mediaConnections: meeting?.mediaConnections || mediaConnections,
663
+ webexConferenceIdStr,
664
+ globalMeetingId,
665
+ });
666
+
667
+ // create client event object
668
+ const clientEventObject: ClientEvent['payload'] = {
669
+ name,
670
+ canProceed: true,
671
+ identifiers,
672
+ errors,
673
+ eventData: {
674
+ webClientDomain: window.location.hostname,
675
+ },
676
+ userType: meeting.getCurUserType(),
677
+ loginType:
678
+ 'loginType' in meeting.callStateForMetrics
679
+ ? meeting.callStateForMetrics.loginType
680
+ : this.getCurLoginType(),
681
+ isConvergedArchitectureEnabled: this.getIsConvergedArchitectureEnabled({
682
+ meetingId,
683
+ }),
684
+ webexSubServiceType: this.getSubServiceType(meeting),
685
+ };
686
+
687
+ return clientEventObject;
688
+ }
689
+
690
+ /**
691
+ * Create client event object for pre meeting events
692
+ * @param arg - create args
693
+ * @param arg.event - event key
694
+ * @param arg.options - payload
695
+ * @returns object
696
+ */
697
+ private createClientEventObjectPreMeeting({
698
+ name,
699
+ options,
700
+ errors,
701
+ }: {
702
+ name: ClientEvent['name'];
703
+ options?: SubmitClientEventOptions;
704
+ errors?: ClientEventPayloadError;
705
+ }) {
706
+ const {correlationId, globalMeetingId, webexConferenceIdStr, preLoginId} = options;
707
+
708
+ // grab identifiers
709
+ const identifiers = this.getIdentifiers({
710
+ correlationId,
711
+ preLoginId,
712
+ globalMeetingId,
713
+ webexConferenceIdStr,
714
+ });
715
+
716
+ // create client event object
717
+ const clientEventObject: ClientEvent['payload'] = {
718
+ name,
719
+ errors,
720
+ canProceed: true,
721
+ identifiers,
722
+ eventData: {
723
+ webClientDomain: window.location.hostname,
724
+ },
725
+ loginType: this.getCurLoginType(),
726
+ };
727
+
728
+ return clientEventObject;
729
+ }
730
+
731
+ /**
732
+ * Prepare Client Event CA event.
733
+ * @param arg - submit params
734
+ * @param arg.event - event key
735
+ * @param arg.payload - additional payload to be merged with default payload
736
+ * @param arg.options - payload
737
+ * @returns {any} options to be with fetch
738
+ * @throws
739
+ */
740
+ private prepareClientEvent({
741
+ name,
742
+ payload,
743
+ options,
744
+ }: {
745
+ name: ClientEvent['name'];
746
+ payload?: ClientEventPayload;
747
+ options?: SubmitClientEventOptions;
748
+ }) {
749
+ const {meetingId, correlationId, rawError} = options;
750
+ let clientEventObject: ClientEvent['payload'];
751
+
752
+ // check if we need to generate errors
753
+ const errors: ClientEventPayloadError = [];
754
+
755
+ if (rawError) {
756
+ const generatedError = this.generateClientEventErrorPayload(rawError);
757
+ if (generatedError) {
758
+ errors.push(generatedError);
759
+ }
760
+ this.logger.log(
761
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
762
+ 'CallDiagnosticMetrics: @prepareClientEvent. Generated errors:',
763
+ `generatedError: ${JSON.stringify(generatedError)}`
764
+ );
765
+ }
766
+
767
+ // events that will most likely happen in join phase
768
+ if (meetingId) {
769
+ clientEventObject = this.createClientEventObjectInMeeting({name, options, errors});
770
+ } else if (correlationId) {
771
+ // any pre join events or events that are outside the meeting.
772
+ clientEventObject = this.createClientEventObjectPreMeeting({name, options, errors});
773
+ } else {
774
+ throw new Error('Not implemented');
775
+ }
776
+
777
+ // merge any new properties, or override existing ones
778
+ clientEventObject = merge(clientEventObject, payload);
779
+
780
+ // append client event data to the call diagnostic event
781
+ const diagnosticEvent = this.prepareDiagnosticEvent(clientEventObject, options);
782
+
783
+ return diagnosticEvent;
784
+ }
785
+
786
+ /**
787
+ * Submit Client Event CA event.
788
+ * @param arg - submit params
789
+ * @param arg.event - event key
790
+ * @param arg.payload - additional payload to be merged with default payload
791
+ * @param arg.options - payload
792
+ * @throws
793
+ */
794
+ public submitClientEvent({
795
+ name,
796
+ payload,
797
+ options,
798
+ }: {
799
+ name: ClientEvent['name'];
800
+ payload?: ClientEventPayload;
801
+ options?: SubmitClientEventOptions;
802
+ }) {
803
+ this.logger.log(
804
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
805
+ 'CallDiagnosticMetrics: @submitClientEvent. Submit Client Event CA event.',
806
+ `name: ${name}`
807
+ );
808
+ const diagnosticEvent = this.prepareClientEvent({name, payload, options});
809
+
810
+ if (options?.preLoginId) {
811
+ return this.submitToCallDiagnosticsPreLogin(diagnosticEvent, options?.preLoginId);
812
+ }
813
+
814
+ this.validator({type: 'ce', event: diagnosticEvent});
815
+
816
+ return this.submitToCallDiagnostics(diagnosticEvent);
817
+ }
818
+
819
+ /**
820
+ * Prepare the event and send the request to metrics-a service.
821
+ * @param event
822
+ * @returns promise
823
+ */
824
+ submitToCallDiagnostics(event: Event): Promise<any> {
825
+ // build metrics-a event type
826
+ const finalEvent = {
827
+ eventPayload: event,
828
+ type: ['diagnostic-event'],
829
+ };
830
+
831
+ return this.callDiagnosticEventsBatcher.request(finalEvent);
832
+ }
833
+
834
+ /**
835
+ * Prepare the event and send the request to metrics-a service, pre login.
836
+ * @param event
837
+ * @param preLoginId
838
+ * @returns
839
+ */
840
+ submitToCallDiagnosticsPreLogin = (event: Event, preLoginId?: string): Promise<any> => {
841
+ // build metrics-a event type
842
+ const finalEvent = {
843
+ eventPayload: event,
844
+ type: ['diagnostic-event'],
845
+ };
846
+ this.preLoginMetricsBatcher.savePreLoginId(preLoginId);
847
+
848
+ return this.preLoginMetricsBatcher.request(finalEvent);
849
+ };
850
+
851
+ /**
852
+ * Builds a request options object to later be passed to fetch().
853
+ * @param arg - submit params
854
+ * @param arg.event - event key
855
+ * @param arg.payload - additional payload to be merged with default payload
856
+ * @param arg.options - client event options
857
+ * @returns {Promise<any>}
858
+ * @throws
859
+ */
860
+ public async buildClientEventFetchRequestOptions({
861
+ name,
862
+ payload,
863
+ options,
864
+ }: {
865
+ name: ClientEvent['name'];
866
+ payload?: ClientEventPayload;
867
+ options?: SubmitClientEventOptions;
868
+ }): Promise<any> {
869
+ this.logger.log(
870
+ CALL_DIAGNOSTIC_LOG_IDENTIFIER,
871
+ 'CallDiagnosticMetrics: @buildClientEventFetchRequestOptions. Building request options object for fetch()...',
872
+ `name: ${name}`
873
+ );
874
+
875
+ const clientEvent = this.prepareClientEvent({name, payload, options});
876
+
877
+ // build metrics-a event type
878
+ // @ts-ignore
879
+ const diagnosticEvent = prepareDiagnosticMetricItem(this.webex, {
880
+ eventPayload: clientEvent,
881
+ type: ['diagnostic-event'],
882
+ });
883
+
884
+ const request = {
885
+ method: 'POST',
886
+ service: 'metrics',
887
+ resource: 'clientmetrics',
888
+ body: {
889
+ metrics: [diagnosticEvent],
890
+ },
891
+ headers: {},
892
+ // @ts-ignore
893
+ waitForServiceTimeout: this.webex.internal.metrics.config.waitForServiceTimeout,
894
+ };
895
+
896
+ if (options.preLoginId) {
897
+ request.headers = {
898
+ authorization: false,
899
+ 'x-prelogin-userid': options.preLoginId,
900
+ };
901
+ request.resource = 'clientmetrics-prelogin';
902
+ }
903
+
904
+ // @ts-ignore
905
+ return this.webex.prepareFetchOptions(request);
906
+ }
907
+
908
+ /**
909
+ * Returns true if the specified serviceErrorCode maps to an expected error.
910
+ * @param {number} serviceErrorCode the service error code
911
+ * @returns {boolean}
912
+ */
913
+ public isServiceErrorExpected(serviceErrorCode: number): boolean {
914
+ const clientErrorCode = SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP[serviceErrorCode];
915
+ const clientErrorPayload = CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD[clientErrorCode];
916
+
917
+ return clientErrorPayload?.category === 'expected';
918
+ }
919
+ }