@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.
Files changed (163) hide show
  1. package/README.md +1 -1
  2. package/dist/__test-utils__/test-runtime.d.ts.map +1 -1
  3. package/dist/__test-utils__/test-runtime.js +9 -2
  4. package/dist/__test-utils__/test-runtime.js.map +1 -1
  5. package/dist/__tests__/core/sanitize.test.d.ts +2 -0
  6. package/dist/__tests__/core/sanitize.test.d.ts.map +1 -0
  7. package/dist/__tests__/hardening.test.d.ts +2 -0
  8. package/dist/__tests__/hardening.test.d.ts.map +1 -0
  9. package/dist/__tests__/integrations/host-mfe.test.d.ts +2 -0
  10. package/dist/__tests__/integrations/host-mfe.test.d.ts.map +1 -0
  11. package/dist/__tests__/integrations/use-journey-step.test.d.ts +2 -0
  12. package/dist/__tests__/integrations/use-journey-step.test.d.ts.map +1 -0
  13. package/dist/config.d.ts +57 -13
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +87 -24
  16. package/dist/config.js.map +1 -1
  17. package/dist/core/abort-error.d.ts +4 -0
  18. package/dist/core/abort-error.d.ts.map +1 -0
  19. package/dist/core/abort-error.js +22 -0
  20. package/dist/core/abort-error.js.map +1 -0
  21. package/dist/core/api.d.ts +22 -9
  22. package/dist/core/api.d.ts.map +1 -1
  23. package/dist/core/api.js +77 -30
  24. package/dist/core/api.js.map +1 -1
  25. package/dist/core/endpoint-policy.d.ts +14 -4
  26. package/dist/core/endpoint-policy.d.ts.map +1 -1
  27. package/dist/core/endpoint-policy.js +32 -1
  28. package/dist/core/endpoint-policy.js.map +1 -1
  29. package/dist/core/index.d.ts +4 -3
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/core/index.js +3 -3
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/limits.d.ts +29 -0
  34. package/dist/core/limits.d.ts.map +1 -0
  35. package/dist/core/limits.js +63 -0
  36. package/dist/core/limits.js.map +1 -0
  37. package/dist/core/request-policy.d.ts +39 -0
  38. package/dist/core/request-policy.d.ts.map +1 -0
  39. package/dist/core/request-policy.js +18 -0
  40. package/dist/core/request-policy.js.map +1 -0
  41. package/dist/core/runtime.d.ts +65 -15
  42. package/dist/core/runtime.d.ts.map +1 -1
  43. package/dist/core/runtime.js +403 -126
  44. package/dist/core/runtime.js.map +1 -1
  45. package/dist/core/sanitize.d.ts +23 -0
  46. package/dist/core/sanitize.d.ts.map +1 -0
  47. package/dist/core/sanitize.js +143 -0
  48. package/dist/core/sanitize.js.map +1 -0
  49. package/dist/core/step-tag.d.ts +25 -6
  50. package/dist/core/step-tag.d.ts.map +1 -1
  51. package/dist/core/step-tag.js +56 -10
  52. package/dist/core/step-tag.js.map +1 -1
  53. package/dist/core/types.d.ts +73 -25
  54. package/dist/core/types.d.ts.map +1 -1
  55. package/dist/core/types.js +4 -1
  56. package/dist/core/types.js.map +1 -1
  57. package/dist/global.d.ts +91 -13
  58. package/dist/global.d.ts.map +1 -1
  59. package/dist/global.js +235 -25
  60. package/dist/global.js.map +1 -1
  61. package/dist/index.d.ts +7 -8
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +4 -6
  64. package/dist/index.js.map +1 -1
  65. package/dist/integrations/axios.d.ts +5 -1
  66. package/dist/integrations/axios.d.ts.map +1 -1
  67. package/dist/integrations/axios.js +146 -34
  68. package/dist/integrations/axios.js.map +1 -1
  69. package/dist/integrations/fetch.d.ts +3 -2
  70. package/dist/integrations/fetch.d.ts.map +1 -1
  71. package/dist/integrations/fetch.js +105 -26
  72. package/dist/integrations/fetch.js.map +1 -1
  73. package/dist/integrations/http-report.d.ts +27 -0
  74. package/dist/integrations/http-report.d.ts.map +1 -0
  75. package/dist/integrations/http-report.js +123 -0
  76. package/dist/integrations/http-report.js.map +1 -0
  77. package/dist/integrations/jquery/index.d.ts +8 -4
  78. package/dist/integrations/jquery/index.d.ts.map +1 -1
  79. package/dist/integrations/jquery/index.js +26 -8
  80. package/dist/integrations/jquery/index.js.map +1 -1
  81. package/dist/integrations/jquery/instrument.d.ts +14 -5
  82. package/dist/integrations/jquery/instrument.d.ts.map +1 -1
  83. package/dist/integrations/jquery/instrument.js +111 -29
  84. package/dist/integrations/jquery/instrument.js.map +1 -1
  85. package/dist/integrations/react/context.d.ts +4 -1
  86. package/dist/integrations/react/context.d.ts.map +1 -1
  87. package/dist/integrations/react/context.js +4 -1
  88. package/dist/integrations/react/context.js.map +1 -1
  89. package/dist/integrations/react/scope.d.ts +14 -3
  90. package/dist/integrations/react/scope.d.ts.map +1 -1
  91. package/dist/integrations/react/scope.js +46 -8
  92. package/dist/integrations/react/scope.js.map +1 -1
  93. package/dist/integrations/react/stable-config-keys.d.ts +6 -0
  94. package/dist/integrations/react/stable-config-keys.d.ts.map +1 -0
  95. package/dist/integrations/react/stable-config-keys.js +21 -0
  96. package/dist/integrations/react/stable-config-keys.js.map +1 -0
  97. package/dist/integrations/react/use-journey-step.d.ts +11 -0
  98. package/dist/integrations/react/use-journey-step.d.ts.map +1 -0
  99. package/dist/integrations/react/use-journey-step.js +19 -0
  100. package/dist/integrations/react/use-journey-step.js.map +1 -0
  101. package/dist/integrations/request-key.d.ts +8 -3
  102. package/dist/integrations/request-key.d.ts.map +1 -1
  103. package/dist/integrations/request-key.js +10 -9
  104. package/dist/integrations/request-key.js.map +1 -1
  105. package/dist/integrations/soft-skip.d.ts +7 -0
  106. package/dist/integrations/soft-skip.d.ts.map +1 -0
  107. package/dist/integrations/soft-skip.js +30 -0
  108. package/dist/integrations/soft-skip.js.map +1 -0
  109. package/dist/sinks/console.d.ts.map +1 -1
  110. package/dist/sinks/console.js +2 -1
  111. package/dist/sinks/console.js.map +1 -1
  112. package/dist/sinks/datadog.d.ts +1 -1
  113. package/dist/sinks/datadog.d.ts.map +1 -1
  114. package/dist/sinks/datadog.js +16 -20
  115. package/dist/sinks/datadog.js.map +1 -1
  116. package/package.json +2 -2
  117. package/src/__test-utils__/test-runtime.ts +9 -2
  118. package/src/__tests__/config.test.ts +150 -31
  119. package/src/__tests__/core/ambient.test.ts +16 -10
  120. package/src/__tests__/core/api.test.ts +7 -7
  121. package/src/__tests__/core/define-journey.test.ts +28 -11
  122. package/src/__tests__/core/requests.test.ts +49 -31
  123. package/src/__tests__/core/runtime.test.ts +67 -20
  124. package/src/__tests__/core/sanitize.test.ts +69 -0
  125. package/src/__tests__/core/step.test.ts +132 -36
  126. package/src/__tests__/core/timeouts.test.ts +69 -4
  127. package/src/__tests__/global.test.ts +44 -3
  128. package/src/__tests__/hardening.test.ts +613 -0
  129. package/src/__tests__/integrations/axios.test.ts +247 -27
  130. package/src/__tests__/integrations/expose-app-journey.test.ts +9 -3
  131. package/src/__tests__/integrations/fetch.test.ts +153 -19
  132. package/src/__tests__/integrations/host-mfe.test.ts +548 -0
  133. package/src/__tests__/integrations/jquery.test.ts +209 -34
  134. package/src/__tests__/integrations/scope.test.tsx +160 -2
  135. package/src/__tests__/integrations/use-journey-step.test.tsx +43 -0
  136. package/src/__tests__/sinks/console.test.ts +9 -4
  137. package/src/__tests__/sinks/datadog.test.ts +47 -0
  138. package/src/config.ts +162 -38
  139. package/src/core/abort-error.ts +24 -0
  140. package/src/core/api.ts +83 -33
  141. package/src/core/endpoint-policy.ts +46 -4
  142. package/src/core/index.ts +12 -4
  143. package/src/core/limits.ts +73 -0
  144. package/src/core/request-policy.ts +66 -0
  145. package/src/core/runtime.ts +507 -133
  146. package/src/core/sanitize.ts +177 -0
  147. package/src/core/step-tag.ts +82 -12
  148. package/src/core/types.ts +79 -25
  149. package/src/global.ts +312 -42
  150. package/src/index.ts +17 -17
  151. package/src/integrations/axios.ts +183 -45
  152. package/src/integrations/fetch.ts +125 -39
  153. package/src/integrations/http-report.ts +184 -0
  154. package/src/integrations/jquery/index.ts +34 -10
  155. package/src/integrations/jquery/instrument.ts +126 -42
  156. package/src/integrations/react/context.ts +4 -1
  157. package/src/integrations/react/scope.tsx +59 -9
  158. package/src/integrations/react/stable-config-keys.ts +28 -0
  159. package/src/integrations/react/use-journey-step.ts +31 -0
  160. package/src/integrations/request-key.ts +18 -10
  161. package/src/integrations/soft-skip.ts +35 -0
  162. package/src/sinks/console.ts +2 -1
  163. package/src/sinks/datadog.ts +23 -29
@@ -9,17 +9,17 @@ import {
9
9
  completeJourney,
10
10
  journeyStep,
11
11
  reportBackendRequest,
12
- resolveJourneyStep,
13
12
  type JourneyDef,
14
- type JourneyStepRef,
13
+ type JourneyStepStamp,
15
14
  } from '../../core';
15
+ import type { StepRecord } from '../../core/types';
16
16
  import { moduleRuntime } from '../../core/runtime';
17
17
 
18
18
  const emitMock = jest.fn();
19
19
  const baseDef = { ...baseJourneyDef, name: 'requests-journey' };
20
20
 
21
21
  describe('[journey] reportBackendRequest', () => {
22
- let stepRef: JourneyStepRef | undefined;
22
+ let stepRef: StepRecord | undefined;
23
23
  let status: number | undefined;
24
24
  let durationMs: number;
25
25
  let key: string;
@@ -36,12 +36,12 @@ describe('[journey] reportBackendRequest', () => {
36
36
  const subject = () => reportBackendRequest(stepRef, status, durationMs, key);
37
37
 
38
38
  async function withOpenStep(
39
- run: (ref: JourneyStepRef) => void | Promise<void>,
40
- def: JourneyDef = { ...baseDef, requestTimeoutMs: 5_000 }
39
+ run: (ref: StepRecord) => void | Promise<void>,
40
+ def: JourneyDef = { ...baseDef, slowRequestMs: 5_000 }
41
41
  ) {
42
42
  moduleRuntime.startJourney(def);
43
43
  await journeyStep(baseDef.name, 'do-work', async step => {
44
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
44
+ stepRef = (step.stamp() as JourneyStepStamp).step;
45
45
  await run(stepRef!);
46
46
  });
47
47
  }
@@ -100,7 +100,7 @@ describe('[journey] reportBackendRequest', () => {
100
100
  () => {
101
101
  subject();
102
102
  },
103
- { ...baseDef, requestTimeoutMs: 50 }
103
+ { ...baseDef, slowRequestMs: 50 }
104
104
  );
105
105
 
106
106
  expect(lastJourney(emitMock).reason).toBe('request-latency');
@@ -160,7 +160,7 @@ describe('[journey] reportBackendRequest', () => {
160
160
  },
161
161
  {
162
162
  ...baseDef,
163
- requestTimeoutMs: 5_000,
163
+ slowRequestMs: 5_000,
164
164
  endpoints: [{ match: '/flaky', ignore: true }],
165
165
  }
166
166
  );
@@ -170,28 +170,29 @@ describe('[journey] reportBackendRequest', () => {
170
170
  });
171
171
  });
172
172
 
173
- describe('when a journey endpoint timeoutMs is 0', () => {
174
- test('falls through to the journey requestTimeoutMs', async () => {
173
+ describe('when a journey endpoint slowRequestMs is 0', () => {
174
+ test('never scores latency for that prefix', async () => {
175
175
  moduleRuntime.startJourney({
176
176
  ...baseDef,
177
- requestTimeoutMs: 30,
178
- endpoints: [{ match: '/special', timeoutMs: 0 }],
177
+ slowRequestMs: 30,
178
+ endpoints: [{ match: '/special', slowRequestMs: 0 }],
179
179
  });
180
180
 
181
181
  await journeyStep(baseDef.name, 'do-work', step => {
182
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
182
+ stepRef = (step.stamp() as JourneyStepStamp).step;
183
183
  status = 200;
184
184
  durationMs = 100;
185
185
  key = '/special/path';
186
186
  subject();
187
187
  });
188
+ completeJourney(baseDef.name);
188
189
 
189
- expect(lastJourney(emitMock).reason).toBe('request-latency');
190
+ expect(lastJourney(emitMock).outcome).toBe('good');
190
191
  });
191
192
  });
192
193
 
193
194
  describe('when journey endpoint timeoutMs is set', () => {
194
- test('wins over the journey requestTimeoutMs', async () => {
195
+ test('wins over the journey slowRequestMs', async () => {
195
196
  await withOpenStep(
196
197
  () => {
197
198
  status = 200;
@@ -201,8 +202,8 @@ describe('[journey] reportBackendRequest', () => {
201
202
  },
202
203
  {
203
204
  ...baseDef,
204
- requestTimeoutMs: 5_000,
205
- endpoints: [{ match: '/special', timeoutMs: 20 }],
205
+ slowRequestMs: 5_000,
206
+ endpoints: [{ match: '/special', slowRequestMs: 20 }],
206
207
  }
207
208
  );
208
209
 
@@ -213,7 +214,7 @@ describe('[journey] reportBackendRequest', () => {
213
214
  describe('when the journey endpoint ignore overrides the app-wide policy', () => {
214
215
  beforeEach(() => {
215
216
  configureJourney({
216
- requestTimeouts: {
217
+ slowRequests: {
217
218
  defaultMs: 4_000,
218
219
  endpoints: [{ match: '/flaky', ignore: true }],
219
220
  },
@@ -229,7 +230,7 @@ describe('[journey] reportBackendRequest', () => {
229
230
  },
230
231
  {
231
232
  ...baseDef,
232
- requestTimeoutMs: 5_000,
233
+ slowRequestMs: 5_000,
233
234
  endpoints: [{ match: '/flaky', ignore: false }],
234
235
  }
235
236
  );
@@ -241,11 +242,11 @@ describe('[journey] reportBackendRequest', () => {
241
242
  describe('when only app-wide endpoint policies apply', () => {
242
243
  beforeEach(() => {
243
244
  configureJourney({
244
- requestTimeouts: {
245
+ slowRequests: {
245
246
  defaultMs: 4_000,
246
247
  endpoints: [
247
248
  { match: '/ignored', ignore: true },
248
- { match: '/tight', timeoutMs: 30 },
249
+ { match: '/tight', slowRequestMs: 30 },
249
250
  ],
250
251
  },
251
252
  });
@@ -274,41 +275,42 @@ describe('[journey] reportBackendRequest', () => {
274
275
  });
275
276
  });
276
277
 
277
- describe('when app endpoint timeoutMs is 0', () => {
278
+ describe('when app endpoint slowRequestMs is 0', () => {
278
279
  beforeEach(() => {
279
280
  configureJourney({
280
- requestTimeouts: {
281
+ slowRequests: {
281
282
  defaultMs: 40,
282
- endpoints: [{ match: '/loose', timeoutMs: 0 }],
283
+ endpoints: [{ match: '/loose', slowRequestMs: 0 }],
283
284
  },
284
285
  });
285
286
  });
286
287
 
287
- test('falls through to defaultMs', async () => {
288
+ test('never scores latency for that prefix', async () => {
288
289
  moduleRuntime.startJourney(baseDef);
289
290
 
290
291
  await journeyStep(baseDef.name, 'do-work', step => {
291
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
292
+ stepRef = (step.stamp() as JourneyStepStamp).step;
292
293
  status = 200;
293
294
  durationMs = 100;
294
295
  key = '/loose/x';
295
296
  subject();
296
297
  });
298
+ completeJourney(baseDef.name);
297
299
 
298
- expect(lastJourney(emitMock).reason).toBe('request-latency');
300
+ expect(lastJourney(emitMock).outcome).toBe('good');
299
301
  });
300
302
  });
301
303
 
302
- describe('when the journey has no requestTimeoutMs', () => {
304
+ describe('when the journey has no slowRequestMs', () => {
303
305
  beforeEach(() => {
304
- configureJourney({ requestTimeouts: { defaultMs: 40, endpoints: [] } });
306
+ configureJourney({ slowRequests: { defaultMs: 40, endpoints: [] } });
305
307
  });
306
308
 
307
309
  test('uses the app defaultMs', async () => {
308
310
  moduleRuntime.startJourney(baseDef);
309
311
 
310
312
  await journeyStep(baseDef.name, 'do-work', step => {
311
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
313
+ stepRef = (step.stamp() as JourneyStepStamp).step;
312
314
  status = 200;
313
315
  durationMs = 100;
314
316
  key = '/any';
@@ -320,7 +322,7 @@ describe('[journey] reportBackendRequest', () => {
320
322
  });
321
323
 
322
324
  describe('against a closed journey step ref', () => {
323
- let closedRef: JourneyStepRef | undefined;
325
+ let closedRef: StepRecord | undefined;
324
326
 
325
327
  beforeEach(async () => {
326
328
  await withOpenStep(ref => {
@@ -338,4 +340,20 @@ describe('[journey] reportBackendRequest', () => {
338
340
  expect(emitMock).not.toHaveBeenCalled();
339
341
  });
340
342
  });
343
+
344
+ describe('when many distinct keys are attributed', () => {
345
+ test('stops recording after 50 keys', async () => {
346
+ await withOpenStep(ref => {
347
+ for (let i = 0; i < 55; i += 1) {
348
+ reportBackendRequest(ref, 200, 1, `/api/${i}`);
349
+ }
350
+ expect(ref.attributedRequestKeys).toHaveLength(50);
351
+ expect(
352
+ moduleRuntime.getDebugSnapshot().openJourneys[0].steps[0].attributedRequestKeys
353
+ ).toHaveLength(50);
354
+ });
355
+ completeJourney(baseDef.name);
356
+ expect(lastJourney(emitMock).outcome).toBe('good');
357
+ });
358
+ });
341
359
  });
@@ -1,14 +1,15 @@
1
1
  /** @jest-environment jsdom */
2
2
  import { lastJourney, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
- import { configureJourney, getRequestTimeouts } from '../../config';
3
+ import { configureJourney, getSlowRequests } from '../../config';
4
4
  import {
5
- activeJourneyStep,
6
5
  completeJourney,
7
6
  defineJourney,
8
7
  journeyStep,
9
8
  resetJourney,
9
+ singleInFlightJourneyStep,
10
10
  } from '../../core';
11
11
  import { createJourneyRuntime, moduleRuntime } from '../../core/runtime';
12
+ import { registerJourneyRuntime, setJourneyDefaultEnabled } from '../../global';
12
13
  import { sendToConsole } from '../../sinks/console';
13
14
  import { sendToDatadog } from '../../sinks/datadog';
14
15
 
@@ -52,29 +53,31 @@ describe('[journey] resetJourney', () => {
52
53
 
53
54
  describe('when request timeouts were customized', () => {
54
55
  beforeEach(() => {
55
- configureJourney({ requestTimeouts: { defaultMs: 100, endpoints: [] } });
56
+ configureJourney({ slowRequests: { defaultMs: 100, endpoints: [] } });
56
57
  });
57
58
 
58
59
  const subject = () => resetJourney({ sinks: [emitMock] });
59
60
 
60
- test('restores the 4s default', () => {
61
+ test('clears this runtime overlay and inherits shared policies', () => {
61
62
  subject();
62
63
 
63
- expect(getRequestTimeouts().defaultMs).toBe(4_000);
64
+ expect(moduleRuntime.getDebugSnapshot().policyOverlay.slowRequests).toBe(false);
65
+ expect(moduleRuntime.getSlowRequests().defaultMs).toBe(100);
66
+ expect(getSlowRequests().defaultMs).toBe(100);
64
67
  });
65
68
  });
66
69
 
67
- describe('when reset passes custom requestTimeouts', () => {
70
+ describe('when reset passes custom slowRequests', () => {
68
71
  const subject = () =>
69
72
  resetJourney({
70
73
  sinks: [emitMock],
71
- requestTimeouts: { defaultMs: 250, endpoints: [] },
74
+ slowRequests: { defaultMs: 250, endpoints: [] },
72
75
  });
73
76
 
74
- test('applies those timeouts', () => {
77
+ test('applies those timeouts on the module engine', () => {
75
78
  subject();
76
79
 
77
- expect(getRequestTimeouts().defaultMs).toBe(250);
80
+ expect(moduleRuntime.getSlowRequests().defaultMs).toBe(250);
78
81
  });
79
82
  });
80
83
 
@@ -120,11 +123,24 @@ describe('[journey] resetJourney', () => {
120
123
  const subject = () => resetJourney({ sinks: [emitMock] });
121
124
 
122
125
  test('clears the ambient step', () => {
123
- expect(activeJourneyStep()).not.toBeNull();
126
+ expect(singleInFlightJourneyStep()).not.toBeNull();
124
127
 
125
128
  subject();
126
129
 
127
- expect(activeJourneyStep()).toBeNull();
130
+ expect(singleInFlightJourneyStep()).toBeNull();
131
+ });
132
+
133
+ test('reuses the same ambient stamp while the active step is unchanged', async () => {
134
+ const first = singleInFlightJourneyStep();
135
+ const second = singleInFlightJourneyStep();
136
+
137
+ expect(first).not.toBeNull();
138
+ expect(second).toBe(first);
139
+
140
+ resolveWork();
141
+ await stepPromise;
142
+
143
+ expect(singleInFlightJourneyStep()).toBeNull();
128
144
  });
129
145
  });
130
146
 
@@ -157,19 +173,35 @@ describe('[journey] createJourneyRuntime', () => {
157
173
  let runtime: ReturnType<typeof createJourneyRuntime>;
158
174
 
159
175
  beforeEach(() => {
176
+ setJourneyDefaultEnabled(true);
160
177
  resetJourney({
161
178
  sinks: [defaultEmit],
162
- requestTimeouts: { defaultMs: 4_000, endpoints: [] },
179
+ slowRequests: { defaultMs: 4_000, endpoints: [] },
163
180
  });
164
181
  defaultEmit.mockClear();
165
182
  isolatedEmit.mockClear();
166
183
  runtime = createJourneyRuntime({ sinks: [isolatedEmit] });
167
184
  });
168
185
 
186
+ test('auto-assigns a unique runtime-N id when registered without an id', () => {
187
+ registerJourneyRuntime(runtime);
188
+
189
+ expect(moduleRuntime.id).toMatch(/^runtime-\d+$/);
190
+ expect(runtime.id).toMatch(/^runtime-\d+$/);
191
+ expect(runtime.id).not.toBe(moduleRuntime.id);
192
+ });
193
+
194
+ test('uses the provided id', () => {
195
+ const named = createJourneyRuntime({ id: 'job-booking', sinks: [isolatedEmit] });
196
+ expect(named.id).toBe('job-booking');
197
+ });
198
+
169
199
  afterEach(() => {
200
+ setJourneyDefaultEnabled(false);
201
+ localStorage.clear();
170
202
  resetJourney({
171
203
  sinks: [sendToDatadog, sendToConsole],
172
- requestTimeouts: { defaultMs: 4_000, endpoints: [] },
204
+ slowRequests: { defaultMs: 4_000, endpoints: [] },
173
205
  });
174
206
  });
175
207
 
@@ -192,18 +224,20 @@ describe('[journey] createJourneyRuntime', () => {
192
224
  });
193
225
  });
194
226
 
195
- describe('when the isolated runtime has its own requestTimeouts', () => {
227
+ describe('when the isolated runtime has its own slowRequests', () => {
196
228
  beforeEach(() => {
197
229
  runtime = createJourneyRuntime({
198
230
  sinks: [isolatedEmit],
199
- requestTimeouts: { defaultMs: 10, endpoints: [] },
231
+ slowRequests: { defaultMs: 10, endpoints: [] },
200
232
  });
201
- configureJourney({ requestTimeouts: { defaultMs: 9_000, endpoints: [] } });
233
+ configureJourney({ slowRequests: { defaultMs: 9_000, endpoints: [] } });
202
234
  });
203
235
 
204
- test('keeps timeouts separate from the module engine', () => {
205
- expect(runtime.getRequestTimeouts().defaultMs).toBe(10);
206
- expect(getRequestTimeouts().defaultMs).toBe(9_000);
236
+ test('inherits shared host thresholds unless this runtime overlays', () => {
237
+ expect(runtime.getSlowRequests().defaultMs).toBe(9_000);
238
+ runtime.setPolicyOverlay({ slowRequests: { defaultMs: 10, endpoints: [] } });
239
+ expect(runtime.getSlowRequests().defaultMs).toBe(10);
240
+ expect(getSlowRequests().defaultMs).toBe(9_000);
207
241
  });
208
242
  });
209
243
 
@@ -227,7 +261,7 @@ describe('[journey] createJourneyRuntime', () => {
227
261
  describe('when defineJourney is used', () => {
228
262
  const subject = () =>
229
263
  defineJourney(def).step('load', step => {
230
- step.complete();
264
+ step.completeJourney();
231
265
  });
232
266
 
233
267
  test('binds to the module runtime', async () => {
@@ -270,4 +304,17 @@ describe('[journey] createJourneyRuntime', () => {
270
304
  expect(isolatedEmit).toHaveBeenCalledTimes(1);
271
305
  });
272
306
  });
307
+
308
+ describe('maxSteps bounds', () => {
309
+ test('clamps invalid and oversized values', () => {
310
+ moduleRuntime.setMaxSteps(Infinity);
311
+ expect(moduleRuntime.getMaxSteps()).toBe(50);
312
+ moduleRuntime.setMaxSteps(0);
313
+ expect(moduleRuntime.getMaxSteps()).toBe(50);
314
+ moduleRuntime.setMaxSteps(500);
315
+ expect(moduleRuntime.getMaxSteps()).toBe(200);
316
+ moduleRuntime.setMaxSteps(Number.NaN);
317
+ expect(moduleRuntime.getMaxSteps()).toBe(50);
318
+ });
319
+ });
273
320
  });
@@ -0,0 +1,69 @@
1
+ /** @jest-environment jsdom */
2
+ import { MAX_ATTRIBUTE_KEYS, MAX_TAG_KEYS, isUnsafeKey } from '../../core/limits';
3
+ import { assignRecord, copyRecord, emptyRecord, sanitizeJourneyEvent } from '../../core/sanitize';
4
+ import type { JourneyEvent } from '../../core/types';
5
+
6
+ describe('[journey] sanitize / copyRecord', () => {
7
+ test('isUnsafeKey blocks proto and secret names', () => {
8
+ expect(isUnsafeKey('__proto__')).toBe(true);
9
+ expect(isUnsafeKey('constructor')).toBe(true);
10
+ expect(isUnsafeKey('Authorization')).toBe(true);
11
+ expect(isUnsafeKey('token')).toBe(true);
12
+ expect(isUnsafeKey('customer.id')).toBe(false);
13
+ });
14
+
15
+ test('JSON __proto__ assign does not pollute Object.prototype', () => {
16
+ const target = emptyRecord();
17
+ const polluted = JSON.parse('{"__proto__":{"polluted":true},"ok":1}') as Record<
18
+ string,
19
+ unknown
20
+ >;
21
+ assignRecord(target, polluted, MAX_TAG_KEYS);
22
+ expect(target.ok).toBe(1);
23
+ expect(({} as { polluted?: boolean }).polluted).toBeUndefined();
24
+ expect(Object.prototype.hasOwnProperty.call(target, '__proto__')).toBe(false);
25
+ });
26
+
27
+ test('copyRecord caps keys and drops secrets', () => {
28
+ const src: Record<string, string | number> = { token: 'secret', keep: 1 };
29
+ for (let i = 0; i < MAX_ATTRIBUTE_KEYS + 5; i += 1) {
30
+ src[`k${i}`] = i;
31
+ }
32
+ const out = copyRecord(src, { maxKeys: MAX_ATTRIBUTE_KEYS, normalizeKeys: true });
33
+ expect(out.token).toBeUndefined();
34
+ expect(Object.keys(out).length).toBe(MAX_ATTRIBUTE_KEYS);
35
+ });
36
+
37
+ test('sanitizeJourneyEvent normalizes step names and tag keys', () => {
38
+ const event: JourneyEvent = {
39
+ journey: {
40
+ name: 'Make A Sale!!',
41
+ team: 'Sales',
42
+ group: 'retail',
43
+ service: 'checkout',
44
+ outcome: 'good',
45
+ durationMs: 10,
46
+ expected: ['Create Sale'],
47
+ tags: { 'User Email': 'a@b.com', 'token': 'nope', 'plan': 'Pro' },
48
+ steps: [
49
+ {
50
+ name: 'Load Customer 1',
51
+ startMs: 0,
52
+ durationMs: 4,
53
+ outcome: 'good',
54
+ attributes: { 'token': 'x', 'Draft.id': 'abc' },
55
+ },
56
+ ],
57
+ },
58
+ };
59
+ const j = sanitizeJourneyEvent(event).journey;
60
+ expect(j.name).toBe('make_a_sale');
61
+ expect(j.expected).toEqual(['create_sale']);
62
+ expect(j.steps[0].name).toBe('load_customer_1');
63
+ expect(j.tags?.user_email).toBe('a_b.com');
64
+ expect(j.tags?.token).toBeUndefined();
65
+ expect(j.tags?.plan).toBe('pro');
66
+ expect(j.steps[0].attributes?.token).toBeUndefined();
67
+ expect(j.steps[0].attributes?.['draft.id']).toBe('abc');
68
+ });
69
+ });