@webex/internal-plugin-metrics 3.0.0-beta.28 → 3.0.0-beta.280

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