@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 { instrumentJquery } from '../../integrations/jquery/instrument';
|
|
6
7
|
|
|
7
8
|
const emitMock = jest.fn();
|
|
@@ -60,6 +61,87 @@ describe('[journey] instrumentJquery', () => {
|
|
|
60
61
|
expect($.__completes).toHaveLength(1);
|
|
61
62
|
});
|
|
62
63
|
|
|
64
|
+
test('restore then rewire does not stack prefilters', () => {
|
|
65
|
+
const $ = createJqueryMock();
|
|
66
|
+
const restore = instrumentJquery($);
|
|
67
|
+
expect($.__prefilters).toHaveLength(1);
|
|
68
|
+
|
|
69
|
+
restore();
|
|
70
|
+
instrumentJquery($);
|
|
71
|
+
expect($.__prefilters).toHaveLength(1);
|
|
72
|
+
expect($.__completes).toHaveLength(1);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test.each([undefined, null])('warns and soft-skips when $ is missing (%p)', bad => {
|
|
76
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
77
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
78
|
+
|
|
79
|
+
expect(() => instrumentJquery(bad)).not.toThrow();
|
|
80
|
+
expect(warn).toHaveBeenCalledWith(
|
|
81
|
+
expect.stringContaining('[journey] instrumentJquery: skipped — missing')
|
|
82
|
+
);
|
|
83
|
+
expect(error).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test.each([{}, { ajaxPrefilter: 1 }, { ajaxPrefilter: undefined }])(
|
|
87
|
+
'errors and soft-skips when $ has invalid shape (%p)',
|
|
88
|
+
bad => {
|
|
89
|
+
const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
|
|
90
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
91
|
+
|
|
92
|
+
expect(() => instrumentJquery(bad as never)).not.toThrow();
|
|
93
|
+
expect(error).toHaveBeenCalledWith(
|
|
94
|
+
expect.stringContaining('[journey] instrumentJquery: skipped — invalid shape (')
|
|
95
|
+
);
|
|
96
|
+
expect(error.mock.calls[0]).toHaveLength(1);
|
|
97
|
+
expect(warn).not.toHaveBeenCalled();
|
|
98
|
+
}
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
test('does not re-register handlers after a partial wire failure', () => {
|
|
102
|
+
const $ = createJqueryMock();
|
|
103
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
104
|
+
const throwing = Object.assign(function throwAjaxComplete() {
|
|
105
|
+
throw new Error('ajaxComplete boom');
|
|
106
|
+
}, $) as ReturnType<typeof createJqueryMock>;
|
|
107
|
+
throwing.ajaxPrefilter = $.ajaxPrefilter;
|
|
108
|
+
throwing.__prefilters = $.__prefilters;
|
|
109
|
+
throwing.__completes = $.__completes;
|
|
110
|
+
|
|
111
|
+
instrumentJquery(throwing);
|
|
112
|
+
expect(throwing.__prefilters).toHaveLength(1);
|
|
113
|
+
|
|
114
|
+
instrumentJquery(throwing);
|
|
115
|
+
expect(throwing.__prefilters).toHaveLength(1);
|
|
116
|
+
expect(error).toHaveBeenCalled();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test('restore after a partial wire failure allows a later retry', () => {
|
|
120
|
+
const $ = createJqueryMock();
|
|
121
|
+
const error = jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
122
|
+
let boom = true;
|
|
123
|
+
const flaky = Object.assign(function maybeThrow(selector: Document) {
|
|
124
|
+
if (boom) {
|
|
125
|
+
throw new Error('ajaxComplete boom');
|
|
126
|
+
}
|
|
127
|
+
return $(selector);
|
|
128
|
+
}, $) as ReturnType<typeof createJqueryMock>;
|
|
129
|
+
flaky.ajaxPrefilter = $.ajaxPrefilter;
|
|
130
|
+
flaky.__prefilters = $.__prefilters;
|
|
131
|
+
flaky.__completes = $.__completes;
|
|
132
|
+
|
|
133
|
+
const restore = instrumentJquery(flaky);
|
|
134
|
+
expect(flaky.__prefilters).toHaveLength(1);
|
|
135
|
+
expect(flaky.__completes).toHaveLength(0);
|
|
136
|
+
restore();
|
|
137
|
+
|
|
138
|
+
boom = false;
|
|
139
|
+
instrumentJquery(flaky);
|
|
140
|
+
expect(flaky.__prefilters).toHaveLength(1);
|
|
141
|
+
expect(flaky.__completes).toHaveLength(1);
|
|
142
|
+
expect(error).toHaveBeenCalled();
|
|
143
|
+
});
|
|
144
|
+
|
|
63
145
|
test('ignores ajax outside an active journey step', () => {
|
|
64
146
|
const $ = createJqueryMock();
|
|
65
147
|
instrumentJquery($);
|
|
@@ -70,7 +152,7 @@ describe('[journey] instrumentJquery', () => {
|
|
|
70
152
|
group: 'test',
|
|
71
153
|
service: 'test',
|
|
72
154
|
timeoutMs: 0,
|
|
73
|
-
|
|
155
|
+
slowRequestMs: 50,
|
|
74
156
|
});
|
|
75
157
|
|
|
76
158
|
simulateAjax($, '/CallScreen/BookJob', 500);
|
|
@@ -80,7 +162,66 @@ describe('[journey] instrumentJquery', () => {
|
|
|
80
162
|
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
81
163
|
});
|
|
82
164
|
|
|
83
|
-
test('
|
|
165
|
+
test('ignore: true on ajax settings skips a 5xx', async () => {
|
|
166
|
+
const $ = createJqueryMock();
|
|
167
|
+
instrumentJquery($);
|
|
168
|
+
|
|
169
|
+
moduleRuntime.startJourney({
|
|
170
|
+
name: 'jq-meta-ignore',
|
|
171
|
+
team: 'test',
|
|
172
|
+
group: 'test',
|
|
173
|
+
service: 'test',
|
|
174
|
+
timeoutMs: 0,
|
|
175
|
+
slowRequestMs: 50,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await journeyStep('jq-meta-ignore', 'book-job', () => {
|
|
179
|
+
const xhr = { status: 500 };
|
|
180
|
+
const settings = { url: '/telemetry', ignore: true };
|
|
181
|
+
for (const prefilter of $.__prefilters) {
|
|
182
|
+
prefilter(settings, settings, xhr);
|
|
183
|
+
}
|
|
184
|
+
for (const complete of $.__completes) {
|
|
185
|
+
complete({}, xhr, settings);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
completeJourney('jq-meta-ignore');
|
|
189
|
+
|
|
190
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
191
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
test('step.stamp({ ignore: true }) skips a 5xx', async () => {
|
|
195
|
+
const $ = createJqueryMock();
|
|
196
|
+
instrumentJquery($);
|
|
197
|
+
|
|
198
|
+
moduleRuntime.startJourney({
|
|
199
|
+
name: 'jq-stamp-ignore',
|
|
200
|
+
team: 'test',
|
|
201
|
+
group: 'test',
|
|
202
|
+
service: 'test',
|
|
203
|
+
timeoutMs: 0,
|
|
204
|
+
slowRequestMs: 50,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
await journeyStep('jq-stamp-ignore', 'book-job', step => {
|
|
208
|
+
const tagged = step.stamp({ ignore: true });
|
|
209
|
+
const xhr = { status: 500, stamp: tagged };
|
|
210
|
+
const settings = { url: '/CallScreen/BookJob', stamp: tagged };
|
|
211
|
+
for (const prefilter of $.__prefilters) {
|
|
212
|
+
prefilter(settings, settings, xhr);
|
|
213
|
+
}
|
|
214
|
+
for (const complete of $.__completes) {
|
|
215
|
+
complete({}, xhr, settings);
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
completeJourney('jq-stamp-ignore');
|
|
219
|
+
|
|
220
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
221
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test('stamped stamp is JSON-safe on jqXHR and ajax settings', async () => {
|
|
84
225
|
const $ = createJqueryMock();
|
|
85
226
|
instrumentJquery($);
|
|
86
227
|
|
|
@@ -90,28 +231,30 @@ describe('[journey] instrumentJquery', () => {
|
|
|
90
231
|
group: 'test',
|
|
91
232
|
service: 'test',
|
|
92
233
|
timeoutMs: 0,
|
|
93
|
-
|
|
234
|
+
slowRequestMs: 5_000,
|
|
94
235
|
});
|
|
95
236
|
|
|
96
237
|
await journeyStep('jq-json-safe', 'book-job', step => {
|
|
97
238
|
// Ambient stamp lands on jqXHR — common to serialize for logging.
|
|
98
239
|
const ambientXhr: {
|
|
99
240
|
status: number;
|
|
100
|
-
|
|
241
|
+
stamp?: unknown;
|
|
101
242
|
journeyStart?: number;
|
|
102
243
|
} = { status: 200 };
|
|
103
244
|
const ambientSettings = { url: '/CallScreen/BookJob' };
|
|
104
245
|
for (const prefilter of $.__prefilters) {
|
|
105
246
|
prefilter(ambientSettings, ambientSettings, ambientXhr);
|
|
106
247
|
}
|
|
107
|
-
expect(ambientXhr.
|
|
108
|
-
expect(JSON.parse(JSON.stringify(ambientXhr)).
|
|
109
|
-
|
|
110
|
-
// Explicit step.
|
|
111
|
-
const tagged = step.
|
|
112
|
-
const settings = { url: '/CallScreen/BookJob',
|
|
113
|
-
|
|
114
|
-
|
|
248
|
+
expect(ambientXhr.stamp).toBeUndefined();
|
|
249
|
+
expect(JSON.parse(JSON.stringify(ambientXhr)).stamp).toBeUndefined();
|
|
250
|
+
|
|
251
|
+
// Explicit step.stamp() spread into ajax settings — stripped after capture.
|
|
252
|
+
const tagged = step.stamp();
|
|
253
|
+
const settings = { url: '/CallScreen/BookJob', stamp: tagged };
|
|
254
|
+
for (const prefilter of $.__prefilters) {
|
|
255
|
+
prefilter(settings, settings, { status: 200 });
|
|
256
|
+
}
|
|
257
|
+
expect(settings.stamp).toBeUndefined();
|
|
115
258
|
});
|
|
116
259
|
completeJourney('jq-json-safe');
|
|
117
260
|
});
|
|
@@ -126,7 +269,7 @@ describe('[journey] instrumentJquery', () => {
|
|
|
126
269
|
group: 'test',
|
|
127
270
|
service: 'test',
|
|
128
271
|
timeoutMs: 0,
|
|
129
|
-
|
|
272
|
+
slowRequestMs: 5_000,
|
|
130
273
|
});
|
|
131
274
|
|
|
132
275
|
await journeyStep('jq-5xx', 'book-job', () => {
|
|
@@ -149,7 +292,7 @@ describe('[journey] instrumentJquery', () => {
|
|
|
149
292
|
group: 'test',
|
|
150
293
|
service: 'test',
|
|
151
294
|
timeoutMs: 0,
|
|
152
|
-
|
|
295
|
+
slowRequestMs: 50,
|
|
153
296
|
});
|
|
154
297
|
|
|
155
298
|
await journeyStep('jq-slow', 'book-job', () => {
|
|
@@ -172,7 +315,7 @@ describe('[journey] instrumentJquery', () => {
|
|
|
172
315
|
group: 'test',
|
|
173
316
|
service: 'test',
|
|
174
317
|
timeoutMs: 0,
|
|
175
|
-
|
|
318
|
+
slowRequestMs: 5_000,
|
|
176
319
|
});
|
|
177
320
|
|
|
178
321
|
await journeyStep('jq-network', 'book-job', () => {
|
|
@@ -194,7 +337,7 @@ describe('[journey] instrumentJquery', () => {
|
|
|
194
337
|
group: 'test',
|
|
195
338
|
service: 'test',
|
|
196
339
|
timeoutMs: 0,
|
|
197
|
-
|
|
340
|
+
slowRequestMs: 5_000,
|
|
198
341
|
});
|
|
199
342
|
|
|
200
343
|
await journeyStep('jq-4xx', 'book-job', () => {
|
|
@@ -206,7 +349,39 @@ describe('[journey] instrumentJquery', () => {
|
|
|
206
349
|
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
207
350
|
});
|
|
208
351
|
|
|
209
|
-
test('
|
|
352
|
+
test('marks request-aborted when jqXHR statusText is abort', async () => {
|
|
353
|
+
getSharedPolicies().httpAbortedRequests = 'bad';
|
|
354
|
+
const $ = createJqueryMock();
|
|
355
|
+
instrumentJquery($);
|
|
356
|
+
|
|
357
|
+
moduleRuntime.startJourney({
|
|
358
|
+
name: 'jq-abort',
|
|
359
|
+
team: 'test',
|
|
360
|
+
group: 'test',
|
|
361
|
+
service: 'test',
|
|
362
|
+
timeoutMs: 0,
|
|
363
|
+
slowRequestMs: 5_000,
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
await journeyStep('jq-abort', 'book-job', () => {
|
|
367
|
+
const xhr = { status: 0, statusText: 'abort' };
|
|
368
|
+
const settings = { url: '/CallScreen/BookJob' };
|
|
369
|
+
const start = globalThis.performance.now();
|
|
370
|
+
for (const prefilter of $.__prefilters) {
|
|
371
|
+
prefilter(settings, settings, xhr);
|
|
372
|
+
}
|
|
373
|
+
(globalThis.performance.now as jest.Mock).mockReturnValue(start + 10);
|
|
374
|
+
for (const complete of $.__completes) {
|
|
375
|
+
complete({}, xhr, settings);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
380
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
|
|
381
|
+
expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-aborted');
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test('honors an explicit stamp already stamped on the XHR', async () => {
|
|
210
385
|
const $ = createJqueryMock();
|
|
211
386
|
instrumentJquery($);
|
|
212
387
|
|
|
@@ -216,23 +391,23 @@ describe('[journey] instrumentJquery', () => {
|
|
|
216
391
|
group: 'test',
|
|
217
392
|
service: 'test',
|
|
218
393
|
timeoutMs: 0,
|
|
219
|
-
|
|
394
|
+
slowRequestMs: 5_000,
|
|
220
395
|
});
|
|
221
396
|
|
|
222
397
|
await journeyStep('jq-explicit', 'book-job', step => {
|
|
223
|
-
const tagged = step.
|
|
398
|
+
const tagged = step.stamp();
|
|
224
399
|
const xhr: {
|
|
225
400
|
status: number;
|
|
226
|
-
|
|
401
|
+
stamp: unknown;
|
|
227
402
|
journeyStart?: number;
|
|
228
|
-
} = { status: 500,
|
|
403
|
+
} = { status: 500, stamp: tagged };
|
|
229
404
|
const settings = { url: '/CallScreen/BookJob' };
|
|
230
405
|
const start = globalThis.performance.now();
|
|
231
406
|
for (const prefilter of $.__prefilters) {
|
|
232
407
|
prefilter(settings, settings, xhr);
|
|
233
408
|
}
|
|
234
409
|
// Prefilter must not clear the explicit step; it should stamp a start time.
|
|
235
|
-
expect(xhr.
|
|
410
|
+
expect(xhr.stamp).toBeUndefined();
|
|
236
411
|
expect(xhr.journeyStart).toBeDefined();
|
|
237
412
|
(globalThis.performance.now as jest.Mock).mockReturnValue(start + 20);
|
|
238
413
|
for (const complete of $.__completes) {
|
|
@@ -255,13 +430,13 @@ describe('[journey] instrumentJquery', () => {
|
|
|
255
430
|
group: 'test',
|
|
256
431
|
service: 'test',
|
|
257
432
|
timeoutMs: 0,
|
|
258
|
-
|
|
433
|
+
slowRequestMs: 5_000,
|
|
259
434
|
});
|
|
260
435
|
|
|
261
436
|
await journeyStep('jq-no-start', 'book-job', step => {
|
|
262
|
-
const tagged = step.
|
|
437
|
+
const tagged = step.stamp();
|
|
263
438
|
// Bypass prefilter stamping — only fire ajaxComplete with a step and no start.
|
|
264
|
-
const xhr = { status: 200,
|
|
439
|
+
const xhr = { status: 200, stamp: tagged };
|
|
265
440
|
for (const complete of $.__completes) {
|
|
266
441
|
complete({}, xhr, { url: '/ok' });
|
|
267
442
|
}
|
|
@@ -272,7 +447,7 @@ describe('[journey] instrumentJquery', () => {
|
|
|
272
447
|
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
273
448
|
});
|
|
274
449
|
|
|
275
|
-
test('honors step.
|
|
450
|
+
test('honors step.stamp() on ajax settings without deep-merge stack overflow', async () => {
|
|
276
451
|
const $ = createJqueryMock();
|
|
277
452
|
instrumentJquery($);
|
|
278
453
|
|
|
@@ -312,21 +487,21 @@ describe('[journey] instrumentJquery', () => {
|
|
|
312
487
|
group: 'test',
|
|
313
488
|
service: 'test',
|
|
314
489
|
timeoutMs: 0,
|
|
315
|
-
|
|
490
|
+
slowRequestMs: 5_000,
|
|
316
491
|
});
|
|
317
492
|
|
|
318
493
|
await journeyStep('jq-settings-tag', 'book-job', step => {
|
|
319
|
-
const tagged = step.
|
|
320
|
-
expect(() => deepExtend({}, tagged)).not.toThrow();
|
|
321
|
-
const settings = deepExtend({ url: '/CallScreen/BookJob' }, tagged);
|
|
322
|
-
const xhr: { status: number;
|
|
494
|
+
const tagged = step.stamp();
|
|
495
|
+
expect(() => deepExtend({}, { stamp: tagged })).not.toThrow();
|
|
496
|
+
const settings = deepExtend({ url: '/CallScreen/BookJob' }, { stamp: tagged });
|
|
497
|
+
const xhr: { status: number; stamp?: unknown; journeyStart?: number } = {
|
|
323
498
|
status: 500,
|
|
324
499
|
};
|
|
325
500
|
const start = globalThis.performance.now();
|
|
326
501
|
for (const prefilter of $.__prefilters) {
|
|
327
502
|
prefilter(settings, settings, xhr);
|
|
328
503
|
}
|
|
329
|
-
expect(xhr.
|
|
504
|
+
expect(xhr.stamp).toBeUndefined();
|
|
330
505
|
(globalThis.performance.now as jest.Mock).mockReturnValue(start + 20);
|
|
331
506
|
for (const complete of $.__completes) {
|
|
332
507
|
complete({}, xhr, settings);
|
|
@@ -348,7 +523,7 @@ describe('[journey] instrumentJquery', () => {
|
|
|
348
523
|
group: 'test',
|
|
349
524
|
service: 'test',
|
|
350
525
|
timeoutMs: 0,
|
|
351
|
-
|
|
526
|
+
slowRequestMs: 5_000,
|
|
352
527
|
endpoints: [{ match: 'pdf.vendor.com/render', ignore: true }],
|
|
353
528
|
});
|
|
354
529
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/** @jest-environment jsdom */
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
|
-
import { useContext } from 'react';
|
|
3
|
+
import { useContext, useEffect } from 'react';
|
|
4
4
|
|
|
5
5
|
import { useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
|
|
6
|
-
import { completeJourney, failJourney } from '../../core';
|
|
6
|
+
import { completeJourney, failJourney, journeyStep } from '../../core';
|
|
7
|
+
import { onHttpComplete } from '../../integrations/http-report';
|
|
7
8
|
import { JourneyNameContext } from '../../integrations/react/context';
|
|
8
9
|
import { JourneyScope } from '../../integrations/react/scope';
|
|
9
10
|
|
|
@@ -25,6 +26,14 @@ function NameProbe() {
|
|
|
25
26
|
describe('[journey] JourneyScope', () => {
|
|
26
27
|
useJourneyTestRuntime(emitMock);
|
|
27
28
|
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
jest.useFakeTimers();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
afterEach(() => {
|
|
34
|
+
jest.useRealTimers();
|
|
35
|
+
});
|
|
36
|
+
|
|
28
37
|
test('opens a journey on mount and publishes the name via context', () => {
|
|
29
38
|
render(
|
|
30
39
|
<JourneyScope {...def}>
|
|
@@ -125,4 +134,153 @@ describe('[journey] JourneyScope', () => {
|
|
|
125
134
|
expect(emitMock).not.toHaveBeenCalled();
|
|
126
135
|
});
|
|
127
136
|
});
|
|
137
|
+
|
|
138
|
+
test('applies updated tags without restarting', () => {
|
|
139
|
+
const { rerender } = render(
|
|
140
|
+
<JourneyScope {...def} tags={{ plan: 'a' }}>
|
|
141
|
+
<NameProbe />
|
|
142
|
+
</JourneyScope>
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
rerender(
|
|
146
|
+
<JourneyScope {...def} tags={{ plan: 'b' }}>
|
|
147
|
+
<NameProbe />
|
|
148
|
+
</JourneyScope>
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
completeJourney(def.name);
|
|
152
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
153
|
+
expect(emitMock.mock.calls[0][0].journey.tags).toEqual({ plan: 'b' });
|
|
154
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test('child abort cleanup then unmount is excluded not request-aborted', async () => {
|
|
158
|
+
let stamp: Parameters<typeof onHttpComplete>[0]['stamp'];
|
|
159
|
+
let release!: () => void;
|
|
160
|
+
const hang = new Promise<void>(resolve => {
|
|
161
|
+
release = resolve;
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
function Child() {
|
|
165
|
+
useEffect(
|
|
166
|
+
() => () => {
|
|
167
|
+
if (stamp) {
|
|
168
|
+
onHttpComplete({
|
|
169
|
+
stamp,
|
|
170
|
+
aborted: true,
|
|
171
|
+
requestKey: '/x',
|
|
172
|
+
durationMs: 5,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
[]
|
|
177
|
+
);
|
|
178
|
+
return null;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const { unmount } = render(
|
|
182
|
+
<JourneyScope {...def}>
|
|
183
|
+
<Child />
|
|
184
|
+
</JourneyScope>
|
|
185
|
+
);
|
|
186
|
+
|
|
187
|
+
const stepPromise = journeyStep(def.name, 'load', async step => {
|
|
188
|
+
stamp = step.stamp();
|
|
189
|
+
await hang;
|
|
190
|
+
});
|
|
191
|
+
await Promise.resolve();
|
|
192
|
+
unmount();
|
|
193
|
+
|
|
194
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
195
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('excluded');
|
|
196
|
+
|
|
197
|
+
release();
|
|
198
|
+
await stepPromise;
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test('rerendering with equal endpoint values does not re-arm timeout', async () => {
|
|
202
|
+
const timeoutDef = {
|
|
203
|
+
...def,
|
|
204
|
+
name: 'scope-timer-stable',
|
|
205
|
+
timeoutMs: 30,
|
|
206
|
+
endpoints: [{ match: '/api/jobs', slowRequestMs: 100 }],
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const { rerender } = render(
|
|
210
|
+
<JourneyScope {...timeoutDef}>
|
|
211
|
+
<NameProbe />
|
|
212
|
+
</JourneyScope>
|
|
213
|
+
);
|
|
214
|
+
|
|
215
|
+
await jest.advanceTimersByTimeAsync(20);
|
|
216
|
+
|
|
217
|
+
rerender(
|
|
218
|
+
<JourneyScope {...timeoutDef} endpoints={[{ match: '/api/jobs', slowRequestMs: 100 }]}>
|
|
219
|
+
<NameProbe />
|
|
220
|
+
</JourneyScope>
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await jest.advanceTimersByTimeAsync(15);
|
|
224
|
+
|
|
225
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
226
|
+
expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
|
|
227
|
+
expect(emitMock.mock.calls[0][0].journey.reason).toBe('journey-timeout');
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test('live config updates keep original timeout budget', async () => {
|
|
231
|
+
const timeoutDef = {
|
|
232
|
+
...def,
|
|
233
|
+
name: 'scope-timeout-updates',
|
|
234
|
+
timeoutMs: 30,
|
|
235
|
+
team: 'team-a',
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const { rerender } = render(
|
|
239
|
+
<JourneyScope {...timeoutDef}>
|
|
240
|
+
<NameProbe />
|
|
241
|
+
</JourneyScope>
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
await jest.advanceTimersByTimeAsync(20);
|
|
245
|
+
|
|
246
|
+
rerender(
|
|
247
|
+
<JourneyScope {...timeoutDef} team="team-b">
|
|
248
|
+
<NameProbe />
|
|
249
|
+
</JourneyScope>
|
|
250
|
+
);
|
|
251
|
+
|
|
252
|
+
await jest.advanceTimersByTimeAsync(15);
|
|
253
|
+
|
|
254
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
255
|
+
expect(emitMock.mock.calls[0][0].journey.name).toBe(timeoutDef.name);
|
|
256
|
+
expect(emitMock.mock.calls[0][0].journey.reason).toBe('journey-timeout');
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test('live maxSteps updates apply without restarting', async () => {
|
|
260
|
+
const maxStepsDef = {
|
|
261
|
+
...def,
|
|
262
|
+
name: 'scope-max-steps',
|
|
263
|
+
maxSteps: 1,
|
|
264
|
+
};
|
|
265
|
+
|
|
266
|
+
const { rerender } = render(
|
|
267
|
+
<JourneyScope {...maxStepsDef}>
|
|
268
|
+
<NameProbe />
|
|
269
|
+
</JourneyScope>
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
await journeyStep(maxStepsDef.name, 'first', async () => {});
|
|
273
|
+
|
|
274
|
+
rerender(
|
|
275
|
+
<JourneyScope {...maxStepsDef} maxSteps={2}>
|
|
276
|
+
<NameProbe />
|
|
277
|
+
</JourneyScope>
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
await journeyStep(maxStepsDef.name, 'second', async () => {});
|
|
281
|
+
|
|
282
|
+
completeJourney(maxStepsDef.name);
|
|
283
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
284
|
+
expect(emitMock.mock.calls[0][0].journey.steps).toHaveLength(2);
|
|
285
|
+
});
|
|
128
286
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { renderHook } from '@testing-library/react';
|
|
3
|
+
import { type ReactNode } from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
useJourneyTestRuntime,
|
|
7
|
+
baseJourneyDef,
|
|
8
|
+
lastJourney,
|
|
9
|
+
} from '../../__test-utils__/test-runtime';
|
|
10
|
+
import { completeJourney, defineJourney } from '../../core';
|
|
11
|
+
import { JourneyScope } from '../../integrations/react/scope';
|
|
12
|
+
import { useJourneyStep } from '../../integrations/react/use-journey-step';
|
|
13
|
+
|
|
14
|
+
const emitMock = jest.fn();
|
|
15
|
+
|
|
16
|
+
describe('[journey] useJourneyStep', () => {
|
|
17
|
+
useJourneyTestRuntime(emitMock);
|
|
18
|
+
|
|
19
|
+
test('runs a step on the JourneyScope journey', async () => {
|
|
20
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
21
|
+
<JourneyScope {...baseJourneyDef} name="hook-journey">
|
|
22
|
+
{children}
|
|
23
|
+
</JourneyScope>
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const { result } = renderHook(() => useJourneyStep(), { wrapper });
|
|
27
|
+
await result.current('load', () => 'ok');
|
|
28
|
+
completeJourney('hook-journey');
|
|
29
|
+
|
|
30
|
+
expect(lastJourney(emitMock).steps[0].name).toBe('load');
|
|
31
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test('auto-starts when given a handle', async () => {
|
|
35
|
+
const handle = defineJourney({ ...baseJourneyDef, name: 'hook-handle' });
|
|
36
|
+
const { result } = renderHook(() => useJourneyStep(handle));
|
|
37
|
+
await result.current('load', () => 'ok');
|
|
38
|
+
handle.complete();
|
|
39
|
+
|
|
40
|
+
expect(lastJourney(emitMock).steps[0].name).toBe('load');
|
|
41
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
@@ -23,12 +23,14 @@ describe('[journey] sendToConsole / isJourneyDebugEnabled', () => {
|
|
|
23
23
|
|
|
24
24
|
beforeEach(() => {
|
|
25
25
|
localStorage.clear();
|
|
26
|
+
sessionStorage.clear();
|
|
26
27
|
debugSpy = jest.spyOn(console, 'debug').mockImplementation(() => undefined);
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
afterEach(() => {
|
|
30
31
|
debugSpy.mockRestore();
|
|
31
32
|
localStorage.clear();
|
|
33
|
+
sessionStorage.clear();
|
|
32
34
|
});
|
|
33
35
|
|
|
34
36
|
describe('when st:journey:debug is not set', () => {
|
|
@@ -45,7 +47,7 @@ describe('[journey] sendToConsole / isJourneyDebugEnabled', () => {
|
|
|
45
47
|
|
|
46
48
|
describe('when st:journey:debug is a non-true value', () => {
|
|
47
49
|
beforeEach(() => {
|
|
48
|
-
|
|
50
|
+
sessionStorage.setItem(JOURNEY_DEBUG_STORAGE_KEY, '1');
|
|
49
51
|
});
|
|
50
52
|
|
|
51
53
|
test('isJourneyDebugEnabled is false', () => {
|
|
@@ -61,7 +63,7 @@ describe('[journey] sendToConsole / isJourneyDebugEnabled', () => {
|
|
|
61
63
|
|
|
62
64
|
describe('when st:journey:debug is true', () => {
|
|
63
65
|
beforeEach(() => {
|
|
64
|
-
|
|
66
|
+
sessionStorage.setItem(JOURNEY_DEBUG_STORAGE_KEY, 'true');
|
|
65
67
|
});
|
|
66
68
|
|
|
67
69
|
test('isJourneyDebugEnabled is true', () => {
|
|
@@ -74,11 +76,14 @@ describe('[journey] sendToConsole / isJourneyDebugEnabled', () => {
|
|
|
74
76
|
sendToConsole(event);
|
|
75
77
|
|
|
76
78
|
expect(debugSpy).toHaveBeenCalledTimes(1);
|
|
77
|
-
expect(debugSpy).toHaveBeenCalledWith(
|
|
79
|
+
expect(debugSpy).toHaveBeenCalledWith(
|
|
80
|
+
'[journey] book_job → bad',
|
|
81
|
+
expect.objectContaining({ name: 'book_job', outcome: 'bad' })
|
|
82
|
+
);
|
|
78
83
|
});
|
|
79
84
|
});
|
|
80
85
|
|
|
81
|
-
describe('when
|
|
86
|
+
describe('when sessionStorage throws', () => {
|
|
82
87
|
beforeEach(() => {
|
|
83
88
|
jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => {
|
|
84
89
|
throw new Error('denied');
|