@webex/internal-plugin-metrics 3.12.0-next.4 → 3.12.0-next.40

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 (48) hide show
  1. package/dist/automated-user.js +19 -0
  2. package/dist/automated-user.js.map +1 -0
  3. package/dist/batcher.js +3 -0
  4. package/dist/batcher.js.map +1 -1
  5. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js +638 -65
  6. package/dist/call-diagnostic/call-diagnostic-metrics-latencies.js.map +1 -1
  7. package/dist/call-diagnostic/call-diagnostic-metrics.js +167 -76
  8. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
  9. package/dist/call-diagnostic/call-diagnostic-metrics.util.js +5 -0
  10. package/dist/call-diagnostic/call-diagnostic-metrics.util.js.map +1 -1
  11. package/dist/call-diagnostic/config.js +16 -3
  12. package/dist/call-diagnostic/config.js.map +1 -1
  13. package/dist/config.js +1 -0
  14. package/dist/config.js.map +1 -1
  15. package/dist/index.js +15 -0
  16. package/dist/index.js.map +1 -1
  17. package/dist/metrics.js +1 -1
  18. package/dist/metrics.types.js +4 -0
  19. package/dist/metrics.types.js.map +1 -1
  20. package/dist/new-metrics.js +16 -6
  21. package/dist/new-metrics.js.map +1 -1
  22. package/dist/types/automated-user.d.ts +2 -0
  23. package/dist/types/call-diagnostic/call-diagnostic-metrics-latencies.d.ts +208 -5
  24. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +67 -14
  25. package/dist/types/call-diagnostic/config.d.ts +4 -0
  26. package/dist/types/config.d.ts +1 -0
  27. package/dist/types/index.d.ts +5 -3
  28. package/dist/types/metrics.types.d.ts +4 -2
  29. package/dist/types/new-metrics.d.ts +4 -0
  30. package/package.json +12 -11
  31. package/src/automated-user.ts +16 -0
  32. package/src/batcher.js +4 -0
  33. package/src/call-diagnostic/call-diagnostic-metrics-latencies.ts +800 -73
  34. package/src/call-diagnostic/call-diagnostic-metrics.ts +101 -15
  35. package/src/call-diagnostic/call-diagnostic-metrics.util.ts +5 -0
  36. package/src/call-diagnostic/config.ts +14 -0
  37. package/src/config.js +1 -0
  38. package/src/index.ts +5 -0
  39. package/src/metrics.types.ts +16 -3
  40. package/src/new-metrics.ts +12 -4
  41. package/test/unit/spec/automated-user.ts +43 -0
  42. package/test/unit/spec/batcher.js +43 -0
  43. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-batcher.ts +14 -2
  44. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics-latencies.ts +1434 -303
  45. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +852 -159
  46. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.util.ts +27 -0
  47. package/test/unit/spec/new-metrics.ts +39 -2
  48. package/test/unit/spec/prelogin-metrics-batcher.ts +71 -36
@@ -6,6 +6,7 @@ import uuid from 'uuid';
6
6
  import {merge} from 'lodash';
7
7
  import {StatelessWebexPlugin} from '@webex/webex-core';
8
8
  import {getOSNameInternal} from '../metrics';
9
+ import * as AutomatedUserUtils from '../automated-user';
9
10
 
10
11
  import {
11
12
  anonymizeIPAddress,
@@ -99,7 +100,12 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
99
100
  // @ts-ignore
100
101
  private preLoginMetricsBatcher: PreLoginMetricsBatcher;
101
102
 
102
- private logger: any; // to avoid adding @ts-ignore everywhere
103
+ // lazy getter to avoid @ts-ignore on every call site
104
+ private get logger(): any {
105
+ // @ts-ignore
106
+ return this.webex.logger;
107
+ }
108
+
103
109
  private hasLoggedBrowserSerial: boolean;
104
110
  private device: any;
105
111
  private delayedClientEvents: DelayedClientEvent[] = [];
@@ -108,6 +114,8 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
108
114
  private isMercuryConnected = false;
109
115
  private eventLimitTracker: Map<string, number> = new Map();
110
116
  private eventLimitWarningsLogged: Set<string> = new Set();
117
+ private isTelemetryOptOutManual = false;
118
+ private isTelemetryOptOutAutomatic = false;
111
119
 
112
120
  // the default validator before piping an event to the batcher
113
121
  // this function can be overridden by the user
@@ -124,8 +132,6 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
124
132
  constructor(...args) {
125
133
  super(...args);
126
134
  // @ts-ignore
127
- this.logger = this.webex.logger;
128
- // @ts-ignore
129
135
  this.callDiagnosticEventsBatcher = new CallDiagnosticEventsBatcher({}, {parent: this.webex});
130
136
  // @ts-ignore
131
137
  this.preLoginMetricsBatcher = new PreLoginMetricsBatcher({}, {parent: this.webex});
@@ -145,6 +151,58 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
145
151
  return null;
146
152
  }
147
153
 
154
+ /**
155
+ * Returns the user activation state reported from the browser's navigator.userActivation API
156
+ * @returns object with hasBeenActive and isActive booleans, or undefined if unavailable
157
+ */
158
+ getUserActivation(): {hasBeenActive: boolean; isActive: boolean} | undefined {
159
+ const userActivation =
160
+ typeof navigator !== 'undefined'
161
+ ? (navigator as {userActivation?: {hasBeenActive: boolean; isActive: boolean}})
162
+ .userActivation
163
+ : undefined;
164
+
165
+ if (userActivation) {
166
+ return {
167
+ hasBeenActive: userActivation.hasBeenActive,
168
+ isActive: userActivation.isActive,
169
+ };
170
+ }
171
+
172
+ return undefined;
173
+ }
174
+
175
+ /**
176
+ * Returns the telemetryOptOut value of the current user
177
+ * @returns one of 'manual', 'automatic', undefined
178
+ */
179
+ public getTelemetryOptOut() {
180
+ if (this.isTelemetryOptOutManual) {
181
+ return 'manual';
182
+ }
183
+ if (this.isTelemetryOptOutAutomatic) {
184
+ return 'automatic';
185
+ }
186
+
187
+ return undefined;
188
+ }
189
+
190
+ /**
191
+ * Sets the manual telemetry opt-out status for the current user
192
+ * @param value - boolean value indicating manual telemetry opt-out status
193
+ */
194
+ public setIsTelemetryOptOutManual(value: boolean) {
195
+ this.isTelemetryOptOutManual = value;
196
+ }
197
+
198
+ /**
199
+ * Sets the automatic telemetry opt-out status for the current user
200
+ * @param value - boolean value indicating automatic telemetry opt-out status
201
+ */
202
+ public setIsTelemetryOptOutAutomatic(value: boolean) {
203
+ this.isTelemetryOptOutAutomatic = value;
204
+ }
205
+
148
206
  /**
149
207
  * Returns if the meeting has converged architecture enabled
150
208
  * @param options.meetingId
@@ -190,6 +248,11 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
190
248
 
191
249
  // if ConvergedArchitecture enable and isConvergedWebinarWebcast -- then webcast
192
250
  if (meetingInfo?.enableConvergedArchitecture && meetingInfo?.enableEvent) {
251
+ // if enableConvergedWebinarLargeScale - then large scale webinar
252
+ if (meetingInfo?.enableConvergedWebinarLargeScale) {
253
+ return WEBEX_SUB_SERVICE_TYPES.LARGE_SCALE_WEBINAR;
254
+ }
255
+
193
256
  return meetingInfo?.isConvergedWebinarWebcast
194
257
  ? WEBEX_SUB_SERVICE_TYPES.WEBCAST
195
258
  : WEBEX_SUB_SERVICE_TYPES.WEBINAR;
@@ -213,12 +276,12 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
213
276
  getOrigin(options: GetOriginOptions, meetingId?: string) {
214
277
  const defaultClientType: ClientType =
215
278
  // @ts-ignore
216
- this.webex.meetings.config?.metrics?.clientType;
279
+ this.webex.meetings?.config?.metrics?.clientType;
217
280
  const defaultSubClientType: SubClientType =
218
281
  // @ts-ignore
219
- this.webex.meetings.config?.metrics?.subClientType;
282
+ this.webex.meetings?.config?.metrics?.subClientType;
220
283
  // @ts-ignore
221
- const providedClientVersion: string = this.webex.meetings.config?.metrics?.clientVersion;
284
+ const providedClientVersion: string = this.webex.meetings?.config?.metrics?.clientVersion;
222
285
  // @ts-ignore
223
286
  const defaultSDKClientVersion = `${CLIENT_NAME}/${this.webex.version}`;
224
287
 
@@ -229,6 +292,18 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
229
292
  versionMetadata = extractVersionMetadata(providedClientVersion);
230
293
  }
231
294
 
295
+ // Browser details and the support verdict can all be supplied by the host client, which is able
296
+ // to resolve wrapper browsers (Electron, WebOS) to the engine that actually determines
297
+ // capability. The browser fields fall back to the SDK's own detection when not supplied, the
298
+ // same way clientVersion falls back to the SDK version.
299
+ const {
300
+ browser: providedBrowser,
301
+ browserVersion: providedBrowserVersion,
302
+ isSupportedBrowserFamily,
303
+ isOutdatedBrowserVersion,
304
+ // @ts-ignore
305
+ } = this.webex.meetings?.config?.metrics ?? {};
306
+
232
307
  if (!this.hasLoggedBrowserSerial) {
233
308
  this.logger.log(
234
309
  CALL_DIAGNOSTIC_LOG_IDENTIFIER,
@@ -258,25 +333,27 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
258
333
  ...versionMetadata,
259
334
  publicNetworkPrefix:
260
335
  // @ts-ignore
261
- anonymizeIPAddress(this.webex.meetings.geoHintInfo?.clientAddress) || undefined,
336
+ anonymizeIPAddress(this.webex.meetings?.geoHintInfo?.clientAddress) || undefined,
262
337
  localNetworkPrefix:
263
338
  anonymizeIPAddress(
264
339
  // @ts-ignore
265
- this.webex.meetings.meetingCollection
266
- .get(meetingId)
340
+ this.webex.meetings?.meetingCollection
341
+ ?.get(meetingId)
267
342
  ?.statsAnalyzer?.getLocalIpAddress()
268
343
  ) || undefined,
269
344
  osVersion: getOSVersion() || 'unknown',
270
345
  subClientType: options?.subClientType || defaultSubClientType,
271
346
  os: getOSNameInternal(),
272
- browser: getBrowserName(),
273
- browserVersion: getBrowserVersion(),
347
+ browser: providedBrowser || getBrowserName(),
348
+ browserVersion: providedBrowserVersion || getBrowserVersion(),
349
+ ...(isSupportedBrowserFamily === undefined ? {} : {isSupportedBrowserFamily}),
350
+ ...(isOutdatedBrowserVersion === undefined ? {} : {isOutdatedBrowserVersion}),
274
351
  },
275
352
  };
276
353
 
277
354
  if (meetingId) {
278
355
  // @ts-ignore
279
- const meeting = this.webex.meetings.getBasicMeetingInformation(meetingId);
356
+ const meeting = this.webex.meetings?.getBasicMeetingInformation(meetingId);
280
357
  if (meeting?.environment) {
281
358
  origin.environment = meeting.environment;
282
359
  }
@@ -426,7 +503,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
426
503
  sent: 'not_defined_yet',
427
504
  },
428
505
  // @ts-ignore
429
- senderCountryCode: this.webex.meetings.geoHintInfo?.countryCode,
506
+ senderCountryCode: this.webex.meetings?.geoHintInfo?.countryCode,
430
507
  event: eventData,
431
508
  };
432
509
 
@@ -602,6 +679,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
602
679
  mediaEngineSoftwareVersion: getOSVersion() || 'unknown',
603
680
  startTime: new Date().toISOString(),
604
681
  },
682
+ webexSubServiceType: this.getSubServiceType(meeting),
605
683
  };
606
684
 
607
685
  // merge any new properties, or override existing ones
@@ -984,7 +1062,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
984
1062
  sessionCorrelationId,
985
1063
  });
986
1064
 
987
- // create common event object structur
1065
+ // create common event object structure
988
1066
  const commonEventObject = {
989
1067
  name,
990
1068
  canProceed: true,
@@ -997,6 +1075,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
997
1075
  'loginType' in meeting.callStateForMetrics
998
1076
  ? meeting.callStateForMetrics.loginType
999
1077
  : this.getCurLoginType(),
1078
+ telemetryOptOut: this.getTelemetryOptOut(),
1000
1079
  isConvergedArchitectureEnabled: this.getIsConvergedArchitectureEnabled({
1001
1080
  meetingId,
1002
1081
  }),
@@ -1005,6 +1084,9 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
1005
1084
  webexSubServiceType: this.getSubServiceType(meeting),
1006
1085
  // @ts-ignore
1007
1086
  webClientPreload: this.webex.meetings?.config?.metrics?.webClientPreload,
1087
+ isVipMeeting: meeting?.meetingInfo?.vipmeeting || false,
1088
+ isAutomatedUser: AutomatedUserUtils.isAutomatedUser(),
1089
+ userActivation: this.getUserActivation(),
1008
1090
  };
1009
1091
 
1010
1092
  const joinFlowVersion = options.joinFlowVersion ?? meeting.callStateForMetrics?.joinFlowVersion;
@@ -1124,8 +1206,11 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
1124
1206
  isMercuryConnected: this.isMercuryConnected,
1125
1207
  },
1126
1208
  loginType: this.getCurLoginType(),
1209
+ telemetryOptOut: this.getTelemetryOptOut(),
1127
1210
  // @ts-ignore
1128
1211
  webClientPreload: this.webex.meetings?.config?.metrics?.webClientPreload,
1212
+ isAutomatedUser: AutomatedUserUtils.isAutomatedUser(),
1213
+ userActivation: this.getUserActivation(),
1129
1214
  };
1130
1215
 
1131
1216
  if (options.joinFlowVersion) {
@@ -1335,6 +1420,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
1335
1420
  eventPayload: event,
1336
1421
  type: ['diagnostic-event'],
1337
1422
  };
1423
+
1338
1424
  this.preLoginMetricsBatcher.savePreLoginId(preLoginId);
1339
1425
 
1340
1426
  return this.preLoginMetricsBatcher.request(finalEvent);
@@ -1382,7 +1468,7 @@ export default class CallDiagnosticMetrics extends StatelessWebexPlugin {
1382
1468
  },
1383
1469
  headers: {},
1384
1470
  // @ts-ignore
1385
- waitForServiceTimeout: this.webex.internal.metrics.config.waitForServiceTimeout,
1471
+ waitForServiceTimeout: this.webex.internal.metrics?.config?.waitForServiceTimeout,
1386
1472
  };
1387
1473
 
1388
1474
  if (options.preLoginId) {
@@ -390,6 +390,11 @@ export const prepareDiagnosticMetricItem = (webex: any, item: any) => {
390
390
 
391
391
  item.eventPayload.origin = Object.assign(origin, item.eventPayload.origin);
392
392
 
393
+ // Mark call milestones in logs for easier filtering and analysis
394
+ if (eventName) {
395
+ webex.logger.log('Milestone,CallDiagnostic', eventName);
396
+ }
397
+
393
398
  webex.logger.log(
394
399
  `CallDiagnosticLatencies,prepareDiagnosticMetricItem: ${JSON.stringify({
395
400
  latencies: Object.fromEntries(cdl.latencyTimestamps),
@@ -26,6 +26,7 @@ export const WEBEX_SUB_SERVICE_TYPES: Record<string, ClientSubServiceType> = {
26
26
  SCHEDULED_MEETING: 'ScheduledMeeting',
27
27
  WEBINAR: 'Webinar',
28
28
  WEBCAST: 'Webcast',
29
+ LARGE_SCALE_WEBINAR: 'LargeScaleWebinar',
29
30
  };
30
31
 
31
32
  // Found in https://developer.mozilla.org/en-US/docs/Web/API/MediaDevices/getUserMedia
@@ -136,6 +137,7 @@ export const ERROR_DESCRIPTIONS = {
136
137
  WEBRTC_API_NOT_AVAILABLE: 'WebrtcApiNotAvailableError',
137
138
  WDM_RESTRICTED_REGION: 'WdmRestrictedRegion',
138
139
  USER_NOT_ALLOWED_JOIN_WEBINAR: 'UserNotAllowedJoinWebinar',
140
+ INVALID_MEETING_INFO: 'InvalidMeetingInfo',
139
141
  };
140
142
 
141
143
  export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = {
@@ -146,6 +148,8 @@ export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = {
146
148
  99002: 4100,
147
149
  // Cannot find the data. Unkown meeting.
148
150
  99009: 4100,
151
+ // The input parameters contain invalid item
152
+ 99019: 4105,
149
153
  // Meeting is not allow to cross env
150
154
  58500: 4100,
151
155
  // Input parameters contain invalit item
@@ -201,6 +205,8 @@ export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = {
201
205
  423005: 4005,
202
206
  // Wrong password or host key with too many requests
203
207
  423006: 4005,
208
+ // PanelistPasswordError too many time,please input captcha code
209
+ 423008: 4005,
204
210
  // PasswordError with right captcha, please input captcha code
205
211
  423010: 4005,
206
212
  // PasswordOrHostKeyError with right captcha, please input captcha code
@@ -227,6 +233,8 @@ export const SERVICE_ERROR_CODES_TO_CLIENT_ERROR_CODES_MAP = {
227
233
  403003: 4101,
228
234
  // Attendee email is required
229
235
  403030: 4101,
236
+ // webinar need login when un-invited attendee join
237
+ 403106: 4104,
230
238
 
231
239
  // ---- Locus ------
232
240
  // FREE_USER_MAX_PARTICIPANTS_EXCEEDED
@@ -696,6 +704,12 @@ export const CLIENT_ERROR_CODE_TO_ERROR_PAYLOAD: Record<number, Partial<ClientEv
696
704
  category: 'expected',
697
705
  fatal: true,
698
706
  },
707
+ 4105: {
708
+ errorDescription: ERROR_DESCRIPTIONS.INVALID_MEETING_INFO,
709
+ category: 'expected',
710
+ fatal: false,
711
+ shownToUser: true,
712
+ },
699
713
  2729: {
700
714
  errorDescription: ERROR_DESCRIPTIONS.NO_MEDIA_FOUND,
701
715
  category: 'expected',
package/src/config.js CHANGED
@@ -19,6 +19,7 @@ export default {
19
19
  batcherMaxCalls: 50,
20
20
  batcherMaxWait: 1500,
21
21
  batcherRetryPlateau: 32000,
22
+ batcherRetryOnNetworkError: true,
22
23
  waitForServiceTimeout: 30,
23
24
  },
24
25
  };
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import Metrics from './metrics';
8
8
  import config from './config';
9
9
  import NewMetrics from './new-metrics';
10
10
  import * as Utils from './utils';
11
+ import * as AutomatedUserUtils from './automated-user';
11
12
  import {
12
13
  ClientEvent,
13
14
  ClientEventLeaveReason,
@@ -19,6 +20,7 @@ import {
19
20
  SubmitMQE,
20
21
  PreComputedLatencies,
21
22
  SubmitFeatureEvent,
23
+ LocusSyncLatencyEventName,
22
24
  } from './metrics.types';
23
25
  import * as CALL_DIAGNOSTIC_CONFIG from './call-diagnostic/config';
24
26
  import * as CallDiagnosticUtils from './call-diagnostic/call-diagnostic-metrics.util';
@@ -43,6 +45,7 @@ export {default, getOSNameInternal} from './metrics';
43
45
  export {
44
46
  config,
45
47
  CALL_DIAGNOSTIC_CONFIG,
48
+ AutomatedUserUtils,
46
49
  NewMetrics,
47
50
  Utils,
48
51
  CallDiagnosticUtils,
@@ -54,6 +57,7 @@ export {
54
57
  RtcMetrics,
55
58
  PreLoginMetrics,
56
59
  };
60
+ export {isAutomatedUser, isAutomatedUserAgent} from './automated-user';
57
61
  export type {
58
62
  ClientEvent,
59
63
  ClientEventLeaveReason,
@@ -65,4 +69,5 @@ export type {
65
69
  SubmitBusinessEvent,
66
70
  PreComputedLatencies,
67
71
  SubmitFeatureEvent,
72
+ LocusSyncLatencyEventName,
68
73
  };
@@ -149,6 +149,17 @@ export type SubmitMQEOptions = {
149
149
  globalMeetingId?: string;
150
150
  };
151
151
 
152
+ export const LOCUS_SYNC_LATENCY_EVENT_NAMES = [
153
+ 'internal.client.locus.sync.start',
154
+ 'internal.client.locus.hashtree.request',
155
+ 'internal.client.locus.hashtree.response',
156
+ 'internal.client.locus.sync.request',
157
+ 'internal.client.locus.sync.response',
158
+ 'internal.client.locus.sync.message.received',
159
+ ] as const;
160
+
161
+ export type LocusSyncLatencyEventName = (typeof LOCUS_SYNC_LATENCY_EVENT_NAMES)[number];
162
+
152
163
  export type InternalEvent = {
153
164
  name:
154
165
  | 'internal.client.meetinginfo.request'
@@ -156,14 +167,14 @@ export type InternalEvent = {
156
167
  | 'internal.register.device.request'
157
168
  | 'internal.register.device.response'
158
169
  | 'internal.reset.join.latencies'
159
- | 'internal.client.meeting.click.joinbutton'
160
170
  | 'internal.host.meeting.participant.admitted'
161
171
  | 'internal.client.meeting.interstitial-window.showed'
162
172
  | 'internal.client.interstitial-window.click.joinbutton'
163
173
  | 'internal.client.add-media.turn-discovery.start'
164
174
  | 'internal.client.add-media.turn-discovery.end'
165
175
  | 'internal.client.share.initiated'
166
- | 'internal.client.share.stopped';
176
+ | 'internal.client.share.stopped'
177
+ | LocusSyncLatencyEventName;
167
178
 
168
179
  payload?: never;
169
180
  options?: never;
@@ -319,12 +330,14 @@ export type PreComputedLatencies =
319
330
  | 'internal.get.cluster.time'
320
331
  | 'internal.click.to.interstitial'
321
332
  | 'internal.click.to.interstitial.with.user.delay'
333
+ | 'internal.click.to.interstitial.for.client.jmt'
322
334
  | 'internal.refresh.captcha.time'
323
335
  | 'internal.exchange.ci.token.time'
324
336
  | 'internal.get.u2c.time'
325
337
  | 'internal.call.init.join.req'
326
338
  | 'internal.other.app.api.time'
327
- | 'internal.api.fetch.intelligence.models';
339
+ | 'internal.api.fetch.intelligence.models'
340
+ | 'internal.client.locus.sync.random.backoff';
328
341
 
329
342
  export interface IdType {
330
343
  meetingId?: string;
@@ -30,6 +30,7 @@ import {
30
30
  import CallDiagnosticLatencies from './call-diagnostic/call-diagnostic-metrics-latencies';
31
31
  import {setMetricTimings} from './call-diagnostic/call-diagnostic-metrics.util';
32
32
  import {generateCommonErrorMetadata} from './utils';
33
+ import {isAutomatedUser as detectAutomatedUser} from './automated-user';
33
34
 
34
35
  /**
35
36
  * Metrics plugin to centralize all types of metrics.
@@ -79,6 +80,8 @@ class Metrics extends WebexPlugin {
79
80
 
80
81
  // @ts-ignore
81
82
  this.callDiagnosticLatencies = new CallDiagnosticLatencies({}, {parent: this.webex});
83
+ // @ts-ignore
84
+ this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
82
85
  this.onReady();
83
86
  }
84
87
 
@@ -88,8 +91,6 @@ class Metrics extends WebexPlugin {
88
91
  private onReady() {
89
92
  // @ts-ignore
90
93
  this.webex.once('ready', () => {
91
- // @ts-ignore
92
- this.callDiagnosticMetrics = new CallDiagnosticMetrics({}, {parent: this.webex});
93
94
  this.preLoginMetrics = new PreLoginMetrics(
94
95
  // @ts-ignore
95
96
  new PreLoginMetricsBatcher({}, {parent: this.webex}),
@@ -182,6 +183,13 @@ class Metrics extends WebexPlugin {
182
183
  return this.businessMetrics?.isReadyToSubmitEvents() ?? false;
183
184
  }
184
185
 
186
+ /**
187
+ * @returns whether the current user agent belongs to an automated user
188
+ */
189
+ isAutomatedUser() {
190
+ return detectAutomatedUser();
191
+ }
192
+
185
193
  /**
186
194
  * Behavioral event
187
195
  * @param args
@@ -325,7 +333,7 @@ class Metrics extends WebexPlugin {
325
333
  payload?: RecursivePartial<FeatureEvent['payload']>;
326
334
  options: any;
327
335
  }) {
328
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
336
+ if (!this.isReady) {
329
337
  // @ts-ignore
330
338
  this.webex.logger.log(
331
339
  `NewMetrics: @submitFeatureEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -360,7 +368,7 @@ class Metrics extends WebexPlugin {
360
368
  payload?: RecursivePartial<ClientEvent['payload']>;
361
369
  options?: SubmitClientEventOptions;
362
370
  }): Promise<any> {
363
- if (!this.callDiagnosticLatencies || !this.callDiagnosticMetrics) {
371
+ if (!this.isReady) {
364
372
  // @ts-ignore
365
373
  this.webex.logger.log(
366
374
  `NewMetrics: @submitClientEvent. Attempted to submit before webex.ready. Event name: ${name}`
@@ -0,0 +1,43 @@
1
+ import {assert} from '@webex/test-helper-chai';
2
+
3
+ import {isAutomatedUser, isAutomatedUserAgent} from '../../../src/automated-user';
4
+
5
+ describe('automated user detection', () => {
6
+ [
7
+ {userAgent: 'SkypeUriPreview', expected: true},
8
+ {userAgent: 'skypeuripreview', expected: true},
9
+ {userAgent: 'Mozilla/5.0 (compatible; SkypeUriPreview/0.1)', expected: true},
10
+ {userAgent: 'Googlebot/2.1 (+http://www.google.com/bot.html)', expected: true},
11
+ {
12
+ userAgent:
13
+ 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 Chrome/126.0.0.0 Safari/537.36',
14
+ expected: false,
15
+ },
16
+ {userAgent: undefined, expected: false},
17
+ ].forEach(({userAgent, expected}) => {
18
+ it(`returns ${expected} for ${userAgent || 'an undefined user agent'}`, () => {
19
+ assert.equal(isAutomatedUserAgent(userAgent), expected);
20
+ });
21
+ });
22
+
23
+ it('returns the cached classification when navigator changes', () => {
24
+ const cachedResult = isAutomatedUser();
25
+ const originalDescriptor = Object.getOwnPropertyDescriptor(global, 'navigator');
26
+
27
+ Object.defineProperty(global, 'navigator', {
28
+ value: cachedResult
29
+ ? {userAgent: 'Mozilla/5.0', webdriver: false}
30
+ : {userAgent: 'SkypeUriPreview', webdriver: true},
31
+ configurable: true,
32
+ writable: true,
33
+ });
34
+
35
+ assert.equal(isAutomatedUser(), cachedResult);
36
+
37
+ if (originalDescriptor) {
38
+ Object.defineProperty(global, 'navigator', originalDescriptor);
39
+ } else {
40
+ delete (global as any).navigator;
41
+ }
42
+ });
43
+ });
@@ -177,6 +177,49 @@ describe('plugin-metrics', () => {
177
177
  assert.lengthOf(webex.internal.metrics.batcher.queue, 0);
178
178
  });
179
179
  });
180
+
181
+ it('rejects the deferred without reenqueuing when batcherRetryOnNetworkError is false', () => {
182
+ webex.config.metrics = {...config.metrics, batcherRetryOnNetworkError: false};
183
+
184
+ webex.request = function () {
185
+ // noop
186
+ };
187
+
188
+ sinon.stub(webex, 'request').callsFake((options) => {
189
+ options.headers = {
190
+ trackingid: 'test-no-retry',
191
+ };
192
+
193
+ return Promise.reject(
194
+ new WebexHttpError.NetworkOrCORSError({
195
+ statusCode: 0,
196
+ options,
197
+ })
198
+ );
199
+ });
200
+
201
+ const promise = webex.internal.metrics.batcher.request({
202
+ key: 'testMetric',
203
+ });
204
+
205
+ return promiseTick(50)
206
+ .then(() => assert.lengthOf(webex.internal.metrics.batcher.queue, 1))
207
+ .then(() => clock.tick(config.metrics.batcherWait))
208
+ .then(() => assert.calledOnce(webex.request))
209
+ .then(() => promiseTick(50))
210
+ .then(() =>
211
+ promise.then(
212
+ () => {
213
+ assert.fail('promise should have been rejected');
214
+ },
215
+ (reason) => {
216
+ assert.instanceOf(reason, WebexHttpError.NetworkOrCORSError);
217
+ assert.lengthOf(webex.internal.metrics.batcher.queue, 0);
218
+ assert.calledOnce(webex.request);
219
+ }
220
+ )
221
+ );
222
+ });
180
223
  });
181
224
  });
182
225
  });
@@ -192,6 +192,12 @@ describe('plugin-metrics', () => {
192
192
  webex.internal.newMetrics.callDiagnosticLatencies.getTotalJMTWithUserDelay = sinon
193
193
  .stub()
194
194
  .returns(64);
195
+ webex.internal.newMetrics.callDiagnosticLatencies.getInterstitialToJoinOK = sinon
196
+ .stub()
197
+ .returns(10);
198
+ webex.internal.newMetrics.callDiagnosticLatencies.getTotalJMT = sinon
199
+ .stub()
200
+ .returns(20);
195
201
  const promise = webex.internal.newMetrics.callDiagnosticMetrics.submitToCallDiagnostics(
196
202
  //@ts-ignore
197
203
  {event: {name: 'client.locus.join.response'}}
@@ -346,6 +352,12 @@ describe('plugin-metrics', () => {
346
352
  webex.internal.newMetrics.callDiagnosticLatencies.getStayLobbyTime = sinon
347
353
  .stub()
348
354
  .returns(1);
355
+ webex.internal.newMetrics.callDiagnosticLatencies.getStayLobbyTimeCappedBy = sinon
356
+ .stub()
357
+ .returns(1);
358
+ webex.internal.newMetrics.callDiagnosticLatencies.getTotalMediaJMT = sinon
359
+ .stub()
360
+ .returns(44);
349
361
  webex.internal.newMetrics.callDiagnosticLatencies.getTotalMediaJMTWithUserDelay = sinon
350
362
  .stub()
351
363
  .returns(43);
@@ -366,9 +378,9 @@ describe('plugin-metrics', () => {
366
378
  assert.deepEqual(webex.request.getCalls()[0].args[0].body.metrics[0].eventPayload.event, {
367
379
  name: 'client.media-engine.ready',
368
380
  joinTimes: {
369
- totalMediaJMT: 61,
381
+ totalMediaJMT: 44,
370
382
  interstitialToMediaOKJMT: 22,
371
- callInitMediaEngineReady: 10,
383
+ callInitMediaEngineReady: 22,
372
384
  totalMediaJMTWithUserDelay: 43,
373
385
  totalJMTWithUserDelay: 64,
374
386
  },