@webex/contact-center 3.12.0-llmrefactor.7 → 3.12.0-llmrefactor.9

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 (69) hide show
  1. package/.sdd/manifest.json +3 -2
  2. package/ai-docs/CONTRACTS.md +3 -2
  3. package/dist/cc.js +108 -6
  4. package/dist/cc.js.map +1 -1
  5. package/dist/metrics/MetricsManager.js +16 -0
  6. package/dist/metrics/MetricsManager.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +91 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +16 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/AnswerCallOnWebexService.js +34 -6
  12. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  13. package/dist/services/WebexCrossClientService.js +54 -3
  14. package/dist/services/WebexCrossClientService.js.map +1 -1
  15. package/dist/services/WxAppTelephonyMercurySync.js +21 -6
  16. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  17. package/dist/services/task/state-machine/TaskStateMachine.js +0 -17
  18. package/dist/services/task/state-machine/TaskStateMachine.js.map +1 -1
  19. package/dist/services/task/state-machine/guards.js +7 -37
  20. package/dist/services/task/state-machine/guards.js.map +1 -1
  21. package/dist/services/task/voice/Voice.js +81 -14
  22. package/dist/services/task/voice/Voice.js.map +1 -1
  23. package/dist/services/task/voice/wxAppVoiceMethods.js +154 -1
  24. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  25. package/dist/services/wxAppDiagnosticLogging.js +122 -0
  26. package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
  27. package/dist/types/cc.d.ts +1 -0
  28. package/dist/types/metrics/MetricsManager.d.ts +6 -0
  29. package/dist/types/metrics/constants.d.ts +15 -0
  30. package/dist/types/services/AnswerCallOnWebexService.d.ts +1 -0
  31. package/dist/types/services/WebexCrossClientService.d.ts +4 -0
  32. package/dist/types/services/task/state-machine/TaskStateMachine.d.ts +0 -42
  33. package/dist/types/services/task/state-machine/guards.d.ts +4 -4
  34. package/dist/types/services/task/voice/Voice.d.ts +5 -1
  35. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +3 -0
  36. package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
  37. package/dist/webex.js +1 -1
  38. package/package.json +8 -8
  39. package/src/cc.ts +155 -7
  40. package/src/metrics/MetricsManager.ts +16 -0
  41. package/src/metrics/ai-docs/metrics-spec.md +71 -9
  42. package/src/metrics/behavioral-events.ts +92 -0
  43. package/src/metrics/constants.ts +18 -0
  44. package/src/services/AnswerCallOnWebexService.ts +41 -8
  45. package/src/services/WebexCrossClientService.ts +85 -4
  46. package/src/services/WxAppTelephonyMercurySync.ts +31 -4
  47. package/src/services/agent/ai-docs/agent-spec.md +1 -0
  48. package/src/services/task/ai-docs/task-spec.md +17 -16
  49. package/src/services/task/state-machine/TaskStateMachine.ts +0 -20
  50. package/src/services/task/state-machine/ai-docs/AGENTS.md +2 -4
  51. package/src/services/task/state-machine/ai-docs/ARCHITECTURE.md +8 -9
  52. package/src/services/task/state-machine/ai-docs/task-state-machine-spec.md +5 -7
  53. package/src/services/task/state-machine/guards.ts +7 -63
  54. package/src/services/task/voice/Voice.ts +115 -26
  55. package/src/services/task/voice/wxAppVoiceMethods.ts +211 -2
  56. package/src/services/wxAppDiagnosticLogging.ts +174 -0
  57. package/test/unit/spec/cc.ts +251 -22
  58. package/test/unit/spec/metrics/MetricsManager.ts +25 -0
  59. package/test/unit/spec/metrics/behavioral-events.ts +42 -0
  60. package/test/unit/spec/services/AnswerCallOnWebexService.ts +41 -0
  61. package/test/unit/spec/services/WebexCrossClientService.ts +207 -1
  62. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +23 -0
  63. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +72 -46
  64. package/test/unit/spec/services/task/state-machine/guards.ts +6 -6
  65. package/test/unit/spec/services/task/voice/Voice.ts +380 -0
  66. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +145 -1
  67. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
  68. package/umd/contact-center.min.js +2 -2
  69. package/umd/contact-center.min.js.map +1 -1
@@ -729,7 +729,7 @@ describe('webex.cc', () => {
729
729
  expect(registerWebCallingLineSpy).toHaveBeenCalled();
730
730
  expect(stationLoginMock).toHaveBeenCalledWith({
731
731
  data: {
732
- dialNumber: 'agentId',
732
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
733
733
  teamId: 'teamId',
734
734
  deviceType: LoginOption.BROWSER,
735
735
  isExtension: false,
@@ -889,7 +889,7 @@ describe('webex.cc', () => {
889
889
  expect(registerWebCallingLineSpy).not.toHaveBeenCalled();
890
890
  expect(stationLoginSpy).toHaveBeenCalledWith({
891
891
  data: {
892
- dialNumber: 'agentId',
892
+ dialNumber: `${WEB_RTC_PREFIX}agentId`,
893
893
  teamId: 'teamId',
894
894
  deviceType: LoginOption.BROWSER,
895
895
  isExtension: false,
@@ -1057,6 +1057,7 @@ describe('webex.cc', () => {
1057
1057
  const subscribeSpy = jest
1058
1058
  .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1059
1059
  .mockImplementation(() => {});
1060
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1060
1061
 
1061
1062
  jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1062
1063
  data: {
@@ -1073,7 +1074,7 @@ describe('webex.cc', () => {
1073
1074
  dialNumber: '1001',
1074
1075
  });
1075
1076
 
1076
- expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
1077
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1077
1078
  expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
1078
1079
  });
1079
1080
 
@@ -1135,6 +1136,7 @@ describe('webex.cc', () => {
1135
1136
  jest
1136
1137
  .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1137
1138
  .mockResolvedValue(undefined);
1139
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1138
1140
 
1139
1141
  jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1140
1142
  data: {
@@ -1154,10 +1156,62 @@ describe('webex.cc', () => {
1154
1156
  expect(publishSpy).not.toHaveBeenCalled();
1155
1157
  expect(teardownSpy).not.toHaveBeenCalled();
1156
1158
  expect(unsubscribeSpy).toHaveBeenCalled();
1159
+ expect(metricSpy).toHaveBeenCalledWith(
1160
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1161
+ expect.objectContaining({skipReason: 'mercury_subscribe_failed'}),
1162
+ ['operational', 'behavioral']
1163
+ );
1157
1164
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1158
1165
  expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
1159
1166
  });
1160
1167
 
1168
+ it('should record publish_failed when usersub publish fails after Mercury init', async () => {
1169
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1170
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1171
+ webex.cc.agentConfig = {
1172
+ agentId: 'agentId',
1173
+ webRtcEnabled: false,
1174
+ loginVoiceOptions: ['EXTENSION'],
1175
+ };
1176
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1177
+ mockTaskManager.applyEnableWxBetterTogether(true);
1178
+
1179
+ jest.spyOn(webex.cc as any, 'ensureWxAppMercuryConnected').mockResolvedValue(undefined);
1180
+ jest
1181
+ .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
1182
+ .mockImplementation(() => {});
1183
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1184
+ jest
1185
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1186
+ .mockRejectedValue(new Error('publish failed'));
1187
+ jest
1188
+ .spyOn(webex.cc as any, 'releaseWxAppMercuryResources')
1189
+ .mockResolvedValue(undefined);
1190
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1191
+
1192
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1193
+ data: {
1194
+ agentId: 'agentId',
1195
+ teamId: 'teamId',
1196
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1197
+ },
1198
+ trackingId: 'track-1',
1199
+ } as StationLoginSuccess);
1200
+
1201
+ await webex.cc.stationLogin({
1202
+ teamId: 'teamId',
1203
+ loginOption: LoginOption.EXTENSION,
1204
+ dialNumber: '1001',
1205
+ });
1206
+
1207
+ expect(metricSpy).toHaveBeenCalledWith(
1208
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1209
+ expect.objectContaining({skipReason: 'publish_failed'}),
1210
+ ['operational', 'behavioral']
1211
+ );
1212
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
1213
+ });
1214
+
1161
1215
  it('should register device before publishing usersub true on extension station login', async () => {
1162
1216
  webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1163
1217
  webex.internal.device = {
@@ -1182,6 +1236,7 @@ describe('webex.cc', () => {
1182
1236
  };
1183
1237
  webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1184
1238
  jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1239
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1185
1240
 
1186
1241
  const callOrder: string[] = [];
1187
1242
  jest.spyOn(webex.internal.device, 'register').mockImplementation(async function (this: {
@@ -1330,7 +1385,7 @@ describe('webex.cc', () => {
1330
1385
  await publishSpy.mock.results[1]?.value;
1331
1386
 
1332
1387
  expect(publishSpy).toHaveBeenCalledTimes(2);
1333
- expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
1388
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1334
1389
  } finally {
1335
1390
  jest.useRealTimers();
1336
1391
  }
@@ -1399,9 +1454,174 @@ describe('webex.cc', () => {
1399
1454
  dialNumber: '1001',
1400
1455
  });
1401
1456
 
1402
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1457
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1403
1458
  expect(teardownSpy).toHaveBeenCalled();
1404
1459
  expect(unsubscribeSpy).toHaveBeenCalled();
1460
+ expect(mockMetricsManager.trackEvent).toHaveBeenCalledWith(
1461
+ METRIC_EVENT_NAMES.WXAPP_SESSION_SKIPPED,
1462
+ expect.objectContaining({
1463
+ enableWxBetterTogether: false,
1464
+ skipReason: 'flag_disabled',
1465
+ }),
1466
+ ['operational', 'behavioral']
1467
+ );
1468
+ });
1469
+
1470
+ it('should not emit WXAPP_SESSION_INIT_FAILED when flag-disabled cleanup fails', async () => {
1471
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
1472
+ webex.internal.device = {userId: 'user-123', url: 'https://wdm.example.com/devices/dev-1'};
1473
+ webex.cc.agentConfig = {
1474
+ agentId: 'agentId',
1475
+ webRtcEnabled: false,
1476
+ loginVoiceOptions: ['EXTENSION'],
1477
+ };
1478
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1479
+
1480
+ jest
1481
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1482
+ .mockRejectedValue(new Error('forced false publish failed'));
1483
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1484
+
1485
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1486
+ data: {
1487
+ agentId: 'agentId',
1488
+ teamId: 'teamId',
1489
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1490
+ },
1491
+ trackingId: 'track-1',
1492
+ } as StationLoginSuccess);
1493
+
1494
+ await webex.cc.stationLogin({
1495
+ teamId: 'teamId',
1496
+ loginOption: LoginOption.EXTENSION,
1497
+ dialNumber: '1001',
1498
+ });
1499
+
1500
+ expect(metricSpy).not.toHaveBeenCalledWith(
1501
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1502
+ expect.anything(),
1503
+ expect.anything()
1504
+ );
1505
+ });
1506
+
1507
+ it('should still publish usersub but record session init failed when Mercury is unavailable', async () => {
1508
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1509
+ webex.internal.device = {
1510
+ userId: 'user-123',
1511
+ url: 'https://wdm.example.com/devices/dev-1',
1512
+ registered: true,
1513
+ register: jest.fn().mockResolvedValue(undefined),
1514
+ };
1515
+ delete webex.internal.mercury;
1516
+ webex.cc.agentConfig = {
1517
+ agentId: 'agentId',
1518
+ webRtcEnabled: false,
1519
+ loginVoiceOptions: ['EXTENSION'],
1520
+ };
1521
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1522
+ mockTaskManager.applyEnableWxBetterTogether(true);
1523
+
1524
+ const publishSpy = jest
1525
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1526
+ .mockResolvedValue(undefined);
1527
+ jest
1528
+ .spyOn(webex.cc['webexCrossClientService'], 'isAnswerCallsStateActive')
1529
+ .mockReturnValue(true);
1530
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1531
+
1532
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1533
+ data: {
1534
+ agentId: 'agentId',
1535
+ teamId: 'teamId',
1536
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1537
+ },
1538
+ trackingId: 'track-1',
1539
+ } as StationLoginSuccess);
1540
+
1541
+ await webex.cc.stationLogin({
1542
+ teamId: 'teamId',
1543
+ loginOption: LoginOption.EXTENSION,
1544
+ dialNumber: '1001',
1545
+ });
1546
+
1547
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
1548
+ expect(metricSpy).toHaveBeenCalledWith(
1549
+ METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
1550
+ expect.objectContaining({error: 'Mercury is unavailable for wxApp mute sync'}),
1551
+ ['operational', 'behavioral']
1552
+ );
1553
+ expect(metricSpy).not.toHaveBeenCalledWith(
1554
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1555
+ expect.anything(),
1556
+ expect.anything()
1557
+ );
1558
+ expect(metricSpy).toHaveBeenCalledWith(
1559
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1560
+ expect.objectContaining({skipReason: 'mercury_not_subscribed'}),
1561
+ ['operational', 'behavioral']
1562
+ );
1563
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1564
+ });
1565
+
1566
+ it('should record session init failed when userId is missing but not emit success', async () => {
1567
+ webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: true};
1568
+ webex.internal.device = {
1569
+ url: 'https://wdm.example.com/devices/dev-1',
1570
+ registered: true,
1571
+ register: jest.fn().mockResolvedValue(undefined),
1572
+ };
1573
+ webex.internal.mercury = {
1574
+ connected: true,
1575
+ connect: jest.fn().mockResolvedValue(undefined),
1576
+ on: jest.fn(),
1577
+ off: jest.fn(),
1578
+ };
1579
+ webex.cc.agentConfig = {
1580
+ agentId: 'agentId',
1581
+ webRtcEnabled: false,
1582
+ loginVoiceOptions: ['EXTENSION'],
1583
+ };
1584
+ webex.cc.webCallingService.loginOption = LoginOption.EXTENSION;
1585
+ mockTaskManager.applyEnableWxBetterTogether(true);
1586
+
1587
+ const publishSpy = jest
1588
+ .spyOn(webex.cc['webexCrossClientService'], 'setManageWebexCallingInWxcc')
1589
+ .mockResolvedValue(undefined);
1590
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1591
+ const metricSpy = jest.spyOn(mockMetricsManager, 'trackEvent');
1592
+
1593
+ jest.spyOn(webex.cc.services.agent, 'stationLogin').mockResolvedValue({
1594
+ data: {
1595
+ agentId: 'agentId',
1596
+ teamId: 'teamId',
1597
+ channelsMap: {chat: [], email: [], social: [], telephony: []},
1598
+ },
1599
+ trackingId: 'track-1',
1600
+ } as StationLoginSuccess);
1601
+
1602
+ await webex.cc.stationLogin({
1603
+ teamId: 'teamId',
1604
+ loginOption: LoginOption.EXTENSION,
1605
+ dialNumber: '1001',
1606
+ });
1607
+
1608
+ expect(publishSpy).not.toHaveBeenCalledWith(true, expect.anything());
1609
+ expect(metricSpy).not.toHaveBeenCalledWith(
1610
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_SUCCESS,
1611
+ expect.anything(),
1612
+ expect.anything()
1613
+ );
1614
+ expect(metricSpy).toHaveBeenCalledWith(
1615
+ METRIC_EVENT_NAMES.WXAPP_SESSION_INIT_FAILED,
1616
+ expect.objectContaining({skipReason: 'usersub_not_published'}),
1617
+ ['operational', 'behavioral']
1618
+ );
1619
+ expect(metricSpy).toHaveBeenCalledWith(
1620
+ METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
1621
+ expect.objectContaining({skipReason: 'user_id_unavailable'}),
1622
+ ['operational', 'behavioral']
1623
+ );
1624
+ expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
1405
1625
  });
1406
1626
 
1407
1627
  it('should clear stale usersub on login after prior session enabled wxApp then hard refresh', async () => {
@@ -1444,7 +1664,7 @@ describe('webex.cc', () => {
1444
1664
  dialNumber: '1001',
1445
1665
  });
1446
1666
 
1447
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1667
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1448
1668
  });
1449
1669
  });
1450
1670
 
@@ -1521,6 +1741,7 @@ describe('webex.cc', () => {
1521
1741
  disconnect: jest.fn().mockResolvedValue(undefined),
1522
1742
  };
1523
1743
  jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1744
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1524
1745
  webex.request = jest.fn().mockResolvedValue({body: {}});
1525
1746
  webex.cc['agentConfig'] = {
1526
1747
  agentId: 'agent-123',
@@ -1542,7 +1763,7 @@ describe('webex.cc', () => {
1542
1763
 
1543
1764
  await webex.cc.stationLogout({logoutReason: 'Logout reason'});
1544
1765
 
1545
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1766
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1546
1767
  expect(teardownSpy).toHaveBeenCalled();
1547
1768
  expect(unsubscribeSpy).toHaveBeenCalled();
1548
1769
  });
@@ -1613,7 +1834,7 @@ describe('webex.cc', () => {
1613
1834
 
1614
1835
  expect(result).toEqual({trackingId: 'track-1'});
1615
1836
  expect(publishSpy).toHaveBeenCalledTimes(1);
1616
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
1837
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1617
1838
  expect(teardownSpy).toHaveBeenCalled();
1618
1839
  expect(unsubscribeSpy).toHaveBeenCalled();
1619
1840
 
@@ -1621,7 +1842,7 @@ describe('webex.cc', () => {
1621
1842
  await publishSpy.mock.results[1]?.value;
1622
1843
 
1623
1844
  expect(publishSpy).toHaveBeenCalledTimes(2);
1624
- expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
1845
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
1625
1846
  } finally {
1626
1847
  jest.useRealTimers();
1627
1848
  }
@@ -1643,6 +1864,7 @@ describe('webex.cc', () => {
1643
1864
  disconnect: jest.fn().mockResolvedValue(undefined),
1644
1865
  };
1645
1866
  jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
1867
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
1646
1868
  webex.cc['agentConfig'] = {
1647
1869
  agentId: 'agent-123',
1648
1870
  deviceType: LoginOption.EXTENSION,
@@ -1683,8 +1905,8 @@ describe('webex.cc', () => {
1683
1905
  });
1684
1906
 
1685
1907
  expect(publishSpy).toHaveBeenCalledTimes(2);
1686
- expect(publishSpy).toHaveBeenNthCalledWith(1, false, {userId: 'user-123'});
1687
- expect(publishSpy).toHaveBeenNthCalledWith(2, true, {userId: 'user-123'});
1908
+ expect(publishSpy).toHaveBeenNthCalledWith(1, false, {userId: 'user-123', trackPublishMetrics: true});
1909
+ expect(publishSpy).toHaveBeenNthCalledWith(2, true, {userId: 'user-123', trackPublishMetrics: true});
1688
1910
  expect(teardownSpy).toHaveBeenCalledTimes(1);
1689
1911
 
1690
1912
  jest.advanceTimersByTime(30_000);
@@ -2208,10 +2430,11 @@ describe('webex.cc', () => {
2208
2430
  const subscribeSpy = jest
2209
2431
  .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
2210
2432
  .mockImplementation(() => {});
2433
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
2211
2434
 
2212
2435
  await webex.cc['silentRelogin']();
2213
2436
 
2214
- expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
2437
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
2215
2438
  expect(subscribeSpy).toHaveBeenCalledWith('agentId', expect.any(Function));
2216
2439
  });
2217
2440
 
@@ -2244,7 +2467,7 @@ describe('webex.cc', () => {
2244
2467
 
2245
2468
  await webex.cc['silentRelogin']();
2246
2469
 
2247
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
2470
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2248
2471
  expect(teardownSpy).toHaveBeenCalled();
2249
2472
  expect(unsubscribeSpy).toHaveBeenCalled();
2250
2473
  });
@@ -2725,7 +2948,7 @@ describe('webex.cc', () => {
2725
2948
 
2726
2949
  await (webex.cc as any).teardownWxAppLocalState();
2727
2950
 
2728
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
2951
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
2729
2952
 
2730
2953
  resolveEnable();
2731
2954
  await enableInFlight;
@@ -3068,6 +3291,7 @@ describe('webex.cc', () => {
3068
3291
  jest
3069
3292
  .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3070
3293
  .mockImplementation(() => {});
3294
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3071
3295
  jest.spyOn(webex.cc as any, 'releaseWxAppMercuryResources').mockResolvedValue(undefined);
3072
3296
 
3073
3297
  jest.spyOn(webex.cc.services.agent, 'logout').mockResolvedValue({
@@ -3091,8 +3315,8 @@ describe('webex.cc', () => {
3091
3315
  });
3092
3316
 
3093
3317
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3094
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
3095
- expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3318
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3319
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3096
3320
  });
3097
3321
 
3098
3322
  it('should use provided teamId if passed in payload', async () => {
@@ -3622,6 +3846,7 @@ describe('webex.cc', () => {
3622
3846
  disconnect: jest.fn().mockResolvedValue(undefined),
3623
3847
  };
3624
3848
  jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
3849
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3625
3850
  webex.cc.$config = {...webex.cc.$config, enableWxBetterTogether: false};
3626
3851
  webex.cc['agentConfig'] = {
3627
3852
  agentId: 'agent-123',
@@ -3646,7 +3871,7 @@ describe('webex.cc', () => {
3646
3871
 
3647
3872
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3648
3873
  expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(true);
3649
- expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3874
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3650
3875
  });
3651
3876
 
3652
3877
  it('should publish false and update config when disabled after wxApp was enabled', async () => {
@@ -3666,7 +3891,7 @@ describe('webex.cc', () => {
3666
3891
 
3667
3892
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(false);
3668
3893
  expect(mockTaskManager.applyEnableWxBetterTogether).toHaveBeenCalledWith(false);
3669
- expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123'});
3894
+ expect(publishSpy).toHaveBeenCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3670
3895
  });
3671
3896
 
3672
3897
  it('should skip usersub publish when disabling without prior wxApp publish', async () => {
@@ -3747,7 +3972,7 @@ describe('webex.cc', () => {
3747
3972
  await (webex.cc as any).setManageWebexCallingInWxcc(true);
3748
3973
 
3749
3974
  expect(webex.cc.isWxBetterTogetherEnabled()).toBe(true);
3750
- expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123'});
3975
+ expect(publishSpy).toHaveBeenCalledWith(true, {userId: 'user-123', trackPublishMetrics: true});
3751
3976
  });
3752
3977
 
3753
3978
  it('should no-op usersub publish when disabling before login', async () => {
@@ -3778,6 +4003,7 @@ describe('webex.cc', () => {
3778
4003
  const subscribeSpy = jest
3779
4004
  .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3780
4005
  .mockImplementation(() => {});
4006
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3781
4007
 
3782
4008
  await (webex.cc as any).setManageWebexCallingInWxcc(true);
3783
4009
 
@@ -3792,6 +4018,7 @@ describe('webex.cc', () => {
3792
4018
  const unsubscribeSpy = jest
3793
4019
  .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'unsubscribe')
3794
4020
  .mockImplementation(() => {});
4021
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3795
4022
 
3796
4023
  await (webex.cc as any).setManageWebexCallingInWxcc(false);
3797
4024
 
@@ -3815,6 +4042,7 @@ describe('webex.cc', () => {
3815
4042
  const subscribeSpy = jest
3816
4043
  .spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe')
3817
4044
  .mockImplementation(() => {});
4045
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3818
4046
 
3819
4047
  await (webex.cc as any).setManageWebexCallingInWxcc(true);
3820
4048
 
@@ -3879,6 +4107,7 @@ describe('webex.cc', () => {
3879
4107
  .mockRejectedValueOnce(new Error('compensating false failed'))
3880
4108
  .mockResolvedValue(undefined);
3881
4109
  jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'subscribe').mockImplementation(() => {});
4110
+ jest.spyOn(webex.cc['wxAppTelephonyMercurySync'], 'isSubscribed').mockReturnValue(true);
3882
4111
  jest
3883
4112
  .spyOn(webex.cc as any, 'ensureWxAppMercuryConnected')
3884
4113
  .mockResolvedValue(undefined);
@@ -3901,14 +4130,14 @@ describe('webex.cc', () => {
3901
4130
 
3902
4131
  expect(scheduleRetrySpy).toHaveBeenCalled();
3903
4132
  expect(publishSpy).toHaveBeenCalledTimes(2);
3904
- expect(publishSpy).toHaveBeenNthCalledWith(1, true, {userId: 'user-123'});
3905
- expect(publishSpy).toHaveBeenNthCalledWith(2, false, {userId: 'user-123'});
4133
+ expect(publishSpy).toHaveBeenNthCalledWith(1, true, {userId: 'user-123', trackPublishMetrics: true});
4134
+ expect(publishSpy).toHaveBeenNthCalledWith(2, false, {userId: 'user-123', trackPublishMetrics: true});
3906
4135
 
3907
4136
  jest.advanceTimersByTime(30_000);
3908
4137
  await publishSpy.mock.results[2]?.value;
3909
4138
 
3910
4139
  expect(publishSpy).toHaveBeenCalledTimes(3);
3911
- expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123'});
4140
+ expect(publishSpy).toHaveBeenLastCalledWith(false, {userId: 'user-123', trackPublishMetrics: true});
3912
4141
  } finally {
3913
4142
  jest.useRealTimers();
3914
4143
  }
@@ -252,6 +252,31 @@ describe('MetricsManagerImplementation', () => {
252
252
  });
253
253
  });
254
254
 
255
+ describe('cancelTimedEvent', () => {
256
+ it('should clear a running timer without attaching duration to a later unrelated event', () => {
257
+ jest.spyOn(MetricsManager.prototype as any, 'isMetricsDisabled').mockReturnValue(false);
258
+ metricsManager.timeEvent(['timedSuccess', 'timedFailed']);
259
+ metricsManager.cancelTimedEvent(['timedSuccess', 'timedFailed']);
260
+
261
+ const result = metricsManager['addDurationIfTimed']('timedFailed');
262
+
263
+ expect(result).toEqual({});
264
+ expect(metricsManager['runningEvents']['timedSuccess']).toBeUndefined();
265
+ });
266
+
267
+ it('should not clear timers when metrics are disabled', () => {
268
+ jest.spyOn(MetricsManager.prototype as any, 'isMetricsDisabled').mockReturnValue(true);
269
+ metricsManager['runningEvents']['event1'] = {
270
+ startTime: 100,
271
+ keys: new Set(['event1']),
272
+ };
273
+
274
+ metricsManager.cancelTimedEvent('event1');
275
+
276
+ expect(metricsManager['runningEvents']['event1']).toBeDefined();
277
+ });
278
+ });
279
+
255
280
  describe('addDurationIfTimed', () => {
256
281
  it('should return options when event name is not in runningEvents', () => {
257
282
  const options: EventPayload = {key: 'value'};
@@ -184,6 +184,48 @@ describe('metrics/behavioral-events', () => {
184
184
  verb: 'fail',
185
185
  });
186
186
 
187
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS)).toEqual({
188
+ product,
189
+ agent: 'user',
190
+ target: 'task_accept_webex_together',
191
+ verb: 'complete',
192
+ });
193
+
194
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_SESSION_SKIPPED)).toEqual({
195
+ product,
196
+ agent: 'user',
197
+ target: 'webex_together_session_init',
198
+ verb: 'ignore',
199
+ });
200
+
201
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS)).toEqual({
202
+ product,
203
+ agent: 'user',
204
+ target: 'webex_together_usersub_publish',
205
+ verb: 'complete',
206
+ });
207
+
208
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED)).toEqual({
209
+ product,
210
+ agent: 'user',
211
+ target: 'webex_together_mercury_subscribe',
212
+ verb: 'fail',
213
+ });
214
+
215
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_SUCCESS)).toEqual({
216
+ product,
217
+ agent: 'user',
218
+ target: 'task_mute_webex_together',
219
+ verb: 'complete',
220
+ });
221
+
222
+ expect(getEventTaxonomy(METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_FAILED)).toEqual({
223
+ product,
224
+ agent: 'user',
225
+ target: 'task_dtmf_webex_together',
226
+ verb: 'fail',
227
+ });
228
+
187
229
  expect(getEventTaxonomy('' as METRIC_EVENT_NAMES)).toEqual(undefined);
188
230
  });
189
231
  });
@@ -128,6 +128,27 @@ describe('AnswerCallOnWebexService', () => {
128
128
  });
129
129
  expect(LoggerProxy.error).toHaveBeenCalled();
130
130
  });
131
+
132
+ it('preserves top-level trackingId on request failure', async () => {
133
+ const err = {
134
+ statusCode: 503,
135
+ trackingId: 'track-top-level-1',
136
+ message: 'Service unavailable',
137
+ };
138
+ (webex.request as jest.Mock).mockRejectedValue(err);
139
+
140
+ await expect(service.answerCall({callId: 'c1', endpointId: 'e1'})).rejects.toMatchObject({
141
+ isWxAppTelephonyError: true,
142
+ trackingId: 'track-top-level-1',
143
+ status: 503,
144
+ });
145
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
146
+ expect.stringContaining('failed'),
147
+ expect.objectContaining({
148
+ data: expect.objectContaining({trackingId: 'track-top-level-1'}),
149
+ })
150
+ );
151
+ });
131
152
  });
132
153
 
133
154
  describe('rejectCall', () => {
@@ -213,6 +234,26 @@ describe('AnswerCallOnWebexService', () => {
213
234
  expect(LoggerProxy.error).toHaveBeenCalled();
214
235
  });
215
236
 
237
+ it('logs HTTP status on non-404 getCallDetails failure', async () => {
238
+ const err = {
239
+ statusCode: 503,
240
+ trackingId: 'track-top-level-1',
241
+ message: 'Service unavailable',
242
+ };
243
+ (webex.request as jest.Mock).mockRejectedValue(err);
244
+
245
+ await expect(service.getCallDetails({callId: 'c1'})).rejects.toBeDefined();
246
+ expect(LoggerProxy.error).toHaveBeenCalledWith(
247
+ expect.stringContaining('getCallDetails failed'),
248
+ expect.objectContaining({
249
+ data: expect.objectContaining({
250
+ trackingId: 'track-top-level-1',
251
+ status: 503,
252
+ }),
253
+ })
254
+ );
255
+ });
256
+
216
257
  it('rejects without error log for expected call-not-found during mute backfill race', async () => {
217
258
  (webex.request as jest.Mock).mockRejectedValue({status: 400, message: 'Call not found'});
218
259