@webex/contact-center 3.12.0-llmrefactor.13 → 3.12.0-llmrefactor.15
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.
- package/.sdd/manifest.json +3 -2
- package/ai-docs/CONTRACTS.md +7 -1
- package/ai-docs/contact-center-spec.md +7 -7
- package/ai-docs/features/contact-owner-change-refactor.md +78 -0
- package/dist/cc.js +39 -7
- package/dist/cc.js.map +1 -1
- package/dist/metrics/behavioral-events.js +6 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +1 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +4 -4
- package/dist/services/AnswerCallOnWebexService.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +5 -2
- package/dist/services/WebexCrossClientService.js.map +1 -1
- package/dist/services/WxAppTelephonyMercurySync.js +6 -3
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
- package/dist/services/config/types.js.map +1 -1
- package/dist/services/task/TaskFactory.js +1 -0
- package/dist/services/task/TaskFactory.js.map +1 -1
- package/dist/services/task/TaskManager.js +50 -58
- package/dist/services/task/TaskManager.js.map +1 -1
- package/dist/services/task/types.js.map +1 -1
- package/dist/services/task/voice/Voice.js +63 -44
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/wxAppOfferObservability.js +145 -0
- package/dist/services/task/voice/wxAppOfferObservability.js.map +1 -0
- package/dist/services/task/voice/wxAppVoiceMethods.js +52 -11
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
- package/dist/services/wxAppDiagnosticLogging.js +50 -13
- package/dist/services/wxAppDiagnosticLogging.js.map +1 -1
- package/dist/types/metrics/constants.d.ts +1 -0
- package/dist/types/services/config/types.d.ts +2 -0
- package/dist/types/services/task/types.d.ts +2 -0
- package/dist/types/services/task/voice/Voice.d.ts +15 -2
- package/dist/types/services/task/voice/wxAppOfferObservability.d.ts +39 -0
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +19 -6
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +12 -0
- package/dist/types.js.map +1 -1
- package/dist/webex.js +1 -1
- package/package.json +9 -9
- package/src/cc.ts +43 -7
- package/src/metrics/ai-docs/metrics-spec.md +44 -2
- package/src/metrics/behavioral-events.ts +6 -0
- package/src/metrics/constants.ts +1 -0
- package/src/services/AnswerCallOnWebexService.ts +4 -4
- package/src/services/WebexCrossClientService.ts +7 -2
- package/src/services/WxAppTelephonyMercurySync.ts +3 -3
- package/src/services/config/types.ts +2 -0
- package/src/services/task/TaskFactory.ts +1 -0
- package/src/services/task/TaskManager.ts +85 -87
- package/src/services/task/ai-docs/task-spec.md +7 -3
- package/src/services/task/types.ts +16 -0
- package/src/services/task/voice/Voice.ts +74 -52
- package/src/services/task/voice/wxAppOfferObservability.ts +204 -0
- package/src/services/task/voice/wxAppVoiceMethods.ts +83 -24
- package/src/services/wxAppDiagnosticLogging.ts +135 -23
- package/src/types.ts +2 -0
- package/test/unit/spec/cc.ts +201 -19
- package/test/unit/spec/services/WebexCrossClientService.ts +45 -3
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +1 -1
- package/test/unit/spec/services/task/TaskManager.ts +214 -3
- package/test/unit/spec/services/task/state-machine/TaskStateMachine.ts +33 -0
- package/test/unit/spec/services/task/voice/Voice.ts +10 -51
- package/test/unit/spec/services/task/voice/wxAppOfferObservability.ts +361 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +121 -5
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +44 -8
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import MetricsManager from '../../../../../../src/metrics/MetricsManager';
|
|
2
|
+
import {METRIC_EVENT_NAMES} from '../../../../../../src/metrics/constants';
|
|
3
|
+
import {TaskState} from '../../../../../../src/services/task/state-machine';
|
|
4
|
+
import {TaskData, VOICE_VARIANT} from '../../../../../../src/services/task/types';
|
|
5
|
+
import {
|
|
6
|
+
WxAppOfferObservability,
|
|
7
|
+
WxAppOfferObservabilityContext,
|
|
8
|
+
WXAPP_PARTICIPANT_MISMATCH_GRACE_MS,
|
|
9
|
+
} from '../../../../../../src/services/task/voice/wxAppOfferObservability';
|
|
10
|
+
import {WxAppVoiceDependencies} from '../../../../../../src/services/task/voice/wxAppVoiceMethods';
|
|
11
|
+
import * as wxAppDiagnosticLogging from '../../../../../../src/services/wxAppDiagnosticLogging';
|
|
12
|
+
import {createTaskData} from '../taskTestUtils';
|
|
13
|
+
|
|
14
|
+
const wxAppParticipant = {
|
|
15
|
+
deviceType: 'wxApp',
|
|
16
|
+
deviceId: 'device-id-1',
|
|
17
|
+
deviceCallId: 'call-id-1',
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function makeTaskDataWithoutWxAppFields(): TaskData {
|
|
21
|
+
return createTaskData({
|
|
22
|
+
agentId: 'agent-1',
|
|
23
|
+
interaction: {
|
|
24
|
+
participants: {
|
|
25
|
+
'agent-1': {id: 'agent-1'},
|
|
26
|
+
},
|
|
27
|
+
} as TaskData['interaction'],
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function makeTaskDataWithWxAppFields(): TaskData {
|
|
32
|
+
return createTaskData({
|
|
33
|
+
agentId: 'agent-1',
|
|
34
|
+
interaction: {
|
|
35
|
+
participants: {
|
|
36
|
+
'agent-1': {id: 'agent-1', ...wxAppParticipant},
|
|
37
|
+
},
|
|
38
|
+
} as TaskData['interaction'],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function makeMockMetricsManager(): jest.Mocked<Pick<MetricsManager, 'trackEvent'>> {
|
|
43
|
+
return {trackEvent: jest.fn()};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeContext(
|
|
47
|
+
overrides: {
|
|
48
|
+
taskData?: TaskData;
|
|
49
|
+
taskState?: TaskState;
|
|
50
|
+
usersubPublished?: boolean;
|
|
51
|
+
wxAppAcceptInFlight?: boolean;
|
|
52
|
+
wxAppAnswerPending?: boolean;
|
|
53
|
+
voiceVariant?: typeof VOICE_VARIANT.PSTN;
|
|
54
|
+
acceptEnabled?: boolean;
|
|
55
|
+
isOutdial?: boolean;
|
|
56
|
+
} = {}
|
|
57
|
+
): WxAppOfferObservabilityContext & {
|
|
58
|
+
setTaskData: (data: TaskData) => void;
|
|
59
|
+
setTaskState: (s: TaskState) => void;
|
|
60
|
+
setUsersubPublished: (value: boolean) => void;
|
|
61
|
+
} {
|
|
62
|
+
let taskData = overrides.taskData ?? makeTaskDataWithoutWxAppFields();
|
|
63
|
+
let taskState = overrides.taskState ?? TaskState.OFFERED;
|
|
64
|
+
let usersubPublished = overrides.usersubPublished ?? true;
|
|
65
|
+
const metricsManager = makeMockMetricsManager() as unknown as MetricsManager;
|
|
66
|
+
|
|
67
|
+
const deps = (): WxAppVoiceDependencies => ({
|
|
68
|
+
enableWxBetterTogether: true,
|
|
69
|
+
agentId: 'agent-1',
|
|
70
|
+
metricsManager,
|
|
71
|
+
getTaskData: () => taskData,
|
|
72
|
+
getTaskState: () => taskState,
|
|
73
|
+
getWxAppMuted: () => false,
|
|
74
|
+
setWxAppMuted: jest.fn(),
|
|
75
|
+
getUsersubPublished: () => usersubPublished,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
getInteractionId: () => taskData.interactionId,
|
|
80
|
+
getTaskState: () => taskState,
|
|
81
|
+
getAcceptControl: () => ({
|
|
82
|
+
isVisible: true,
|
|
83
|
+
isEnabled: overrides.acceptEnabled ?? false,
|
|
84
|
+
}),
|
|
85
|
+
getVoiceVariant: () => overrides.voiceVariant ?? VOICE_VARIANT.PSTN,
|
|
86
|
+
isOutdial: () => overrides.isOutdial ?? false,
|
|
87
|
+
isWxAppInboundOffer: () => {
|
|
88
|
+
const p = taskData.interaction?.participants?.['agent-1'] as
|
|
89
|
+
| {deviceType?: string; deviceCallId?: string; deviceId?: string}
|
|
90
|
+
| undefined;
|
|
91
|
+
return (
|
|
92
|
+
p?.deviceType === 'wxApp' &&
|
|
93
|
+
typeof p.deviceCallId === 'string' &&
|
|
94
|
+
p.deviceCallId.trim() !== '' &&
|
|
95
|
+
typeof p.deviceId === 'string' &&
|
|
96
|
+
p.deviceId.trim() !== ''
|
|
97
|
+
);
|
|
98
|
+
},
|
|
99
|
+
isWxAppCallingOffer: () => {
|
|
100
|
+
const p = taskData.interaction?.participants?.['agent-1'] as
|
|
101
|
+
| {deviceType?: string; deviceCallId?: string; deviceId?: string}
|
|
102
|
+
| undefined;
|
|
103
|
+
return (
|
|
104
|
+
p?.deviceType === 'wxApp' &&
|
|
105
|
+
typeof p.deviceCallId === 'string' &&
|
|
106
|
+
p.deviceCallId.trim() !== '' &&
|
|
107
|
+
typeof p.deviceId === 'string' &&
|
|
108
|
+
p.deviceId.trim() !== ''
|
|
109
|
+
);
|
|
110
|
+
},
|
|
111
|
+
getWxAppAcceptInFlight: () => overrides.wxAppAcceptInFlight ?? false,
|
|
112
|
+
getWxAppAnswerPending: () => overrides.wxAppAnswerPending ?? false,
|
|
113
|
+
getEnableWxBetterTogether: () => true,
|
|
114
|
+
getUsersubPublished: () => usersubPublished,
|
|
115
|
+
getWxAppVoiceDependencies: deps,
|
|
116
|
+
getMetricsManager: () => metricsManager,
|
|
117
|
+
setTaskData: (data: TaskData) => {
|
|
118
|
+
taskData = data;
|
|
119
|
+
},
|
|
120
|
+
setTaskState: (state: TaskState) => {
|
|
121
|
+
taskState = state;
|
|
122
|
+
},
|
|
123
|
+
setUsersubPublished: (value: boolean) => {
|
|
124
|
+
usersubPublished = value;
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
describe('WxAppOfferObservability', () => {
|
|
130
|
+
beforeEach(() => {
|
|
131
|
+
jest.useFakeTimers();
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
afterEach(() => {
|
|
135
|
+
jest.useRealTimers();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
describe('offer decision logging', () => {
|
|
139
|
+
it('logs wxApp_offer_ready when participant fields are present', () => {
|
|
140
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
141
|
+
const observability = new WxAppOfferObservability();
|
|
142
|
+
const ctx = makeContext({taskData: makeTaskDataWithWxAppFields(), acceptEnabled: true});
|
|
143
|
+
|
|
144
|
+
observability.handleUiControlsUpdate(ctx);
|
|
145
|
+
|
|
146
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
147
|
+
expect.objectContaining({acceptReason: 'wxApp_offer_ready'})
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('logs accept reason transitions only once per reason', () => {
|
|
152
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
153
|
+
const observability = new WxAppOfferObservability();
|
|
154
|
+
const ctx = makeContext({
|
|
155
|
+
taskData: makeTaskDataWithWxAppFields(),
|
|
156
|
+
wxAppAcceptInFlight: true,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
observability.handleUiControlsUpdate(ctx);
|
|
160
|
+
observability.handleUiControlsUpdate(ctx);
|
|
161
|
+
|
|
162
|
+
expect(logSpy).toHaveBeenCalledTimes(1);
|
|
163
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
164
|
+
expect.objectContaining({acceptReason: 'wxApp_accept_in_flight'})
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
it('logs browser_webrtc_offer for WebRTC variant without wxApp participant', () => {
|
|
169
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
170
|
+
const observability = new WxAppOfferObservability();
|
|
171
|
+
const ctx = makeContext({
|
|
172
|
+
taskData: createTaskData({
|
|
173
|
+
agentId: 'agent-1',
|
|
174
|
+
interaction: {
|
|
175
|
+
participants: {
|
|
176
|
+
'agent-1': {id: 'agent-1', deviceType: 'phone', deviceId: 'device-id-1'},
|
|
177
|
+
},
|
|
178
|
+
} as TaskData['interaction'],
|
|
179
|
+
}),
|
|
180
|
+
voiceVariant: VOICE_VARIANT.WEBRTC,
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
observability.handleUiControlsUpdate(ctx);
|
|
184
|
+
|
|
185
|
+
expect(logSpy).toHaveBeenCalledWith(
|
|
186
|
+
expect.objectContaining({acceptReason: 'browser_webrtc_offer'})
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('deferred participant mismatch', () => {
|
|
192
|
+
it('does not warn when wxApp fields arrive before grace period elapses', () => {
|
|
193
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
194
|
+
const observability = new WxAppOfferObservability();
|
|
195
|
+
const ctx = makeContext();
|
|
196
|
+
|
|
197
|
+
observability.handleUiControlsUpdate(ctx);
|
|
198
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
199
|
+
|
|
200
|
+
ctx.setTaskData(makeTaskDataWithWxAppFields());
|
|
201
|
+
observability.handleUiControlsUpdate(ctx);
|
|
202
|
+
|
|
203
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
204
|
+
|
|
205
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
206
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
207
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
208
|
+
expect.anything(),
|
|
209
|
+
expect.anything()
|
|
210
|
+
);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('warns and emits metric when fields remain missing after grace period', () => {
|
|
214
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
215
|
+
const observability = new WxAppOfferObservability();
|
|
216
|
+
const ctx = makeContext();
|
|
217
|
+
|
|
218
|
+
observability.handleUiControlsUpdate(ctx);
|
|
219
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
220
|
+
|
|
221
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
222
|
+
|
|
223
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
224
|
+
expect.objectContaining({
|
|
225
|
+
interactionId: 'interaction-1',
|
|
226
|
+
usersubPublished: true,
|
|
227
|
+
hasDeviceCallId: false,
|
|
228
|
+
hasDeviceId: false,
|
|
229
|
+
acceptReason: 'extension_non_wxApp_offer',
|
|
230
|
+
})
|
|
231
|
+
);
|
|
232
|
+
expect(ctx.getMetricsManager().trackEvent).toHaveBeenCalledWith(
|
|
233
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
234
|
+
expect.objectContaining({
|
|
235
|
+
taskId: 'interaction-1',
|
|
236
|
+
usersubPublished: true,
|
|
237
|
+
hasDeviceCallId: false,
|
|
238
|
+
hasDeviceId: false,
|
|
239
|
+
acceptReason: 'extension_non_wxApp_offer',
|
|
240
|
+
}),
|
|
241
|
+
['operational', 'behavioral']
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('cancels pending mismatch when task leaves OFFERED before grace period', () => {
|
|
246
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
247
|
+
const observability = new WxAppOfferObservability();
|
|
248
|
+
const ctx = makeContext();
|
|
249
|
+
|
|
250
|
+
observability.handleUiControlsUpdate(ctx);
|
|
251
|
+
ctx.setTaskState(TaskState.TERMINATED);
|
|
252
|
+
observability.handleUiControlsUpdate(ctx);
|
|
253
|
+
|
|
254
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
255
|
+
|
|
256
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('clears grace timer on dispose', () => {
|
|
260
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
261
|
+
const observability = new WxAppOfferObservability();
|
|
262
|
+
const ctx = makeContext();
|
|
263
|
+
|
|
264
|
+
observability.handleUiControlsUpdate(ctx);
|
|
265
|
+
observability.dispose();
|
|
266
|
+
|
|
267
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
268
|
+
|
|
269
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it('does not reschedule diagnostics after dispose', () => {
|
|
273
|
+
const logSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferDecision');
|
|
274
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
275
|
+
const observability = new WxAppOfferObservability();
|
|
276
|
+
const ctx = makeContext();
|
|
277
|
+
|
|
278
|
+
observability.dispose();
|
|
279
|
+
observability.handleUiControlsUpdate(ctx);
|
|
280
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
281
|
+
|
|
282
|
+
expect(logSpy).not.toHaveBeenCalled();
|
|
283
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
284
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
285
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
286
|
+
expect.anything(),
|
|
287
|
+
expect.anything()
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('does not warn or emit metric for outdial offers with missing wxApp fields', () => {
|
|
292
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
293
|
+
const observability = new WxAppOfferObservability();
|
|
294
|
+
const ctx = makeContext({isOutdial: true});
|
|
295
|
+
|
|
296
|
+
observability.handleUiControlsUpdate(ctx);
|
|
297
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
298
|
+
|
|
299
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
300
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
301
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
302
|
+
expect.anything(),
|
|
303
|
+
expect.anything()
|
|
304
|
+
);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('does not schedule mismatch while usersub is unpublished, then emits after refresh when usersub becomes active', () => {
|
|
308
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
309
|
+
const observability = new WxAppOfferObservability();
|
|
310
|
+
const ctx = makeContext({usersubPublished: false});
|
|
311
|
+
|
|
312
|
+
observability.handleUiControlsUpdate(ctx);
|
|
313
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
314
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
315
|
+
|
|
316
|
+
ctx.setUsersubPublished(true);
|
|
317
|
+
observability.handleUiControlsUpdate(ctx);
|
|
318
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
319
|
+
|
|
320
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
321
|
+
|
|
322
|
+
expect(warnSpy).toHaveBeenCalledWith(
|
|
323
|
+
expect.objectContaining({
|
|
324
|
+
usersubPublished: true,
|
|
325
|
+
hasDeviceCallId: false,
|
|
326
|
+
hasDeviceId: false,
|
|
327
|
+
})
|
|
328
|
+
);
|
|
329
|
+
expect(ctx.getMetricsManager().trackEvent).toHaveBeenCalledWith(
|
|
330
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
331
|
+
expect.objectContaining({
|
|
332
|
+
usersubPublished: true,
|
|
333
|
+
hasDeviceCallId: false,
|
|
334
|
+
hasDeviceId: false,
|
|
335
|
+
}),
|
|
336
|
+
['operational', 'behavioral']
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('does not emit mismatch after usersub refresh when participant fields are already valid', () => {
|
|
341
|
+
const warnSpy = jest.spyOn(wxAppDiagnosticLogging, 'logWxAppOfferParticipantMismatch');
|
|
342
|
+
const observability = new WxAppOfferObservability();
|
|
343
|
+
const ctx = makeContext({
|
|
344
|
+
taskData: makeTaskDataWithWxAppFields(),
|
|
345
|
+
usersubPublished: false,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
observability.handleUiControlsUpdate(ctx);
|
|
349
|
+
ctx.setUsersubPublished(true);
|
|
350
|
+
observability.handleUiControlsUpdate(ctx);
|
|
351
|
+
jest.advanceTimersByTime(WXAPP_PARTICIPANT_MISMATCH_GRACE_MS);
|
|
352
|
+
|
|
353
|
+
expect(warnSpy).not.toHaveBeenCalled();
|
|
354
|
+
expect(ctx.getMetricsManager().trackEvent).not.toHaveBeenCalledWith(
|
|
355
|
+
METRIC_EVENT_NAMES.WXAPP_OFFER_PARTICIPANT_FIELDS_MISSING,
|
|
356
|
+
expect.anything(),
|
|
357
|
+
expect.anything()
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -431,7 +431,12 @@ describe('runWxAppAccept', () => {
|
|
|
431
431
|
]);
|
|
432
432
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
433
433
|
METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS,
|
|
434
|
-
{
|
|
434
|
+
expect.objectContaining({
|
|
435
|
+
taskId: 'interaction-1',
|
|
436
|
+
acceptReason: 'wxApp_offer_ready',
|
|
437
|
+
hasDeviceCallId: true,
|
|
438
|
+
hasDeviceId: true,
|
|
439
|
+
}),
|
|
435
440
|
['operational', 'behavioral']
|
|
436
441
|
);
|
|
437
442
|
expect(lifecycle.setWxAppAcceptInFlight).toHaveBeenNthCalledWith(1, true);
|
|
@@ -455,6 +460,7 @@ describe('runWxAppAccept', () => {
|
|
|
455
460
|
expect.objectContaining({
|
|
456
461
|
taskId: 'interaction-1',
|
|
457
462
|
trackingId: 'track-1',
|
|
463
|
+
acceptReason: 'wxApp_offer_ready',
|
|
458
464
|
}),
|
|
459
465
|
['operational', 'behavioral']
|
|
460
466
|
);
|
|
@@ -462,6 +468,42 @@ describe('runWxAppAccept', () => {
|
|
|
462
468
|
expect(lifecycle.mapWxAppVoiceError).toHaveBeenCalledWith(error, METHODS.ACCEPT);
|
|
463
469
|
expect(lifecycle.setWxAppAcceptInFlight).toHaveBeenLastCalledWith(false);
|
|
464
470
|
});
|
|
471
|
+
|
|
472
|
+
it('includes captured acceptReason on accept success when pending', async () => {
|
|
473
|
+
const deps = makeDeps();
|
|
474
|
+
const lifecycle = makeLifecycle();
|
|
475
|
+
|
|
476
|
+
await runWxAppAccept(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'});
|
|
477
|
+
|
|
478
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
479
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_SUCCESS,
|
|
480
|
+
expect.objectContaining({
|
|
481
|
+
taskId: 'interaction-1',
|
|
482
|
+
acceptReason: 'wxApp_answer_pending',
|
|
483
|
+
}),
|
|
484
|
+
['operational', 'behavioral']
|
|
485
|
+
);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('includes captured acceptReason on accept failure when pending', async () => {
|
|
489
|
+
const deps = makeDeps();
|
|
490
|
+
const error = new Error('answer failed');
|
|
491
|
+
deps.answerCallOnWebexService!.answerCall = jest.fn().mockRejectedValue(error);
|
|
492
|
+
const lifecycle = makeLifecycle();
|
|
493
|
+
|
|
494
|
+
await expect(
|
|
495
|
+
runWxAppAccept(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'})
|
|
496
|
+
).rejects.toThrow('answer failed');
|
|
497
|
+
|
|
498
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
499
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_ACCEPT_FAILED,
|
|
500
|
+
expect.objectContaining({
|
|
501
|
+
taskId: 'interaction-1',
|
|
502
|
+
acceptReason: 'wxApp_answer_pending',
|
|
503
|
+
}),
|
|
504
|
+
['operational', 'behavioral']
|
|
505
|
+
);
|
|
506
|
+
});
|
|
465
507
|
});
|
|
466
508
|
|
|
467
509
|
describe('runWxAppReject', () => {
|
|
@@ -473,7 +515,12 @@ describe('runWxAppReject', () => {
|
|
|
473
515
|
|
|
474
516
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
475
517
|
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
476
|
-
{
|
|
518
|
+
expect.objectContaining({
|
|
519
|
+
taskId: 'interaction-1',
|
|
520
|
+
acceptReason: 'wxApp_offer_ready',
|
|
521
|
+
hasDeviceCallId: true,
|
|
522
|
+
hasDeviceId: true,
|
|
523
|
+
}),
|
|
477
524
|
['operational', 'behavioral']
|
|
478
525
|
);
|
|
479
526
|
});
|
|
@@ -488,11 +535,50 @@ describe('runWxAppReject', () => {
|
|
|
488
535
|
|
|
489
536
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
490
537
|
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
|
|
491
|
-
expect.objectContaining({
|
|
538
|
+
expect.objectContaining({
|
|
539
|
+
taskId: 'interaction-1',
|
|
540
|
+
acceptReason: 'wxApp_offer_ready',
|
|
541
|
+
}),
|
|
492
542
|
['operational', 'behavioral']
|
|
493
543
|
);
|
|
494
544
|
expect(lifecycle.mapWxAppVoiceError).toHaveBeenCalledWith(error, METHODS.REJECT);
|
|
495
545
|
});
|
|
546
|
+
|
|
547
|
+
it('includes captured acceptReason on decline success when pending', async () => {
|
|
548
|
+
const deps = makeDeps();
|
|
549
|
+
const lifecycle = makeLifecycle();
|
|
550
|
+
|
|
551
|
+
await runWxAppReject(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'});
|
|
552
|
+
|
|
553
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
554
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
555
|
+
expect.objectContaining({
|
|
556
|
+
taskId: 'interaction-1',
|
|
557
|
+
acceptReason: 'wxApp_answer_pending',
|
|
558
|
+
}),
|
|
559
|
+
['operational', 'behavioral']
|
|
560
|
+
);
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
it('includes captured acceptReason on decline failure when pending', async () => {
|
|
564
|
+
const deps = makeDeps();
|
|
565
|
+
const error = new Error('reject failed');
|
|
566
|
+
deps.answerCallOnWebexService!.rejectCall = jest.fn().mockRejectedValue(error);
|
|
567
|
+
const lifecycle = makeLifecycle();
|
|
568
|
+
|
|
569
|
+
await expect(
|
|
570
|
+
runWxAppReject(deps, lifecycle, {acceptReason: 'wxApp_answer_pending'})
|
|
571
|
+
).rejects.toThrow('reject failed');
|
|
572
|
+
|
|
573
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
574
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_FAILED,
|
|
575
|
+
expect.objectContaining({
|
|
576
|
+
taskId: 'interaction-1',
|
|
577
|
+
acceptReason: 'wxApp_answer_pending',
|
|
578
|
+
}),
|
|
579
|
+
['operational', 'behavioral']
|
|
580
|
+
);
|
|
581
|
+
});
|
|
496
582
|
});
|
|
497
583
|
|
|
498
584
|
describe('runWxAppOutdialDecline', () => {
|
|
@@ -509,7 +595,30 @@ describe('runWxAppOutdialDecline', () => {
|
|
|
509
595
|
]);
|
|
510
596
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
511
597
|
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
512
|
-
{
|
|
598
|
+
expect.objectContaining({
|
|
599
|
+
taskId: 'interaction-1',
|
|
600
|
+
acceptReason: 'wxApp_offer_ready',
|
|
601
|
+
hasDeviceCallId: true,
|
|
602
|
+
hasDeviceId: true,
|
|
603
|
+
}),
|
|
604
|
+
['operational', 'behavioral']
|
|
605
|
+
);
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('tracks post-accept acceptReason on outdial cancel when provided', async () => {
|
|
609
|
+
const deps = makeDeps();
|
|
610
|
+
const cancelResult = {type: 'RoutingMessage', trackingId: 'track-1', data: {}};
|
|
611
|
+
|
|
612
|
+
await runWxAppOutdialDecline(deps, async () => cancelResult, {
|
|
613
|
+
acceptReason: 'wxApp_answer_pending',
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
617
|
+
METRIC_EVENT_NAMES.WXAPP_TASK_DECLINE_SUCCESS,
|
|
618
|
+
expect.objectContaining({
|
|
619
|
+
taskId: 'interaction-1',
|
|
620
|
+
acceptReason: 'wxApp_answer_pending',
|
|
621
|
+
}),
|
|
513
622
|
['operational', 'behavioral']
|
|
514
623
|
);
|
|
515
624
|
});
|
|
@@ -555,6 +664,8 @@ describe('runWxAppOutdialDecline', () => {
|
|
|
555
664
|
trackingId: 'aqm-track-1',
|
|
556
665
|
failureReason: 'TASK_NOT_FOUND',
|
|
557
666
|
reasonCode: 404,
|
|
667
|
+
hasDeviceCallId: true,
|
|
668
|
+
hasDeviceId: true,
|
|
558
669
|
}),
|
|
559
670
|
['operational', 'behavioral']
|
|
560
671
|
);
|
|
@@ -596,7 +707,12 @@ describe('runWxAppTransmitDtmf metrics', () => {
|
|
|
596
707
|
|
|
597
708
|
expect(deps.metricsManager.trackEvent).toHaveBeenCalledWith(
|
|
598
709
|
METRIC_EVENT_NAMES.WXAPP_TASK_DTMF_SUCCESS,
|
|
599
|
-
{
|
|
710
|
+
expect.objectContaining({
|
|
711
|
+
taskId: 'interaction-1',
|
|
712
|
+
dtmfLength: 3,
|
|
713
|
+
hasDeviceCallId: true,
|
|
714
|
+
hasDeviceId: true,
|
|
715
|
+
}),
|
|
600
716
|
['operational', 'behavioral']
|
|
601
717
|
);
|
|
602
718
|
});
|
|
@@ -3,13 +3,15 @@ import {
|
|
|
3
3
|
callIdSuffix,
|
|
4
4
|
deriveWxAppAcceptReason,
|
|
5
5
|
logWxAppOfferDecision,
|
|
6
|
+
logWxAppOfferParticipantMismatch,
|
|
6
7
|
logWxAppSessionReadiness,
|
|
7
8
|
logWxAppTelephonyAction,
|
|
8
9
|
WXAPP_LOG_PREFIX,
|
|
9
10
|
} from '../../../../src/services/wxAppDiagnosticLogging';
|
|
10
11
|
|
|
11
12
|
jest.mock('../../../../src/logger-proxy', () => ({
|
|
12
|
-
|
|
13
|
+
log: jest.fn(),
|
|
14
|
+
warn: jest.fn(),
|
|
13
15
|
error: jest.fn(),
|
|
14
16
|
}));
|
|
15
17
|
|
|
@@ -76,7 +78,7 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
describe('log helpers', () => {
|
|
79
|
-
it('logs session readiness with
|
|
81
|
+
it('logs session readiness at log level with grep-friendly suffix', () => {
|
|
80
82
|
logWxAppSessionReadiness({
|
|
81
83
|
enableWxBetterTogether: true,
|
|
82
84
|
loginOption: 'EXTENSION',
|
|
@@ -86,8 +88,8 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
86
88
|
telephonyTaskType: 'Voice',
|
|
87
89
|
});
|
|
88
90
|
|
|
89
|
-
expect(LoggerProxy.
|
|
90
|
-
`${WXAPP_LOG_PREFIX} session readiness
|
|
91
|
+
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
92
|
+
expect.stringContaining(`${WXAPP_LOG_PREFIX} session readiness`),
|
|
91
93
|
expect.objectContaining({
|
|
92
94
|
module: 'wxAppDiagnosticLogging',
|
|
93
95
|
method: 'logWxAppSessionReadiness',
|
|
@@ -95,27 +97,61 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
95
97
|
feature: 'wxApp',
|
|
96
98
|
event: 'session_readiness',
|
|
97
99
|
loginOption: 'EXTENSION',
|
|
100
|
+
usersubPublished: true,
|
|
101
|
+
mercurySubscribed: true,
|
|
98
102
|
}),
|
|
99
103
|
})
|
|
100
104
|
);
|
|
105
|
+
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
106
|
+
expect.stringContaining('usersubPublished=true'),
|
|
107
|
+
expect.anything()
|
|
108
|
+
);
|
|
101
109
|
});
|
|
102
110
|
|
|
103
|
-
it('logs offer decision with acceptReason', () => {
|
|
111
|
+
it('logs offer decision at log level with acceptReason suffix', () => {
|
|
104
112
|
logWxAppOfferDecision({
|
|
105
113
|
interactionId: 'task-1',
|
|
106
114
|
acceptVisible: true,
|
|
107
115
|
acceptEnabled: false,
|
|
108
116
|
acceptReason: 'extension_non_wxApp_offer',
|
|
109
117
|
hasDeviceCallId: false,
|
|
118
|
+
hasDeviceId: false,
|
|
119
|
+
usersubPublished: true,
|
|
110
120
|
});
|
|
111
121
|
|
|
112
|
-
expect(LoggerProxy.
|
|
113
|
-
|
|
122
|
+
expect(LoggerProxy.log).toHaveBeenCalledWith(
|
|
123
|
+
expect.stringContaining('acceptReason=extension_non_wxApp_offer'),
|
|
114
124
|
expect.objectContaining({
|
|
115
125
|
data: expect.objectContaining({
|
|
116
126
|
event: 'offer_decision',
|
|
117
127
|
acceptReason: 'extension_non_wxApp_offer',
|
|
118
128
|
interactionId: 'task-1',
|
|
129
|
+
usersubPublished: true,
|
|
130
|
+
}),
|
|
131
|
+
})
|
|
132
|
+
);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('logs participant mismatch at warn level', () => {
|
|
136
|
+
logWxAppOfferParticipantMismatch({
|
|
137
|
+
interactionId: 'task-1',
|
|
138
|
+
usersubPublished: true,
|
|
139
|
+
hasDeviceCallId: false,
|
|
140
|
+
hasDeviceId: false,
|
|
141
|
+
participantDeviceType: 'wxApp',
|
|
142
|
+
acceptVisible: true,
|
|
143
|
+
acceptEnabled: false,
|
|
144
|
+
acceptReason: 'extension_non_wxApp_offer',
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
expect(LoggerProxy.warn).toHaveBeenCalledWith(
|
|
148
|
+
expect.stringContaining('participant fields mismatch'),
|
|
149
|
+
expect.objectContaining({
|
|
150
|
+
method: 'logWxAppOfferParticipantMismatch',
|
|
151
|
+
data: expect.objectContaining({
|
|
152
|
+
event: 'participant_fields_mismatch',
|
|
153
|
+
usersubPublished: true,
|
|
154
|
+
hasDeviceCallId: false,
|
|
119
155
|
}),
|
|
120
156
|
})
|
|
121
157
|
);
|
|
@@ -131,7 +167,7 @@ describe('wxAppDiagnosticLogging', () => {
|
|
|
131
167
|
});
|
|
132
168
|
|
|
133
169
|
expect(LoggerProxy.error).toHaveBeenCalledWith(
|
|
134
|
-
|
|
170
|
+
expect.stringContaining('telephony accept failed'),
|
|
135
171
|
expect.objectContaining({
|
|
136
172
|
data: expect.objectContaining({
|
|
137
173
|
trackingId: 'track-abc',
|