@servicetitan/journey 2.1.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/__test-utils__/test-runtime.d.ts.map +1 -1
- package/dist/__test-utils__/test-runtime.js +9 -2
- package/dist/__test-utils__/test-runtime.js.map +1 -1
- package/dist/__tests__/core/sanitize.test.d.ts +2 -0
- package/dist/__tests__/core/sanitize.test.d.ts.map +1 -0
- package/dist/__tests__/hardening.test.d.ts +2 -0
- package/dist/__tests__/hardening.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/host-mfe.test.d.ts +2 -0
- package/dist/__tests__/integrations/host-mfe.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/use-journey-step.test.d.ts +2 -0
- package/dist/__tests__/integrations/use-journey-step.test.d.ts.map +1 -0
- package/dist/config.d.ts +57 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +87 -24
- package/dist/config.js.map +1 -1
- package/dist/core/abort-error.d.ts +4 -0
- package/dist/core/abort-error.d.ts.map +1 -0
- package/dist/core/abort-error.js +22 -0
- package/dist/core/abort-error.js.map +1 -0
- package/dist/core/api.d.ts +22 -9
- package/dist/core/api.d.ts.map +1 -1
- package/dist/core/api.js +77 -30
- package/dist/core/api.js.map +1 -1
- package/dist/core/endpoint-policy.d.ts +14 -4
- package/dist/core/endpoint-policy.d.ts.map +1 -1
- package/dist/core/endpoint-policy.js +32 -1
- package/dist/core/endpoint-policy.js.map +1 -1
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/limits.d.ts +29 -0
- package/dist/core/limits.d.ts.map +1 -0
- package/dist/core/limits.js +63 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/request-policy.d.ts +39 -0
- package/dist/core/request-policy.d.ts.map +1 -0
- package/dist/core/request-policy.js +18 -0
- package/dist/core/request-policy.js.map +1 -0
- package/dist/core/runtime.d.ts +65 -15
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +403 -126
- package/dist/core/runtime.js.map +1 -1
- package/dist/core/sanitize.d.ts +23 -0
- package/dist/core/sanitize.d.ts.map +1 -0
- package/dist/core/sanitize.js +143 -0
- package/dist/core/sanitize.js.map +1 -0
- package/dist/core/step-tag.d.ts +25 -6
- package/dist/core/step-tag.d.ts.map +1 -1
- package/dist/core/step-tag.js +56 -10
- package/dist/core/step-tag.js.map +1 -1
- package/dist/core/types.d.ts +73 -25
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +4 -1
- package/dist/core/types.js.map +1 -1
- package/dist/global.d.ts +91 -13
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js +235 -25
- package/dist/global.js.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/axios.d.ts +5 -1
- package/dist/integrations/axios.d.ts.map +1 -1
- package/dist/integrations/axios.js +146 -34
- package/dist/integrations/axios.js.map +1 -1
- package/dist/integrations/fetch.d.ts +3 -2
- package/dist/integrations/fetch.d.ts.map +1 -1
- package/dist/integrations/fetch.js +105 -26
- package/dist/integrations/fetch.js.map +1 -1
- package/dist/integrations/http-report.d.ts +27 -0
- package/dist/integrations/http-report.d.ts.map +1 -0
- package/dist/integrations/http-report.js +123 -0
- package/dist/integrations/http-report.js.map +1 -0
- package/dist/integrations/jquery/index.d.ts +8 -4
- package/dist/integrations/jquery/index.d.ts.map +1 -1
- package/dist/integrations/jquery/index.js +26 -8
- package/dist/integrations/jquery/index.js.map +1 -1
- package/dist/integrations/jquery/instrument.d.ts +14 -5
- package/dist/integrations/jquery/instrument.d.ts.map +1 -1
- package/dist/integrations/jquery/instrument.js +111 -29
- package/dist/integrations/jquery/instrument.js.map +1 -1
- package/dist/integrations/react/context.d.ts +4 -1
- package/dist/integrations/react/context.d.ts.map +1 -1
- package/dist/integrations/react/context.js +4 -1
- package/dist/integrations/react/context.js.map +1 -1
- package/dist/integrations/react/scope.d.ts +14 -3
- package/dist/integrations/react/scope.d.ts.map +1 -1
- package/dist/integrations/react/scope.js +46 -8
- package/dist/integrations/react/scope.js.map +1 -1
- package/dist/integrations/react/stable-config-keys.d.ts +6 -0
- package/dist/integrations/react/stable-config-keys.d.ts.map +1 -0
- package/dist/integrations/react/stable-config-keys.js +21 -0
- package/dist/integrations/react/stable-config-keys.js.map +1 -0
- package/dist/integrations/react/use-journey-step.d.ts +11 -0
- package/dist/integrations/react/use-journey-step.d.ts.map +1 -0
- package/dist/integrations/react/use-journey-step.js +19 -0
- package/dist/integrations/react/use-journey-step.js.map +1 -0
- package/dist/integrations/request-key.d.ts +8 -3
- package/dist/integrations/request-key.d.ts.map +1 -1
- package/dist/integrations/request-key.js +10 -9
- package/dist/integrations/request-key.js.map +1 -1
- package/dist/integrations/soft-skip.d.ts +7 -0
- package/dist/integrations/soft-skip.d.ts.map +1 -0
- package/dist/integrations/soft-skip.js +30 -0
- package/dist/integrations/soft-skip.js.map +1 -0
- package/dist/sinks/console.d.ts.map +1 -1
- package/dist/sinks/console.js +2 -1
- package/dist/sinks/console.js.map +1 -1
- package/dist/sinks/datadog.d.ts +1 -1
- package/dist/sinks/datadog.d.ts.map +1 -1
- package/dist/sinks/datadog.js +16 -20
- package/dist/sinks/datadog.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-utils__/test-runtime.ts +9 -2
- package/src/__tests__/config.test.ts +150 -31
- package/src/__tests__/core/ambient.test.ts +16 -10
- package/src/__tests__/core/api.test.ts +7 -7
- package/src/__tests__/core/define-journey.test.ts +28 -11
- package/src/__tests__/core/requests.test.ts +49 -31
- package/src/__tests__/core/runtime.test.ts +67 -20
- package/src/__tests__/core/sanitize.test.ts +69 -0
- package/src/__tests__/core/step.test.ts +132 -36
- package/src/__tests__/core/timeouts.test.ts +69 -4
- package/src/__tests__/global.test.ts +44 -3
- package/src/__tests__/hardening.test.ts +613 -0
- package/src/__tests__/integrations/axios.test.ts +247 -27
- package/src/__tests__/integrations/expose-app-journey.test.ts +9 -3
- package/src/__tests__/integrations/fetch.test.ts +153 -19
- package/src/__tests__/integrations/host-mfe.test.ts +548 -0
- package/src/__tests__/integrations/jquery.test.ts +209 -34
- package/src/__tests__/integrations/scope.test.tsx +160 -2
- package/src/__tests__/integrations/use-journey-step.test.tsx +43 -0
- package/src/__tests__/sinks/console.test.ts +9 -4
- package/src/__tests__/sinks/datadog.test.ts +47 -0
- package/src/config.ts +162 -38
- package/src/core/abort-error.ts +24 -0
- package/src/core/api.ts +83 -33
- package/src/core/endpoint-policy.ts +46 -4
- package/src/core/index.ts +12 -4
- package/src/core/limits.ts +73 -0
- package/src/core/request-policy.ts +66 -0
- package/src/core/runtime.ts +507 -133
- package/src/core/sanitize.ts +177 -0
- package/src/core/step-tag.ts +82 -12
- package/src/core/types.ts +79 -25
- package/src/global.ts +312 -42
- package/src/index.ts +17 -17
- package/src/integrations/axios.ts +183 -45
- package/src/integrations/fetch.ts +125 -39
- package/src/integrations/http-report.ts +184 -0
- package/src/integrations/jquery/index.ts +34 -10
- package/src/integrations/jquery/instrument.ts +126 -42
- package/src/integrations/react/context.ts +4 -1
- package/src/integrations/react/scope.tsx +59 -9
- package/src/integrations/react/stable-config-keys.ts +28 -0
- package/src/integrations/react/use-journey-step.ts +31 -0
- package/src/integrations/request-key.ts +18 -10
- package/src/integrations/soft-skip.ts +35 -0
- package/src/sinks/console.ts +2 -1
- package/src/sinks/datadog.ts +23 -29
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/** @jest-environment jsdom */
|
|
2
2
|
import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
|
|
3
|
-
import { completeJourney,
|
|
3
|
+
import { completeJourney, journeyStep } from '../../core';
|
|
4
4
|
import { moduleRuntime } from '../../core/runtime';
|
|
5
|
+
import { getSharedPolicies } from '../../global';
|
|
5
6
|
import { instrumentAxios, type AxiosLikeInstance } from '../../integrations/axios';
|
|
6
7
|
|
|
7
8
|
const emitMock = jest.fn();
|
|
@@ -25,7 +26,12 @@ function createAxiosMock(): AxiosLikeInstance & {
|
|
|
25
26
|
request: {
|
|
26
27
|
use(onFulfilled) {
|
|
27
28
|
request.push(onFulfilled);
|
|
28
|
-
return
|
|
29
|
+
return request.length - 1;
|
|
30
|
+
},
|
|
31
|
+
eject(id) {
|
|
32
|
+
if (typeof id === 'number') {
|
|
33
|
+
request.splice(id, 1);
|
|
34
|
+
}
|
|
29
35
|
},
|
|
30
36
|
},
|
|
31
37
|
response: {
|
|
@@ -34,7 +40,13 @@ function createAxiosMock(): AxiosLikeInstance & {
|
|
|
34
40
|
if (onRejected) {
|
|
35
41
|
responseErr.push(onRejected);
|
|
36
42
|
}
|
|
37
|
-
return
|
|
43
|
+
return responseOk.length - 1;
|
|
44
|
+
},
|
|
45
|
+
eject(id) {
|
|
46
|
+
if (typeof id === 'number') {
|
|
47
|
+
responseOk.splice(id, 1);
|
|
48
|
+
responseErr.splice(id, 1);
|
|
49
|
+
}
|
|
38
50
|
},
|
|
39
51
|
},
|
|
40
52
|
},
|
|
@@ -51,7 +63,7 @@ describe('[journey] instrumentAxios', () => {
|
|
|
51
63
|
group: 'test',
|
|
52
64
|
service: 'test',
|
|
53
65
|
timeoutMs: 0,
|
|
54
|
-
|
|
66
|
+
slowRequestMs: 5_000,
|
|
55
67
|
};
|
|
56
68
|
|
|
57
69
|
useJourneyTestRuntime(emitMock);
|
|
@@ -74,6 +86,71 @@ describe('[journey] instrumentAxios', () => {
|
|
|
74
86
|
expect(axios.responseErrHandlers).toHaveLength(1);
|
|
75
87
|
});
|
|
76
88
|
|
|
89
|
+
test.each([undefined, null])('warns and soft-skips when axios is missing (%p)', bad => {
|
|
90
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
91
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
92
|
+
|
|
93
|
+
expect(() => instrumentAxios(bad)).not.toThrow();
|
|
94
|
+
expect(warn).toHaveBeenCalledWith(
|
|
95
|
+
expect.stringContaining('[journey] instrumentAxios: skipped — missing')
|
|
96
|
+
);
|
|
97
|
+
expect(error).not.toHaveBeenCalled();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test.each([{}, { interceptors: {} }, { interceptors: { request: {}, response: {} } }])(
|
|
101
|
+
'errors and soft-skips when axios has invalid shape (%p)',
|
|
102
|
+
bad => {
|
|
103
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
104
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
105
|
+
|
|
106
|
+
expect(() => instrumentAxios(bad as never)).not.toThrow();
|
|
107
|
+
expect(error).toHaveBeenCalledWith(
|
|
108
|
+
expect.stringContaining('[journey] instrumentAxios: skipped — invalid shape (')
|
|
109
|
+
);
|
|
110
|
+
expect(error.mock.calls[0]).toHaveLength(1);
|
|
111
|
+
expect(warn).not.toHaveBeenCalled();
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
test('retries instrumentation after a partial wire failure', () => {
|
|
116
|
+
const axios = createAxiosMock();
|
|
117
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
118
|
+
axios.interceptors.response.use = () => {
|
|
119
|
+
throw new Error('response interceptor boom');
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
instrumentAxios(axios);
|
|
123
|
+
expect(axios.requestHandlers).toHaveLength(0);
|
|
124
|
+
|
|
125
|
+
axios.interceptors.response.use = (onFulfilled, onRejected) => {
|
|
126
|
+
axios.responseOkHandlers.push(onFulfilled);
|
|
127
|
+
if (onRejected) {
|
|
128
|
+
axios.responseErrHandlers.push(onRejected);
|
|
129
|
+
}
|
|
130
|
+
return axios.responseOkHandlers.length - 1;
|
|
131
|
+
};
|
|
132
|
+
instrumentAxios(axios);
|
|
133
|
+
|
|
134
|
+
expect(axios.requestHandlers).toHaveLength(1);
|
|
135
|
+
expect(axios.responseOkHandlers).toHaveLength(1);
|
|
136
|
+
expect(error).toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('restore ejects interceptors so a later wire does not stack', () => {
|
|
140
|
+
const axios = createAxiosMock();
|
|
141
|
+
const restore = instrumentAxios(axios);
|
|
142
|
+
expect(axios.requestHandlers).toHaveLength(1);
|
|
143
|
+
expect(axios.responseOkHandlers).toHaveLength(1);
|
|
144
|
+
|
|
145
|
+
restore();
|
|
146
|
+
expect(axios.requestHandlers).toHaveLength(0);
|
|
147
|
+
expect(axios.responseOkHandlers).toHaveLength(0);
|
|
148
|
+
|
|
149
|
+
instrumentAxios(axios);
|
|
150
|
+
expect(axios.requestHandlers).toHaveLength(1);
|
|
151
|
+
expect(axios.responseOkHandlers).toHaveLength(1);
|
|
152
|
+
});
|
|
153
|
+
|
|
77
154
|
test('reports a successful response against the ambient step', async () => {
|
|
78
155
|
const axios = createAxiosMock();
|
|
79
156
|
instrumentAxios(axios);
|
|
@@ -81,7 +158,7 @@ describe('[journey] instrumentAxios', () => {
|
|
|
81
158
|
moduleRuntime.startJourney(def);
|
|
82
159
|
await journeyStep(def.name, 'book-job', () => {
|
|
83
160
|
const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
|
|
84
|
-
expect(config.
|
|
161
|
+
expect(config.stamp).toBeUndefined();
|
|
85
162
|
expect(config.journeyStart).toBe(1_000);
|
|
86
163
|
|
|
87
164
|
(globalThis.performance.now as jest.Mock).mockReturnValue(1_050);
|
|
@@ -94,7 +171,7 @@ describe('[journey] instrumentAxios', () => {
|
|
|
94
171
|
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
95
172
|
});
|
|
96
173
|
|
|
97
|
-
test('stamped
|
|
174
|
+
test('stamped stamp is JSON-safe (AxiosError.toJSON includes config)', async () => {
|
|
98
175
|
const axios = createAxiosMock();
|
|
99
176
|
instrumentAxios(axios);
|
|
100
177
|
|
|
@@ -102,24 +179,23 @@ describe('[journey] instrumentAxios', () => {
|
|
|
102
179
|
await journeyStep(def.name, 'book-job', step => {
|
|
103
180
|
// Ambient stamp — AxiosError.toJSON() embeds config by reference.
|
|
104
181
|
const ambient = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
|
|
105
|
-
expect(ambient.
|
|
182
|
+
expect(ambient.stamp).toBeUndefined();
|
|
106
183
|
const ambientSerialized = JSON.stringify({
|
|
107
184
|
message: 'Request failed',
|
|
108
185
|
name: 'AxiosError',
|
|
109
186
|
config: ambient,
|
|
110
187
|
status: 500,
|
|
111
188
|
});
|
|
112
|
-
expect(JSON.parse(ambientSerialized).config.
|
|
189
|
+
expect(JSON.parse(ambientSerialized).config.stamp).toBeUndefined();
|
|
113
190
|
|
|
114
|
-
// Explicit step.
|
|
115
|
-
const tagged = step.
|
|
191
|
+
// Explicit step.stamp() stays on config, but serializes safely via toJSON.
|
|
192
|
+
const tagged = step.stamp();
|
|
116
193
|
const explicit = axios.requestHandlers[0]({
|
|
117
194
|
url: '/CallScreen/BookJob',
|
|
118
|
-
|
|
195
|
+
stamp: tagged,
|
|
119
196
|
});
|
|
120
|
-
expect(explicit.
|
|
121
|
-
expect(explicit.
|
|
122
|
-
expect(JSON.parse(JSON.stringify(explicit)).journeyStep).toBe('[JourneyStepTag]');
|
|
197
|
+
expect(explicit.stamp).toBe(tagged);
|
|
198
|
+
expect(JSON.parse(JSON.stringify(explicit)).stamp).toBe('[JourneyStepStamp]');
|
|
123
199
|
});
|
|
124
200
|
completeJourney(def.name);
|
|
125
201
|
});
|
|
@@ -145,18 +221,18 @@ describe('[journey] instrumentAxios', () => {
|
|
|
145
221
|
expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
|
|
146
222
|
});
|
|
147
223
|
|
|
148
|
-
test('honors an explicit
|
|
224
|
+
test('honors an explicit stamp from step.stamp()', async () => {
|
|
149
225
|
const axios = createAxiosMock();
|
|
150
226
|
instrumentAxios(axios);
|
|
151
227
|
|
|
152
228
|
moduleRuntime.startJourney(def);
|
|
153
229
|
await journeyStep(def.name, 'book-job', async step => {
|
|
154
|
-
const tagged = step.
|
|
230
|
+
const tagged = step.stamp();
|
|
155
231
|
const config = axios.requestHandlers[0]({
|
|
156
232
|
url: '/CallScreen/BookJob',
|
|
157
|
-
|
|
233
|
+
stamp: tagged,
|
|
158
234
|
});
|
|
159
|
-
expect(config.
|
|
235
|
+
expect(config.stamp).toBe(tagged);
|
|
160
236
|
|
|
161
237
|
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
162
238
|
await expect(
|
|
@@ -170,6 +246,102 @@ describe('[journey] instrumentAxios', () => {
|
|
|
170
246
|
expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
|
|
171
247
|
});
|
|
172
248
|
|
|
249
|
+
test('step.stamp score can exclude a 5xx from the response body', async () => {
|
|
250
|
+
const axios = createAxiosMock();
|
|
251
|
+
instrumentAxios(axios);
|
|
252
|
+
|
|
253
|
+
moduleRuntime.startJourney(def);
|
|
254
|
+
await expect(
|
|
255
|
+
journeyStep(def.name, 'book-job', async step => {
|
|
256
|
+
const tagged = step.stamp(ctx => {
|
|
257
|
+
const data = (ctx.response as { data?: { code?: string } } | undefined)?.data;
|
|
258
|
+
return data?.code === 'USER_DATA' ? 'exclude' : undefined;
|
|
259
|
+
});
|
|
260
|
+
const config = axios.requestHandlers[0]({
|
|
261
|
+
url: '/CallScreen/BookJob',
|
|
262
|
+
stamp: tagged,
|
|
263
|
+
});
|
|
264
|
+
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
265
|
+
await axios.responseErrHandlers[0]({
|
|
266
|
+
config,
|
|
267
|
+
response: { status: 500, data: { code: 'USER_DATA' } },
|
|
268
|
+
});
|
|
269
|
+
})
|
|
270
|
+
).rejects.toMatchObject({ response: { status: 500 } });
|
|
271
|
+
|
|
272
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
273
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('excluded');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('ambient auto-attribute does not run a stamp score', async () => {
|
|
277
|
+
const axios = createAxiosMock();
|
|
278
|
+
instrumentAxios(axios);
|
|
279
|
+
|
|
280
|
+
moduleRuntime.startJourney(def);
|
|
281
|
+
await expect(
|
|
282
|
+
journeyStep(def.name, 'book-job', async step => {
|
|
283
|
+
step.stamp(() => 'exclude');
|
|
284
|
+
const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
|
|
285
|
+
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
286
|
+
await axios.responseErrHandlers[0]({
|
|
287
|
+
config,
|
|
288
|
+
response: { status: 500, data: { code: 'USER_DATA' } },
|
|
289
|
+
});
|
|
290
|
+
})
|
|
291
|
+
).rejects.toMatchObject({ response: { status: 500 } });
|
|
292
|
+
|
|
293
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
|
|
294
|
+
expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
test('step.stamp({ ignore: true }) does not score a 5xx', async () => {
|
|
298
|
+
const axios = createAxiosMock();
|
|
299
|
+
instrumentAxios(axios);
|
|
300
|
+
|
|
301
|
+
moduleRuntime.startJourney(def);
|
|
302
|
+
await journeyStep(def.name, 'book-job', async step => {
|
|
303
|
+
const tagged = step.stamp({ ignore: true });
|
|
304
|
+
const config = axios.requestHandlers[0]({
|
|
305
|
+
url: '/CallScreen/BookJob',
|
|
306
|
+
stamp: tagged,
|
|
307
|
+
});
|
|
308
|
+
expect(config.stamp).toBe(tagged);
|
|
309
|
+
await expect(
|
|
310
|
+
axios.responseErrHandlers[0]({
|
|
311
|
+
config,
|
|
312
|
+
response: { status: 500 },
|
|
313
|
+
})
|
|
314
|
+
).rejects.toMatchObject({ response: { status: 500 } });
|
|
315
|
+
});
|
|
316
|
+
completeJourney(def.name);
|
|
317
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
test('ignore: true skips auto-attribute of a 5xx', async () => {
|
|
321
|
+
const axios = createAxiosMock();
|
|
322
|
+
instrumentAxios(axios);
|
|
323
|
+
|
|
324
|
+
moduleRuntime.startJourney(def);
|
|
325
|
+
await journeyStep(def.name, 'book-job', async () => {
|
|
326
|
+
const config = axios.requestHandlers[0]({
|
|
327
|
+
url: '/telemetry',
|
|
328
|
+
ignore: true,
|
|
329
|
+
});
|
|
330
|
+
expect(config.stamp).toBeUndefined();
|
|
331
|
+
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
332
|
+
await expect(
|
|
333
|
+
axios.responseErrHandlers[0]({
|
|
334
|
+
config,
|
|
335
|
+
response: { status: 500 },
|
|
336
|
+
})
|
|
337
|
+
).rejects.toMatchObject({ response: { status: 500 } });
|
|
338
|
+
});
|
|
339
|
+
completeJourney(def.name);
|
|
340
|
+
|
|
341
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
342
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
343
|
+
});
|
|
344
|
+
|
|
173
345
|
test('soft ambient: overlapping steps do not auto-stamp HTTP', async () => {
|
|
174
346
|
const other = { ...def, name: 'axios-journey-other' };
|
|
175
347
|
const axios = createAxiosMock();
|
|
@@ -189,7 +361,7 @@ describe('[journey] instrumentAxios', () => {
|
|
|
189
361
|
|
|
190
362
|
await journeyStep(other.name, 'book', async () => {
|
|
191
363
|
const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
|
|
192
|
-
expect(config.
|
|
364
|
+
expect(config.stamp).toBeUndefined();
|
|
193
365
|
|
|
194
366
|
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
195
367
|
await expect(
|
|
@@ -210,7 +382,7 @@ describe('[journey] instrumentAxios', () => {
|
|
|
210
382
|
expect(emitMock.mock.calls.every(c => c[0].journey.outcome === 'good')).toBe(true);
|
|
211
383
|
});
|
|
212
384
|
|
|
213
|
-
test('soft ambient: step.
|
|
385
|
+
test('soft ambient: step.stamp() still attributes during overlap', async () => {
|
|
214
386
|
const other = { ...def, name: 'axios-journey-tagged' };
|
|
215
387
|
const axios = createAxiosMock();
|
|
216
388
|
instrumentAxios(axios);
|
|
@@ -229,12 +401,12 @@ describe('[journey] instrumentAxios', () => {
|
|
|
229
401
|
|
|
230
402
|
await expect(
|
|
231
403
|
journeyStep(other.name, 'book', async step => {
|
|
232
|
-
const tagged = step.
|
|
404
|
+
const tagged = step.stamp();
|
|
233
405
|
const config = axios.requestHandlers[0]({
|
|
234
406
|
url: '/CallScreen/BookJob',
|
|
235
|
-
|
|
407
|
+
stamp: tagged,
|
|
236
408
|
});
|
|
237
|
-
expect(config.
|
|
409
|
+
expect(config.stamp).toBe(tagged);
|
|
238
410
|
|
|
239
411
|
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
240
412
|
await axios.responseErrHandlers[0]({
|
|
@@ -255,13 +427,40 @@ describe('[journey] instrumentAxios', () => {
|
|
|
255
427
|
completeJourney(def.name);
|
|
256
428
|
});
|
|
257
429
|
|
|
430
|
+
test('keeps explicit stamp attribution when a later interceptor clones config', async () => {
|
|
431
|
+
const axios = createAxiosMock();
|
|
432
|
+
instrumentAxios(axios);
|
|
433
|
+
|
|
434
|
+
moduleRuntime.startJourney(def);
|
|
435
|
+
await expect(
|
|
436
|
+
journeyStep(def.name, 'book-job', async step => {
|
|
437
|
+
const tagged = step.stamp();
|
|
438
|
+
const config = axios.requestHandlers[0]({
|
|
439
|
+
url: '/CallScreen/BookJob',
|
|
440
|
+
stamp: tagged,
|
|
441
|
+
});
|
|
442
|
+
const clonedConfig = { ...config };
|
|
443
|
+
|
|
444
|
+
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
445
|
+
await axios.responseErrHandlers[0]({
|
|
446
|
+
config: clonedConfig,
|
|
447
|
+
response: { status: 500 },
|
|
448
|
+
});
|
|
449
|
+
})
|
|
450
|
+
).rejects.toMatchObject({ response: { status: 500 } });
|
|
451
|
+
|
|
452
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
453
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
|
|
454
|
+
expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
|
|
455
|
+
});
|
|
456
|
+
|
|
258
457
|
test('ignores responses with no journey step on the config', () => {
|
|
259
458
|
const axios = createAxiosMock();
|
|
260
459
|
instrumentAxios(axios);
|
|
261
460
|
|
|
262
461
|
// Outside a step — request interceptor finds no ambient step.
|
|
263
462
|
const config = axios.requestHandlers[0]({ url: '/orphan' });
|
|
264
|
-
expect(config.
|
|
463
|
+
expect(config.stamp).toBeUndefined();
|
|
265
464
|
axios.responseOkHandlers[0]({ status: 500, config });
|
|
266
465
|
|
|
267
466
|
expect(emitMock).not.toHaveBeenCalled();
|
|
@@ -281,10 +480,10 @@ describe('[journey] instrumentAxios', () => {
|
|
|
281
480
|
|
|
282
481
|
moduleRuntime.startJourney(def);
|
|
283
482
|
await journeyStep(def.name, 'book-job', step => {
|
|
284
|
-
const tagged = step.
|
|
483
|
+
const tagged = step.stamp();
|
|
285
484
|
const config = {
|
|
286
485
|
url: '/CallScreen/BookJob',
|
|
287
|
-
|
|
486
|
+
stamp: tagged,
|
|
288
487
|
};
|
|
289
488
|
(globalThis.performance.now as jest.Mock).mockReturnValue(1_200);
|
|
290
489
|
axios.responseOkHandlers[0]({ status: 200, config });
|
|
@@ -299,7 +498,7 @@ describe('[journey] instrumentAxios', () => {
|
|
|
299
498
|
const axios = createAxiosMock();
|
|
300
499
|
instrumentAxios(axios);
|
|
301
500
|
|
|
302
|
-
moduleRuntime.startJourney({ ...def,
|
|
501
|
+
moduleRuntime.startJourney({ ...def, slowRequestMs: 50 });
|
|
303
502
|
await journeyStep(def.name, 'book-job', () => {
|
|
304
503
|
const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
|
|
305
504
|
(globalThis.performance.now as jest.Mock).mockReturnValue(1_200);
|
|
@@ -332,6 +531,27 @@ describe('[journey] instrumentAxios', () => {
|
|
|
332
531
|
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
333
532
|
});
|
|
334
533
|
|
|
534
|
+
test('marks request-aborted when axios reports a cancel', async () => {
|
|
535
|
+
getSharedPolicies().httpAbortedRequests = 'bad';
|
|
536
|
+
const axios = createAxiosMock();
|
|
537
|
+
instrumentAxios(axios);
|
|
538
|
+
|
|
539
|
+
moduleRuntime.startJourney(def);
|
|
540
|
+
await expect(
|
|
541
|
+
journeyStep(def.name, 'book-job', async () => {
|
|
542
|
+
const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
|
|
543
|
+
(globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
|
|
544
|
+
const cancelError: any = { config, name: 'CanceledError' };
|
|
545
|
+
cancelError.__CANCEL__ = true;
|
|
546
|
+
await axios.responseErrHandlers[0](cancelError);
|
|
547
|
+
})
|
|
548
|
+
).rejects.toMatchObject({ name: 'CanceledError' });
|
|
549
|
+
|
|
550
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
551
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
|
|
552
|
+
expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-aborted');
|
|
553
|
+
});
|
|
554
|
+
|
|
335
555
|
test('network errors without a response status mark request-error', async () => {
|
|
336
556
|
const axios = createAxiosMock();
|
|
337
557
|
instrumentAxios(axios);
|
|
@@ -20,9 +20,15 @@ describe('[journey] exposeAppJourney', () => {
|
|
|
20
20
|
|
|
21
21
|
expect(target.App).toBeDefined();
|
|
22
22
|
expect(target.App.Journey).toBe(api);
|
|
23
|
+
expect(Object.isFrozen(target.App.Journey)).toBe(true);
|
|
24
|
+
expect(() => {
|
|
25
|
+
target.App.Journey.defineJourney = () => {
|
|
26
|
+
throw new Error('swapped');
|
|
27
|
+
};
|
|
28
|
+
}).toThrow();
|
|
23
29
|
expect(typeof api.defineJourney).toBe('function');
|
|
24
30
|
expect(typeof api.journeyStep).toBe('function');
|
|
25
|
-
expect(typeof api.
|
|
31
|
+
expect(typeof api.firstOpenJourneyName).toBe('function');
|
|
26
32
|
});
|
|
27
33
|
|
|
28
34
|
test('preserves existing window.App properties', () => {
|
|
@@ -104,10 +110,10 @@ describe('[journey] exposeAppJourney', () => {
|
|
|
104
110
|
group: 'test',
|
|
105
111
|
service: 'test',
|
|
106
112
|
timeoutMs: 0,
|
|
107
|
-
|
|
113
|
+
expected: ['load'],
|
|
108
114
|
});
|
|
109
115
|
bookJob.start();
|
|
110
|
-
expect(j.
|
|
116
|
+
expect(j.firstOpenJourneyName(['missing', 'ko-bridge-tags'])).toBe('ko-bridge-tags');
|
|
111
117
|
|
|
112
118
|
await bookJob.step('load', step => {
|
|
113
119
|
step.setAttribute('phase', 'load');
|