@webex/contact-center 3.12.0-llmrefactor.8 → 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.
- package/.sdd/manifest.json +3 -2
- package/ai-docs/CONTRACTS.md +2 -1
- package/dist/cc.js +108 -6
- package/dist/cc.js.map +1 -1
- package/dist/metrics/MetricsManager.js +16 -0
- package/dist/metrics/MetricsManager.js.map +1 -1
- package/dist/metrics/behavioral-events.js +91 -0
- package/dist/metrics/behavioral-events.js.map +1 -1
- package/dist/metrics/constants.js +16 -0
- package/dist/metrics/constants.js.map +1 -1
- package/dist/services/AnswerCallOnWebexService.js +34 -6
- package/dist/services/AnswerCallOnWebexService.js.map +1 -1
- package/dist/services/WebexCrossClientService.js +54 -3
- package/dist/services/WebexCrossClientService.js.map +1 -1
- package/dist/services/WxAppTelephonyMercurySync.js +21 -6
- package/dist/services/WxAppTelephonyMercurySync.js.map +1 -1
- package/dist/services/task/voice/Voice.js +81 -14
- package/dist/services/task/voice/Voice.js.map +1 -1
- package/dist/services/task/voice/wxAppVoiceMethods.js +154 -1
- package/dist/services/task/voice/wxAppVoiceMethods.js.map +1 -1
- package/dist/services/wxAppDiagnosticLogging.js +122 -0
- package/dist/services/wxAppDiagnosticLogging.js.map +1 -0
- package/dist/types/cc.d.ts +1 -0
- package/dist/types/metrics/MetricsManager.d.ts +6 -0
- package/dist/types/metrics/constants.d.ts +15 -0
- package/dist/types/services/AnswerCallOnWebexService.d.ts +1 -0
- package/dist/types/services/WebexCrossClientService.d.ts +4 -0
- package/dist/types/services/task/voice/Voice.d.ts +5 -1
- package/dist/types/services/task/voice/wxAppVoiceMethods.d.ts +3 -0
- package/dist/types/services/wxAppDiagnosticLogging.d.ts +51 -0
- package/dist/webex.js +1 -1
- package/package.json +8 -8
- package/src/cc.ts +155 -7
- package/src/metrics/MetricsManager.ts +16 -0
- package/src/metrics/ai-docs/metrics-spec.md +71 -9
- package/src/metrics/behavioral-events.ts +92 -0
- package/src/metrics/constants.ts +18 -0
- package/src/services/AnswerCallOnWebexService.ts +41 -8
- package/src/services/WebexCrossClientService.ts +85 -4
- package/src/services/WxAppTelephonyMercurySync.ts +31 -4
- package/src/services/agent/ai-docs/agent-spec.md +1 -0
- package/src/services/task/ai-docs/task-spec.md +2 -0
- package/src/services/task/voice/Voice.ts +115 -26
- package/src/services/task/voice/wxAppVoiceMethods.ts +211 -2
- package/src/services/wxAppDiagnosticLogging.ts +174 -0
- package/test/unit/spec/cc.ts +251 -22
- package/test/unit/spec/metrics/MetricsManager.ts +25 -0
- package/test/unit/spec/metrics/behavioral-events.ts +42 -0
- package/test/unit/spec/services/AnswerCallOnWebexService.ts +41 -0
- package/test/unit/spec/services/WebexCrossClientService.ts +207 -1
- package/test/unit/spec/services/WxAppTelephonyMercurySync.ts +23 -0
- package/test/unit/spec/services/task/voice/Voice.ts +380 -0
- package/test/unit/spec/services/task/voice/wxAppVoiceMethods.ts +145 -1
- package/test/unit/spec/services/wxAppDiagnosticLogging.ts +143 -0
- package/umd/contact-center.min.js +2 -2
- package/umd/contact-center.min.js.map +1 -1
|
@@ -12,19 +12,30 @@ export type WxAppTelephonyError = Error & {
|
|
|
12
12
|
status?: number | string;
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
15
|
+
const extractWxAppTrackingId = (source: unknown): string | undefined => {
|
|
16
|
+
const sourceObj = source as {details?: {trackingId?: string}; trackingId?: string};
|
|
17
|
+
|
|
18
|
+
return sourceObj?.details?.trackingId || sourceObj?.trackingId;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const extractWxAppHttpStatus = (source: unknown): number | string | undefined => {
|
|
22
|
+
const obj = source as {
|
|
20
23
|
statusCode?: number;
|
|
21
24
|
status?: number | string;
|
|
25
|
+
details?: {status?: number | string};
|
|
22
26
|
};
|
|
23
|
-
|
|
27
|
+
|
|
28
|
+
return obj?.statusCode ?? obj?.status ?? obj?.details?.status;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const markWxAppTelephonyError = (error: Error, source: unknown): WxAppTelephonyError => {
|
|
32
|
+
const marked = error as WxAppTelephonyError;
|
|
33
|
+
marked.isWxAppTelephonyError = true;
|
|
34
|
+
const trackingId = extractWxAppTrackingId(source);
|
|
24
35
|
if (trackingId) {
|
|
25
36
|
marked.trackingId = trackingId;
|
|
26
37
|
}
|
|
27
|
-
const status =
|
|
38
|
+
const status = extractWxAppHttpStatus(source);
|
|
28
39
|
if (status !== undefined) {
|
|
29
40
|
marked.status = status;
|
|
30
41
|
}
|
|
@@ -73,6 +84,10 @@ export default class AnswerCallOnWebexService {
|
|
|
73
84
|
return `${hydraUrl.replace(/\/$/, '')}/telephony/calls`;
|
|
74
85
|
}
|
|
75
86
|
|
|
87
|
+
private extractTrackingId(source: unknown): string | undefined {
|
|
88
|
+
return extractWxAppTrackingId(source);
|
|
89
|
+
}
|
|
90
|
+
|
|
76
91
|
private async telephonyRequest(
|
|
77
92
|
resource: string,
|
|
78
93
|
method: HTTP_METHODS,
|
|
@@ -88,16 +103,29 @@ export default class AnswerCallOnWebexService {
|
|
|
88
103
|
data: {uri, bodyKeys: Object.keys(body)},
|
|
89
104
|
});
|
|
90
105
|
|
|
91
|
-
|
|
106
|
+
const response = (await this.webex.request({
|
|
92
107
|
uri,
|
|
93
108
|
method,
|
|
94
109
|
body,
|
|
95
110
|
addAuthHeader: true,
|
|
96
111
|
})) as IHttpResponse;
|
|
112
|
+
|
|
113
|
+
LoggerProxy.info(`AnswerCallOnWebexService.${logMethod} success`, {
|
|
114
|
+
module: ANSWER_CALL_ON_WEBEX_FILE,
|
|
115
|
+
method: logMethod,
|
|
116
|
+
data: {statusCode: response.statusCode},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
return response;
|
|
97
120
|
} catch (error) {
|
|
121
|
+
const trackingId = this.extractTrackingId(error);
|
|
98
122
|
LoggerProxy.error(`AnswerCallOnWebexService.${logMethod} failed: ${error}`, {
|
|
99
123
|
module: ANSWER_CALL_ON_WEBEX_FILE,
|
|
100
124
|
method: logMethod,
|
|
125
|
+
data: {
|
|
126
|
+
trackingId,
|
|
127
|
+
status: extractWxAppHttpStatus(error),
|
|
128
|
+
},
|
|
101
129
|
});
|
|
102
130
|
const {error: detailedError} = getErrorDetails(error, logMethod, CC_FILE);
|
|
103
131
|
throw markWxAppTelephonyError(detailedError, error);
|
|
@@ -191,9 +219,14 @@ export default class AnswerCallOnWebexService {
|
|
|
191
219
|
throw markWxAppTelephonyError(err, error);
|
|
192
220
|
}
|
|
193
221
|
|
|
222
|
+
const trackingId = this.extractTrackingId(error);
|
|
194
223
|
LoggerProxy.error(`AnswerCallOnWebexService.getCallDetails failed: ${error}`, {
|
|
195
224
|
module: ANSWER_CALL_ON_WEBEX_FILE,
|
|
196
225
|
method: METHODS.GET_CALL_DETAILS_ON_WEBEX,
|
|
226
|
+
data: {
|
|
227
|
+
trackingId,
|
|
228
|
+
status: extractWxAppHttpStatus(error),
|
|
229
|
+
},
|
|
197
230
|
});
|
|
198
231
|
const {error: detailedError} = getErrorDetails(
|
|
199
232
|
error,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import LoggerProxy from '../logger-proxy';
|
|
2
2
|
import {METHODS} from '../constants';
|
|
3
3
|
import {WebexSDK} from '../types';
|
|
4
|
+
import MetricsManager from '../metrics/MetricsManager';
|
|
5
|
+
import {METRIC_EVENT_NAMES} from '../metrics/constants';
|
|
4
6
|
|
|
5
7
|
const WEBEX_CROSS_CLIENT_FILE = 'WebexCrossClientService';
|
|
6
8
|
const DEFAULT_CROSS_CLIENT_STATE_TTL = 900;
|
|
@@ -23,6 +25,8 @@ export default class WebexCrossClientService {
|
|
|
23
25
|
private appName = DEFAULT_APP_NAME;
|
|
24
26
|
/** Incremented on teardown/disable to ignore stale refresh timer callbacks. */
|
|
25
27
|
private refreshGeneration = 0;
|
|
28
|
+
/** Monotonic id for usersub publish metrics timers; stale publishes cancel only their own timer. */
|
|
29
|
+
private usersubPublishMetricsId = 0;
|
|
26
30
|
|
|
27
31
|
constructor(webex: WebexSDK) {
|
|
28
32
|
this.webex = webex;
|
|
@@ -144,21 +148,31 @@ export default class WebexCrossClientService {
|
|
|
144
148
|
|
|
145
149
|
public async setManageWebexCallingInWxcc(
|
|
146
150
|
enable: boolean,
|
|
147
|
-
options?: {userId?: string; ttl?: number; appName?: string}
|
|
151
|
+
options?: {userId?: string; ttl?: number; appName?: string; trackPublishMetrics?: boolean}
|
|
148
152
|
): Promise<void> {
|
|
149
153
|
const operationGeneration = this.refreshGeneration;
|
|
150
154
|
const userId = options?.userId ?? this.webex.internal.device?.userId;
|
|
151
155
|
const ttl = options?.ttl ?? DEFAULT_CROSS_CLIENT_STATE_TTL;
|
|
152
156
|
const appName = options?.appName ?? DEFAULT_APP_NAME;
|
|
157
|
+
const trackPublishMetrics = options?.trackPublishMetrics ?? false;
|
|
153
158
|
this.appName = appName;
|
|
154
159
|
|
|
155
160
|
if (!userId) {
|
|
156
|
-
|
|
161
|
+
const error = 'User ID is unavailable for cross-client publish';
|
|
162
|
+
this.trackUsersubPreflightFailure(enable, 'user_id_unavailable', error, trackPublishMetrics);
|
|
163
|
+
throw new Error(error);
|
|
157
164
|
}
|
|
158
165
|
|
|
159
166
|
const deviceUrl = this.getDeviceUrl();
|
|
160
167
|
if (!deviceUrl) {
|
|
161
|
-
|
|
168
|
+
const error = 'Device URL is unavailable for cross-client publish';
|
|
169
|
+
this.trackUsersubPreflightFailure(
|
|
170
|
+
enable,
|
|
171
|
+
'device_url_unavailable',
|
|
172
|
+
error,
|
|
173
|
+
trackPublishMetrics
|
|
174
|
+
);
|
|
175
|
+
throw new Error(error);
|
|
162
176
|
}
|
|
163
177
|
|
|
164
178
|
const composition = {
|
|
@@ -173,9 +187,47 @@ export default class WebexCrossClientService {
|
|
|
173
187
|
],
|
|
174
188
|
};
|
|
175
189
|
|
|
176
|
-
|
|
190
|
+
const metricsManager = MetricsManager.getInstance();
|
|
191
|
+
const usersubPublishEventKeys = [
|
|
192
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
|
|
193
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
let publishMetricsId = 0;
|
|
197
|
+
if (trackPublishMetrics) {
|
|
198
|
+
this.usersubPublishMetricsId += 1;
|
|
199
|
+
publishMetricsId = this.usersubPublishMetricsId;
|
|
200
|
+
metricsManager.timeEvent(usersubPublishEventKeys);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
try {
|
|
204
|
+
await this.publishCrossClientState([userId], ttl, composition, 'setManageWebexCallingInWxcc');
|
|
205
|
+
} catch (error) {
|
|
206
|
+
if (trackPublishMetrics) {
|
|
207
|
+
if (operationGeneration !== this.refreshGeneration) {
|
|
208
|
+
if (publishMetricsId === this.usersubPublishMetricsId) {
|
|
209
|
+
metricsManager.cancelTimedEvent(usersubPublishEventKeys);
|
|
210
|
+
}
|
|
211
|
+
} else {
|
|
212
|
+
metricsManager.trackEvent(
|
|
213
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
214
|
+
{
|
|
215
|
+
enableWxBetterTogether: enable,
|
|
216
|
+
error: error instanceof Error ? error.toString() : String(error),
|
|
217
|
+
},
|
|
218
|
+
['operational', 'behavioral']
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
177
225
|
|
|
178
226
|
if (operationGeneration !== this.refreshGeneration) {
|
|
227
|
+
if (trackPublishMetrics && publishMetricsId === this.usersubPublishMetricsId) {
|
|
228
|
+
metricsManager.cancelTimedEvent(usersubPublishEventKeys);
|
|
229
|
+
}
|
|
230
|
+
|
|
179
231
|
return;
|
|
180
232
|
}
|
|
181
233
|
|
|
@@ -188,6 +240,14 @@ export default class WebexCrossClientService {
|
|
|
188
240
|
this.clearRefreshTimer();
|
|
189
241
|
}
|
|
190
242
|
|
|
243
|
+
if (trackPublishMetrics) {
|
|
244
|
+
metricsManager.trackEvent(
|
|
245
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_SUCCESS,
|
|
246
|
+
{enableWxBetterTogether: enable},
|
|
247
|
+
['operational', 'behavioral']
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
191
251
|
LoggerProxy.info(`Cross-client answer-calls-on-wxcc set to ${enable}`, {
|
|
192
252
|
module: WEBEX_CROSS_CLIENT_FILE,
|
|
193
253
|
method: METHODS.SET_MANAGE_WEBEX_CALLING_IN_WXCC,
|
|
@@ -199,6 +259,27 @@ export default class WebexCrossClientService {
|
|
|
199
259
|
});
|
|
200
260
|
}
|
|
201
261
|
|
|
262
|
+
private trackUsersubPreflightFailure(
|
|
263
|
+
enable: boolean,
|
|
264
|
+
skipReason: 'user_id_unavailable' | 'device_url_unavailable',
|
|
265
|
+
error: string,
|
|
266
|
+
trackPublishMetrics: boolean
|
|
267
|
+
): void {
|
|
268
|
+
if (!trackPublishMetrics) {
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
MetricsManager.getInstance().trackEvent(
|
|
273
|
+
METRIC_EVENT_NAMES.WXAPP_USERSUB_PUBLISH_FAILED,
|
|
274
|
+
{
|
|
275
|
+
enableWxBetterTogether: enable,
|
|
276
|
+
skipReason,
|
|
277
|
+
error,
|
|
278
|
+
},
|
|
279
|
+
['operational', 'behavioral']
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
|
|
202
283
|
public teardown(): void {
|
|
203
284
|
this.refreshGeneration += 1;
|
|
204
285
|
this.clearRefreshTimer();
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import LoggerProxy from '../logger-proxy';
|
|
2
2
|
import {METHODS} from '../constants';
|
|
3
3
|
import {WebexSDK} from '../types';
|
|
4
|
+
import MetricsManager from '../metrics/MetricsManager';
|
|
5
|
+
import {METRIC_EVENT_NAMES} from '../metrics/constants';
|
|
6
|
+
import {callIdSuffix, logWxAppMercuryMuteSync} from './wxAppDiagnosticLogging';
|
|
4
7
|
|
|
5
8
|
const WXAPP_TELEPHONY_MERCURY_SYNC_FILE = 'WxAppTelephonyMercurySync';
|
|
6
9
|
|
|
@@ -42,6 +45,11 @@ export default class WxAppTelephonyMercurySync {
|
|
|
42
45
|
private handleMuteStateEvent = (agentId: string, event: UpdatedCallMuteStateEvent): void => {
|
|
43
46
|
const {actorId, callId, muted} = event?.data ?? {};
|
|
44
47
|
if (!actorId || !callId || muted === undefined) {
|
|
48
|
+
logWxAppMercuryMuteSync({
|
|
49
|
+
phase: 'dropped',
|
|
50
|
+
dropReason: 'missing_mercury_event_fields',
|
|
51
|
+
});
|
|
52
|
+
|
|
45
53
|
return;
|
|
46
54
|
}
|
|
47
55
|
|
|
@@ -49,13 +57,20 @@ export default class WxAppTelephonyMercurySync {
|
|
|
49
57
|
const decodedCallId = this.decodeBase64(callId);
|
|
50
58
|
|
|
51
59
|
if (!decodedActorId.endsWith(agentId)) {
|
|
60
|
+
logWxAppMercuryMuteSync({
|
|
61
|
+
phase: 'dropped',
|
|
62
|
+
muted,
|
|
63
|
+
callIdSuffix: callIdSuffix(decodedCallId),
|
|
64
|
+
dropReason: 'actor_id_mismatch',
|
|
65
|
+
});
|
|
66
|
+
|
|
52
67
|
return;
|
|
53
68
|
}
|
|
54
69
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
70
|
+
logWxAppMercuryMuteSync({
|
|
71
|
+
phase: 'received',
|
|
72
|
+
muted,
|
|
73
|
+
callIdSuffix: callIdSuffix(decodedCallId),
|
|
59
74
|
});
|
|
60
75
|
|
|
61
76
|
this.muteChangeHandler?.(decodedCallId, muted);
|
|
@@ -71,6 +86,12 @@ export default class WxAppTelephonyMercurySync {
|
|
|
71
86
|
method: METHODS.SYNC_WXAPP_MUTE_FROM_MERCURY,
|
|
72
87
|
});
|
|
73
88
|
|
|
89
|
+
MetricsManager.getInstance().trackEvent(
|
|
90
|
+
METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_FAILED,
|
|
91
|
+
{error: 'Mercury is unavailable for wxApp mute sync'},
|
|
92
|
+
['operational', 'behavioral']
|
|
93
|
+
);
|
|
94
|
+
|
|
74
95
|
return;
|
|
75
96
|
}
|
|
76
97
|
|
|
@@ -87,6 +108,12 @@ export default class WxAppTelephonyMercurySync {
|
|
|
87
108
|
mercury.on(TELEPHONY_CALL_MUTED, this.boundMuteHandler);
|
|
88
109
|
mercury.on(TELEPHONY_CALL_UNMUTED, this.boundUnmuteHandler);
|
|
89
110
|
|
|
111
|
+
MetricsManager.getInstance().trackEvent(
|
|
112
|
+
METRIC_EVENT_NAMES.WXAPP_MERCURY_SUBSCRIBE_SUCCESS,
|
|
113
|
+
{},
|
|
114
|
+
['operational', 'behavioral']
|
|
115
|
+
);
|
|
116
|
+
|
|
90
117
|
LoggerProxy.info('Subscribed to wxApp telephony Mercury mute sync', {
|
|
91
118
|
module: WXAPP_TELEPHONY_MERCURY_SYNC_FILE,
|
|
92
119
|
method: METHODS.SYNC_WXAPP_MUTE_FROM_MERCURY,
|
|
@@ -87,6 +87,7 @@ Key nested WebSocket binds use the actual outer/data constants, for example `CC_
|
|
|
87
87
|
| AGENT-R-003 | Buddy-agent lookup must preserve its typed request and correlated response contract. | Consult/transfer selection depends on backend-filtered availability. | `src/services/agent/index.ts` | `test/unit/spec/services/agent/index.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
|
|
88
88
|
| AGENT-R-004 | The agent factory exposes `reload`, while ContactCenter alone decides when automated relogin is permitted. | Transport recovery lacks the profile/policy context required to mutate an agent session safely. | `src/cc.ts` | `test/unit/spec/cc.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
|
|
89
89
|
| AGENT-R-005 | Device/profile update must be documented as `ContactCenter.updateAgentProfile`, not as an agent-factory method. | Calling a non-existent `routingAgent.deviceUpdate` would fail at runtime. | `src/cc.ts` | `test/unit/spec/cc.ts` | None; source and test evidence rechecked during the 2026-07-09 remediation; independent document revalidation pending. | PRESENT |
|
|
90
|
+
| AGENT-R-006 | BROWSER station login must send `dialNumber` and `deviceId` as `webrtc-{agentId}` on `POST /v1/agents/login`. EXTENSION and AGENT_DN continue to send the caller-supplied dial number for both fields. | QRM stores `agent.dn` from `dialNumber`. Legacy VPOP RTMS only treats a DN as WebRTC when it starts with `webrtc-`; a bare UUID gets a steering prefix and the agent invite fails with SIP 484. | `src/cc.ts` | `test/unit/spec/cc.ts` | Public `stationLogin({loginOption: 'BROWSER'})` input is unchanged; only the backend login payload is prefixed. | PRESENT |
|
|
90
91
|
|
|
91
92
|
## Design Overview
|
|
92
93
|
`routingAgent` is a pure AQM factory. It receives an initialized AqmReqs instance and returns five request functions: `reload`, `logout`, `stationLogin`, `stateChange`, and `buddyAgents`. Each request config declares the endpoint/payload plus exact `CC_EVENTS` notification binds.
|
|
@@ -619,6 +619,8 @@ This keeps transcript and suggestion delivery aligned on the same per-task event
|
|
|
619
619
|
|
|
620
620
|
**wxApp offer UI (`uiControlsComputer`):** `wxAppAcceptInFlight` disables accept/decline during the accept REST call. `wxAppAnswerPending` additionally disables accept and decline for **inbound** offers until ASSIGN; wxApp **outdial** keeps decline enabled during the post-accept "Calling…" phase so `cancelTask` remains available.
|
|
621
621
|
|
|
622
|
+
**wxApp decline observability:** Inbound wxApp offers decline via telephony `rejectCall` (`runWxAppReject` → `WXAPP_TASK_DECLINE_*`). wxApp outdial cancellations use CC routing `cancelTask` (`runWxAppOutdialDecline` → additive `WXAPP_TASK_DECLINE_*` plus existing `TASK_DECLINE_*`).
|
|
623
|
+
|
|
622
624
|
**wxApp mute backfill guard (`Voice.syncWxAppMuteFromCallDetails`):** Skips telephony `GET /calls/{callId}` when the interaction is terminated or the task is a pre-accept wxApp OFFERED offer (`wxAppAnswerPending` false). Post-accept OFFERED and engaged CONNECTED sync still run. `isWxAppEngagedForControls` and `getWebexCallingCallId` exclude `TERMINATED` and `COMPLETED`. Expected 400 / "Call not found" / `101002` responses are not logged as errors.
|
|
623
625
|
|
|
624
626
|
For BROWSER login, TaskManager integrates with WebCalling:
|
|
@@ -41,12 +41,20 @@ import {
|
|
|
41
41
|
isWebexAppInboundCallingOffer as wxIsWebexAppInboundCallingOffer,
|
|
42
42
|
mapWxAppVoiceError,
|
|
43
43
|
runWxAppAccept,
|
|
44
|
+
runWxAppOutdialDecline,
|
|
44
45
|
runWxAppReject,
|
|
45
46
|
runWxAppToggleMute,
|
|
46
47
|
runWxAppTransmitDtmf,
|
|
47
48
|
WxAppVoiceDependencies,
|
|
48
49
|
WxAppVoiceLifecycle,
|
|
49
50
|
} from './wxAppVoiceMethods';
|
|
51
|
+
import {
|
|
52
|
+
deriveWxAppAcceptReason,
|
|
53
|
+
logWxAppMercuryMuteSync,
|
|
54
|
+
logWxAppOfferDecision,
|
|
55
|
+
callIdSuffix,
|
|
56
|
+
WxAppAcceptReason,
|
|
57
|
+
} from '../../wxAppDiagnosticLogging';
|
|
50
58
|
|
|
51
59
|
export default class Voice extends Task implements IVoice {
|
|
52
60
|
private static readonly WXAPP_MUTE_SYNC_RETRY_DELAY_MS = 50;
|
|
@@ -58,7 +66,9 @@ export default class Voice extends Task implements IVoice {
|
|
|
58
66
|
private wxAppAnswerPending = false;
|
|
59
67
|
private wxAppAcceptInFlight = false;
|
|
60
68
|
private wxAppMuteSyncInFlight?: Promise<boolean | undefined>;
|
|
61
|
-
private
|
|
69
|
+
private wxAppMuteToggleTail: Promise<void> = Promise.resolve();
|
|
70
|
+
private wxAppDtmfTail: Promise<void> = Promise.resolve();
|
|
71
|
+
private lastLoggedWxAppAcceptReason?: WxAppAcceptReason;
|
|
62
72
|
|
|
63
73
|
constructor(
|
|
64
74
|
contact: ReturnType<typeof routingContact>,
|
|
@@ -95,6 +105,7 @@ export default class Voice extends Task implements IVoice {
|
|
|
95
105
|
enableWxBetterTogether: this.enableWxBetterTogether,
|
|
96
106
|
answerCallOnWebexService: this.answerCallOnWebexService,
|
|
97
107
|
agentId: this.agentId,
|
|
108
|
+
metricsManager: this.metricsManager,
|
|
98
109
|
getTaskData: () => this.data,
|
|
99
110
|
getTaskState: () => this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined,
|
|
100
111
|
getWxAppMuted: () => this.wxAppMuted,
|
|
@@ -135,17 +146,98 @@ export default class Voice extends Task implements IVoice {
|
|
|
135
146
|
this.updateUiControls(true);
|
|
136
147
|
}
|
|
137
148
|
|
|
149
|
+
protected updateUiControls(forceEmit = false): void {
|
|
150
|
+
super.updateUiControls(forceEmit);
|
|
151
|
+
this.logWxAppOfferDecisionIfNeeded();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private logWxAppOfferDecisionIfNeeded(): void {
|
|
155
|
+
if (!this.enableWxBetterTogether) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const state = this.stateMachineService?.getSnapshot?.()?.value as TaskState | undefined;
|
|
160
|
+
if (state !== TaskState.OFFERED) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const accept = this.currentUiControls?.main?.accept;
|
|
165
|
+
if (!accept?.isVisible) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const deps = this.getWxAppVoiceDependencies();
|
|
170
|
+
const isOutdial = this.data?.interaction?.outboundType === 'OUTDIAL';
|
|
171
|
+
const isWxAppInboundOffer = this.isWebexAppInboundCallingOffer();
|
|
172
|
+
const isWxAppOutdialOffer = this.isWebexAppCallingOffer() && isOutdial;
|
|
173
|
+
const deviceDetails = getCallingDeviceDetails(deps);
|
|
174
|
+
const acceptReason = deriveWxAppAcceptReason({
|
|
175
|
+
isWxAppInboundOffer,
|
|
176
|
+
isWxAppOutdialOffer,
|
|
177
|
+
isWebrtc: this.uiControlConfig.voiceVariant === VOICE_VARIANT.WEBRTC,
|
|
178
|
+
isOutdial,
|
|
179
|
+
wxAppAcceptInFlight: this.wxAppAcceptInFlight,
|
|
180
|
+
wxAppAnswerPending: this.wxAppAnswerPending,
|
|
181
|
+
enableWxBetterTogether: this.enableWxBetterTogether,
|
|
182
|
+
hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
if (acceptReason === this.lastLoggedWxAppAcceptReason) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.lastLoggedWxAppAcceptReason = acceptReason;
|
|
190
|
+
|
|
191
|
+
logWxAppOfferDecision({
|
|
192
|
+
interactionId: this.data.interactionId,
|
|
193
|
+
acceptVisible: accept.isVisible,
|
|
194
|
+
acceptEnabled: accept.isEnabled,
|
|
195
|
+
acceptReason,
|
|
196
|
+
wxAppParticipantDeviceType: deviceDetails?.deviceType,
|
|
197
|
+
hasDeviceCallId: Boolean(deviceDetails?.deviceCallId),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
|
|
138
201
|
public applyWxAppMuteStateFromSync(incomingCallId: string, muted: boolean): void {
|
|
139
202
|
if (!this.enableWxBetterTogether) {
|
|
140
203
|
return;
|
|
141
204
|
}
|
|
142
205
|
|
|
143
206
|
const activeCallId = this.getWebexCallingCallId();
|
|
144
|
-
|
|
207
|
+
const incomingSuffix = callIdSuffix(incomingCallId);
|
|
208
|
+
const activeSuffix = callIdSuffix(activeCallId);
|
|
209
|
+
|
|
210
|
+
if (!activeCallId || !incomingCallId.endsWith(activeCallId)) {
|
|
211
|
+
logWxAppMercuryMuteSync({
|
|
212
|
+
phase: 'dropped',
|
|
213
|
+
muted,
|
|
214
|
+
callIdSuffix: incomingSuffix,
|
|
215
|
+
interactionId: this.data.interactionId,
|
|
216
|
+
dropReason: !activeCallId ? 'no_active_wxApp_call' : 'call_id_mismatch',
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (this.wxAppMuted === muted) {
|
|
223
|
+
logWxAppMercuryMuteSync({
|
|
224
|
+
phase: 'dropped',
|
|
225
|
+
muted,
|
|
226
|
+
callIdSuffix: incomingSuffix,
|
|
227
|
+
interactionId: this.data.interactionId,
|
|
228
|
+
dropReason: 'mute_state_unchanged',
|
|
229
|
+
});
|
|
230
|
+
|
|
145
231
|
return;
|
|
146
232
|
}
|
|
147
233
|
|
|
148
234
|
this.wxAppMuted = muted;
|
|
235
|
+
logWxAppMercuryMuteSync({
|
|
236
|
+
phase: 'applied',
|
|
237
|
+
muted,
|
|
238
|
+
callIdSuffix: activeSuffix,
|
|
239
|
+
interactionId: this.data.interactionId,
|
|
240
|
+
});
|
|
149
241
|
this.emit(TASK_EVENTS.TASK_WXAPP_MUTE_STATE_UPDATED, {muted});
|
|
150
242
|
}
|
|
151
243
|
|
|
@@ -354,7 +446,9 @@ export default class Voice extends Task implements IVoice {
|
|
|
354
446
|
]);
|
|
355
447
|
|
|
356
448
|
this.setWxAppAnswerPending(false);
|
|
357
|
-
const response = await this.
|
|
449
|
+
const response = await runWxAppOutdialDecline(this.getWxAppVoiceDependencies(), () =>
|
|
450
|
+
this.contact.cancelTask({interactionId})
|
|
451
|
+
);
|
|
358
452
|
|
|
359
453
|
this.metricsManager.trackEvent(
|
|
360
454
|
METRIC_EVENT_NAMES.TASK_DECLINE_SUCCESS,
|
|
@@ -388,25 +482,13 @@ export default class Voice extends Task implements IVoice {
|
|
|
388
482
|
*/
|
|
389
483
|
public async toggleMute(options?: TaskToggleMuteOptions): Promise<void> {
|
|
390
484
|
if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
this.getWxAppVoiceDependencies(),
|
|
397
|
-
this.createWxAppLifecycle(),
|
|
398
|
-
options
|
|
399
|
-
);
|
|
400
|
-
|
|
401
|
-
this.wxAppMuteToggleInFlight = togglePromise;
|
|
402
|
-
togglePromise
|
|
403
|
-
.finally(() => {
|
|
404
|
-
if (this.wxAppMuteToggleInFlight === togglePromise) {
|
|
405
|
-
this.wxAppMuteToggleInFlight = undefined;
|
|
406
|
-
}
|
|
407
|
-
})
|
|
408
|
-
.catch(() => undefined);
|
|
485
|
+
const togglePromise = this.wxAppMuteToggleTail
|
|
486
|
+
.catch(() => undefined)
|
|
487
|
+
.then(() =>
|
|
488
|
+
runWxAppToggleMute(this.getWxAppVoiceDependencies(), this.createWxAppLifecycle(), options)
|
|
489
|
+
);
|
|
409
490
|
|
|
491
|
+
this.wxAppMuteToggleTail = togglePromise.catch(() => undefined);
|
|
410
492
|
await togglePromise;
|
|
411
493
|
|
|
412
494
|
return;
|
|
@@ -420,11 +502,18 @@ export default class Voice extends Task implements IVoice {
|
|
|
420
502
|
*/
|
|
421
503
|
public async transmitDtmf(options: TaskTransmitDtmfOptions): Promise<void> {
|
|
422
504
|
if (this.enableWxBetterTogether && this.getWebexCallingCallId()) {
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
505
|
+
const dtmfPromise = this.wxAppDtmfTail
|
|
506
|
+
.catch(() => undefined)
|
|
507
|
+
.then(() =>
|
|
508
|
+
runWxAppTransmitDtmf(
|
|
509
|
+
this.getWxAppVoiceDependencies(),
|
|
510
|
+
this.createWxAppLifecycle(),
|
|
511
|
+
options
|
|
512
|
+
)
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
this.wxAppDtmfTail = dtmfPromise.catch(() => undefined);
|
|
516
|
+
await dtmfPromise;
|
|
428
517
|
|
|
429
518
|
return;
|
|
430
519
|
}
|