@webex/contact-center 3.12.0-llmrefactor.13 → 3.12.0-llmrefactor.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/.sdd/manifest.json +3 -2
  2. package/ai-docs/CONTRACTS.md +7 -1
  3. package/ai-docs/contact-center-spec.md +7 -7
  4. package/ai-docs/features/contact-owner-change-refactor.md +78 -0
  5. package/dist/cc.js +39 -7
  6. package/dist/cc.js.map +1 -1
  7. package/dist/metrics/behavioral-events.js +6 -0
  8. package/dist/metrics/behavioral-events.js.map +1 -1
  9. package/dist/metrics/constants.js +1 -0
  10. package/dist/metrics/constants.js.map +1 -1
  11. package/dist/services/AnswerCallOnWebexService.js +4 -4
  12. package/dist/services/AnswerCallOnWebexService.js.map +1 -1
  13. package/dist/services/WebexCrossClientService.js +5 -2
  14. package/dist/services/WebexCrossClientService.js.map +1 -1
  15. package/dist/services/WxAppTelephonyMercurySync.js +6 -3
  16. package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
  17. package/dist/services/config/types.js.map +1 -1
  18. package/dist/services/task/TaskFactory.js +1 -0
  19. package/dist/services/task/TaskFactory.js.map +1 -1
  20. package/dist/services/task/TaskManager.js +50 -58
  21. package/dist/services/task/TaskManager.js.map +1 -1
  22. package/dist/services/task/types.js.map +1 -1
  23. package/dist/services/task/voice/Voice.js +63 -44
  24. package/dist/services/task/voice/Voice.js.map +1 -1
  25. package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
  26. package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
  27. package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
  28. package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
  29. package/dist/services/wxAppDiagnosticLogging.js +50 -13
  30. package/dist/services/wxAppDiagnosticLogging.js.map +1 -1
  31. package/dist/types/metrics/constants.d.ts +1 -0
  32. package/dist/types/services/config/types.d.ts +2 -0
  33. package/dist/types/services/task/types.d.ts +2 -0
  34. package/dist/types/services/task/voice/Voice.d.ts +15 -2
  35. package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
  36. package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
  37. package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -0
  38. package/dist/types.js.map +1 -1
  39. package/dist/webex.js +1 -1
  40. package/package.json +2 -2
  41. package/src/cc.ts +43 -7
  42. package/src/metrics/ai-docs/metrics-spec.md +44 -2
  43. package/src/metrics/behavioral-events.ts +6 -0
  44. package/src/metrics/constants.ts +1 -0
  45. package/src/services/AnswerCallOnWebexService.ts +4 -4
  46. package/src/services/WebexCrossClientService.ts +7 -2
  47. package/src/services/WxAppTelephonyMercurySync.ts +3 -3
  48. package/src/services/config/types.ts +2 -0
  49. package/src/services/task/TaskFactory.ts +1 -0
  50. package/src/services/task/TaskManager.ts +85 -87
  51. package/src/services/task/ai-docs/task-spec.md +7 -3
  52. package/src/services/task/types.ts +16 -0
  53. package/src/services/task/voice/Voice.ts +74 -52
  54. package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
  55. package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
  56. package/src/services/wxAppDiagnosticLogging.ts +135 -23
  57. package/src/types.ts +2 -0
  58. package/test/unit/spec/cc.ts +201 -19
  59. package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
  60. package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
  61. package/test/unit/spec/services/task/TaskManager.ts +214 -3
  62. package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
  63. package/test/unit/spec/services/task/voice/Voice.ts +10 -51
  64. package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
  65. package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
  66. package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
  67. package/umd/contact-center.min.js +2 -2
  68. package/umd/contact-center.min.js.map +1 -1
@@ -50,12 +50,11 @@ import {
50
50
  WxAppVoiceLifecycle,
51
51
  } from './wxAppVoiceMethods';
52
52
  import {
53
- deriveWxAppAcceptReason,
54
53
  logWxAppMercuryMuteSync,
55
- logWxAppOfferDecision,
56
54
  callIdSuffix,
57
- WxAppAcceptReason,
55
+ type WxAppAcceptReason,
58
56
  } from '../../wxAppDiagnosticLogging';
57
+ import {WxAppOfferObservability, WxAppOfferObservabilityContext} from './wxAppOfferObservability';
59
58
 
60
59
  const hasNonemptyPendingWrapUp = (taskData?: TaskData): boolean =>
61
60
  Array.isArray(taskData?.agentsPendingWrapUp) && taskData.agentsPendingWrapUp.length > 0;
@@ -87,7 +86,9 @@ export default class Voice extends Task implements IVoice {
87
86
  private wxAppMuteSyncInFlight?: Promise<boolean | undefined>;
88
87
  private wxAppMuteToggleTail: Promise<void> = Promise.resolve();
89
88
  private wxAppDtmfTail: Promise<void> = Promise.resolve();
90
- private lastLoggedWxAppAcceptReason?: WxAppAcceptReason;
89
+ /** Lazy — `super()` may call `updateUiControls` before field initializers run. */
90
+ private wxAppOfferObservability: WxAppOfferObservability | null = null;
91
+ private getUsersubPublished?: () => boolean;
91
92
 
92
93
  constructor(
93
94
  contact: ReturnType<typeof routingContact>,
@@ -117,6 +118,7 @@ export default class Voice extends Task implements IVoice {
117
118
 
118
119
  this.enableWxBetterTogether = resolvedOptions.enableWxBetterTogether;
119
120
  this.answerCallOnWebexService = callOptions?.answerCallOnWebexService;
121
+ this.getUsersubPublished = callOptions?.getUsersubPublished;
120
122
  }
121
123
 
122
124
  private getWxAppVoiceDependencies(): WxAppVoiceDependencies {
@@ -131,6 +133,7 @@ export default class Voice extends Task implements IVoice {
131
133
  setWxAppMuted: (muted: boolean) => {
132
134
  this.wxAppMuted = muted;
133
135
  },
136
+ getUsersubPublished: this.getUsersubPublished,
134
137
  };
135
138
  }
136
139
 
@@ -165,56 +168,54 @@ export default class Voice extends Task implements IVoice {
165
168
  this.updateUiControls(true);
166
169
  }
167
170
 
168
- protected updateUiControls(forceEmit = false): void {
169
- super.updateUiControls(forceEmit);
170
- this.logWxAppOfferDecisionIfNeeded();
171
- }
172
-
173
- private logWxAppOfferDecisionIfNeeded(): void {
174
- if (!this.enableWxBetterTogether) {
175
- return;
171
+ private getOrCreateWxAppOfferObservability(): WxAppOfferObservability {
172
+ if (!this.wxAppOfferObservability) {
173
+ this.wxAppOfferObservability = new WxAppOfferObservability();
176
174
  }
177
175
 
178
- const state = this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined;
179
- if (state !== TaskState.OFFERED) {
180
- return;
181
- }
182
-
183
- const accept = this.currentUiControls?.main?.accept;
184
- if (!accept?.isVisible) {
185
- return;
186
- }
176
+ return this.wxAppOfferObservability;
177
+ }
187
178
 
188
- const deps = this.getWxAppVoiceDependencies();
189
- const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
190
- const isWxAppInboundOffer = this.isWebexAppInboundCallingOffer();
191
- const isWxAppOutdialOffer = this.isWebexAppCallingOffer() && isOutdial;
192
- const deviceDetails = getCallingDeviceDetails(deps);
193
- const acceptReason = deriveWxAppAcceptReason({
194
- isWxAppInboundOffer,
195
- isWxAppOutdialOffer,
196
- isWebrtc: this.uiControlConfig.voiceVariant === VOICE_VARIANT.WEBRTC,
197
- isOutdial,
198
- wxAppAcceptInFlight: this.wxAppAcceptInFlight,
199
- wxAppAnswerPending: this.wxAppAnswerPending,
200
- enableWxBetterTogether: this.enableWxBetterTogether,
201
- hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
202
- });
179
+ protected updateUiControls(forceEmit = false): void {
180
+ super.updateUiControls(forceEmit);
181
+ this.getOrCreateWxAppOfferObservability().handleUiControlsUpdate(
182
+ this.getWxAppOfferObservabilityContext()
183
+ );
184
+ }
203
185
 
204
- if (acceptReason === this.lastLoggedWxAppAcceptReason) {
205
- return;
206
- }
186
+ /**
187
+ * Re-run OFFERED wxApp diagnostics without emitting UI controls.
188
+ * Used after usersub publish so a pre-publish offer can schedule mismatch grace.
189
+ */
190
+ public refreshWxAppOfferObservability(): void {
191
+ this.getOrCreateWxAppOfferObservability().handleUiControlsUpdate(
192
+ this.getWxAppOfferObservabilityContext()
193
+ );
194
+ }
207
195
 
208
- this.lastLoggedWxAppAcceptReason = acceptReason;
196
+ /**
197
+ * Cancel pending participant-mismatch grace timer. Observability only.
198
+ */
199
+ public disposeWxAppOfferObservability(): void {
200
+ this.wxAppOfferObservability?.dispose();
201
+ }
209
202
 
210
- logWxAppOfferDecision({
211
- interactionId: this.data.interactionId,
212
- acceptVisible: accept.isVisible,
213
- acceptEnabled: accept.isEnabled,
214
- acceptReason,
215
- wxAppParticipantDeviceType: deviceDetails?.deviceType,
216
- hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
217
- });
203
+ private getWxAppOfferObservabilityContext(): WxAppOfferObservabilityContext {
204
+ return {
205
+ getInteractionId: () => this.data.interactionId,
206
+ getTaskState: () => this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined,
207
+ getAcceptControl: () => this.currentUiControls?.main?.accept,
208
+ getVoiceVariant: () => this.uiControlConfig.voiceVariant ?? VOICE_VARIANT.PSTN,
209
+ isOutdial: () => this.data?.interaction?.outboundType === 'OUTDIAL',
210
+ isWxAppInboundOffer: () => this.isWebexAppInboundCallingOffer(),
211
+ isWxAppCallingOffer: () => this.isWebexAppCallingOffer(),
212
+ getWxAppAcceptInFlight: () => this.wxAppAcceptInFlight,
213
+ getWxAppAnswerPending: () => this.wxAppAnswerPending,
214
+ getEnableWxBetterTogether: () => this.enableWxBetterTogether,
215
+ getUsersubPublished: () => this.getUsersubPublished?.() ?? false,
216
+ getWxAppVoiceDependencies: () => this.getWxAppVoiceDependencies(),
217
+ getMetricsManager: () => this.metricsManager,
218
+ };
218
219
  }
219
220
 
220
221
  public applyWxAppMuteStateFromSync(incomingCallId: string, muted: boolean): void {
@@ -423,12 +424,26 @@ export default class Voice extends Task implements IVoice {
423
424
  return this.stateMachineService?.getSnapshot?.();
424
425
  }
425
426
 
427
+ private getWxAppAcceptReasonForMetrics(): WxAppAcceptReason {
428
+ if (this.wxAppAcceptInFlight) {
429
+ return 'wxApp_accept_in_flight';
430
+ }
431
+
432
+ if (this.wxAppAnswerPending) {
433
+ return 'wxApp_answer_pending';
434
+ }
435
+
436
+ return 'wxApp_offer_ready';
437
+ }
438
+
426
439
  /**
427
440
  * Accepts the task. Routes to wxApp telephony answer when `enableWxBetterTogether` and wxApp offer.
428
441
  */
429
442
  public async accept(): Promise<TaskResponse> {
430
443
  if (this.enableWxBetterTogether && this.isWebexAppCallingOffer()) {
431
- await runWxAppAccept(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle());
444
+ await runWxAppAccept(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), {
445
+ acceptReason: this.getWxAppAcceptReasonForMetrics(),
446
+ });
432
447
 
433
448
  return Promise.resolve();
434
449
  }
@@ -441,7 +456,9 @@ export default class Voice extends Task implements IVoice {
441
456
  */
442
457
  public async decline(): Promise<TaskResponse> {
443
458
  if (this.enableWxBetterTogether && this.isWebexAppInboundCallingOffer()) {
444
- await runWxAppReject(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle());
459
+ await runWxAppReject(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), {
460
+ acceptReason: this.getWxAppAcceptReasonForMetrics(),
461
+ });
445
462
 
446
463
  return Promise.resolve();
447
464
  }
@@ -464,9 +481,14 @@ export default class Voice extends Task implements IVoice {
464
481
  METRIC_EVENT_NAMES.TASK_DECLINE_FAILED,
465
482
  ]);
466
483
 
484
+ const declineAcceptReason = this.wxAppAnswerPending
485
+ ? 'wxApp_answer_pending'
486
+ : 'wxApp_offer_ready';
467
487
  this.setWxAppAnswerPending(false);
468
- const response = await runWxAppOutdialDecline(this.getWxAppVoiceDependencies(), () =>
469
- this.contact.cancelTask({interactionId})
488
+ const response = await runWxAppOutdialDecline(
489
+ this.getWxAppVoiceDependencies(),
490
+ () => this.contact.cancelTask({interactionId}),
491
+ {acceptReason: declineAcceptReason}
470
492
  );
471
493
 
472
494
  this.metricsManager.trackEvent(
@@ -0,0 +1,204 @@
1
+ import MetricsManager from '../../../metrics/MetricsManager';
2
+ import {METRIC_EVENT_NAMES} from '../../../metrics/constants';
3
+ import {
4
+ deriveWxAppAcceptReason,
5
+ logWxAppOfferDecision,
6
+ logWxAppOfferParticipantMismatch,
7
+ WxAppAcceptReason,
8
+ } from '../../wxAppDiagnosticLogging';
9
+ import {TaskState} from '../state-machine';
10
+ import {VoiceVariant, VOICE_VARIANT} from '../types';
11
+ import {
12
+ getCallingDeviceDetails,
13
+ getWxAppParticipantDiagnostics,
14
+ WxAppVoiceDependencies,
15
+ } from './wxAppVoiceMethods';
16
+
17
+ /** Defer mismatch WARN/metric so transient WS ordering gaps (~200–300 ms) do not false-alarm. */
18
+ export const WXAPP_PARTICIPANT_MISMATCH_GRACE_MS = 500;
19
+
20
+ export type WxAppOfferObservabilityContext = {
21
+ getInteractionId: () => string;
22
+ getTaskState: () => TaskState | undefined;
23
+ getAcceptControl: () => {isVisible: boolean; isEnabled: boolean} | undefined;
24
+ getVoiceVariant: () => VoiceVariant;
25
+ isOutdial: () => boolean;
26
+ isWxAppInboundOffer: () => boolean;
27
+ isWxAppCallingOffer: () => boolean;
28
+ getWxAppAcceptInFlight: () => boolean;
29
+ getWxAppAnswerPending: () => boolean;
30
+ getEnableWxBetterTogether: () => boolean;
31
+ getUsersubPublished: () => boolean;
32
+ getWxAppVoiceDependencies: () => WxAppVoiceDependencies;
33
+ getMetricsManager: () => MetricsManager;
34
+ };
35
+
36
+ /**
37
+ * OFFERED-state wxApp offer diagnostics: acceptReason logging and deferred participant mismatch.
38
+ * Invoked from Voice.updateUiControls — observability only.
39
+ */
40
+ export class WxAppOfferObservability {
41
+ private lastLoggedWxAppAcceptReason?: WxAppAcceptReason;
42
+ private lastLoggedWxAppParticipantMismatch = false;
43
+ private wxAppParticipantMismatchGraceTimer?: ReturnType<typeof setTimeout>;
44
+ private disposed = false;
45
+
46
+ public handleUiControlsUpdate(ctx: WxAppOfferObservabilityContext): void {
47
+ if (this.disposed) {
48
+ return;
49
+ }
50
+
51
+ if (!ctx.getEnableWxBetterTogether()) {
52
+ this.cancelGraceTimer();
53
+
54
+ return;
55
+ }
56
+
57
+ const state = ctx.getTaskState();
58
+ if (state !== TaskState.OFFERED) {
59
+ this.cancelGraceTimer();
60
+
61
+ return;
62
+ }
63
+
64
+ const accept = ctx.getAcceptControl();
65
+ if (!accept?.isVisible) {
66
+ return;
67
+ }
68
+
69
+ const deps = ctx.getWxAppVoiceDependencies();
70
+ const isOutdial = ctx.isOutdial();
71
+ const isWxAppInboundOffer = ctx.isWxAppInboundOffer();
72
+ const isWxAppOutdialOffer = ctx.isWxAppCallingOffer() && isOutdial;
73
+ const deviceDetails = getCallingDeviceDetails(deps);
74
+ const participantDiagnostics = getWxAppParticipantDiagnostics(deps);
75
+ const usersubPublished = ctx.getUsersubPublished();
76
+ const acceptReason = deriveWxAppAcceptReason({
77
+ isWxAppInboundOffer,
78
+ isWxAppOutdialOffer,
79
+ isWebrtc: ctx.getVoiceVariant() === VOICE_VARIANT.WEBRTC,
80
+ isOutdial,
81
+ wxAppAcceptInFlight: ctx.getWxAppAcceptInFlight(),
82
+ wxAppAnswerPending: ctx.getWxAppAnswerPending(),
83
+ enableWxBetterTogether: ctx.getEnableWxBetterTogether(),
84
+ hasDeviceCallId: participantDiagnostics.hasDeviceCallId,
85
+ });
86
+
87
+ if (acceptReason !== this.lastLoggedWxAppAcceptReason) {
88
+ this.lastLoggedWxAppAcceptReason = acceptReason;
89
+
90
+ logWxAppOfferDecision({
91
+ interactionId: ctx.getInteractionId(),
92
+ acceptVisible: accept.isVisible,
93
+ acceptEnabled: accept.isEnabled,
94
+ acceptReason,
95
+ wxAppParticipantDeviceType:
96
+ deviceDetails?.deviceType ?? participantDiagnostics.participantDeviceType,
97
+ hasDeviceCallId: participantDiagnostics.hasDeviceCallId,
98
+ hasDeviceId: participantDiagnostics.hasDeviceId,
99
+ usersubPublished,
100
+ });
101
+ }
102
+
103
+ if (participantDiagnostics.hasValidWxAppParticipant || !usersubPublished) {
104
+ this.cancelGraceTimer();
105
+ } else if (!ctx.isOutdial() && !this.lastLoggedWxAppParticipantMismatch) {
106
+ this.scheduleGraceTimer(ctx);
107
+ }
108
+ }
109
+
110
+ public dispose(): void {
111
+ this.disposed = true;
112
+ this.cancelGraceTimer();
113
+ }
114
+
115
+ private cancelGraceTimer(): void {
116
+ if (this.wxAppParticipantMismatchGraceTimer !== undefined) {
117
+ clearTimeout(this.wxAppParticipantMismatchGraceTimer);
118
+ this.wxAppParticipantMismatchGraceTimer = undefined;
119
+ }
120
+ }
121
+
122
+ private scheduleGraceTimer(ctx: WxAppOfferObservabilityContext): void {
123
+ if (
124
+ this.disposed ||
125
+ this.lastLoggedWxAppParticipantMismatch ||
126
+ this.wxAppParticipantMismatchGraceTimer !== undefined
127
+ ) {
128
+ return;
129
+ }
130
+
131
+ this.wxAppParticipantMismatchGraceTimer = setTimeout(() => {
132
+ this.wxAppParticipantMismatchGraceTimer = undefined;
133
+ this.emitParticipantMismatchIfStillNeeded(ctx);
134
+ }, WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
135
+ }
136
+
137
+ private emitParticipantMismatchIfStillNeeded(ctx: WxAppOfferObservabilityContext): void {
138
+ if (
139
+ this.disposed ||
140
+ !ctx.getEnableWxBetterTogether() ||
141
+ this.lastLoggedWxAppParticipantMismatch
142
+ ) {
143
+ return;
144
+ }
145
+
146
+ if (ctx.isOutdial()) {
147
+ return;
148
+ }
149
+
150
+ if (ctx.getTaskState() !== TaskState.OFFERED) {
151
+ return;
152
+ }
153
+
154
+ const accept = ctx.getAcceptControl();
155
+ if (!accept?.isVisible) {
156
+ return;
157
+ }
158
+
159
+ const deps = ctx.getWxAppVoiceDependencies();
160
+ const participantDiagnostics = getWxAppParticipantDiagnostics(deps);
161
+ const usersubPublished = ctx.getUsersubPublished();
162
+
163
+ if (!usersubPublished || participantDiagnostics.hasValidWxAppParticipant) {
164
+ return;
165
+ }
166
+
167
+ const isOutdial = ctx.isOutdial();
168
+ const acceptReason = deriveWxAppAcceptReason({
169
+ isWxAppInboundOffer: ctx.isWxAppInboundOffer(),
170
+ isWxAppOutdialOffer: ctx.isWxAppCallingOffer() && isOutdial,
171
+ isWebrtc: ctx.getVoiceVariant() === VOICE_VARIANT.WEBRTC,
172
+ isOutdial,
173
+ wxAppAcceptInFlight: ctx.getWxAppAcceptInFlight(),
174
+ wxAppAnswerPending: ctx.getWxAppAnswerPending(),
175
+ enableWxBetterTogether: ctx.getEnableWxBetterTogether(),
176
+ hasDeviceCallId: participantDiagnostics.hasDeviceCallId,
177
+ });
178
+
179
+ this.lastLoggedWxAppParticipantMismatch = true;
180
+
181
+ logWxAppOfferParticipantMismatch({
182
+ interactionId: ctx.getInteractionId(),
183
+ usersubPublished,
184
+ hasDeviceCallId: participantDiagnostics.hasDeviceCallId,
185
+ hasDeviceId: participantDiagnostics.hasDeviceId,
186
+ participantDeviceType: participantDiagnostics.participantDeviceType,
187
+ acceptVisible: accept.isVisible,
188
+ acceptEnabled: accept.isEnabled,
189
+ acceptReason,
190
+ });
191
+
192
+ ctx.getMetricsManager().trackEvent(
193
+ METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
194
+ {
195
+ taskId: ctx.getInteractionId(),
196
+ usersubPublished,
197
+ hasDeviceCallId: participantDiagnostics.hasDeviceCallId,
198
+ hasDeviceId: participantDiagnostics.hasDeviceId,
199
+ acceptReason,
200
+ },
201
+ ['operational', 'behavioral']
202
+ );
203
+ }
204
+ }
@@ -17,6 +17,7 @@ import {
17
17
  import {
18
18
  logWxAppTelephonyAction,
19
19
  logWxAppValidationFailure,
20
+ type WxAppAcceptReason,
20
21
  type WxAppTelephonyAction,
21
22
  } from '../../wxAppDiagnosticLogging';
22
23
 
@@ -35,6 +36,15 @@ export type WxAppVoiceDependencies = {
35
36
  getTaskState: () => TaskState | undefined;
36
37
  getWxAppMuted: () => boolean;
37
38
  setWxAppMuted: (muted: boolean) => void;
39
+ /** Read-only observability: whether usersub answer-calls-on-wxcc is active this session. */
40
+ getUsersubPublished?: () => boolean;
41
+ };
42
+
43
+ export type WxAppParticipantDiagnostics = {
44
+ participantDeviceType?: string;
45
+ hasDeviceCallId: boolean;
46
+ hasDeviceId: boolean;
47
+ hasValidWxAppParticipant: boolean;
38
48
  };
39
49
 
40
50
  /** @deprecated Use {@link WxAppVoiceDependencies} */
@@ -76,6 +86,34 @@ function getWxAppAgentParticipant(deps: WxAppVoiceDependencies): unknown {
76
86
  );
77
87
  }
78
88
 
89
+ export function getWxAppParticipantDiagnostics(
90
+ deps: WxAppVoiceDependencies
91
+ ): WxAppParticipantDiagnostics {
92
+ const participant = getWxAppAgentParticipant(deps) as WxAppParticipant | undefined;
93
+ const hasDeviceCallId =
94
+ typeof participant?.deviceCallId === 'string' && participant.deviceCallId.trim() !== '';
95
+ const hasDeviceId =
96
+ typeof participant?.deviceId === 'string' && participant.deviceId.trim() !== '';
97
+
98
+ return {
99
+ participantDeviceType: participant?.deviceType,
100
+ hasDeviceCallId,
101
+ hasDeviceId,
102
+ hasValidWxAppParticipant: isWxAppParticipant(participant),
103
+ };
104
+ }
105
+
106
+ function getWxAppTelephonyMetricContext(
107
+ deps: WxAppVoiceDependencies
108
+ ): Record<string, boolean | string> {
109
+ const diagnostics = getWxAppParticipantDiagnostics(deps);
110
+
111
+ return {
112
+ hasDeviceCallId: diagnostics.hasDeviceCallId,
113
+ hasDeviceId: diagnostics.hasDeviceId,
114
+ };
115
+ }
116
+
79
117
  export function getCallingDeviceDetails(
80
118
  deps: WxAppVoiceDependencies
81
119
  ): WebexCallingDeviceDetails | undefined {
@@ -169,11 +207,12 @@ function logTelephonyFailure(
169
207
  function getWxAppTelephonyMetricFailurePayload(
170
208
  deps: WxAppVoiceDependencies,
171
209
  error: unknown
172
- ): Record<string, string> {
210
+ ): Record<string, string | boolean> {
173
211
  const wxError = error as WxAppTelephonyError;
174
- const payload: Record<string, string> = {
212
+ const payload: Record<string, string | boolean> = {
175
213
  taskId: getInteractionId(deps) ?? '',
176
214
  error: error instanceof Error ? error.toString() : String(error),
215
+ ...getWxAppTelephonyMetricContext(deps),
177
216
  };
178
217
 
179
218
  if (wxError.trackingId) {
@@ -198,7 +237,11 @@ function getWxAppOutdialDeclineFailurePayload(
198
237
  const aqmDetails = (error as AqmWrappedError).details;
199
238
 
200
239
  if (aqmDetails) {
201
- return {...base, ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(aqmDetails)};
240
+ return {
241
+ ...base,
242
+ ...getWxAppTelephonyMetricContext(deps),
243
+ ...MetricsManager.getCommonTrackingFieldForAQMResponseFailed(aqmDetails),
244
+ };
202
245
  }
203
246
 
204
247
  return getWxAppTelephonyMetricFailurePayload(deps, error);
@@ -324,7 +367,7 @@ export async function transmitDtmfOnWebex(
324
367
  throw new Error('WxApp call ID is unavailable');
325
368
  }
326
369
 
327
- LoggerProxy.info('transmitDtmf', {
370
+ LoggerProxy.log('transmitDtmf', {
328
371
  module: 'wxAppVoiceMethods',
329
372
  method: METHODS.TRANSMIT_DTMF,
330
373
  data: {
@@ -343,12 +386,18 @@ export async function transmitDtmfOnWebex(
343
386
  logWxAppTelephonyAction({action: 'dtmf', phase: 'success', interactionId});
344
387
  }
345
388
 
389
+ export type WxAppAcceptRejectOptions = {
390
+ lineOwnerId?: string;
391
+ acceptReason?: WxAppAcceptReason;
392
+ };
393
+
346
394
  export async function runWxAppAccept(
347
395
  deps: WxAppVoiceDependencies,
348
396
  lifecycle: WxAppVoiceLifecycle,
349
- options?: {lineOwnerId?: string}
397
+ options?: WxAppAcceptRejectOptions
350
398
  ): Promise<void> {
351
399
  const taskId = getInteractionId(deps);
400
+ const acceptReason = options?.acceptReason ?? 'wxApp_offer_ready';
352
401
 
353
402
  deps.metricsManager.timeEvent([
354
403
  METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS,
@@ -362,16 +411,17 @@ export async function runWxAppAccept(
362
411
  lifecycle.resetWxAppMuted();
363
412
  await lifecycle.syncWxAppMuteFromCallDetails();
364
413
 
365
- deps.metricsManager.trackEvent(METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS, {taskId}, [
366
- 'operational',
367
- 'behavioral',
368
- ]);
414
+ deps.metricsManager.trackEvent(
415
+ METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS,
416
+ {taskId, acceptReason, ...getWxAppTelephonyMetricContext(deps)},
417
+ ['operational', 'behavioral']
418
+ );
369
419
  } catch (error) {
370
420
  lifecycle.setWxAppAnswerPending(false);
371
421
  logTelephonyFailure('accept', deps, error);
372
422
  deps.metricsManager.trackEvent(
373
423
  METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_FAILED,
374
- getWxAppTelephonyMetricFailurePayload(deps, error),
424
+ {acceptReason, ...getWxAppTelephonyMetricFailurePayload(deps, error)},
375
425
  ['operational', 'behavioral']
376
426
  );
377
427
  lifecycle.mapWxAppVoiceError(error, METHODS.ACCEPT);
@@ -383,9 +433,10 @@ export async function runWxAppAccept(
383
433
  export async function runWxAppReject(
384
434
  deps: WxAppVoiceDependencies,
385
435
  lifecycle: WxAppVoiceLifecycle,
386
- options?: {lineOwnerId?: string}
436
+ options?: WxAppAcceptRejectOptions
387
437
  ): Promise<void> {
388
438
  const taskId = getInteractionId(deps);
439
+ const acceptReason = options?.acceptReason ?? 'wxApp_offer_ready';
389
440
 
390
441
  deps.metricsManager.timeEvent([
391
442
  METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
@@ -395,26 +446,33 @@ export async function runWxAppReject(
395
446
  try {
396
447
  await rejectOnWebex(deps, options);
397
448
 
398
- deps.metricsManager.trackEvent(METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS, {taskId}, [
399
- 'operational',
400
- 'behavioral',
401
- ]);
449
+ deps.metricsManager.trackEvent(
450
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
451
+ {taskId, acceptReason, ...getWxAppTelephonyMetricContext(deps)},
452
+ ['operational', 'behavioral']
453
+ );
402
454
  } catch (error) {
403
455
  logTelephonyFailure('decline', deps, error);
404
456
  deps.metricsManager.trackEvent(
405
457
  METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
406
- getWxAppTelephonyMetricFailurePayload(deps, error),
458
+ {acceptReason, ...getWxAppTelephonyMetricFailurePayload(deps, error)},
407
459
  ['operational', 'behavioral']
408
460
  );
409
461
  lifecycle.mapWxAppVoiceError(error, METHODS.REJECT);
410
462
  }
411
463
  }
412
464
 
465
+ export type WxAppOutdialDeclineOptions = {
466
+ acceptReason?: WxAppAcceptReason;
467
+ };
468
+
413
469
  export async function runWxAppOutdialDecline<T>(
414
470
  deps: WxAppVoiceDependencies,
415
- executeCancel: () => Promise<T>
471
+ executeCancel: () => Promise<T>,
472
+ options?: WxAppOutdialDeclineOptions
416
473
  ): Promise<T> {
417
474
  const taskId = getInteractionId(deps);
475
+ const acceptReason = options?.acceptReason ?? 'wxApp_offer_ready';
418
476
 
419
477
  deps.metricsManager.timeEvent([
420
478
  METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
@@ -424,17 +482,18 @@ export async function runWxAppOutdialDecline<T>(
424
482
  try {
425
483
  const result = await executeCancel();
426
484
 
427
- deps.metricsManager.trackEvent(METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS, {taskId}, [
428
- 'operational',
429
- 'behavioral',
430
- ]);
485
+ deps.metricsManager.trackEvent(
486
+ METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
487
+ {taskId, acceptReason, ...getWxAppTelephonyMetricContext(deps)},
488
+ ['operational', 'behavioral']
489
+ );
431
490
 
432
491
  return result;
433
492
  } catch (error) {
434
493
  logWxAppOutdialDeclineFailure(deps, error);
435
494
  deps.metricsManager.trackEvent(
436
495
  METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
437
- getWxAppOutdialDeclineFailurePayload(deps, error),
496
+ {acceptReason, ...getWxAppOutdialDeclineFailurePayload(deps, error)},
438
497
  ['operational', 'behavioral']
439
498
  );
440
499
  throw error;
@@ -458,7 +517,7 @@ export async function runWxAppToggleMute(
458
517
 
459
518
  deps.metricsManager.trackEvent(
460
519
  METRIC_EVENT_NAMES.WXAPP_TASK_MUTE_SUCCESS,
461
- {taskId, targetMuted: deps.getWxAppMuted()},
520
+ {taskId, targetMuted: deps.getWxAppMuted(), ...getWxAppTelephonyMetricContext(deps)},
462
521
  ['operational', 'behavioral']
463
522
  );
464
523
  } catch (error) {
@@ -489,7 +548,7 @@ export async function runWxAppTransmitDtmf(
489
548
 
490
549
  deps.metricsManager.trackEvent(
491
550
  METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_SUCCESS,
492
- {taskId, dtmfLength: options.dtmf.length},
551
+ {taskId, dtmfLength: options.dtmf.length, ...getWxAppTelephonyMetricContext(deps)},
493
552
  ['operational', 'behavioral']
494
553
  );
495
554
  } catch (error) {