@webex/internal-plugin-metrics 3.0.0-beta.29 → 3.0.0-beta.290

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