@servicetitan/journey 1.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 +7 -0
- package/dist/__mocks__/test-runtime.d.ts +16 -0
- package/dist/__mocks__/test-runtime.d.ts.map +1 -0
- package/dist/__tests__/ambient.test.d.ts +2 -0
- package/dist/__tests__/ambient.test.d.ts.map +1 -0
- package/dist/__tests__/axios.test.d.ts +2 -0
- package/dist/__tests__/axios.test.d.ts.map +1 -0
- package/dist/__tests__/config.test.d.ts +2 -0
- package/dist/__tests__/config.test.d.ts.map +1 -0
- package/dist/__tests__/context.test.d.ts +2 -0
- package/dist/__tests__/context.test.d.ts.map +1 -0
- package/dist/__tests__/datadog.test.d.ts +2 -0
- package/dist/__tests__/datadog.test.d.ts.map +1 -0
- package/dist/__tests__/define-journey.test.d.ts +2 -0
- package/dist/__tests__/define-journey.test.d.ts.map +1 -0
- package/dist/__tests__/expose-app-journey.test.d.ts +2 -0
- package/dist/__tests__/expose-app-journey.test.d.ts.map +1 -0
- package/dist/__tests__/fetch.test.d.ts +2 -0
- package/dist/__tests__/fetch.test.d.ts.map +1 -0
- package/dist/__tests__/jquery.test.d.ts +2 -0
- package/dist/__tests__/jquery.test.d.ts.map +1 -0
- package/dist/__tests__/registry.test.d.ts +2 -0
- package/dist/__tests__/registry.test.d.ts.map +1 -0
- package/dist/__tests__/request-key.test.d.ts +2 -0
- package/dist/__tests__/request-key.test.d.ts.map +1 -0
- package/dist/__tests__/requests.test.d.ts +2 -0
- package/dist/__tests__/requests.test.d.ts.map +1 -0
- package/dist/__tests__/runtime.test.d.ts +2 -0
- package/dist/__tests__/runtime.test.d.ts.map +1 -0
- package/dist/__tests__/scope.test.d.ts +2 -0
- package/dist/__tests__/scope.test.d.ts.map +1 -0
- package/dist/__tests__/sinks.test.d.ts +2 -0
- package/dist/__tests__/sinks.test.d.ts.map +1 -0
- package/dist/__tests__/step.test.d.ts +2 -0
- package/dist/__tests__/step.test.d.ts.map +1 -0
- package/dist/__tests__/timeouts.test.d.ts +2 -0
- package/dist/__tests__/timeouts.test.d.ts.map +1 -0
- package/dist/config.d.ts +42 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +46 -0
- package/dist/config.js.map +1 -0
- package/dist/core/index.d.ts +6 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/index.js +5 -0
- package/dist/core/index.js.map +1 -0
- package/dist/core/registry.d.ts +50 -0
- package/dist/core/registry.d.ts.map +1 -0
- package/dist/core/registry.js +102 -0
- package/dist/core/registry.js.map +1 -0
- package/dist/core/runtime.d.ts +51 -0
- package/dist/core/runtime.d.ts.map +1 -0
- package/dist/core/runtime.js +401 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/step-tag.d.ts +16 -0
- package/dist/core/step-tag.d.ts.map +1 -0
- package/dist/core/step-tag.js +35 -0
- package/dist/core/step-tag.js.map +1 -0
- package/dist/core/types.d.ts +143 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/core/types.js.map +1 -0
- package/dist/endpoint-policy.d.ts +29 -0
- package/dist/endpoint-policy.d.ts.map +1 -0
- package/dist/endpoint-policy.js +39 -0
- package/dist/endpoint-policy.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +19 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/axios.d.ts +23 -0
- package/dist/integrations/axios.d.ts.map +1 -0
- package/dist/integrations/axios.js +51 -0
- package/dist/integrations/axios.js.map +1 -0
- package/dist/integrations/fetch.d.ts +21 -0
- package/dist/integrations/fetch.d.ts.map +1 -0
- package/dist/integrations/fetch.js +63 -0
- package/dist/integrations/fetch.js.map +1 -0
- package/dist/integrations/jquery/index.d.ts +22 -0
- package/dist/integrations/jquery/index.d.ts.map +1 -0
- package/dist/integrations/jquery/index.js +27 -0
- package/dist/integrations/jquery/index.js.map +1 -0
- package/dist/integrations/jquery/instrument.d.ts +18 -0
- package/dist/integrations/jquery/instrument.d.ts.map +1 -0
- package/dist/integrations/jquery/instrument.js +44 -0
- package/dist/integrations/jquery/instrument.js.map +1 -0
- package/dist/integrations/react/context.d.ts +6 -0
- package/dist/integrations/react/context.d.ts.map +1 -0
- package/dist/integrations/react/context.js +14 -0
- package/dist/integrations/react/context.js.map +1 -0
- package/dist/integrations/react/scope.d.ts +13 -0
- package/dist/integrations/react/scope.d.ts.map +1 -0
- package/dist/integrations/react/scope.js +27 -0
- package/dist/integrations/react/scope.js.map +1 -0
- package/dist/integrations/request-key.d.ts +17 -0
- package/dist/integrations/request-key.d.ts.map +1 -0
- package/dist/integrations/request-key.js +39 -0
- package/dist/integrations/request-key.js.map +1 -0
- package/dist/sinks/datadog.d.ts +6 -0
- package/dist/sinks/datadog.d.ts.map +1 -0
- package/dist/sinks/datadog.js +64 -0
- package/dist/sinks/datadog.js.map +1 -0
- package/package.json +44 -0
- package/src/__mocks__/test-runtime.ts +37 -0
- package/src/__tests__/ambient.test.ts +101 -0
- package/src/__tests__/axios.test.ts +365 -0
- package/src/__tests__/config.test.ts +261 -0
- package/src/__tests__/context.test.tsx +84 -0
- package/src/__tests__/datadog.test.ts +160 -0
- package/src/__tests__/define-journey.test.ts +176 -0
- package/src/__tests__/expose-app-journey.test.ts +149 -0
- package/src/__tests__/fetch.test.ts +264 -0
- package/src/__tests__/jquery.test.ts +327 -0
- package/src/__tests__/registry.test.ts +265 -0
- package/src/__tests__/request-key.test.ts +81 -0
- package/src/__tests__/requests.test.ts +337 -0
- package/src/__tests__/runtime.test.ts +273 -0
- package/src/__tests__/scope.test.tsx +128 -0
- package/src/__tests__/sinks.test.ts +99 -0
- package/src/__tests__/step.test.ts +305 -0
- package/src/__tests__/timeouts.test.ts +226 -0
- package/src/config.ts +87 -0
- package/src/core/index.ts +39 -0
- package/src/core/registry.ts +162 -0
- package/src/core/runtime.ts +489 -0
- package/src/core/step-tag.ts +24 -0
- package/src/core/types.ts +160 -0
- package/src/endpoint-policy.ts +66 -0
- package/src/index.ts +51 -0
- package/src/integrations/axios.ts +85 -0
- package/src/integrations/fetch.ts +97 -0
- package/src/integrations/jquery/index.ts +62 -0
- package/src/integrations/jquery/instrument.ts +84 -0
- package/src/integrations/react/context.ts +17 -0
- package/src/integrations/react/scope.tsx +32 -0
- package/src/integrations/request-key.ts +46 -0
- package/src/sinks/datadog.ts +71 -0
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { baseJourneyDef, lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
|
|
3
|
+
import {
|
|
4
|
+
completeJourney,
|
|
5
|
+
failJourney,
|
|
6
|
+
journeyMountStep,
|
|
7
|
+
journeyStep,
|
|
8
|
+
resolveJourneyStep,
|
|
9
|
+
startJourney,
|
|
10
|
+
} from '../core';
|
|
11
|
+
|
|
12
|
+
const emitMock = jest.fn();
|
|
13
|
+
const baseDef = { ...baseJourneyDef, name: 'step-api' };
|
|
14
|
+
const otherDef = { ...baseJourneyDef, name: 'step-other' };
|
|
15
|
+
|
|
16
|
+
describe('[journey] journeyStep', () => {
|
|
17
|
+
useJourneyTestRuntime(emitMock);
|
|
18
|
+
|
|
19
|
+
describe('with a candidate list', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
startJourney(baseDef);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const subject = () => journeyStep(['missing', baseDef.name], 'do-work', step => step.name);
|
|
25
|
+
|
|
26
|
+
test('resolves the first open name', async () => {
|
|
27
|
+
await expect(subject()).resolves.toBe(baseDef.name);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
describe('with multiple open candidates', () => {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
startJourney(otherDef);
|
|
34
|
+
startJourney(baseDef);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const subject = () =>
|
|
38
|
+
journeyStep([baseDef.name, otherDef.name], 'do-work', step => step.name);
|
|
39
|
+
|
|
40
|
+
test('prefers the first name in order', async () => {
|
|
41
|
+
await expect(subject()).resolves.toBe(baseDef.name);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('with no matching open journey', () => {
|
|
46
|
+
const subject = () =>
|
|
47
|
+
journeyStep(['missing-a', 'missing-b'], 'do-work', step => {
|
|
48
|
+
step.complete({ a: 1 });
|
|
49
|
+
step.fail('noop');
|
|
50
|
+
return step.name;
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('runs untraced', async () => {
|
|
54
|
+
await expect(subject()).resolves.toBeNull();
|
|
55
|
+
expect(emitMock).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('with a null target', () => {
|
|
60
|
+
const subject = () =>
|
|
61
|
+
journeyStep(null, 'untraced', step => step.tag() as Record<string, unknown>);
|
|
62
|
+
|
|
63
|
+
test('returns an empty tag object', async () => {
|
|
64
|
+
await expect(subject()).resolves.toEqual({});
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe('with an open journey', () => {
|
|
69
|
+
beforeEach(() => {
|
|
70
|
+
startJourney(baseDef);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
describe('when the step completes with tags', () => {
|
|
74
|
+
const subject = () =>
|
|
75
|
+
journeyStep(baseDef.name, 'do-work', step => {
|
|
76
|
+
step.setAttribute('x', 1);
|
|
77
|
+
step.complete({ 'payment.id': 42 });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test('emits good with tags and attributes', async () => {
|
|
81
|
+
await subject();
|
|
82
|
+
|
|
83
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
84
|
+
expect(lastJourney(emitMock).tags['payment.id']).toBe(42);
|
|
85
|
+
expect(lastJourney(emitMock).steps[0].attributes).toEqual({ x: 1 });
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
describe('when setTags is called without completing', () => {
|
|
90
|
+
const subject = () =>
|
|
91
|
+
journeyStep(baseDef.name, 'load', step => {
|
|
92
|
+
step.setTags({ 'customer.id': 9 });
|
|
93
|
+
step.setTags({ 'location.id': 3 });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('merges tags without closing', async () => {
|
|
97
|
+
await subject();
|
|
98
|
+
|
|
99
|
+
expect(emitMock).not.toHaveBeenCalled();
|
|
100
|
+
completeJourney(baseDef.name);
|
|
101
|
+
expect(lastJourney(emitMock).tags).toEqual({
|
|
102
|
+
'customer.id': 9,
|
|
103
|
+
'location.id': 3,
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe('when step.tag is called', () => {
|
|
109
|
+
let tagged: Record<string, unknown> | null;
|
|
110
|
+
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
tagged = null;
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
const subject = () =>
|
|
116
|
+
journeyStep(baseDef.name, 'do-work', step => {
|
|
117
|
+
tagged = step.tag() as Record<string, unknown>;
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('binds journeyStep', async () => {
|
|
121
|
+
await subject();
|
|
122
|
+
|
|
123
|
+
expect(tagged).toHaveProperty('journeyStep');
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
describe('when step.fail is called', () => {
|
|
128
|
+
const subject = () =>
|
|
129
|
+
journeyStep(baseDef.name, 'do-work', step => {
|
|
130
|
+
step.fail('empty-catalog');
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('marks bad without throwing', async () => {
|
|
134
|
+
await subject();
|
|
135
|
+
|
|
136
|
+
expect(lastJourney(emitMock).outcome).toBe('bad');
|
|
137
|
+
expect(lastJourney(emitMock).reason).toBe('empty-catalog');
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
describe('when the step function throws an Error', () => {
|
|
142
|
+
const subject = () =>
|
|
143
|
+
journeyStep(baseDef.name, 'do-work', () => {
|
|
144
|
+
throw new Error('boom');
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test('marks step-error and rethrows', async () => {
|
|
148
|
+
await expect(subject()).rejects.toThrow('boom');
|
|
149
|
+
expect(lastJourney(emitMock).reason).toBe('step-error');
|
|
150
|
+
expect(lastJourney(emitMock).steps[0].attributes).toEqual({ error: 'boom' });
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
describe('when the step function throws a non-Error', () => {
|
|
155
|
+
const subject = () =>
|
|
156
|
+
journeyStep(baseDef.name, 'do-work', () => {
|
|
157
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error -- intentional non-Error throw
|
|
158
|
+
throw 'string-boom';
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
test('stringifies the error attribute', async () => {
|
|
162
|
+
await expect(subject()).rejects.toBe('string-boom');
|
|
163
|
+
expect(lastJourney(emitMock).steps[0].attributes).toEqual({
|
|
164
|
+
error: 'string-boom',
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe('when error is pre-set on the step', () => {
|
|
170
|
+
const subject = () =>
|
|
171
|
+
journeyStep(baseDef.name, 'do-work', step => {
|
|
172
|
+
const ref = resolveJourneyStep(
|
|
173
|
+
(step.tag() as { journeyStep: unknown }).journeyStep
|
|
174
|
+
)!;
|
|
175
|
+
ref.attributes.error = 'already';
|
|
176
|
+
throw new Error('boom');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('preserves the first error attribute', async () => {
|
|
180
|
+
await expect(subject()).rejects.toThrow('boom');
|
|
181
|
+
expect(lastJourney(emitMock).steps[0].attributes).toEqual({ error: 'already' });
|
|
182
|
+
});
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
describe('with multiple steps', () => {
|
|
186
|
+
beforeEach(() => {
|
|
187
|
+
// Parent already opened baseDef; restart with expected steps.
|
|
188
|
+
emitMock.mockClear();
|
|
189
|
+
startJourney({ ...baseDef, steps: ['load', 'save'] });
|
|
190
|
+
emitMock.mockClear();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
test('only the final complete closes once', async () => {
|
|
194
|
+
await journeyStep(baseDef.name, 'load', step => {
|
|
195
|
+
step.setAttribute('phase', 'load');
|
|
196
|
+
});
|
|
197
|
+
expect(emitMock).not.toHaveBeenCalled();
|
|
198
|
+
|
|
199
|
+
await journeyStep(baseDef.name, 'save', step => {
|
|
200
|
+
step.complete({ done: true });
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(emitMock).toHaveBeenCalledTimes(1);
|
|
204
|
+
expect(lastJourney(emitMock).expected).toEqual(['load', 'save']);
|
|
205
|
+
expect(lastJourney(emitMock).steps.map((s: { name: string }) => s.name)).toEqual([
|
|
206
|
+
'load',
|
|
207
|
+
'save',
|
|
208
|
+
]);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
describe('with a JourneyDef-like object target', () => {
|
|
213
|
+
const subject = () =>
|
|
214
|
+
journeyStep(baseDef, 'via-def', step => {
|
|
215
|
+
step.complete();
|
|
216
|
+
return step.name;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
test('resolves the def name', async () => {
|
|
220
|
+
await expect(subject()).resolves.toBe(baseDef.name);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe('when completing without attributes', () => {
|
|
225
|
+
const subject = () =>
|
|
226
|
+
journeyStep(baseDef.name, 'load', () => {}).then(() =>
|
|
227
|
+
completeJourney(baseDef.name)
|
|
228
|
+
);
|
|
229
|
+
|
|
230
|
+
test('omits empty step attributes from the event', async () => {
|
|
231
|
+
await subject();
|
|
232
|
+
|
|
233
|
+
expect(lastJourney(emitMock).steps[0].attributes).toBeUndefined();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
describe('journeyMountStep', () => {
|
|
239
|
+
describe('with candidates', () => {
|
|
240
|
+
beforeEach(() => {
|
|
241
|
+
startJourney(baseDef);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
const subject = () =>
|
|
245
|
+
journeyMountStep(['missing', baseDef.name], 'mount-load', step => {
|
|
246
|
+
step.complete();
|
|
247
|
+
return step.name;
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
test('resolves the open candidate', async () => {
|
|
251
|
+
await expect(subject()).resolves.toBe(baseDef.name);
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
describe('when started after scheduling', () => {
|
|
256
|
+
const order: string[] = [];
|
|
257
|
+
|
|
258
|
+
beforeEach(() => {
|
|
259
|
+
order.length = 0;
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
test('defers until after a sync startJourney', async () => {
|
|
263
|
+
const mount = journeyMountStep(baseDef.name, 'boot', () => {
|
|
264
|
+
order.push('mount-step');
|
|
265
|
+
});
|
|
266
|
+
startJourney(baseDef);
|
|
267
|
+
order.push('started');
|
|
268
|
+
await mount;
|
|
269
|
+
completeJourney(baseDef.name);
|
|
270
|
+
|
|
271
|
+
expect(order).toEqual(['started', 'mount-step']);
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
describe('when fail-fast happens mid-step', () => {
|
|
277
|
+
beforeEach(() => {
|
|
278
|
+
jest.spyOn(globalThis.performance, 'now').mockReturnValue(1_000);
|
|
279
|
+
startJourney(baseDef);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
afterEach(() => {
|
|
283
|
+
jest.restoreAllMocks();
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test('finalizes in-flight step duration', async () => {
|
|
287
|
+
let resolveWork!: () => void;
|
|
288
|
+
const hang = new Promise<void>(resolve => {
|
|
289
|
+
resolveWork = resolve;
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
const stepPromise = journeyStep(baseDef.name, 'slow', async () => {
|
|
293
|
+
(globalThis.performance.now as jest.Mock).mockReturnValue(1_150);
|
|
294
|
+
failJourney(baseDef.name, 'aborted');
|
|
295
|
+
await hang;
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
await Promise.resolve();
|
|
299
|
+
expect(lastJourney(emitMock).steps[0].durationMs).toBe(150);
|
|
300
|
+
|
|
301
|
+
resolveWork();
|
|
302
|
+
await stepPromise;
|
|
303
|
+
});
|
|
304
|
+
});
|
|
305
|
+
});
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/** @jest-environment jsdom */
|
|
2
|
+
import { baseJourneyDef, lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
|
|
3
|
+
import { completeJourney, journeyStep, resolveJourneyStep, startJourney } from '../core';
|
|
4
|
+
|
|
5
|
+
const emitMock = jest.fn();
|
|
6
|
+
const baseDef = { ...baseJourneyDef, name: 'timeout-api' };
|
|
7
|
+
|
|
8
|
+
describe('[journey] timeouts', () => {
|
|
9
|
+
useJourneyTestRuntime(emitMock);
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
jest.useFakeTimers();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
jest.useRealTimers();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('with a journey timeoutMs', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
startJourney({ ...baseDef, timeoutMs: 50 });
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const subject = () => jest.advanceTimersByTimeAsync(50);
|
|
25
|
+
|
|
26
|
+
test('closes bad with journey-timeout', async () => {
|
|
27
|
+
await subject();
|
|
28
|
+
|
|
29
|
+
expect(lastJourney(emitMock).outcome).toBe('bad');
|
|
30
|
+
expect(lastJourney(emitMock).reason).toBe('journey-timeout');
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
describe('with timeoutMs 0', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
startJourney({ ...baseDef, timeoutMs: 0 });
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const subject = () => jest.advanceTimersByTimeAsync(60_000);
|
|
40
|
+
|
|
41
|
+
test('does not arm a countdown', async () => {
|
|
42
|
+
await subject();
|
|
43
|
+
|
|
44
|
+
expect(emitMock).not.toHaveBeenCalled();
|
|
45
|
+
completeJourney(baseDef.name);
|
|
46
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
describe('with a step timeoutMs', () => {
|
|
51
|
+
let resolveWork!: () => void;
|
|
52
|
+
let stepPromise: Promise<unknown>;
|
|
53
|
+
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
startJourney({ ...baseDef, stepTimeoutMs: 40 });
|
|
56
|
+
const hang = new Promise<void>(resolve => {
|
|
57
|
+
resolveWork = resolve;
|
|
58
|
+
});
|
|
59
|
+
stepPromise = journeyStep(baseDef.name, 'slow', async () => {
|
|
60
|
+
await hang;
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
afterEach(async () => {
|
|
65
|
+
resolveWork();
|
|
66
|
+
await stepPromise;
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
const subject = () => jest.advanceTimersByTimeAsync(40);
|
|
70
|
+
|
|
71
|
+
test('closes bad with step-timeout', async () => {
|
|
72
|
+
await subject();
|
|
73
|
+
|
|
74
|
+
expect(lastJourney(emitMock).reason).toBe('step-timeout');
|
|
75
|
+
expect(lastJourney(emitMock).steps[0].attributes.timeout_ms).toBe(40);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('with per-call step timeoutMs', () => {
|
|
80
|
+
let resolveWork!: () => void;
|
|
81
|
+
let stepPromise: Promise<unknown>;
|
|
82
|
+
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
startJourney({ ...baseDef, stepTimeoutMs: 5_000 });
|
|
85
|
+
const hang = new Promise<void>(resolve => {
|
|
86
|
+
resolveWork = resolve;
|
|
87
|
+
});
|
|
88
|
+
stepPromise = journeyStep(
|
|
89
|
+
baseDef.name,
|
|
90
|
+
'slow',
|
|
91
|
+
async () => {
|
|
92
|
+
await hang;
|
|
93
|
+
},
|
|
94
|
+
{ timeoutMs: 25 }
|
|
95
|
+
);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
afterEach(async () => {
|
|
99
|
+
resolveWork();
|
|
100
|
+
await stepPromise;
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const subject = () => jest.advanceTimersByTimeAsync(25);
|
|
104
|
+
|
|
105
|
+
test('overrides the journey default', async () => {
|
|
106
|
+
await subject();
|
|
107
|
+
|
|
108
|
+
expect(lastJourney(emitMock).steps[0].attributes.timeout_ms).toBe(25);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('when the journey closes before the step timeout', () => {
|
|
113
|
+
let resolveWork!: () => void;
|
|
114
|
+
let stepPromise: Promise<unknown>;
|
|
115
|
+
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
startJourney({ ...baseDef, stepTimeoutMs: 40 });
|
|
118
|
+
const hang = new Promise<void>(resolve => {
|
|
119
|
+
resolveWork = resolve;
|
|
120
|
+
});
|
|
121
|
+
stepPromise = journeyStep(baseDef.name, 'slow', async () => {
|
|
122
|
+
await hang;
|
|
123
|
+
});
|
|
124
|
+
completeJourney(baseDef.name);
|
|
125
|
+
emitMock.mockClear();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(async () => {
|
|
129
|
+
resolveWork();
|
|
130
|
+
await stepPromise;
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const subject = () => jest.advanceTimersByTimeAsync(40);
|
|
134
|
+
|
|
135
|
+
test('the step timeout is a no-op', async () => {
|
|
136
|
+
await subject();
|
|
137
|
+
|
|
138
|
+
expect(emitMock).not.toHaveBeenCalled();
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
describe('when step reason is pre-set before timeout', () => {
|
|
143
|
+
let resolveWork!: () => void;
|
|
144
|
+
let stepPromise: Promise<unknown>;
|
|
145
|
+
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
startJourney({ ...baseDef, stepTimeoutMs: 40 });
|
|
148
|
+
const hang = new Promise<void>(resolve => {
|
|
149
|
+
resolveWork = resolve;
|
|
150
|
+
});
|
|
151
|
+
stepPromise = journeyStep(baseDef.name, 'slow', async step => {
|
|
152
|
+
const ref = resolveJourneyStep(
|
|
153
|
+
(step.tag() as { journeyStep: unknown }).journeyStep
|
|
154
|
+
)!;
|
|
155
|
+
ref.reason = 'pre-existing';
|
|
156
|
+
ref.attributes.timeout_ms = 999;
|
|
157
|
+
await hang;
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
afterEach(async () => {
|
|
162
|
+
resolveWork();
|
|
163
|
+
await stepPromise;
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
const subject = () => jest.advanceTimersByTimeAsync(40);
|
|
167
|
+
|
|
168
|
+
test('preserves the first step reason', async () => {
|
|
169
|
+
await subject();
|
|
170
|
+
|
|
171
|
+
expect(lastJourney(emitMock).steps[0].reason).toBe('pre-existing');
|
|
172
|
+
expect(lastJourney(emitMock).steps[0].attributes?.timeout_ms).toBe(999);
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe('when clearTimeout is bypassed after complete', () => {
|
|
177
|
+
beforeEach(() => {
|
|
178
|
+
jest.spyOn(globalThis, 'clearTimeout').mockImplementation(() => {});
|
|
179
|
+
startJourney({ ...baseDef, timeoutMs: 50 });
|
|
180
|
+
completeJourney(baseDef.name);
|
|
181
|
+
emitMock.mockClear();
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
afterEach(() => {
|
|
185
|
+
jest.mocked(globalThis.clearTimeout).mockRestore();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
const subject = () => jest.advanceTimersByTimeAsync(50);
|
|
189
|
+
|
|
190
|
+
test('the late timer does not emit', async () => {
|
|
191
|
+
await subject();
|
|
192
|
+
|
|
193
|
+
expect(emitMock).not.toHaveBeenCalled();
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
describe('with stepTimeoutMs 0', () => {
|
|
198
|
+
let resolveWork!: () => void;
|
|
199
|
+
let stepPromise: Promise<unknown>;
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
startJourney({ ...baseDef, stepTimeoutMs: 0 });
|
|
203
|
+
const hang = new Promise<void>(resolve => {
|
|
204
|
+
resolveWork = resolve;
|
|
205
|
+
});
|
|
206
|
+
stepPromise = journeyStep(baseDef.name, 'slow', async () => {
|
|
207
|
+
await hang;
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
afterEach(async () => {
|
|
212
|
+
resolveWork();
|
|
213
|
+
await stepPromise;
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const subject = () => jest.advanceTimersByTimeAsync(60_000);
|
|
217
|
+
|
|
218
|
+
test('does not arm a step countdown', async () => {
|
|
219
|
+
await subject();
|
|
220
|
+
|
|
221
|
+
expect(emitMock).not.toHaveBeenCalled();
|
|
222
|
+
completeJourney(baseDef.name);
|
|
223
|
+
expect(lastJourney(emitMock).outcome).toBe('good');
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
});
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { defaultRuntime } from './core/runtime';
|
|
2
|
+
import { setJourneySinks, type JourneySink } from './core/registry';
|
|
3
|
+
import {
|
|
4
|
+
sortEndpointsByLongestMatch,
|
|
5
|
+
type AppRequestTimeouts,
|
|
6
|
+
type EndpointPolicy,
|
|
7
|
+
} from './endpoint-policy';
|
|
8
|
+
import { instrumentAxios, type AxiosLikeInstance } from './integrations/axios';
|
|
9
|
+
import { instrumentFetch, type InstrumentFetchOptions } from './integrations/fetch';
|
|
10
|
+
import {
|
|
11
|
+
exposeAppJourney,
|
|
12
|
+
instrumentJquery,
|
|
13
|
+
type JqueryStatic,
|
|
14
|
+
type WindowWithAppJourney,
|
|
15
|
+
} from './integrations/jquery';
|
|
16
|
+
import { sendToDatadog } from './sinks/datadog';
|
|
17
|
+
|
|
18
|
+
export { longestEndpointMatch, sortEndpointsByLongestMatch } from './endpoint-policy';
|
|
19
|
+
export type { AppRequestTimeouts, EndpointPolicy };
|
|
20
|
+
|
|
21
|
+
/** Optional app-wide / per-bundle configuration. */
|
|
22
|
+
export interface JourneyConfig {
|
|
23
|
+
/** App-wide request-timeout fallback. Defaults to { defaultMs: 4000, endpoints: [] }. */
|
|
24
|
+
requestTimeouts?: AppRequestTimeouts;
|
|
25
|
+
/**
|
|
26
|
+
* Replace the sink list used when a journey finishes.
|
|
27
|
+
* Only applied when provided — omit to leave the current list unchanged.
|
|
28
|
+
* Package default is `[sendToDatadog]`; pass `[]` to disable emission.
|
|
29
|
+
*/
|
|
30
|
+
sinks?: readonly JourneySink[];
|
|
31
|
+
/**
|
|
32
|
+
* Wire this bundle's axios instance (host or MFE). Same as `instrumentAxios(axios)`.
|
|
33
|
+
* Register any async request interceptors (e.g. auth) before `configureJourney`.
|
|
34
|
+
*/
|
|
35
|
+
axios?: AxiosLikeInstance;
|
|
36
|
+
/**
|
|
37
|
+
* Wire fetch. `true` wraps `globalThis.fetch`; pass options for a scoped target / baseURL.
|
|
38
|
+
* Same as `instrumentFetch(...)`.
|
|
39
|
+
*/
|
|
40
|
+
fetch?: true | InstrumentFetchOptions;
|
|
41
|
+
/** Wire this bundle's jQuery static. Same as `instrumentJquery($)`. */
|
|
42
|
+
jquery?: JqueryStatic;
|
|
43
|
+
/**
|
|
44
|
+
* Publish `window.App.Journey` for Knockout / legacy hosts.
|
|
45
|
+
* `true` uses the real window; pass a target object in tests.
|
|
46
|
+
*/
|
|
47
|
+
exposeAppJourney?: boolean | WindowWithAppJourney;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/*
|
|
51
|
+
* Default emission target. Kept as a module side effect so any import of this file
|
|
52
|
+
* (see index.ts `import './config'`) installs Datadog. configureJourney only replaces
|
|
53
|
+
* sinks when `sinks` is passed explicitly.
|
|
54
|
+
*/
|
|
55
|
+
setJourneySinks([sendToDatadog]);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Apply bundle options: request thresholds, sinks, and/or HTTP / legacy wiring.
|
|
59
|
+
* Optional — call once at startup. Pass this bundle's own axios / `$` / fetch target.
|
|
60
|
+
*/
|
|
61
|
+
export function configureJourney(config: JourneyConfig): void {
|
|
62
|
+
if (config.requestTimeouts !== undefined) {
|
|
63
|
+
defaultRuntime.setRequestTimeouts({
|
|
64
|
+
...config.requestTimeouts,
|
|
65
|
+
endpoints: sortEndpointsByLongestMatch(config.requestTimeouts.endpoints),
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
if (config.sinks !== undefined) {
|
|
69
|
+
setJourneySinks(config.sinks);
|
|
70
|
+
}
|
|
71
|
+
if (config.axios !== undefined) {
|
|
72
|
+
instrumentAxios(config.axios);
|
|
73
|
+
}
|
|
74
|
+
if (config.fetch !== undefined) {
|
|
75
|
+
instrumentFetch(config.fetch === true ? {} : config.fetch);
|
|
76
|
+
}
|
|
77
|
+
if (config.jquery !== undefined) {
|
|
78
|
+
instrumentJquery(config.jquery);
|
|
79
|
+
}
|
|
80
|
+
if (config.exposeAppJourney) {
|
|
81
|
+
exposeAppJourney(config.exposeAppJourney === true ? undefined : config.exposeAppJourney);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getRequestTimeouts(): AppRequestTimeouts {
|
|
86
|
+
return defaultRuntime.getRequestTimeouts();
|
|
87
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Core engine barrel — free helpers over the default runtime, plus handles and types. */
|
|
2
|
+
|
|
3
|
+
export {
|
|
4
|
+
activeJourneyStep,
|
|
5
|
+
addJourneySink,
|
|
6
|
+
completeJourney,
|
|
7
|
+
defineJourney,
|
|
8
|
+
emitJourneyEvent,
|
|
9
|
+
excludeJourney,
|
|
10
|
+
failJourney,
|
|
11
|
+
firstOpenJourney,
|
|
12
|
+
journeyMountStep,
|
|
13
|
+
journeyStep,
|
|
14
|
+
reportBackendRequest,
|
|
15
|
+
setJourneySinks,
|
|
16
|
+
setJourneyTags,
|
|
17
|
+
startJourney,
|
|
18
|
+
type JourneyHandle,
|
|
19
|
+
type JourneySink,
|
|
20
|
+
} from './registry';
|
|
21
|
+
export {
|
|
22
|
+
createJourneyRuntime,
|
|
23
|
+
defaultRuntime,
|
|
24
|
+
resetJourney,
|
|
25
|
+
type JourneyRuntime,
|
|
26
|
+
type JourneyRuntimeOptions,
|
|
27
|
+
} from './runtime';
|
|
28
|
+
export type {
|
|
29
|
+
JourneyDef,
|
|
30
|
+
JourneyEvent,
|
|
31
|
+
JourneyNameLike,
|
|
32
|
+
JourneyStepEvent,
|
|
33
|
+
JourneyStepOptions,
|
|
34
|
+
JourneyStepRef,
|
|
35
|
+
JourneyStepTarget,
|
|
36
|
+
StepHandle,
|
|
37
|
+
TagValue,
|
|
38
|
+
} from './types';
|
|
39
|
+
export { JourneyStepTag, resolveJourneyStep } from './step-tag';
|