@servicetitan/journey 2.0.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 (233) hide show
  1. package/README.md +1 -1
  2. package/dist/__test-utils__/test-runtime.d.ts.map +1 -0
  3. package/dist/__test-utils__/test-runtime.js +54 -0
  4. package/dist/__test-utils__/test-runtime.js.map +1 -0
  5. package/dist/__tests__/core/ambient.test.d.ts.map +1 -0
  6. package/dist/__tests__/core/api.test.d.ts +2 -0
  7. package/dist/__tests__/core/api.test.d.ts.map +1 -0
  8. package/dist/__tests__/core/define-journey.test.d.ts.map +1 -0
  9. package/dist/__tests__/core/requests.test.d.ts.map +1 -0
  10. package/dist/__tests__/core/runtime.test.d.ts.map +1 -0
  11. package/dist/__tests__/core/sanitize.test.d.ts +2 -0
  12. package/dist/__tests__/core/sanitize.test.d.ts.map +1 -0
  13. package/dist/__tests__/core/step.test.d.ts.map +1 -0
  14. package/dist/__tests__/core/timeouts.test.d.ts.map +1 -0
  15. package/dist/__tests__/global.test.d.ts +3 -0
  16. package/dist/__tests__/global.test.d.ts.map +1 -0
  17. package/dist/__tests__/hardening.test.d.ts +2 -0
  18. package/dist/__tests__/hardening.test.d.ts.map +1 -0
  19. package/dist/__tests__/integrations/axios.test.d.ts.map +1 -0
  20. package/dist/__tests__/integrations/context.test.d.ts.map +1 -0
  21. package/dist/__tests__/integrations/expose-app-journey.test.d.ts.map +1 -0
  22. package/dist/__tests__/integrations/fetch.test.d.ts.map +1 -0
  23. package/dist/__tests__/integrations/host-mfe.test.d.ts +2 -0
  24. package/dist/__tests__/integrations/host-mfe.test.d.ts.map +1 -0
  25. package/dist/__tests__/integrations/jquery.test.d.ts.map +1 -0
  26. package/dist/__tests__/integrations/request-key.test.d.ts.map +1 -0
  27. package/dist/__tests__/integrations/scope.test.d.ts.map +1 -0
  28. package/dist/__tests__/integrations/use-journey-step.test.d.ts +2 -0
  29. package/dist/__tests__/integrations/use-journey-step.test.d.ts.map +1 -0
  30. package/dist/__tests__/sinks/console.test.d.ts +2 -0
  31. package/dist/__tests__/sinks/console.test.d.ts.map +1 -0
  32. package/dist/__tests__/sinks/datadog.test.d.ts.map +1 -0
  33. package/dist/__tests__/sinks/sinks.test.d.ts.map +1 -0
  34. package/dist/config.d.ts +61 -13
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +98 -29
  37. package/dist/config.js.map +1 -1
  38. package/dist/core/abort-error.d.ts +4 -0
  39. package/dist/core/abort-error.d.ts.map +1 -0
  40. package/dist/core/abort-error.js +22 -0
  41. package/dist/core/abort-error.js.map +1 -0
  42. package/dist/core/api.d.ts +43 -0
  43. package/dist/core/api.d.ts.map +1 -0
  44. package/dist/core/api.js +122 -0
  45. package/dist/core/api.js.map +1 -0
  46. package/dist/{endpoint-policy.d.ts → core/endpoint-policy.d.ts} +14 -4
  47. package/dist/core/endpoint-policy.d.ts.map +1 -0
  48. package/dist/core/endpoint-policy.js +70 -0
  49. package/dist/core/endpoint-policy.js.map +1 -0
  50. package/dist/core/index.d.ts +6 -5
  51. package/dist/core/index.d.ts.map +1 -1
  52. package/dist/core/index.js +4 -3
  53. package/dist/core/index.js.map +1 -1
  54. package/dist/core/limits.d.ts +29 -0
  55. package/dist/core/limits.d.ts.map +1 -0
  56. package/dist/core/limits.js +63 -0
  57. package/dist/core/limits.js.map +1 -0
  58. package/dist/core/request-policy.d.ts +39 -0
  59. package/dist/core/request-policy.d.ts.map +1 -0
  60. package/dist/core/request-policy.js +18 -0
  61. package/dist/core/request-policy.js.map +1 -0
  62. package/dist/core/runtime.d.ts +95 -17
  63. package/dist/core/runtime.d.ts.map +1 -1
  64. package/dist/core/runtime.js +435 -124
  65. package/dist/core/runtime.js.map +1 -1
  66. package/dist/core/sanitize.d.ts +23 -0
  67. package/dist/core/sanitize.d.ts.map +1 -0
  68. package/dist/core/sanitize.js +143 -0
  69. package/dist/core/sanitize.js.map +1 -0
  70. package/dist/core/step-tag.d.ts +31 -6
  71. package/dist/core/step-tag.d.ts.map +1 -1
  72. package/dist/core/step-tag.js +62 -9
  73. package/dist/core/step-tag.js.map +1 -1
  74. package/dist/core/types.d.ts +76 -26
  75. package/dist/core/types.d.ts.map +1 -1
  76. package/dist/core/types.js +4 -1
  77. package/dist/core/types.js.map +1 -1
  78. package/dist/global.d.ts +145 -0
  79. package/dist/global.d.ts.map +1 -0
  80. package/dist/global.js +326 -0
  81. package/dist/global.js.map +1 -0
  82. package/dist/index.d.ts +9 -5
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +9 -5
  85. package/dist/index.js.map +1 -1
  86. package/dist/integrations/axios.d.ts +5 -1
  87. package/dist/integrations/axios.d.ts.map +1 -1
  88. package/dist/integrations/axios.js +146 -34
  89. package/dist/integrations/axios.js.map +1 -1
  90. package/dist/integrations/fetch.d.ts +6 -4
  91. package/dist/integrations/fetch.d.ts.map +1 -1
  92. package/dist/integrations/fetch.js +105 -26
  93. package/dist/integrations/fetch.js.map +1 -1
  94. package/dist/integrations/http-report.d.ts +27 -0
  95. package/dist/integrations/http-report.d.ts.map +1 -0
  96. package/dist/integrations/http-report.js +123 -0
  97. package/dist/integrations/http-report.js.map +1 -0
  98. package/dist/integrations/jquery/index.d.ts +10 -6
  99. package/dist/integrations/jquery/index.d.ts.map +1 -1
  100. package/dist/integrations/jquery/index.js +28 -9
  101. package/dist/integrations/jquery/index.js.map +1 -1
  102. package/dist/integrations/jquery/instrument.d.ts +14 -5
  103. package/dist/integrations/jquery/instrument.d.ts.map +1 -1
  104. package/dist/integrations/jquery/instrument.js +111 -29
  105. package/dist/integrations/jquery/instrument.js.map +1 -1
  106. package/dist/integrations/react/context.d.ts +4 -1
  107. package/dist/integrations/react/context.d.ts.map +1 -1
  108. package/dist/integrations/react/context.js +4 -1
  109. package/dist/integrations/react/context.js.map +1 -1
  110. package/dist/integrations/react/scope.d.ts +14 -3
  111. package/dist/integrations/react/scope.d.ts.map +1 -1
  112. package/dist/integrations/react/scope.js +48 -9
  113. package/dist/integrations/react/scope.js.map +1 -1
  114. package/dist/integrations/react/stable-config-keys.d.ts +6 -0
  115. package/dist/integrations/react/stable-config-keys.d.ts.map +1 -0
  116. package/dist/integrations/react/stable-config-keys.js +21 -0
  117. package/dist/integrations/react/stable-config-keys.js.map +1 -0
  118. package/dist/integrations/react/use-journey-step.d.ts +11 -0
  119. package/dist/integrations/react/use-journey-step.d.ts.map +1 -0
  120. package/dist/integrations/react/use-journey-step.js +19 -0
  121. package/dist/integrations/react/use-journey-step.js.map +1 -0
  122. package/dist/integrations/request-key.d.ts +8 -3
  123. package/dist/integrations/request-key.d.ts.map +1 -1
  124. package/dist/integrations/request-key.js +10 -9
  125. package/dist/integrations/request-key.js.map +1 -1
  126. package/dist/integrations/soft-skip.d.ts +7 -0
  127. package/dist/integrations/soft-skip.d.ts.map +1 -0
  128. package/dist/integrations/soft-skip.js +30 -0
  129. package/dist/integrations/soft-skip.js.map +1 -0
  130. package/dist/sinks/console.d.ts +11 -0
  131. package/dist/sinks/console.d.ts.map +1 -0
  132. package/dist/sinks/console.js +19 -0
  133. package/dist/sinks/console.js.map +1 -0
  134. package/dist/sinks/datadog.d.ts +12 -1
  135. package/dist/sinks/datadog.d.ts.map +1 -1
  136. package/dist/sinks/datadog.js +42 -25
  137. package/dist/sinks/datadog.js.map +1 -1
  138. package/package.json +2 -2
  139. package/src/{__mocks__ → __test-utils__}/test-runtime.ts +15 -4
  140. package/src/__tests__/config.test.ts +158 -42
  141. package/src/__tests__/{ambient.test.ts → core/ambient.test.ts} +23 -16
  142. package/src/__tests__/{registry.test.ts → core/api.test.ts} +35 -30
  143. package/src/__tests__/{define-journey.test.ts → core/define-journey.test.ts} +33 -15
  144. package/src/__tests__/{requests.test.ts → core/requests.test.ts} +61 -39
  145. package/src/__tests__/{runtime.test.ts → core/runtime.test.ts} +84 -37
  146. package/src/__tests__/core/sanitize.test.ts +69 -0
  147. package/src/__tests__/{step.test.ts → core/step.test.ts} +146 -52
  148. package/src/__tests__/{timeouts.test.ts → core/timeouts.test.ts} +83 -13
  149. package/src/__tests__/global.test.ts +154 -0
  150. package/src/__tests__/hardening.test.ts +613 -0
  151. package/src/__tests__/integrations/axios.test.ts +616 -0
  152. package/src/__tests__/{context.test.tsx → integrations/context.test.tsx} +1 -1
  153. package/src/__tests__/{expose-app-journey.test.ts → integrations/expose-app-journey.test.ts} +11 -5
  154. package/src/__tests__/{fetch.test.ts → integrations/fetch.test.ts} +159 -25
  155. package/src/__tests__/integrations/host-mfe.test.ts +548 -0
  156. package/src/__tests__/integrations/jquery.test.ts +539 -0
  157. package/src/__tests__/{request-key.test.ts → integrations/request-key.test.ts} +1 -1
  158. package/src/__tests__/integrations/scope.test.tsx +286 -0
  159. package/src/__tests__/integrations/use-journey-step.test.tsx +43 -0
  160. package/src/__tests__/sinks/console.test.ts +103 -0
  161. package/src/__tests__/{datadog.test.ts → sinks/datadog.test.ts} +109 -5
  162. package/src/__tests__/{sinks.test.ts → sinks/sinks.test.ts} +13 -12
  163. package/src/config.ts +173 -41
  164. package/src/core/abort-error.ts +24 -0
  165. package/src/core/api.ts +159 -0
  166. package/src/{endpoint-policy.ts → core/endpoint-policy.ts} +46 -4
  167. package/src/core/index.ts +16 -17
  168. package/src/core/limits.ts +73 -0
  169. package/src/core/request-policy.ts +66 -0
  170. package/src/core/runtime.ts +560 -137
  171. package/src/core/sanitize.ts +177 -0
  172. package/src/core/step-tag.ts +90 -11
  173. package/src/core/types.ts +82 -26
  174. package/src/global.ts +466 -0
  175. package/src/index.ts +31 -18
  176. package/src/integrations/axios.ts +183 -45
  177. package/src/integrations/fetch.ts +127 -41
  178. package/src/integrations/http-report.ts +184 -0
  179. package/src/integrations/jquery/index.ts +37 -18
  180. package/src/integrations/jquery/instrument.ts +126 -42
  181. package/src/integrations/react/context.ts +4 -1
  182. package/src/integrations/react/scope.tsx +61 -10
  183. package/src/integrations/react/stable-config-keys.ts +28 -0
  184. package/src/integrations/react/use-journey-step.ts +31 -0
  185. package/src/integrations/request-key.ts +18 -10
  186. package/src/integrations/soft-skip.ts +35 -0
  187. package/src/sinks/console.ts +21 -0
  188. package/src/sinks/datadog.ts +52 -33
  189. package/dist/__mocks__/test-runtime.d.ts.map +0 -1
  190. package/dist/__tests__/ambient.test.d.ts.map +0 -1
  191. package/dist/__tests__/axios.test.d.ts.map +0 -1
  192. package/dist/__tests__/context.test.d.ts.map +0 -1
  193. package/dist/__tests__/datadog.test.d.ts.map +0 -1
  194. package/dist/__tests__/define-journey.test.d.ts.map +0 -1
  195. package/dist/__tests__/expose-app-journey.test.d.ts.map +0 -1
  196. package/dist/__tests__/fetch.test.d.ts.map +0 -1
  197. package/dist/__tests__/jquery.test.d.ts.map +0 -1
  198. package/dist/__tests__/registry.test.d.ts +0 -2
  199. package/dist/__tests__/registry.test.d.ts.map +0 -1
  200. package/dist/__tests__/request-key.test.d.ts.map +0 -1
  201. package/dist/__tests__/requests.test.d.ts.map +0 -1
  202. package/dist/__tests__/runtime.test.d.ts.map +0 -1
  203. package/dist/__tests__/scope.test.d.ts.map +0 -1
  204. package/dist/__tests__/sinks.test.d.ts.map +0 -1
  205. package/dist/__tests__/step.test.d.ts.map +0 -1
  206. package/dist/__tests__/timeouts.test.d.ts.map +0 -1
  207. package/dist/core/registry.d.ts +0 -50
  208. package/dist/core/registry.d.ts.map +0 -1
  209. package/dist/core/registry.js +0 -102
  210. package/dist/core/registry.js.map +0 -1
  211. package/dist/endpoint-policy.d.ts.map +0 -1
  212. package/dist/endpoint-policy.js +0 -39
  213. package/dist/endpoint-policy.js.map +0 -1
  214. package/src/__tests__/axios.test.ts +0 -365
  215. package/src/__tests__/jquery.test.ts +0 -327
  216. package/src/__tests__/scope.test.tsx +0 -128
  217. package/src/core/registry.ts +0 -162
  218. /package/dist/{__mocks__ → __test-utils__}/test-runtime.d.ts +0 -0
  219. /package/dist/__tests__/{ambient.test.d.ts → core/ambient.test.d.ts} +0 -0
  220. /package/dist/__tests__/{define-journey.test.d.ts → core/define-journey.test.d.ts} +0 -0
  221. /package/dist/__tests__/{requests.test.d.ts → core/requests.test.d.ts} +0 -0
  222. /package/dist/__tests__/{runtime.test.d.ts → core/runtime.test.d.ts} +0 -0
  223. /package/dist/__tests__/{step.test.d.ts → core/step.test.d.ts} +0 -0
  224. /package/dist/__tests__/{timeouts.test.d.ts → core/timeouts.test.d.ts} +0 -0
  225. /package/dist/__tests__/{axios.test.d.ts → integrations/axios.test.d.ts} +0 -0
  226. /package/dist/__tests__/{context.test.d.ts → integrations/context.test.d.ts} +0 -0
  227. /package/dist/__tests__/{expose-app-journey.test.d.ts → integrations/expose-app-journey.test.d.ts} +0 -0
  228. /package/dist/__tests__/{fetch.test.d.ts → integrations/fetch.test.d.ts} +0 -0
  229. /package/dist/__tests__/{jquery.test.d.ts → integrations/jquery.test.d.ts} +0 -0
  230. /package/dist/__tests__/{request-key.test.d.ts → integrations/request-key.test.d.ts} +0 -0
  231. /package/dist/__tests__/{scope.test.d.ts → integrations/scope.test.d.ts} +0 -0
  232. /package/dist/__tests__/{datadog.test.d.ts → sinks/datadog.test.d.ts} +0 -0
  233. /package/dist/__tests__/{sinks.test.d.ts → sinks/sinks.test.d.ts} +0 -0
@@ -1,6 +1,7 @@
1
1
  /** @jest-environment jsdom */
2
- import { lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { defineJourney, journeyStep, startJourney } from '../core';
2
+ import { lastJourney, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
+ import { defineJourney, journeyStep } from '../../core';
4
+ import { moduleRuntime } from '../../core/runtime';
4
5
 
5
6
  const emitMock = jest.fn();
6
7
 
@@ -25,7 +26,7 @@ describe('[journey] defineJourney', () => {
25
26
  const subject = () =>
26
27
  journey.step('do-work', step => {
27
28
  step.setAttribute('x', 1);
28
- step.complete({ 'payment.id': 42 });
29
+ step.completeJourney({ 'payment.id': 42 });
29
30
  });
30
31
 
31
32
  test('opens, records the step, and completes good', async () => {
@@ -40,7 +41,7 @@ describe('[journey] defineJourney', () => {
40
41
 
41
42
  test('mountStep also opens the journey', async () => {
42
43
  await journey.mountStep('boot', step => {
43
- step.complete();
44
+ step.completeJourney();
44
45
  });
45
46
 
46
47
  expect(emitMock).toHaveBeenCalledTimes(1);
@@ -51,12 +52,12 @@ describe('[journey] defineJourney', () => {
51
52
 
52
53
  describe('when a JourneyScope already opened the journey', () => {
53
54
  beforeEach(() => {
54
- startJourney(base);
55
+ moduleRuntime.startJourney(base);
55
56
  });
56
57
 
57
58
  const subject = () =>
58
59
  journey.step('do-work', step => {
59
- step.complete();
60
+ step.completeJourney();
60
61
  });
61
62
 
62
63
  test('does not restart the open journey', async () => {
@@ -75,7 +76,7 @@ describe('[journey] defineJourney', () => {
75
76
 
76
77
  const subject = () =>
77
78
  journeyStep(journey, 'via-free', step => {
78
- step.complete();
79
+ step.completeJourney();
79
80
  });
80
81
 
81
82
  test('resolves the handle name', async () => {
@@ -147,12 +148,12 @@ describe('[journey] defineJourney', () => {
147
148
 
148
149
  describe('when mountStep runs under an already-open journey', () => {
149
150
  beforeEach(() => {
150
- startJourney(base);
151
+ moduleRuntime.startJourney(base);
151
152
  });
152
153
 
153
154
  const subject = () =>
154
155
  journey.mountStep('boot', step => {
155
- step.complete();
156
+ step.completeJourney();
156
157
  });
157
158
 
158
159
  test('does not restart the open journey', async () => {
@@ -165,12 +166,29 @@ describe('[journey] defineJourney', () => {
165
166
  });
166
167
  });
167
168
 
168
- describe('when copying def fields onto the handle', () => {
169
- test('exposes the journey config properties', () => {
170
- expect(journey.name).toBe(base.name);
171
- expect(journey.team).toBe(base.team);
172
- expect(journey.group).toBe(base.group);
173
- expect(journey.service).toBe(base.service);
169
+ describe('when keeping the original config on the handle', () => {
170
+ test('exposes the same config object', () => {
171
+ expect(journey.config).toBe(base);
172
+ expect(journey.config.name).toBe(base.name);
173
+ expect(journey.config.name).toBe(base.name);
174
+ expect(journey.config.team).toBe(base.team);
175
+ expect(journey.config.group).toBe(base.group);
176
+ expect(journey.config.service).toBe(base.service);
177
+ });
178
+ });
179
+
180
+ describe('when stepping via free journeyStep with a handle and nothing open', () => {
181
+ const subject = () =>
182
+ journeyStep(journey, 'via-handle', step => {
183
+ step.completeJourney();
184
+ });
185
+
186
+ test('auto-starts from the handle config', async () => {
187
+ await subject();
188
+
189
+ expect(emitMock).toHaveBeenCalledTimes(1);
190
+ expect(lastJourney(emitMock).outcome).toBe('good');
191
+ expect(lastJourney(emitMock).steps[0].name).toBe('via-handle');
174
192
  });
175
193
  });
176
194
  });
@@ -1,21 +1,25 @@
1
1
  /** @jest-environment jsdom */
2
- import { baseJourneyDef, lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { configureJourney } from '../config';
2
+ import {
3
+ baseJourneyDef,
4
+ lastJourney,
5
+ useJourneyTestRuntime,
6
+ } from '../../__test-utils__/test-runtime';
7
+ import { configureJourney } from '../../config';
4
8
  import {
5
9
  completeJourney,
6
10
  journeyStep,
7
11
  reportBackendRequest,
8
- resolveJourneyStep,
9
- startJourney,
10
12
  type JourneyDef,
11
- type JourneyStepRef,
12
- } from '../core';
13
+ type JourneyStepStamp,
14
+ } from '../../core';
15
+ import type { StepRecord } from '../../core/types';
16
+ import { moduleRuntime } from '../../core/runtime';
13
17
 
14
18
  const emitMock = jest.fn();
15
19
  const baseDef = { ...baseJourneyDef, name: 'requests-journey' };
16
20
 
17
21
  describe('[journey] reportBackendRequest', () => {
18
- let stepRef: JourneyStepRef | undefined;
22
+ let stepRef: StepRecord | undefined;
19
23
  let status: number | undefined;
20
24
  let durationMs: number;
21
25
  let key: string;
@@ -32,12 +36,12 @@ describe('[journey] reportBackendRequest', () => {
32
36
  const subject = () => reportBackendRequest(stepRef, status, durationMs, key);
33
37
 
34
38
  async function withOpenStep(
35
- run: (ref: JourneyStepRef) => void | Promise<void>,
36
- def: JourneyDef = { ...baseDef, requestTimeoutMs: 5_000 }
39
+ run: (ref: StepRecord) => void | Promise<void>,
40
+ def: JourneyDef = { ...baseDef, slowRequestMs: 5_000 }
37
41
  ) {
38
- startJourney(def);
42
+ moduleRuntime.startJourney(def);
39
43
  await journeyStep(baseDef.name, 'do-work', async step => {
40
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
44
+ stepRef = (step.stamp() as JourneyStepStamp).step;
41
45
  await run(stepRef!);
42
46
  });
43
47
  }
@@ -96,7 +100,7 @@ describe('[journey] reportBackendRequest', () => {
96
100
  () => {
97
101
  subject();
98
102
  },
99
- { ...baseDef, requestTimeoutMs: 50 }
103
+ { ...baseDef, slowRequestMs: 50 }
100
104
  );
101
105
 
102
106
  expect(lastJourney(emitMock).reason).toBe('request-latency');
@@ -156,7 +160,7 @@ describe('[journey] reportBackendRequest', () => {
156
160
  },
157
161
  {
158
162
  ...baseDef,
159
- requestTimeoutMs: 5_000,
163
+ slowRequestMs: 5_000,
160
164
  endpoints: [{ match: '/flaky', ignore: true }],
161
165
  }
162
166
  );
@@ -166,28 +170,29 @@ describe('[journey] reportBackendRequest', () => {
166
170
  });
167
171
  });
168
172
 
169
- describe('when a journey endpoint timeoutMs is 0', () => {
170
- test('falls through to the journey requestTimeoutMs', async () => {
171
- startJourney({
173
+ describe('when a journey endpoint slowRequestMs is 0', () => {
174
+ test('never scores latency for that prefix', async () => {
175
+ moduleRuntime.startJourney({
172
176
  ...baseDef,
173
- requestTimeoutMs: 30,
174
- endpoints: [{ match: '/special', timeoutMs: 0 }],
177
+ slowRequestMs: 30,
178
+ endpoints: [{ match: '/special', slowRequestMs: 0 }],
175
179
  });
176
180
 
177
181
  await journeyStep(baseDef.name, 'do-work', step => {
178
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
182
+ stepRef = (step.stamp() as JourneyStepStamp).step;
179
183
  status = 200;
180
184
  durationMs = 100;
181
185
  key = '/special/path';
182
186
  subject();
183
187
  });
188
+ completeJourney(baseDef.name);
184
189
 
185
- expect(lastJourney(emitMock).reason).toBe('request-latency');
190
+ expect(lastJourney(emitMock).outcome).toBe('good');
186
191
  });
187
192
  });
188
193
 
189
194
  describe('when journey endpoint timeoutMs is set', () => {
190
- test('wins over the journey requestTimeoutMs', async () => {
195
+ test('wins over the journey slowRequestMs', async () => {
191
196
  await withOpenStep(
192
197
  () => {
193
198
  status = 200;
@@ -197,8 +202,8 @@ describe('[journey] reportBackendRequest', () => {
197
202
  },
198
203
  {
199
204
  ...baseDef,
200
- requestTimeoutMs: 5_000,
201
- endpoints: [{ match: '/special', timeoutMs: 20 }],
205
+ slowRequestMs: 5_000,
206
+ endpoints: [{ match: '/special', slowRequestMs: 20 }],
202
207
  }
203
208
  );
204
209
 
@@ -209,7 +214,7 @@ describe('[journey] reportBackendRequest', () => {
209
214
  describe('when the journey endpoint ignore overrides the app-wide policy', () => {
210
215
  beforeEach(() => {
211
216
  configureJourney({
212
- requestTimeouts: {
217
+ slowRequests: {
213
218
  defaultMs: 4_000,
214
219
  endpoints: [{ match: '/flaky', ignore: true }],
215
220
  },
@@ -225,7 +230,7 @@ describe('[journey] reportBackendRequest', () => {
225
230
  },
226
231
  {
227
232
  ...baseDef,
228
- requestTimeoutMs: 5_000,
233
+ slowRequestMs: 5_000,
229
234
  endpoints: [{ match: '/flaky', ignore: false }],
230
235
  }
231
236
  );
@@ -237,11 +242,11 @@ describe('[journey] reportBackendRequest', () => {
237
242
  describe('when only app-wide endpoint policies apply', () => {
238
243
  beforeEach(() => {
239
244
  configureJourney({
240
- requestTimeouts: {
245
+ slowRequests: {
241
246
  defaultMs: 4_000,
242
247
  endpoints: [
243
248
  { match: '/ignored', ignore: true },
244
- { match: '/tight', timeoutMs: 30 },
249
+ { match: '/tight', slowRequestMs: 30 },
245
250
  ],
246
251
  },
247
252
  });
@@ -270,41 +275,42 @@ describe('[journey] reportBackendRequest', () => {
270
275
  });
271
276
  });
272
277
 
273
- describe('when app endpoint timeoutMs is 0', () => {
278
+ describe('when app endpoint slowRequestMs is 0', () => {
274
279
  beforeEach(() => {
275
280
  configureJourney({
276
- requestTimeouts: {
281
+ slowRequests: {
277
282
  defaultMs: 40,
278
- endpoints: [{ match: '/loose', timeoutMs: 0 }],
283
+ endpoints: [{ match: '/loose', slowRequestMs: 0 }],
279
284
  },
280
285
  });
281
286
  });
282
287
 
283
- test('falls through to defaultMs', async () => {
284
- startJourney(baseDef);
288
+ test('never scores latency for that prefix', async () => {
289
+ moduleRuntime.startJourney(baseDef);
285
290
 
286
291
  await journeyStep(baseDef.name, 'do-work', step => {
287
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
292
+ stepRef = (step.stamp() as JourneyStepStamp).step;
288
293
  status = 200;
289
294
  durationMs = 100;
290
295
  key = '/loose/x';
291
296
  subject();
292
297
  });
298
+ completeJourney(baseDef.name);
293
299
 
294
- expect(lastJourney(emitMock).reason).toBe('request-latency');
300
+ expect(lastJourney(emitMock).outcome).toBe('good');
295
301
  });
296
302
  });
297
303
 
298
- describe('when the journey has no requestTimeoutMs', () => {
304
+ describe('when the journey has no slowRequestMs', () => {
299
305
  beforeEach(() => {
300
- configureJourney({ requestTimeouts: { defaultMs: 40, endpoints: [] } });
306
+ configureJourney({ slowRequests: { defaultMs: 40, endpoints: [] } });
301
307
  });
302
308
 
303
309
  test('uses the app defaultMs', async () => {
304
- startJourney(baseDef);
310
+ moduleRuntime.startJourney(baseDef);
305
311
 
306
312
  await journeyStep(baseDef.name, 'do-work', step => {
307
- stepRef = resolveJourneyStep((step.tag() as { journeyStep: unknown }).journeyStep);
313
+ stepRef = (step.stamp() as JourneyStepStamp).step;
308
314
  status = 200;
309
315
  durationMs = 100;
310
316
  key = '/any';
@@ -316,7 +322,7 @@ describe('[journey] reportBackendRequest', () => {
316
322
  });
317
323
 
318
324
  describe('against a closed journey step ref', () => {
319
- let closedRef: JourneyStepRef | undefined;
325
+ let closedRef: StepRecord | undefined;
320
326
 
321
327
  beforeEach(async () => {
322
328
  await withOpenStep(ref => {
@@ -334,4 +340,20 @@ describe('[journey] reportBackendRequest', () => {
334
340
  expect(emitMock).not.toHaveBeenCalled();
335
341
  });
336
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
+ });
337
359
  });
@@ -1,18 +1,19 @@
1
1
  /** @jest-environment jsdom */
2
- import { lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { configureJourney, getRequestTimeouts } from '../config';
2
+ import { lastJourney, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
+ import { configureJourney, getSlowRequests } from '../../config';
4
4
  import {
5
- activeJourneyStep,
6
5
  completeJourney,
7
- createJourneyRuntime,
8
6
  defineJourney,
9
7
  journeyStep,
10
8
  resetJourney,
11
- startJourney,
12
- } from '../core';
13
- import { sendToDatadog } from '../sinks/datadog';
14
-
15
- jest.mock('../sinks/datadog', () => ({
9
+ singleInFlightJourneyStep,
10
+ } from '../../core';
11
+ import { createJourneyRuntime, moduleRuntime } from '../../core/runtime';
12
+ import { registerJourneyRuntime, setJourneyDefaultEnabled } from '../../global';
13
+ import { sendToConsole } from '../../sinks/console';
14
+ import { sendToDatadog } from '../../sinks/datadog';
15
+
16
+ jest.mock('../../sinks/datadog', () => ({
16
17
  sendToDatadog: jest.fn(),
17
18
  }));
18
19
 
@@ -31,7 +32,7 @@ describe('[journey] resetJourney', () => {
31
32
 
32
33
  describe('when a journey is open', () => {
33
34
  beforeEach(() => {
34
- startJourney(def);
35
+ moduleRuntime.startJourney(def);
35
36
  });
36
37
 
37
38
  const subject = () => resetJourney({ sinks: [emitMock] });
@@ -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
 
@@ -82,7 +85,7 @@ describe('[journey] resetJourney', () => {
82
85
  beforeEach(() => {
83
86
  resetJourney({ sinks: [emitMock] });
84
87
  emitMock.mockClear();
85
- startJourney(def);
88
+ moduleRuntime.startJourney(def);
86
89
  completeJourney(def.name);
87
90
  emitMock.mockClear();
88
91
  });
@@ -91,7 +94,7 @@ describe('[journey] resetJourney', () => {
91
94
 
92
95
  test('leaves sinks empty', () => {
93
96
  subject();
94
- startJourney(def);
97
+ moduleRuntime.startJourney(def);
95
98
  completeJourney(def.name);
96
99
 
97
100
  expect(emitMock).not.toHaveBeenCalled();
@@ -103,7 +106,7 @@ describe('[journey] resetJourney', () => {
103
106
  let stepPromise: Promise<unknown>;
104
107
 
105
108
  beforeEach(() => {
106
- startJourney(def);
109
+ moduleRuntime.startJourney(def);
107
110
  const hang = new Promise<void>(resolve => {
108
111
  resolveWork = resolve;
109
112
  });
@@ -120,18 +123,31 @@ 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
 
131
147
  describe('when a journey countdown is armed', () => {
132
148
  beforeEach(() => {
133
149
  jest.useFakeTimers();
134
- startJourney({ ...def, timeoutMs: 50 });
150
+ moduleRuntime.startJourney({ ...def, timeoutMs: 50 });
135
151
  });
136
152
 
137
153
  afterEach(() => {
@@ -157,26 +173,42 @@ 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
- sinks: [sendToDatadog],
172
- requestTimeouts: { defaultMs: 4_000, endpoints: [] },
203
+ sinks: [sendToDatadog, sendToConsole],
204
+ slowRequests: { defaultMs: 4_000, endpoints: [] },
173
205
  });
174
206
  });
175
207
 
176
208
  describe('when both runtimes open a journey', () => {
177
209
  beforeEach(() => {
178
210
  runtime.startJourney(def);
179
- startJourney({ ...def, name: 'default-side' });
211
+ moduleRuntime.startJourney({ ...def, name: 'default-side' });
180
212
  });
181
213
 
182
214
  const subject = () => {
@@ -192,30 +224,32 @@ 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 default 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
 
210
244
  describe('when the isolated runtime is reset', () => {
211
245
  beforeEach(() => {
212
246
  runtime.startJourney(def);
213
- startJourney({ ...def, name: 'default-side' });
247
+ moduleRuntime.startJourney({ ...def, name: 'default-side' });
214
248
  });
215
249
 
216
250
  const subject = () => runtime.reset({ sinks: [isolatedEmit] });
217
251
 
218
- test('does not clear the default engine', () => {
252
+ test('does not clear the module engine', () => {
219
253
  subject();
220
254
  completeJourney('default-side');
221
255
 
@@ -227,10 +261,10 @@ 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
- test('binds to the default runtime', async () => {
267
+ test('binds to the module runtime', async () => {
234
268
  await subject();
235
269
 
236
270
  expect(defaultEmit).toHaveBeenCalledTimes(1);
@@ -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
+ });