@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
@@ -16,15 +16,12 @@ jest.mock('../integrations/jquery', () => ({
16
16
  exposeAppJourney: jest.fn(),
17
17
  }));
18
18
 
19
- import {
20
- configureJourney,
21
- getRequestTimeouts,
22
- longestEndpointMatch,
23
- sortEndpointsByLongestMatch,
24
- } from '../config';
19
+ import { configureJourney, getSlowRequests } from '../config';
25
20
  import { resetJourney } from '../core';
26
- import { emitJourneyEvent } from '../core/registry';
21
+ import { moduleRuntime } from '../core/runtime';
27
22
  import type { JourneyEvent } from '../core/types';
23
+ import { longestEndpointMatch, sortEndpointsByLongestMatch } from '../core/endpoint-policy';
24
+ import { resetSharedPolicies, setJourneyDefaultEnabled } from '../global';
28
25
  import { instrumentAxios } from '../integrations/axios';
29
26
  import { instrumentFetch } from '../integrations/fetch';
30
27
  import {
@@ -32,6 +29,7 @@ import {
32
29
  instrumentJquery,
33
30
  type WindowWithAppJourney,
34
31
  } from '../integrations/jquery';
32
+ import { sendToConsole } from '../sinks/console';
35
33
  import { sendToDatadog } from '../sinks/datadog';
36
34
 
37
35
  const instrumentAxiosMock = jest.mocked(instrumentAxios);
@@ -39,11 +37,18 @@ const instrumentFetchMock = jest.mocked(instrumentFetch);
39
37
  const instrumentJqueryMock = jest.mocked(instrumentJquery);
40
38
  const exposeAppJourneyMock = jest.mocked(exposeAppJourney);
41
39
 
42
- describe('[journey] configureJourney / getRequestTimeouts', () => {
40
+ describe('[journey] configureJourney / getSlowRequests', () => {
41
+ beforeEach(() => {
42
+ setJourneyDefaultEnabled(true);
43
+ });
44
+
43
45
  afterEach(() => {
46
+ setJourneyDefaultEnabled(false);
47
+ localStorage.clear();
48
+ resetSharedPolicies();
44
49
  resetJourney({
45
- sinks: [sendToDatadog],
46
- requestTimeouts: { defaultMs: 4_000, endpoints: [] },
50
+ sinks: [sendToDatadog, sendToConsole],
51
+ slowRequests: { defaultMs: 4_000, endpoints: [] },
47
52
  });
48
53
  instrumentAxiosMock.mockClear();
49
54
  instrumentFetchMock.mockClear();
@@ -52,27 +57,36 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
52
57
  });
53
58
 
54
59
  test('defaults to 4s with empty endpoints', () => {
55
- expect(getRequestTimeouts()).toEqual({ defaultMs: 4_000, endpoints: [] });
60
+ expect(getSlowRequests()).toEqual({ defaultMs: 4_000, endpoints: [] });
56
61
  });
57
62
 
58
63
  test('overrides the app-wide request timeout tier', () => {
59
64
  configureJourney({
60
- requestTimeouts: {
65
+ slowRequests: {
61
66
  defaultMs: 2_000,
62
- endpoints: [{ match: '/slow', timeoutMs: 8_000 }],
67
+ endpoints: [{ match: '/slow', slowRequestMs: 8_000 }],
63
68
  },
64
69
  });
65
70
 
66
- expect(getRequestTimeouts()).toEqual({
71
+ expect(getSlowRequests()).toEqual({
67
72
  defaultMs: 2_000,
68
- endpoints: [{ match: '/slow', timeoutMs: 8_000 }],
73
+ endpoints: [{ match: '/slow', slowRequestMs: 8_000 }],
69
74
  });
70
75
  });
71
76
 
72
- test('ignores configureJourney when requestTimeouts is omitted', () => {
73
- configureJourney({ requestTimeouts: { defaultMs: 1_000, endpoints: [] } });
77
+ test('ignores configureJourney when slowRequests is omitted', () => {
78
+ configureJourney({ slowRequests: { defaultMs: 1_000, endpoints: [] } });
74
79
  configureJourney({});
75
- expect(getRequestTimeouts().defaultMs).toBe(1_000);
80
+ expect(getSlowRequests().defaultMs).toBe(1_000);
81
+ });
82
+
83
+ test('clamps maxSteps to 1..200', () => {
84
+ configureJourney({ maxSteps: Infinity });
85
+ expect(moduleRuntime.getMaxSteps()).toBe(50);
86
+ configureJourney({ maxSteps: 0 });
87
+ expect(moduleRuntime.getMaxSteps()).toBe(50);
88
+ configureJourney({ maxSteps: 500 });
89
+ expect(moduleRuntime.getMaxSteps()).toBe(200);
76
90
  });
77
91
 
78
92
  describe('with sinks', () => {
@@ -97,7 +111,7 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
97
111
 
98
112
  test('replaces the sink list', () => {
99
113
  configureJourney({ sinks: [custom] });
100
- emitJourneyEvent(payload);
114
+ moduleRuntime.emit(payload);
101
115
 
102
116
  expect(custom).toHaveBeenCalledWith(payload);
103
117
  expect(sendToDatadog).not.toHaveBeenCalled();
@@ -105,16 +119,30 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
105
119
 
106
120
  test('leaves sinks unchanged when sinks is omitted', () => {
107
121
  configureJourney({ sinks: [custom] });
108
- configureJourney({ requestTimeouts: { defaultMs: 2_000, endpoints: [] } });
109
- emitJourneyEvent(payload);
122
+ configureJourney({ slowRequests: { defaultMs: 2_000, endpoints: [] } });
123
+ moduleRuntime.emit(payload);
124
+
125
+ expect(custom).toHaveBeenCalledWith(payload);
126
+ expect(sendToDatadog).not.toHaveBeenCalled();
127
+ });
128
+
129
+ test('ignores a later sinks replacement on this runtime', () => {
130
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => undefined);
131
+ configureJourney({ sinks: [custom] });
132
+ configureJourney({ sinks: [sendToDatadog] });
133
+ moduleRuntime.emit(payload);
110
134
 
111
135
  expect(custom).toHaveBeenCalledWith(payload);
112
136
  expect(sendToDatadog).not.toHaveBeenCalled();
137
+ expect(warn).toHaveBeenCalledWith(
138
+ expect.stringContaining('sinks already set on this runtime')
139
+ );
140
+ warn.mockRestore();
113
141
  });
114
142
 
115
143
  test('disables emission when sinks is empty', () => {
116
144
  configureJourney({ sinks: [] });
117
- emitJourneyEvent(payload);
145
+ moduleRuntime.emit(payload);
118
146
 
119
147
  expect(sendToDatadog).not.toHaveBeenCalled();
120
148
  expect(custom).not.toHaveBeenCalled();
@@ -128,6 +156,15 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
128
156
  };
129
157
  const jquery = { ajaxPrefilter: jest.fn(), bind: jest.fn() } as never;
130
158
  const fetchTarget = { fetch: jest.fn() };
159
+ let warnSpy: jest.SpyInstance;
160
+
161
+ beforeEach(() => {
162
+ warnSpy = jest.spyOn(console, 'warn').mockImplementation(() => {});
163
+ });
164
+
165
+ afterEach(() => {
166
+ warnSpy.mockRestore();
167
+ });
131
168
 
132
169
  test('instruments axios when provided', () => {
133
170
  configureJourney({ axios: axios as never });
@@ -158,6 +195,38 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
158
195
  expect(instrumentJqueryMock).toHaveBeenCalledWith(jquery);
159
196
  });
160
197
 
198
+ test('instruments global $ when jquery is true', () => {
199
+ const $ = Object.assign(jest.fn(), { ajaxPrefilter: jest.fn() });
200
+ (globalThis as { $?: unknown }).$ = $;
201
+
202
+ try {
203
+ configureJourney({ jquery: true });
204
+
205
+ expect(instrumentJqueryMock).toHaveBeenCalledWith($);
206
+ } finally {
207
+ delete (globalThis as { $?: unknown }).$;
208
+ }
209
+ });
210
+
211
+ test('falls back to global jQuery when jquery is true and $ is absent', () => {
212
+ const jQuery = Object.assign(jest.fn(), { ajaxPrefilter: jest.fn() });
213
+ (globalThis as { jQuery?: unknown }).jQuery = jQuery;
214
+
215
+ try {
216
+ configureJourney({ jquery: true });
217
+
218
+ expect(instrumentJqueryMock).toHaveBeenCalledWith(jQuery);
219
+ } finally {
220
+ delete (globalThis as { jQuery?: unknown }).jQuery;
221
+ }
222
+ });
223
+
224
+ test('skips jquery wiring when jquery is false', () => {
225
+ configureJourney({ jquery: false });
226
+
227
+ expect(instrumentJqueryMock).not.toHaveBeenCalled();
228
+ });
229
+
161
230
  test('exposes App.Journey when exposeAppJourney is true', () => {
162
231
  configureJourney({ exposeAppJourney: true });
163
232
 
@@ -172,21 +241,64 @@ describe('[journey] configureJourney / getRequestTimeouts', () => {
172
241
  });
173
242
 
174
243
  test('skips transport helpers when omitted', () => {
175
- configureJourney({ requestTimeouts: { defaultMs: 1_000, endpoints: [] } });
244
+ configureJourney({ slowRequests: { defaultMs: 1_000, endpoints: [] } });
176
245
 
177
246
  expect(instrumentAxiosMock).not.toHaveBeenCalled();
178
247
  expect(instrumentFetchMock).not.toHaveBeenCalled();
179
248
  expect(instrumentJqueryMock).not.toHaveBeenCalled();
180
249
  expect(exposeAppJourneyMock).not.toHaveBeenCalled();
181
250
  });
251
+
252
+ test('never throws on missing config', () => {
253
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
254
+
255
+ expect(() => configureJourney(null as never)).not.toThrow();
256
+ expect(() => configureJourney(undefined as never)).not.toThrow();
257
+ expect(warn).toHaveBeenCalledWith(
258
+ expect.stringContaining('[journey] configureJourney: skipped — missing')
259
+ );
260
+ });
261
+
262
+ test('skips when journey is disabled', () => {
263
+ setJourneyDefaultEnabled(false);
264
+ const warn = jest.spyOn(console, 'warn').mockImplementation(() => {});
265
+
266
+ configureJourney({ axios: axios as never, fetch: true });
267
+
268
+ expect(warn).toHaveBeenCalledWith(
269
+ expect.stringContaining('[journey] configureJourney: skipped — disabled')
270
+ );
271
+ expect(instrumentAxiosMock).not.toHaveBeenCalled();
272
+ expect(instrumentFetchMock).not.toHaveBeenCalled();
273
+ });
274
+
275
+ test('never throws when a transport step fails', () => {
276
+ const error = jest.spyOn(console, 'error').mockImplementation(() => {});
277
+ instrumentAxiosMock.mockImplementationOnce(() => {
278
+ throw new Error('axios boom');
279
+ });
280
+
281
+ expect(() =>
282
+ configureJourney({
283
+ axios: axios as never,
284
+ jquery,
285
+ exposeAppJourney: true,
286
+ })
287
+ ).not.toThrow();
288
+
289
+ expect(error).toHaveBeenCalledWith(
290
+ '[journey] configureJourney failed',
291
+ expect.any(Error)
292
+ );
293
+ });
182
294
  });
183
295
  });
184
296
 
185
297
  describe('longestEndpointMatch', () => {
186
298
  // Lookup expects longest-match-first (configureJourney / startJourney sort for you).
187
299
  const endpoints = [
188
- { match: '/api/invoices', timeoutMs: 2_000 },
189
- { match: '/api', timeoutMs: 1_000 },
300
+ { match: '/api/invoices', slowRequestMs: 2_000 },
301
+ { match: '/api', slowRequestMs: 1_000 },
190
302
  { match: '/other', ignore: true },
191
303
  ];
192
304
 
@@ -196,8 +308,8 @@ describe('longestEndpointMatch', () => {
196
308
  });
197
309
 
198
310
  test('picks the longest matching prefix (first hit when sorted)', () => {
199
- expect(longestEndpointMatch(endpoints, '/api/invoices/42')?.timeoutMs).toBe(2_000);
200
- expect(longestEndpointMatch(endpoints, '/api/jobs')?.timeoutMs).toBe(1_000);
311
+ expect(longestEndpointMatch(endpoints, '/api/invoices/42')?.slowRequestMs).toBe(2_000);
312
+ expect(longestEndpointMatch(endpoints, '/api/jobs')?.slowRequestMs).toBe(1_000);
201
313
  });
202
314
 
203
315
  test('returns undefined when nothing matches', () => {
@@ -205,40 +317,44 @@ describe('longestEndpointMatch', () => {
205
317
  });
206
318
 
207
319
  test('requires a path boundary after the prefix', () => {
208
- const user = [{ match: '/api/user', timeoutMs: 1_000 }];
209
- expect(longestEndpointMatch(user, '/api/user')?.timeoutMs).toBe(1_000);
210
- expect(longestEndpointMatch(user, '/api/user/42')?.timeoutMs).toBe(1_000);
320
+ const user = [{ match: '/api/user', slowRequestMs: 1_000 }];
321
+ expect(longestEndpointMatch(user, '/api/user')?.slowRequestMs).toBe(1_000);
322
+ expect(longestEndpointMatch(user, '/api/user/42')?.slowRequestMs).toBe(1_000);
211
323
  expect(longestEndpointMatch(user, '/api/user-preferences')).toBeUndefined();
212
324
  expect(longestEndpointMatch(user, '/api/users')).toBeUndefined();
213
325
 
214
- const trailed = [{ match: '/api/user/', timeoutMs: 2_000 }];
215
- expect(longestEndpointMatch(trailed, '/api/user/42')?.timeoutMs).toBe(2_000);
326
+ const trailed = [{ match: '/api/user/', slowRequestMs: 2_000 }];
327
+ expect(longestEndpointMatch(trailed, '/api/user/42')?.slowRequestMs).toBe(2_000);
216
328
  });
217
329
  });
218
330
 
219
331
  describe('configureJourney endpoint ordering', () => {
332
+ beforeEach(() => {
333
+ setJourneyDefaultEnabled(true);
334
+ });
335
+
220
336
  afterEach(() => {
337
+ setJourneyDefaultEnabled(false);
338
+ localStorage.clear();
339
+ resetSharedPolicies();
221
340
  resetJourney({
222
- sinks: [sendToDatadog],
223
- requestTimeouts: { defaultMs: 4_000, endpoints: [] },
341
+ sinks: [sendToDatadog, sendToConsole],
342
+ slowRequests: { defaultMs: 4_000, endpoints: [] },
224
343
  });
225
344
  });
226
345
 
227
346
  test('stores endpoints longest-match-first', () => {
228
347
  configureJourney({
229
- requestTimeouts: {
348
+ slowRequests: {
230
349
  defaultMs: 4_000,
231
350
  endpoints: [
232
- { match: '/api', timeoutMs: 1_000 },
233
- { match: '/api/invoices', timeoutMs: 2_000 },
351
+ { match: '/api', slowRequestMs: 1_000 },
352
+ { match: '/api/invoices', slowRequestMs: 2_000 },
234
353
  ],
235
354
  },
236
355
  });
237
356
 
238
- expect(getRequestTimeouts().endpoints?.map(e => e.match)).toEqual([
239
- '/api/invoices',
240
- '/api',
241
- ]);
357
+ expect(getSlowRequests().endpoints?.map(e => e.match)).toEqual(['/api/invoices', '/api']);
242
358
  });
243
359
  });
244
360
 
@@ -248,7 +364,7 @@ describe('[journey] sortEndpointsByLongestMatch', () => {
248
364
  });
249
365
 
250
366
  test('returns a copy for a single endpoint', () => {
251
- const only = [{ match: '/api', timeoutMs: 1_000 }];
367
+ const only = [{ match: '/api', slowRequestMs: 1_000 }];
252
368
  const sorted = sortEndpointsByLongestMatch(only);
253
369
 
254
370
  expect(sorted).toEqual(only);
@@ -1,27 +1,33 @@
1
1
  /** @jest-environment jsdom */
2
- import { baseJourneyDef, useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { activeJourneyStep, completeJourney, journeyStep, startJourney } from '../core';
2
+ import { baseJourneyDef, useJourneyTestRuntime } from '../../__test-utils__/test-runtime';
3
+ import {
4
+ singleInFlightJourneyStep,
5
+ completeJourney,
6
+ journeyStep,
7
+ JourneyStepStamp,
8
+ } from '../../core';
9
+ import { moduleRuntime } from '../../core/runtime';
4
10
 
5
11
  const emitMock = jest.fn();
6
12
  const baseDef = { ...baseJourneyDef, name: 'ambient-a' };
7
13
  const otherDef = { ...baseJourneyDef, name: 'ambient-b' };
8
14
 
9
- describe('[journey] activeJourneyStep', () => {
15
+ describe('[journey] singleInFlightJourneyStep', () => {
10
16
  useJourneyTestRuntime(emitMock);
11
17
 
12
18
  describe('around a single step', () => {
13
19
  beforeEach(() => {
14
- startJourney(baseDef);
20
+ moduleRuntime.startJourney(baseDef);
15
21
  });
16
22
 
17
23
  test('is set only while the step is running', async () => {
18
- expect(activeJourneyStep()).toBeNull();
24
+ expect(singleInFlightJourneyStep()).toBeNull();
19
25
 
20
26
  await journeyStep(baseDef.name, 'do-work', () => {
21
- expect(activeJourneyStep()).not.toBeNull();
27
+ expect(singleInFlightJourneyStep()).toBeInstanceOf(JourneyStepStamp);
22
28
  });
23
29
 
24
- expect(activeJourneyStep()).toBeNull();
30
+ expect(singleInFlightJourneyStep()).toBeNull();
25
31
  completeJourney(baseDef.name);
26
32
  });
27
33
  });
@@ -31,8 +37,8 @@ describe('[journey] activeJourneyStep', () => {
31
37
  let stepA: Promise<unknown>;
32
38
 
33
39
  beforeEach(() => {
34
- startJourney(baseDef);
35
- startJourney(otherDef);
40
+ moduleRuntime.startJourney(baseDef);
41
+ moduleRuntime.startJourney(otherDef);
36
42
 
37
43
  const gateA = new Promise<void>(r => {
38
44
  releaseA = r;
@@ -49,7 +55,7 @@ describe('[journey] activeJourneyStep', () => {
49
55
  completeJourney(otherDef.name);
50
56
  });
51
57
 
52
- const subject = () => journeyStep(otherDef.name, 'book', () => activeJourneyStep());
58
+ const subject = () => journeyStep(otherDef.name, 'book', () => singleInFlightJourneyStep());
53
59
 
54
60
  test('returns null', async () => {
55
61
  await expect(subject()).resolves.toBeNull();
@@ -59,11 +65,11 @@ describe('[journey] activeJourneyStep', () => {
59
65
  describe('when the first-finishing sibling ends first', () => {
60
66
  let releaseB!: () => void;
61
67
  let stepB: Promise<unknown>;
62
- let ambientWhileBAlone: ReturnType<typeof activeJourneyStep> | 'unset';
68
+ let ambientWhileBAlone: ReturnType<typeof singleInFlightJourneyStep> | 'unset';
63
69
 
64
70
  beforeEach(() => {
65
- startJourney(baseDef);
66
- startJourney(otherDef);
71
+ moduleRuntime.startJourney(baseDef);
72
+ moduleRuntime.startJourney(otherDef);
67
73
  ambientWhileBAlone = 'unset';
68
74
 
69
75
  const gateB = new Promise<void>(r => {
@@ -71,7 +77,7 @@ describe('[journey] activeJourneyStep', () => {
71
77
  });
72
78
  stepB = journeyStep(otherDef.name, 'book', async () => {
73
79
  await gateB;
74
- ambientWhileBAlone = activeJourneyStep();
80
+ ambientWhileBAlone = singleInFlightJourneyStep();
75
81
  });
76
82
  });
77
83
 
@@ -84,7 +90,7 @@ describe('[journey] activeJourneyStep', () => {
84
90
 
85
91
  test('keeps ambient on the remaining step', async () => {
86
92
  await journeyStep(baseDef.name, 'load', () => {
87
- expect(activeJourneyStep()).toBeNull();
93
+ expect(singleInFlightJourneyStep()).toBeNull();
88
94
  });
89
95
 
90
96
  releaseB();
@@ -95,7 +101,8 @@ describe('[journey] activeJourneyStep', () => {
95
101
  if (ambientWhileBAlone === 'unset' || ambientWhileBAlone == null) {
96
102
  throw new Error('expected ambient step');
97
103
  }
98
- expect(ambientWhileBAlone.name).toBe('book');
104
+ expect(ambientWhileBAlone).toBeInstanceOf(JourneyStepStamp);
105
+ expect(ambientWhileBAlone.step.name).toBe('book');
99
106
  });
100
107
  });
101
108
  });
@@ -1,26 +1,29 @@
1
1
  /** @jest-environment jsdom */
2
- import { baseJourneyDef, lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
2
+ import {
3
+ baseJourneyDef,
4
+ lastJourney,
5
+ useJourneyTestRuntime,
6
+ } from '../../__test-utils__/test-runtime';
3
7
  import {
4
8
  completeJourney,
5
9
  excludeJourney,
6
10
  failJourney,
7
- firstOpenJourney,
8
11
  journeyStep,
9
12
  setJourneyTags,
10
- startJourney,
11
- } from '../core';
13
+ } from '../../core';
14
+ import { moduleRuntime } from '../../core/runtime';
12
15
 
13
16
  const emitMock = jest.fn();
14
- const baseDef = { ...baseJourneyDef, name: 'registry-api' };
15
- const otherDef = { ...baseJourneyDef, name: 'registry-other' };
17
+ const baseDef = { ...baseJourneyDef, name: 'api-lifecycle' };
18
+ const otherDef = { ...baseJourneyDef, name: 'api-other' };
16
19
 
17
- describe('[journey] registry', () => {
20
+ describe('[journey] api', () => {
18
21
  useJourneyTestRuntime(emitMock);
19
22
 
20
- describe('firstOpenJourney', () => {
23
+ describe('firstOpenJourneyName', () => {
21
24
  const names = [baseDef.name, otherDef.name];
22
25
 
23
- const subject = () => firstOpenJourney(names);
26
+ const subject = () => moduleRuntime.firstOpenJourneyName(names);
24
27
 
25
28
  describe('when none are open', () => {
26
29
  test('returns null', () => {
@@ -30,7 +33,7 @@ describe('[journey] registry', () => {
30
33
 
31
34
  describe('when only the second name is open', () => {
32
35
  beforeEach(() => {
33
- startJourney(otherDef);
36
+ moduleRuntime.startJourney(otherDef);
34
37
  });
35
38
 
36
39
  test('returns that name', () => {
@@ -40,13 +43,15 @@ describe('[journey] registry', () => {
40
43
 
41
44
  describe('when both are open', () => {
42
45
  beforeEach(() => {
43
- startJourney(otherDef);
44
- startJourney(baseDef);
46
+ moduleRuntime.startJourney(otherDef);
47
+ moduleRuntime.startJourney(baseDef);
45
48
  });
46
49
 
47
50
  test('returns the first match in order', () => {
48
51
  expect(subject()).toBe(baseDef.name);
49
- expect(firstOpenJourney([otherDef.name, baseDef.name])).toBe(otherDef.name);
52
+ expect(moduleRuntime.firstOpenJourneyName([otherDef.name, baseDef.name])).toBe(
53
+ otherDef.name
54
+ );
50
55
  });
51
56
  });
52
57
  });
@@ -54,7 +59,7 @@ describe('[journey] registry', () => {
54
59
  describe('completeJourney', () => {
55
60
  describe('with tags', () => {
56
61
  beforeEach(() => {
57
- startJourney(baseDef);
62
+ moduleRuntime.startJourney(baseDef);
58
63
  });
59
64
 
60
65
  const subject = () => completeJourney(baseDef.name, { 'lead.id': 7 });
@@ -78,9 +83,9 @@ describe('[journey] registry', () => {
78
83
 
79
84
  describe('when already closed', () => {
80
85
  beforeEach(async () => {
81
- startJourney(baseDef);
86
+ moduleRuntime.startJourney(baseDef);
82
87
  await journeyStep(baseDef.name, 'do-work', step => {
83
- step.complete();
88
+ step.completeJourney();
84
89
  });
85
90
  });
86
91
 
@@ -95,9 +100,9 @@ describe('[journey] registry', () => {
95
100
 
96
101
  describe('after a fail', () => {
97
102
  beforeEach(async () => {
98
- startJourney(baseDef);
103
+ moduleRuntime.startJourney(baseDef);
99
104
  await journeyStep(baseDef.name, 'do-work', step => {
100
- step.fail('validation');
105
+ step.failJourney('validation');
101
106
  });
102
107
  });
103
108
 
@@ -115,10 +120,10 @@ describe('[journey] registry', () => {
115
120
  describe('startJourney', () => {
116
121
  describe('when restarting the same name', () => {
117
122
  beforeEach(() => {
118
- startJourney(baseDef);
123
+ moduleRuntime.startJourney(baseDef);
119
124
  });
120
125
 
121
- const subject = () => startJourney(baseDef);
126
+ const subject = () => moduleRuntime.startJourney(baseDef);
122
127
 
123
128
  test('excludes the prior open journey', () => {
124
129
  subject();
@@ -129,13 +134,13 @@ describe('[journey] registry', () => {
129
134
 
130
135
  describe('when restarting after a bad close', () => {
131
136
  beforeEach(async () => {
132
- startJourney(baseDef);
137
+ moduleRuntime.startJourney(baseDef);
133
138
  await journeyStep(baseDef.name, 'do-work', step => {
134
- step.fail('validation');
139
+ step.failJourney('validation');
135
140
  });
136
141
  });
137
142
 
138
- const subject = () => startJourney(baseDef);
143
+ const subject = () => moduleRuntime.startJourney(baseDef);
139
144
 
140
145
  test('opens fresh without a second exclude', () => {
141
146
  subject();
@@ -148,7 +153,7 @@ describe('[journey] registry', () => {
148
153
 
149
154
  describe('with static def tags', () => {
150
155
  const subject = () => {
151
- startJourney({ ...baseDef, tags: { plan: 'pro' } });
156
+ moduleRuntime.startJourney({ ...baseDef, tags: { plan: 'pro' } });
152
157
  completeJourney(baseDef.name, { 'job.id': 5 });
153
158
  };
154
159
 
@@ -162,7 +167,7 @@ describe('[journey] registry', () => {
162
167
 
163
168
  describe('failJourney', () => {
164
169
  beforeEach(() => {
165
- startJourney(baseDef);
170
+ moduleRuntime.startJourney(baseDef);
166
171
  });
167
172
 
168
173
  const subject = () => failJourney(baseDef.name, 'manual-bad');
@@ -188,7 +193,7 @@ describe('[journey] registry', () => {
188
193
  describe('excludeJourney', () => {
189
194
  describe('with a clean journey', () => {
190
195
  beforeEach(() => {
191
- startJourney(otherDef);
196
+ moduleRuntime.startJourney(otherDef);
192
197
  });
193
198
 
194
199
  const subject = () => excludeJourney(otherDef.name);
@@ -202,9 +207,9 @@ describe('[journey] registry', () => {
202
207
 
203
208
  describe('after a breach', () => {
204
209
  beforeEach(async () => {
205
- startJourney(baseDef);
210
+ moduleRuntime.startJourney(baseDef);
206
211
  await journeyStep(baseDef.name, 'do-work', step => {
207
- step.fail('validation');
212
+ step.failJourney('validation');
208
213
  });
209
214
  emitMock.mockClear();
210
215
  });
@@ -221,7 +226,7 @@ describe('[journey] registry', () => {
221
226
  describe('when past the journey budget', () => {
222
227
  beforeEach(() => {
223
228
  jest.spyOn(globalThis.performance, 'now').mockReturnValue(1_000);
224
- startJourney({ ...baseDef, timeoutMs: 50 });
229
+ moduleRuntime.startJourney({ ...baseDef, timeoutMs: 50 });
225
230
  (globalThis.performance.now as jest.Mock).mockReturnValue(1_100);
226
231
  });
227
232
 
@@ -251,7 +256,7 @@ describe('[journey] registry', () => {
251
256
 
252
257
  describe('with a closed journey', () => {
253
258
  beforeEach(() => {
254
- startJourney(baseDef);
259
+ moduleRuntime.startJourney(baseDef);
255
260
  completeJourney(baseDef.name);
256
261
  });
257
262