@webex/internal-plugin-metrics 3.12.0-webex-services-ready.4 → 3.13.0

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 (77) hide show
  1. package/.repo-context.json +466 -0
  2. package/.sdd/manifest.json +626 -0
  3. package/AGENTS.md +208 -0
  4. package/README.md +274 -0
  5. package/dist/call-diagnostic/call-diagnostic-metrics.js +155 -94
  6. package/dist/call-diagnostic/call-diagnostic-metrics.js.map +1 -1
  7. package/dist/call-diagnostic/config.js +3 -1
  8. package/dist/call-diagnostic/config.js.map +1 -1
  9. package/dist/config.js +7 -0
  10. package/dist/config.js.map +1 -1
  11. package/dist/index.js +10 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/metrics.js +118 -5
  14. package/dist/metrics.js.map +1 -1
  15. package/dist/metrics.types.js.map +1 -1
  16. package/dist/network-telemetry.js +610 -0
  17. package/dist/network-telemetry.js.map +1 -0
  18. package/dist/new-metrics.js +47 -11
  19. package/dist/new-metrics.js.map +1 -1
  20. package/dist/privacy-and-security-permission-enricher/constants.js +18 -0
  21. package/dist/privacy-and-security-permission-enricher/constants.js.map +1 -0
  22. package/dist/privacy-and-security-permission-enricher/index.js +143 -0
  23. package/dist/privacy-and-security-permission-enricher/index.js.map +1 -0
  24. package/dist/privacy-and-security-permission-enricher/types.js +7 -0
  25. package/dist/privacy-and-security-permission-enricher/types.js.map +1 -0
  26. package/dist/privacy-and-security-permission-enricher/utils.js +71 -0
  27. package/dist/privacy-and-security-permission-enricher/utils.js.map +1 -0
  28. package/dist/types/call-diagnostic/call-diagnostic-metrics.d.ts +12 -3
  29. package/dist/types/call-diagnostic/config.d.ts +2 -0
  30. package/dist/types/config.d.ts +8 -0
  31. package/dist/types/index.d.ts +2 -2
  32. package/dist/types/metrics.types.d.ts +7 -3
  33. package/dist/types/network-telemetry.d.ts +108 -0
  34. package/dist/types/new-metrics.d.ts +13 -1
  35. package/dist/types/privacy-and-security-permission-enricher/constants.d.ts +6 -0
  36. package/dist/types/privacy-and-security-permission-enricher/index.d.ts +40 -0
  37. package/dist/types/privacy-and-security-permission-enricher/types.d.ts +14 -0
  38. package/dist/types/privacy-and-security-permission-enricher/utils.d.ts +5 -0
  39. package/dist/types/unhandled-exception-telemetry/index.d.ts +60 -0
  40. package/dist/types/unhandled-exception-telemetry/utils.d.ts +6 -0
  41. package/dist/unhandled-exception-telemetry/index.js +331 -0
  42. package/dist/unhandled-exception-telemetry/index.js.map +1 -0
  43. package/dist/unhandled-exception-telemetry/utils.js +105 -0
  44. package/dist/unhandled-exception-telemetry/utils.js.map +1 -0
  45. package/docs/adr/index.md +27 -0
  46. package/docs/architecture.md +437 -0
  47. package/docs/getting-started.md +131 -0
  48. package/docs/index.md +52 -0
  49. package/docs/specs/README.md +88 -0
  50. package/package.json +11 -11
  51. package/schemas/repo-context.schema.json +796 -0
  52. package/src/call-diagnostic/call-diagnostic-metrics.ts +100 -30
  53. package/src/call-diagnostic/config.ts +2 -0
  54. package/src/call-diagnostic/docs/README.md +535 -0
  55. package/src/config.js +7 -0
  56. package/src/docs/README.md +703 -0
  57. package/src/index.ts +11 -0
  58. package/src/metrics.js +123 -5
  59. package/src/metrics.types.ts +13 -3
  60. package/src/network-telemetry.ts +757 -0
  61. package/src/new-metrics.ts +44 -2
  62. package/src/privacy-and-security-permission-enricher/constants.ts +33 -0
  63. package/src/privacy-and-security-permission-enricher/docs/README.md +356 -0
  64. package/src/privacy-and-security-permission-enricher/index.ts +142 -0
  65. package/src/privacy-and-security-permission-enricher/types.ts +21 -0
  66. package/src/privacy-and-security-permission-enricher/utils.ts +82 -0
  67. package/src/rtcMetrics/docs/README.md +447 -0
  68. package/src/unhandled-exception-telemetry/docs/README.md +536 -0
  69. package/src/unhandled-exception-telemetry/index.ts +404 -0
  70. package/src/unhandled-exception-telemetry/utils.ts +101 -0
  71. package/test/unit/spec/call-diagnostic/call-diagnostic-metrics.ts +485 -21
  72. package/test/unit/spec/metrics.js +14 -5
  73. package/test/unit/spec/network-telemetry.ts +610 -0
  74. package/test/unit/spec/new-metrics.ts +190 -48
  75. package/test/unit/spec/privacy-and-security-permission-enricher.ts +318 -0
  76. package/test/unit/spec/unhandled-exception-telemetry/utils.ts +109 -0
  77. package/test/unit/spec/unhandled-exception-telemetry.ts +689 -0
@@ -81,12 +81,20 @@ describe('internal-plugin-metrics', () => {
81
81
  emailInput: 'test@test.com',
82
82
  };
83
83
 
84
+ const fakeMeeting6 = {
85
+ ...fakeMeeting,
86
+ id: '6',
87
+ correlationId: 'correlationId6',
88
+ callStateForMetrics: {meetingJoinPhase: 'in-meeting'},
89
+ };
90
+
84
91
  const fakeMeetings = {
85
92
  1: fakeMeeting,
86
93
  2: fakeMeeting2,
87
94
  3: fakeMeeting3,
88
95
  4: fakeMeeting4,
89
96
  5: fakeMeeting5,
97
+ 6: fakeMeeting6,
90
98
  };
91
99
 
92
100
  let webex;
@@ -364,6 +372,23 @@ describe('internal-plugin-metrics', () => {
364
372
  });
365
373
  });
366
374
 
375
+ it('should build origin correctly when the meetings plugin is not available (before webex is ready)', () => {
376
+ // meetings plugin, geoHintInfo and meetingCollection are all absent before webex.ready
377
+ webex.meetings = undefined;
378
+
379
+ //@ts-ignore
380
+ const res = cd.getOrigin(
381
+ {subClientType: 'WEB_APP', clientType: 'TEAMS_CLIENT'},
382
+ fakeMeeting.id
383
+ );
384
+
385
+ assert.equal(res.clientInfo.clientType, 'TEAMS_CLIENT');
386
+ assert.equal(res.clientInfo.subClientType, 'WEB_APP');
387
+ assert.isUndefined(res.clientInfo.publicNetworkPrefix);
388
+ assert.isUndefined(res.clientInfo.localNetworkPrefix);
389
+ assert.equal(res.name, 'endpoint');
390
+ });
391
+
367
392
  it('builds origin correctly, when overriding clientVersion', () => {
368
393
  webex.meetings.config.metrics.clientVersion = '43.9.0.1234';
369
394
 
@@ -394,6 +419,64 @@ describe('internal-plugin-metrics', () => {
394
419
  });
395
420
  });
396
421
 
422
+ it('builds origin correctly with browser details provided by the host client', () => {
423
+ // host resolves wrapper browsers (WebOS, Electron) to the engine that determines capability
424
+ webex.meetings.config.metrics.browser = 'WebOS Browser';
425
+ webex.meetings.config.metrics.browserVersion = '108.0.0.0';
426
+
427
+ //@ts-ignore
428
+ const res = cd.getOrigin(
429
+ {subClientType: 'WEB_APP', clientType: 'TEAMS_CLIENT'},
430
+ fakeMeeting.id
431
+ );
432
+
433
+ assert.equal(res.clientInfo.browser, 'WebOS Browser');
434
+ assert.equal(res.clientInfo.browserVersion, '108.0.0.0');
435
+ });
436
+
437
+ it('falls back to SDK browser detection when the host provides nothing', () => {
438
+ //@ts-ignore
439
+ const res = cd.getOrigin(
440
+ {subClientType: 'WEB_APP', clientType: 'TEAMS_CLIENT'},
441
+ fakeMeeting.id
442
+ );
443
+
444
+ assert.equal(res.clientInfo.browser, getBrowserName());
445
+ assert.equal(res.clientInfo.browserVersion, getBrowserVersion());
446
+ });
447
+
448
+ it('builds origin correctly with the browser support flags from config', () => {
449
+ // `false` is the meaningful "unsupported family" signal, so it must survive a truthy check
450
+ webex.meetings.config.metrics.isSupportedBrowserFamily = false;
451
+ webex.meetings.config.metrics.isOutdatedBrowserVersion = true;
452
+
453
+ //@ts-ignore
454
+ const res = cd.getOrigin(
455
+ {subClientType: 'WEB_APP', clientType: 'TEAMS_CLIENT'},
456
+ fakeMeeting.id
457
+ );
458
+
459
+ assert.deepEqual(res, {
460
+ clientInfo: {
461
+ browser: getBrowserName(),
462
+ browserVersion: getBrowserVersion(),
463
+ clientType: 'TEAMS_CLIENT',
464
+ clientVersion: 'webex-js-sdk/webex-version',
465
+ publicNetworkPrefix: '1.3.4.0',
466
+ localNetworkPrefix: '192.168.1.80',
467
+ os: getOSNameInternal(),
468
+ osVersion: getOSVersion() || 'unknown',
469
+ subClientType: 'WEB_APP',
470
+ isSupportedBrowserFamily: false,
471
+ isOutdatedBrowserVersion: true,
472
+ },
473
+ environment: 'meeting_evn',
474
+ name: 'endpoint',
475
+ networkType: 'unknown',
476
+ userAgent,
477
+ });
478
+ });
479
+
397
480
  it('should build origin correctly with no meeting or stats analyzer', () => {
398
481
  //@ts-ignore
399
482
  const res = cd.getOrigin();
@@ -640,6 +723,63 @@ describe('internal-plugin-metrics', () => {
640
723
  });
641
724
  });
642
725
 
726
+ it('should build identifiers correctly with a meeting that has meetingInfo with locusClusterUrl', () => {
727
+ const res = cd.getIdentifiers({
728
+ mediaConnections: [
729
+ {mediaAgentAlias: 'mediaAgentAlias', mediaAgentGroupId: 'mediaAgentGroupId'},
730
+ ],
731
+ meeting: {
732
+ ...fakeMeeting,
733
+ meetingInfo: {
734
+ ...fakeMeeting.meetingInfo,
735
+ locusClusterUrl: 'locus-k.wbx2.com',
736
+ },
737
+ },
738
+ });
739
+
740
+ assert.deepEqual(res, {
741
+ correlationId: 'correlationId',
742
+ deviceId: 'deviceUrl',
743
+ locusCluster: 'locus-k.wbx2.com',
744
+ locusId: 'url',
745
+ locusSessionId: 'locusSessionId',
746
+ locusStartTime: 'lastActive',
747
+ locusUrl: 'locus/url',
748
+ mediaAgentAlias: 'mediaAgentAlias',
749
+ mediaAgentGroupId: 'mediaAgentGroupId',
750
+ orgId: 'orgId',
751
+ userId: 'userId',
752
+ });
753
+ });
754
+
755
+ it('should build identifiers correctly given locusCluster', () => {
756
+ const res = cd.getIdentifiers({
757
+ correlationId: 'correlationId',
758
+ locusCluster: 'locus-k.wbx2.com',
759
+ });
760
+
761
+ assert.deepEqual(res, {
762
+ correlationId: 'correlationId',
763
+ deviceId: 'deviceUrl',
764
+ locusCluster: 'locus-k.wbx2.com',
765
+ locusUrl: 'locus-url',
766
+ orgId: 'orgId',
767
+ userId: 'userId',
768
+ });
769
+ });
770
+
771
+ it('should prefer meetingInfo locusClusterUrl over the locusCluster option', () => {
772
+ const res = cd.getIdentifiers({
773
+ meeting: {
774
+ ...fakeMeeting,
775
+ meetingInfo: {...fakeMeeting.meetingInfo, locusClusterUrl: 'locus-k.wbx2.com'},
776
+ },
777
+ locusCluster: 'locus-a.wbx2.com',
778
+ });
779
+
780
+ assert.equal(res.locusCluster, 'locus-k.wbx2.com');
781
+ });
782
+
643
783
  it('should build identifiers correctly with a meeting that has sessionCorrelationId', () => {
644
784
  const res = cd.getIdentifiers({
645
785
  mediaConnections: [
@@ -766,6 +906,60 @@ describe('internal-plugin-metrics', () => {
766
906
  userId: 'preLoginId',
767
907
  });
768
908
  });
909
+
910
+ it('should use the userId from credentials when no device userId is available', () => {
911
+ cd.device.userId = undefined;
912
+ webex.credentials.getUserId = sinon.stub().returns('credentials-user-id');
913
+
914
+ const res = cd.getIdentifiers({
915
+ correlationId: 'correlationId',
916
+ });
917
+
918
+ assert.deepEqual(res, {
919
+ correlationId: 'correlationId',
920
+ locusUrl: 'locus-url',
921
+ deviceId: 'deviceUrl',
922
+ orgId: 'orgId',
923
+ userId: 'credentials-user-id',
924
+ });
925
+ });
926
+
927
+ it('should prefer the device userId over the credentials userId', () => {
928
+ webex.credentials.getUserId = sinon.stub().returns('credentials-user-id');
929
+
930
+ const res = cd.getIdentifiers({
931
+ correlationId: 'correlationId',
932
+ });
933
+
934
+ assert.equal(res.userId, 'userId');
935
+ assert.notCalled(webex.credentials.getUserId);
936
+ });
937
+
938
+ it('should fall back to preLoginId when neither the device nor credentials provide a userId', () => {
939
+ cd.device.userId = undefined;
940
+ webex.credentials.getUserId = sinon.stub().throws(new Error('no user token available'));
941
+
942
+ const res = cd.getIdentifiers({
943
+ correlationId: 'correlationId',
944
+ preLoginId: 'preLoginId',
945
+ });
946
+
947
+ assert.equal(res.userId, 'preLoginId');
948
+ });
949
+ });
950
+
951
+ describe('#getUserIdFromCredentials', () => {
952
+ it('should return the userId from credentials', () => {
953
+ webex.credentials.getUserId = sinon.stub().returns('credentials-user-id');
954
+
955
+ assert.equal(cd.getUserIdFromCredentials(), 'credentials-user-id');
956
+ });
957
+
958
+ it('should return undefined when credentials cannot provide a userId', () => {
959
+ webex.credentials.getUserId = sinon.stub().throws(new Error('no user token available'));
960
+
961
+ assert.isUndefined(cd.getUserIdFromCredentials());
962
+ });
769
963
  });
770
964
 
771
965
  it('should prepare diagnostic event successfully', () => {
@@ -984,6 +1178,7 @@ describe('internal-plugin-metrics', () => {
984
1178
  webexConferenceIdStr: undefined,
985
1179
  sessionCorrelationId: undefined,
986
1180
  globalMeetingId: undefined,
1181
+ locusCluster: undefined,
987
1182
  });
988
1183
  assert.notCalled(generateClientEventErrorPayloadSpy);
989
1184
  assert.calledWith(
@@ -1138,6 +1333,7 @@ describe('internal-plugin-metrics', () => {
1138
1333
  webexConferenceIdStr: undefined,
1139
1334
  sessionCorrelationId: undefined,
1140
1335
  globalMeetingId: undefined,
1336
+ locusCluster: undefined,
1141
1337
  });
1142
1338
  assert.notCalled(generateClientEventErrorPayloadSpy);
1143
1339
  assert.calledWith(
@@ -1292,6 +1488,7 @@ describe('internal-plugin-metrics', () => {
1292
1488
  webexConferenceIdStr: undefined,
1293
1489
  globalMeetingId: undefined,
1294
1490
  sessionCorrelationId: undefined,
1491
+ locusCluster: undefined,
1295
1492
  });
1296
1493
  assert.notCalled(generateClientEventErrorPayloadSpy);
1297
1494
  assert.calledWith(
@@ -1448,6 +1645,7 @@ describe('internal-plugin-metrics', () => {
1448
1645
  webexConferenceIdStr: undefined,
1449
1646
  globalMeetingId: undefined,
1450
1647
  sessionCorrelationId: undefined,
1648
+ locusCluster: undefined,
1451
1649
  });
1452
1650
  assert.notCalled(generateClientEventErrorPayloadSpy);
1453
1651
  assert.calledWith(
@@ -1604,6 +1802,7 @@ describe('internal-plugin-metrics', () => {
1604
1802
  webexConferenceIdStr: undefined,
1605
1803
  globalMeetingId: undefined,
1606
1804
  sessionCorrelationId: undefined,
1805
+ locusCluster: undefined,
1607
1806
  });
1608
1807
  assert.notCalled(generateClientEventErrorPayloadSpy);
1609
1808
  assert.calledWith(
@@ -1822,6 +2021,7 @@ describe('internal-plugin-metrics', () => {
1822
2021
  globalMeetingId: 'globalMeetingId1',
1823
2022
  sessionCorrelationId: 'sessionCorrelationId1',
1824
2023
  preLoginId: undefined,
2024
+ locusCluster: undefined,
1825
2025
  });
1826
2026
 
1827
2027
  assert.notCalled(generateClientEventErrorPayloadSpy);
@@ -1849,6 +2049,7 @@ describe('internal-plugin-metrics', () => {
1849
2049
  webClientPreload: undefined,
1850
2050
  isAutomatedUser: false,
1851
2051
  userActivation: undefined,
2052
+ meetingJoinPhase: 'pre-join',
1852
2053
  },
1853
2054
  options
1854
2055
  );
@@ -1875,6 +2076,7 @@ describe('internal-plugin-metrics', () => {
1875
2076
  webClientPreload: undefined,
1876
2077
  isAutomatedUser: false,
1877
2078
  userActivation: undefined,
2079
+ meetingJoinPhase: 'pre-join',
1878
2080
  },
1879
2081
  eventId: 'my-fake-id',
1880
2082
  origin: {
@@ -1926,6 +2128,7 @@ describe('internal-plugin-metrics', () => {
1926
2128
  globalMeetingId: 'globalMeetingId1',
1927
2129
  preLoginId: 'myPreLoginId',
1928
2130
  sessionCorrelationId: 'sessionCorrelationId1',
2131
+ locusCluster: undefined,
1929
2132
  });
1930
2133
 
1931
2134
  assert.notCalled(generateClientEventErrorPayloadSpy);
@@ -1953,6 +2156,7 @@ describe('internal-plugin-metrics', () => {
1953
2156
  webClientPreload: undefined,
1954
2157
  isAutomatedUser: false,
1955
2158
  userActivation: undefined,
2159
+ meetingJoinPhase: 'pre-join',
1956
2160
  },
1957
2161
  options
1958
2162
  );
@@ -1984,12 +2188,43 @@ describe('internal-plugin-metrics', () => {
1984
2188
  webClientPreload: undefined,
1985
2189
  isAutomatedUser: false,
1986
2190
  userActivation: undefined,
2191
+ meetingJoinPhase: 'pre-join',
1987
2192
  },
1988
2193
  },
1989
2194
  options.preLoginId
1990
2195
  );
1991
2196
  });
1992
2197
 
2198
+ it('should add locusCluster from options to a pre-meeting client event', () => {
2199
+ const prepareDiagnosticEventSpy = sinon.spy(cd, 'prepareDiagnosticEvent');
2200
+ sinon.stub(cd, 'getOrigin').returns({origin: 'fake-origin'});
2201
+
2202
+ cd.submitClientEvent({
2203
+ name: 'client.meetinginfo.response',
2204
+ options: {correlationId: 'correlationId', locusCluster: 'locus-k.wbx2.com'},
2205
+ });
2206
+
2207
+ assert.equal(
2208
+ prepareDiagnosticEventSpy.firstCall.args[0].identifiers.locusCluster,
2209
+ 'locus-k.wbx2.com'
2210
+ );
2211
+ });
2212
+
2213
+ it('should add locusCluster from options to an in-meeting client event when the meeting has none', () => {
2214
+ const prepareDiagnosticEventSpy = sinon.spy(cd, 'prepareDiagnosticEvent');
2215
+ sinon.stub(cd, 'getOrigin').returns({origin: 'fake-origin'});
2216
+
2217
+ cd.submitClientEvent({
2218
+ name: 'client.interstitial-window.launched',
2219
+ options: {meetingId: fakeMeeting.id, locusCluster: 'locus-k.wbx2.com'},
2220
+ });
2221
+
2222
+ assert.equal(
2223
+ prepareDiagnosticEventSpy.firstCall.args[0].identifiers.locusCluster,
2224
+ 'locus-k.wbx2.com'
2225
+ );
2226
+ });
2227
+
1993
2228
  it('should submit client event successfully with emailInput and userNameInput as options', () => {
1994
2229
  cd.device.userId = undefined;
1995
2230
 
@@ -2021,6 +2256,7 @@ describe('internal-plugin-metrics', () => {
2021
2256
  globalMeetingId: 'globalMeetingId1',
2022
2257
  preLoginId: 'myPreLoginId',
2023
2258
  sessionCorrelationId: 'sessionCorrelationId1',
2259
+ locusCluster: undefined,
2024
2260
  });
2025
2261
 
2026
2262
  assert.notCalled(generateClientEventErrorPayloadSpy);
@@ -2050,6 +2286,7 @@ describe('internal-plugin-metrics', () => {
2050
2286
  webClientPreload: undefined,
2051
2287
  isAutomatedUser: false,
2052
2288
  userActivation: undefined,
2289
+ meetingJoinPhase: 'pre-join',
2053
2290
  },
2054
2291
  options
2055
2292
  );
@@ -2083,6 +2320,7 @@ describe('internal-plugin-metrics', () => {
2083
2320
  webClientPreload: undefined,
2084
2321
  isAutomatedUser: false,
2085
2322
  userActivation: undefined,
2323
+ meetingJoinPhase: 'pre-join',
2086
2324
  },
2087
2325
  },
2088
2326
  options.preLoginId
@@ -2250,6 +2488,7 @@ describe('internal-plugin-metrics', () => {
2250
2488
  webClientPreload: true,
2251
2489
  isAutomatedUser: false,
2252
2490
  userActivation: undefined,
2491
+ meetingJoinPhase: 'pre-join',
2253
2492
  },
2254
2493
  options
2255
2494
  );
@@ -2276,6 +2515,7 @@ describe('internal-plugin-metrics', () => {
2276
2515
  webClientPreload: true,
2277
2516
  isAutomatedUser: false,
2278
2517
  userActivation: undefined,
2518
+ meetingJoinPhase: 'pre-join',
2279
2519
  },
2280
2520
  eventId: 'my-fake-id',
2281
2521
  origin: {
@@ -2726,6 +2966,7 @@ describe('internal-plugin-metrics', () => {
2726
2966
  webClientPreload: undefined,
2727
2967
  isAutomatedUser: false,
2728
2968
  userActivation: undefined,
2969
+ meetingJoinPhase: 'pre-join',
2729
2970
  },
2730
2971
  eventId: 'my-fake-id',
2731
2972
  origin: {
@@ -2804,6 +3045,7 @@ describe('internal-plugin-metrics', () => {
2804
3045
  webClientPreload: undefined,
2805
3046
  isAutomatedUser: false,
2806
3047
  userActivation: undefined,
3048
+ meetingJoinPhase: 'pre-join',
2807
3049
  },
2808
3050
  eventId: 'my-fake-id',
2809
3051
  origin: {
@@ -3655,7 +3897,7 @@ describe('internal-plugin-metrics', () => {
3655
3897
  serviceErrorCode: undefined,
3656
3898
  errorCode: 1026,
3657
3899
  rawErrorMessage: '{}\nundefined https://example.com\nWEBEX_TRACKING_ID: undefined\n',
3658
- httpStatusCode: 0,
3900
+ httpCode: 0,
3659
3901
  });
3660
3902
  });
3661
3903
 
@@ -3772,7 +4014,7 @@ describe('internal-plugin-metrics', () => {
3772
4014
  serviceErrorCode: undefined,
3773
4015
  errorCode: 1026,
3774
4016
  rawErrorMessage: '{}\nundefined https://example.com\nWEBEX_TRACKING_ID: undefined\n',
3775
- httpStatusCode: 0,
4017
+ httpCode: 0,
3776
4018
  });
3777
4019
  });
3778
4020
 
@@ -3794,7 +4036,7 @@ describe('internal-plugin-metrics', () => {
3794
4036
  serviceErrorCode: undefined,
3795
4037
  errorCode: 1010,
3796
4038
  rawErrorMessage: '{}\nundefined https://example.com\nWEBEX_TRACKING_ID: undefined\n',
3797
- httpStatusCode: 0,
4039
+ httpCode: 0,
3798
4040
  });
3799
4041
  });
3800
4042
 
@@ -3820,7 +4062,7 @@ describe('internal-plugin-metrics', () => {
3820
4062
  serviceErrorCode: undefined,
3821
4063
  errorCode: 1010,
3822
4064
  rawErrorMessage: '{}\nundefined https://example.com\nWEBEX_TRACKING_ID: undefined\n',
3823
- httpStatusCode: 0,
4065
+ httpCode: 0,
3824
4066
  });
3825
4067
  });
3826
4068
 
@@ -3925,8 +4167,144 @@ describe('internal-plugin-metrics', () => {
3925
4167
  });
3926
4168
  });
3927
4169
 
3928
- describe('httpStatusCode', () => {
3929
- it('should include httpStatusCode for browser media errors', () => {
4170
+ describe('httpCode', () => {
4171
+ it('should identify an outgoing Locus 429', () => {
4172
+ const rawErrorMessage =
4173
+ 'partition 371 on PARALLEL_EXECUTOR_PARTITION_KEY_QUEUE_FULL state';
4174
+ const [res] = cd.generateClientEventErrorPayload({
4175
+ message: rawErrorMessage,
4176
+ statusCode: 429,
4177
+ options: {uri: 'https://locus.example.com/locus/api/v1/loci/call'},
4178
+ });
4179
+
4180
+ assert.deepEqual(res, {
4181
+ category: 'signaling',
4182
+ errorCode: 1002,
4183
+ errorDescription: 'LocusRateLimitedOutgoing',
4184
+ fatal: true,
4185
+ name: 'locus.response',
4186
+ rawErrorMessage,
4187
+ serviceErrorCode: undefined,
4188
+ shownToUser: false,
4189
+ httpCode: 429,
4190
+ });
4191
+ });
4192
+
4193
+ [
4194
+ ['media', 'https://example.com/locus/api/v1/loci/123/media'],
4195
+ ['hashtree', 'https://example.com/locus/api/v1/loci/123/hashtree'],
4196
+ ['host-only Locus URL', 'https://locus.example.com/api/v1/resource'],
4197
+ ['query-only Locus URL', 'https://example.com/api/v1/resource?service=locus'],
4198
+ ].forEach(([operation, uri]) => {
4199
+ it(`should identify a Locus 429 for ${operation}`, () => {
4200
+ const [res] = cd.generateClientEventErrorPayload({
4201
+ message: 'Locus rate limited',
4202
+ statusCode: 429,
4203
+ options: {uri},
4204
+ });
4205
+
4206
+ assert.equal(res.errorCode, 1002);
4207
+ assert.equal(res.errorDescription, 'LocusRateLimitedOutgoing');
4208
+ assert.equal(res.httpCode, 429);
4209
+ });
4210
+ });
4211
+
4212
+ it('should identify an unavailable Locus 503', () => {
4213
+ const rawErrorMessage = 'Service unavailable';
4214
+ const [res] = cd.generateClientEventErrorPayload({
4215
+ message: rawErrorMessage,
4216
+ statusCode: 503,
4217
+ options: {uri: 'https://locus.example.com/locus/api/v1/loci/call'},
4218
+ });
4219
+
4220
+ assert.deepEqual(res, {
4221
+ category: 'signaling',
4222
+ errorCode: 1003,
4223
+ errorDescription: 'LocusUnavailable',
4224
+ fatal: true,
4225
+ name: 'locus.response',
4226
+ rawErrorMessage,
4227
+ serviceErrorCode: undefined,
4228
+ shownToUser: false,
4229
+ httpCode: 503,
4230
+ });
4231
+ });
4232
+
4233
+ it('should prefer a specific service error mapping over the Locus HTTP status', () => {
4234
+ const rawErrorMessage = 'Fraud detected';
4235
+ const [res] = cd.generateClientEventErrorPayload({
4236
+ body: {errorCode: 2423012},
4237
+ message: rawErrorMessage,
4238
+ statusCode: 429,
4239
+ options: {uri: 'https://locus.example.com/locus/api/v1/loci/call'},
4240
+ });
4241
+
4242
+ assert.deepEqual(res, {
4243
+ category: 'expected',
4244
+ errorCode: 12000,
4245
+ errorDescription: 'FraudDetection',
4246
+ fatal: true,
4247
+ name: 'locus.response',
4248
+ rawErrorMessage,
4249
+ serviceErrorCode: 2423012,
4250
+ shownToUser: true,
4251
+ httpCode: 429,
4252
+ });
4253
+ });
4254
+
4255
+ it('should retain an unmapped service error code on the Locus HTTP status payload', () => {
4256
+ const rawErrorMessage = 'Locus rate limited';
4257
+ const [res] = cd.generateClientEventErrorPayload({
4258
+ body: {errorCode: 2429999},
4259
+ message: rawErrorMessage,
4260
+ statusCode: 429,
4261
+ options: {uri: 'https://locus.example.com/locus/api/v1/loci/call'},
4262
+ });
4263
+
4264
+ assert.deepEqual(res, {
4265
+ category: 'signaling',
4266
+ errorCode: 1002,
4267
+ errorDescription: 'LocusRateLimitedOutgoing',
4268
+ fatal: true,
4269
+ name: 'locus.response',
4270
+ rawErrorMessage,
4271
+ serviceErrorCode: 2429999,
4272
+ shownToUser: false,
4273
+ httpCode: 429,
4274
+ });
4275
+ });
4276
+
4277
+ it('should not identify a non-Locus 429 as a Locus rate limit', () => {
4278
+ const [res] = cd.generateClientEventErrorPayload({
4279
+ message: 'Too many requests',
4280
+ statusCode: 429,
4281
+ options: {uri: 'https://example.com/api/v1/resource'},
4282
+ });
4283
+
4284
+ assert.deepEqual(res, {
4285
+ category: 'other',
4286
+ errorCode: 9999,
4287
+ errorDescription: 'UnknownError',
4288
+ fatal: true,
4289
+ name: 'other',
4290
+ rawErrorMessage: 'Too many requests',
4291
+ serviceErrorCode: 9999,
4292
+ shownToUser: false,
4293
+ httpCode: 429,
4294
+ });
4295
+ });
4296
+
4297
+ it('should keep the fallback classification when a 429 has no request URL', () => {
4298
+ const [res] = cd.generateClientEventErrorPayload({
4299
+ message: 'Too many requests',
4300
+ statusCode: 429,
4301
+ });
4302
+
4303
+ assert.equal(res.errorCode, 9999);
4304
+ assert.equal(res.httpCode, 429);
4305
+ });
4306
+
4307
+ it('should include httpCode for browser media errors', () => {
3930
4308
  const [res, cached] = cd.generateClientEventErrorPayload({
3931
4309
  name: 'PermissionDeniedError',
3932
4310
  message: 'bad times',
@@ -3944,11 +4322,11 @@ describe('internal-plugin-metrics', () => {
3944
4322
  rawErrorMessage: 'bad times',
3945
4323
  serviceErrorCode: undefined,
3946
4324
  shownToUser: false,
3947
- httpStatusCode: 401,
4325
+ httpCode: 401,
3948
4326
  });
3949
4327
  });
3950
4328
 
3951
- it('should include httpStatusCode for SdpOfferCreationErrors', () => {
4329
+ it('should include httpCode for SdpOfferCreationErrors', () => {
3952
4330
  const [res, cached] = cd.generateClientEventErrorPayload({
3953
4331
  name: 'SdpOfferCreationError',
3954
4332
  message: 'bad times',
@@ -3966,11 +4344,11 @@ describe('internal-plugin-metrics', () => {
3966
4344
  rawErrorMessage: 'bad times',
3967
4345
  serviceErrorCode: undefined,
3968
4346
  shownToUser: true,
3969
- httpStatusCode: 404,
4347
+ httpCode: 404,
3970
4348
  });
3971
4349
  });
3972
4350
 
3973
- it('should include httpStatusCode for service error codes', () => {
4351
+ it('should include httpCode for service error codes', () => {
3974
4352
  const [res, cached] = cd.generateClientEventErrorPayload({
3975
4353
  body: {errorCode: 58400},
3976
4354
  message: 'bad times',
@@ -3985,11 +4363,11 @@ describe('internal-plugin-metrics', () => {
3985
4363
  rawErrorMessage: 'bad times',
3986
4364
  serviceErrorCode: 58400,
3987
4365
  shownToUser: false,
3988
- httpStatusCode: 400,
4366
+ httpCode: 400,
3989
4367
  });
3990
4368
  });
3991
4369
 
3992
- it('should include httpStatusCode for locus service error codes', () => {
4370
+ it('should include httpCode for locus service error codes', () => {
3993
4371
  const [res, cached] = cd.generateClientEventErrorPayload({
3994
4372
  body: {errorCode: 2403001},
3995
4373
  message: 'bad times',
@@ -4004,11 +4382,11 @@ describe('internal-plugin-metrics', () => {
4004
4382
  rawErrorMessage: 'bad times',
4005
4383
  serviceErrorCode: 2403001,
4006
4384
  shownToUser: false,
4007
- httpStatusCode: 400,
4385
+ httpCode: 400,
4008
4386
  });
4009
4387
  });
4010
4388
 
4011
- it('should include httpStatusCode for meetingInfo service error codes', () => {
4389
+ it('should include httpCode for meetingInfo service error codes', () => {
4012
4390
  const [res, cached] = cd.generateClientEventErrorPayload({
4013
4391
  body: {data: {meetingInfo: {}}},
4014
4392
  message: 'bad times',
@@ -4023,11 +4401,11 @@ describe('internal-plugin-metrics', () => {
4023
4401
  rawErrorMessage: 'bad times',
4024
4402
  serviceErrorCode: undefined,
4025
4403
  shownToUser: false,
4026
- httpStatusCode: 400,
4404
+ httpCode: 400,
4027
4405
  });
4028
4406
  });
4029
4407
 
4030
- it('should include httpStatusCode for network errors', () => {
4408
+ it('should include httpCode for network errors', () => {
4031
4409
  const error = new WebexHttpError.NetworkOrCORSError({
4032
4410
  statusCode: 400,
4033
4411
  options: {service: '', headers: {}},
@@ -4042,11 +4420,11 @@ describe('internal-plugin-metrics', () => {
4042
4420
  rawErrorMessage: 'undefined\nundefined /undefined\nWEBEX_TRACKING_ID: undefined\n',
4043
4421
  serviceErrorCode: undefined,
4044
4422
  shownToUser: false,
4045
- httpStatusCode: 400,
4423
+ httpCode: 400,
4046
4424
  });
4047
4425
  });
4048
4426
 
4049
- it('should include httpStatusCode for unauthorized errors', () => {
4427
+ it('should include httpCode for unauthorized errors', () => {
4050
4428
  const error = new WebexHttpError.Unauthorized({
4051
4429
  statusCode: 401,
4052
4430
  options: {service: '', headers: {}},
@@ -4061,11 +4439,11 @@ describe('internal-plugin-metrics', () => {
4061
4439
  rawErrorMessage: 'undefined\nundefined /undefined\nWEBEX_TRACKING_ID: undefined\n',
4062
4440
  serviceErrorCode: undefined,
4063
4441
  shownToUser: false,
4064
- httpStatusCode: 401,
4442
+ httpCode: 401,
4065
4443
  });
4066
4444
  });
4067
4445
 
4068
- it('should include httpStatusCode for unknown errors', () => {
4446
+ it('should include httpCode for unknown errors', () => {
4069
4447
  const [res, cached] = cd.generateClientEventErrorPayload({
4070
4448
  message: 'bad times',
4071
4449
  statusCode: 404,
@@ -4079,7 +4457,7 @@ describe('internal-plugin-metrics', () => {
4079
4457
  serviceErrorCode: 9999,
4080
4458
  errorDescription: 'UnknownError',
4081
4459
  rawErrorMessage: 'bad times',
4082
- httpStatusCode: 404,
4460
+ httpCode: 404,
4083
4461
  });
4084
4462
  });
4085
4463
  });
@@ -4270,6 +4648,18 @@ describe('internal-plugin-metrics', () => {
4270
4648
  });
4271
4649
 
4272
4650
  describe('#buildClientEventFetchRequestOptions', () => {
4651
+ it('should add the locusCluster option to a pre-meeting event', async () => {
4652
+ const fetchOptions = await cd.buildClientEventFetchRequestOptions({
4653
+ name: 'client.interstitial-window.launched',
4654
+ options: {correlationId: 'correlationId', locusCluster: 'locus-k.wbx2.com'},
4655
+ });
4656
+
4657
+ assert.equal(
4658
+ fetchOptions.body.metrics[0].eventPayload.event.identifiers.locusCluster,
4659
+ 'locus-k.wbx2.com'
4660
+ );
4661
+ });
4662
+
4273
4663
  [undefined, 'myPreLoginId'].forEach((preLoginId) => {
4274
4664
  it('returns expected options without preLoginId', async () => {
4275
4665
  const options = {
@@ -4469,6 +4859,80 @@ describe('internal-plugin-metrics', () => {
4469
4859
  options.meetingJoinPhase
4470
4860
  );
4471
4861
  });
4862
+
4863
+ it('includes expected meetingJoinPhase from meeting callStateForMetrics when in-meeting', async () => {
4864
+ // meetingId means in-meeting
4865
+ const options = {
4866
+ meetingId: fakeMeeting6.id,
4867
+ };
4868
+
4869
+ const fetchOptions = await cd.buildClientEventFetchRequestOptions({
4870
+ name: 'client.exit.app',
4871
+ payload: {trigger: 'user-interaction', canProceed: false},
4872
+ options,
4873
+ });
4874
+
4875
+ assert.equal(fetchOptions.body.metrics[0].eventPayload.event.meetingJoinPhase, 'in-meeting');
4876
+ });
4877
+
4878
+ it('prioritizes meetingJoinPhase from options over meeting callStateForMetrics', async () => {
4879
+ // meetingId means in-meeting
4880
+ const options = {
4881
+ meetingId: fakeMeeting6.id,
4882
+ meetingJoinPhase: 'join',
4883
+ };
4884
+
4885
+ const fetchOptions = await cd.buildClientEventFetchRequestOptions({
4886
+ name: 'client.exit.app',
4887
+ payload: {trigger: 'user-interaction', canProceed: false},
4888
+ options,
4889
+ });
4890
+
4891
+ assert.equal(
4892
+ fetchOptions.body.metrics[0].eventPayload.event.meetingJoinPhase,
4893
+ options.meetingJoinPhase
4894
+ );
4895
+ });
4896
+
4897
+ it('defaults meetingJoinPhase to pre-join during prejoin when not provided in options', async () => {
4898
+ // correlationId and no meeting id means prejoin
4899
+ const options = {
4900
+ correlationId: 'myCorrelationId',
4901
+ preLoginId: 'myPreLoginId',
4902
+ };
4903
+
4904
+ const fetchOptions = await cd.buildClientEventFetchRequestOptions({
4905
+ name: 'client.exit.app',
4906
+ payload: {trigger: 'user-interaction', canProceed: false},
4907
+ options,
4908
+ });
4909
+
4910
+ assert.equal(fetchOptions.body.metrics[0].eventPayload.event.meetingJoinPhase, 'pre-join');
4911
+ });
4912
+
4913
+ it('builds request options before webex is ready (no meetings plugin or internal metrics config)', async () => {
4914
+ // before webex.ready the meetings plugin and internal metrics config are not available
4915
+ webex.meetings = undefined;
4916
+ webex.internal.metrics = undefined;
4917
+
4918
+ const options = {
4919
+ correlationId: 'myCorrelationId',
4920
+ clientType: 'TEAMS_CLIENT',
4921
+ subClientType: 'WEB_APP',
4922
+ };
4923
+
4924
+ const fetchOptions = await cd.buildClientEventFetchRequestOptions({
4925
+ name: 'client.exit.app',
4926
+ payload: {trigger: 'user-interaction', canProceed: false},
4927
+ options,
4928
+ });
4929
+
4930
+ const eventPayload = fetchOptions.body.metrics[0].eventPayload;
4931
+ assert.equal(eventPayload.event.name, 'client.exit.app');
4932
+ assert.isUndefined(eventPayload.senderCountryCode);
4933
+ assert.isUndefined(fetchOptions.waitForServiceTimeout);
4934
+ assert.equal(fetchOptions.resource, 'clientmetrics');
4935
+ });
4472
4936
  });
4473
4937
 
4474
4938
  describe('#submitToCallDiagnosticsPreLogin', () => {