@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,365 +0,0 @@
1
- /** @jest-environment jsdom */
2
- import { useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { completeJourney, JourneyStepTag, journeyStep, startJourney } from '../core';
4
- import { instrumentAxios, type AxiosLikeInstance } from '../integrations/axios';
5
-
6
- const emitMock = jest.fn();
7
-
8
- type RequestHandler = (config: any) => any;
9
- type ResponseOk = (response: any) => any;
10
- type ResponseErr = (error: any) => any;
11
-
12
- function createAxiosMock(): AxiosLikeInstance & {
13
- requestHandlers: RequestHandler[];
14
- responseOkHandlers: ResponseOk[];
15
- responseErrHandlers: ResponseErr[];
16
- } {
17
- const request: RequestHandler[] = [];
18
- const responseOk: ResponseOk[] = [];
19
- const responseErr: ResponseErr[] = [];
20
-
21
- return {
22
- defaults: { baseURL: 'https://app.example.com' },
23
- interceptors: {
24
- request: {
25
- use(onFulfilled) {
26
- request.push(onFulfilled);
27
- return 0;
28
- },
29
- },
30
- response: {
31
- use(onFulfilled, onRejected) {
32
- responseOk.push(onFulfilled);
33
- if (onRejected) {
34
- responseErr.push(onRejected);
35
- }
36
- return 0;
37
- },
38
- },
39
- },
40
- requestHandlers: request,
41
- responseOkHandlers: responseOk,
42
- responseErrHandlers: responseErr,
43
- };
44
- }
45
-
46
- describe('[journey] instrumentAxios', () => {
47
- const def = {
48
- name: 'axios-journey',
49
- team: 'test',
50
- group: 'test',
51
- service: 'test',
52
- timeoutMs: 0,
53
- requestTimeoutMs: 5_000,
54
- };
55
-
56
- useJourneyTestRuntime(emitMock);
57
-
58
- beforeEach(() => {
59
- jest.spyOn(globalThis.performance, 'now').mockReturnValue(1_000);
60
- });
61
-
62
- afterEach(() => {
63
- jest.restoreAllMocks();
64
- });
65
-
66
- test('is idempotent for the same axios instance', () => {
67
- const axios = createAxiosMock();
68
- instrumentAxios(axios);
69
- instrumentAxios(axios);
70
-
71
- expect(axios.requestHandlers).toHaveLength(1);
72
- expect(axios.responseOkHandlers).toHaveLength(1);
73
- expect(axios.responseErrHandlers).toHaveLength(1);
74
- });
75
-
76
- test('reports a successful response against the ambient step', async () => {
77
- const axios = createAxiosMock();
78
- instrumentAxios(axios);
79
-
80
- startJourney(def);
81
- await journeyStep(def.name, 'book-job', () => {
82
- const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
83
- expect(config.journeyStep).toBeInstanceOf(JourneyStepTag);
84
- expect(config.journeyStart).toBe(1_000);
85
-
86
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_050);
87
- const response = axios.responseOkHandlers[0]({ status: 200, config });
88
- expect(response.status).toBe(200);
89
- });
90
- completeJourney(def.name);
91
-
92
- expect(emitMock).toHaveBeenCalledTimes(1);
93
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
94
- });
95
-
96
- test('marks journey bad on 5xx via the rejection interceptor', async () => {
97
- const axios = createAxiosMock();
98
- instrumentAxios(axios);
99
-
100
- startJourney(def);
101
- await expect(
102
- journeyStep(def.name, 'book-job', async () => {
103
- const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
104
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
105
- await axios.responseErrHandlers[0]({
106
- config,
107
- response: { status: 500 },
108
- });
109
- })
110
- ).rejects.toMatchObject({ response: { status: 500 } });
111
-
112
- expect(emitMock).toHaveBeenCalledTimes(1);
113
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
114
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
115
- });
116
-
117
- test('honors an explicit journeyStep from step.tag()', async () => {
118
- const axios = createAxiosMock();
119
- instrumentAxios(axios);
120
-
121
- startJourney(def);
122
- await journeyStep(def.name, 'book-job', async step => {
123
- const tagged = step.tag() as { journeyStep: unknown };
124
- const config = axios.requestHandlers[0]({
125
- url: '/CallScreen/BookJob',
126
- journeyStep: tagged.journeyStep,
127
- });
128
- expect(config.journeyStep).toBe(tagged.journeyStep);
129
-
130
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
131
- await expect(
132
- axios.responseErrHandlers[0]({
133
- config,
134
- response: { status: 503 },
135
- })
136
- ).rejects.toBeDefined();
137
- });
138
-
139
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
140
- });
141
-
142
- test('soft ambient: overlapping steps do not auto-stamp HTTP', async () => {
143
- const other = { ...def, name: 'axios-journey-other' };
144
- const axios = createAxiosMock();
145
- instrumentAxios(axios);
146
-
147
- startJourney(def);
148
- startJourney(other);
149
-
150
- let releaseA!: () => void;
151
- const gateA = new Promise<void>(r => {
152
- releaseA = r;
153
- });
154
-
155
- const stepA = journeyStep(def.name, 'load', async () => {
156
- await gateA;
157
- });
158
-
159
- await journeyStep(other.name, 'book', async () => {
160
- const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
161
- expect(config.journeyStep).toBeUndefined();
162
-
163
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
164
- await expect(
165
- axios.responseErrHandlers[0]({
166
- config,
167
- response: { status: 500 },
168
- })
169
- ).rejects.toMatchObject({ response: { status: 500 } });
170
- });
171
-
172
- releaseA();
173
- await stepA;
174
-
175
- expect(emitMock).not.toHaveBeenCalled();
176
- completeJourney(def.name);
177
- completeJourney(other.name);
178
- expect(emitMock).toHaveBeenCalledTimes(2);
179
- expect(emitMock.mock.calls.every(c => c[0].journey.outcome === 'good')).toBe(true);
180
- });
181
-
182
- test('soft ambient: step.tag() still attributes during overlap', async () => {
183
- const other = { ...def, name: 'axios-journey-tagged' };
184
- const axios = createAxiosMock();
185
- instrumentAxios(axios);
186
-
187
- startJourney(def);
188
- startJourney(other);
189
-
190
- let releaseA!: () => void;
191
- const gateA = new Promise<void>(r => {
192
- releaseA = r;
193
- });
194
-
195
- const stepA = journeyStep(def.name, 'load', async () => {
196
- await gateA;
197
- });
198
-
199
- await expect(
200
- journeyStep(other.name, 'book', async step => {
201
- const tagged = step.tag() as { journeyStep: unknown };
202
- const config = axios.requestHandlers[0]({
203
- url: '/CallScreen/BookJob',
204
- journeyStep: tagged.journeyStep,
205
- });
206
- expect(config.journeyStep).toBe(tagged.journeyStep);
207
-
208
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
209
- await axios.responseErrHandlers[0]({
210
- config,
211
- response: { status: 500 },
212
- });
213
- })
214
- ).rejects.toMatchObject({ response: { status: 500 } });
215
-
216
- releaseA();
217
- await stepA;
218
-
219
- expect(emitMock).toHaveBeenCalledTimes(1);
220
- expect(emitMock.mock.calls[0][0].journey.name).toBe(other.name);
221
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
222
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
223
-
224
- completeJourney(def.name);
225
- });
226
-
227
- test('ignores responses with no journey step on the config', () => {
228
- const axios = createAxiosMock();
229
- instrumentAxios(axios);
230
-
231
- // Outside a step — request interceptor finds no ambient step.
232
- const config = axios.requestHandlers[0]({ url: '/orphan' });
233
- expect(config.journeyStep).toBeUndefined();
234
- axios.responseOkHandlers[0]({ status: 500, config });
235
-
236
- expect(emitMock).not.toHaveBeenCalled();
237
- });
238
-
239
- test('non-axios errors without config are rethrown without reporting', async () => {
240
- const axios = createAxiosMock();
241
- instrumentAxios(axios);
242
-
243
- await expect(axios.responseErrHandlers[0](new Error('boom'))).rejects.toThrow('boom');
244
- expect(emitMock).not.toHaveBeenCalled();
245
- });
246
-
247
- test('falls back to performance.now when journeyStart is missing', async () => {
248
- const axios = createAxiosMock();
249
- instrumentAxios(axios);
250
-
251
- startJourney(def);
252
- await journeyStep(def.name, 'book-job', step => {
253
- const tagged = step.tag() as { journeyStep: unknown };
254
- const config = {
255
- url: '/CallScreen/BookJob',
256
- journeyStep: tagged.journeyStep,
257
- };
258
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_200);
259
- axios.responseOkHandlers[0]({ status: 200, config });
260
- });
261
- completeJourney(def.name);
262
-
263
- expect(emitMock).toHaveBeenCalledTimes(1);
264
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
265
- });
266
-
267
- test('marks request-latency on a slow successful response', async () => {
268
- const axios = createAxiosMock();
269
- instrumentAxios(axios);
270
-
271
- startJourney({ ...def, requestTimeoutMs: 50 });
272
- await journeyStep(def.name, 'book-job', () => {
273
- const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
274
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_200);
275
- axios.responseOkHandlers[0]({ status: 200, config });
276
- });
277
-
278
- expect(emitMock).toHaveBeenCalledTimes(1);
279
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
280
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-latency');
281
- });
282
-
283
- test('does not mark bad on 4xx via the rejection interceptor', async () => {
284
- const axios = createAxiosMock();
285
- instrumentAxios(axios);
286
-
287
- startJourney(def);
288
- await journeyStep(def.name, 'book-job', async () => {
289
- const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
290
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
291
- await expect(
292
- axios.responseErrHandlers[0]({
293
- config,
294
- response: { status: 400 },
295
- })
296
- ).rejects.toMatchObject({ response: { status: 400 } });
297
- });
298
- completeJourney(def.name);
299
-
300
- expect(emitMock).toHaveBeenCalledTimes(1);
301
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
302
- });
303
-
304
- test('network errors without a response status mark request-error', async () => {
305
- const axios = createAxiosMock();
306
- instrumentAxios(axios);
307
-
308
- startJourney(def);
309
- await expect(
310
- journeyStep(def.name, 'book-job', async () => {
311
- const config = axios.requestHandlers[0]({ url: '/CallScreen/BookJob' });
312
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
313
- await axios.responseErrHandlers[0]({ config });
314
- })
315
- ).rejects.toBeDefined();
316
-
317
- expect(emitMock).toHaveBeenCalledTimes(1);
318
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
319
- });
320
-
321
- test('ok interceptor tolerates a missing response config', () => {
322
- const axios = createAxiosMock();
323
- instrumentAxios(axios);
324
-
325
- expect(() => axios.responseOkHandlers[0]({ status: 200 })).not.toThrow();
326
- expect(emitMock).not.toHaveBeenCalled();
327
- });
328
-
329
- test('rejection interceptor tolerates a missing error config', async () => {
330
- const axios = createAxiosMock();
331
- instrumentAxios(axios);
332
-
333
- await expect(axios.responseErrHandlers[0]({ response: { status: 500 } })).rejects.toEqual({
334
- response: { status: 500 },
335
- });
336
- expect(emitMock).not.toHaveBeenCalled();
337
- });
338
-
339
- test('uses config.baseURL when building the request key', async () => {
340
- const axios = createAxiosMock();
341
- instrumentAxios(axios);
342
-
343
- startJourney({
344
- ...def,
345
- endpoints: [{ match: 'api.example.com/invoices', ignore: true }],
346
- });
347
- await journeyStep(def.name, 'book-job', async () => {
348
- const config = axios.requestHandlers[0]({
349
- url: '/invoices',
350
- baseURL: 'https://api.example.com',
351
- });
352
- (globalThis.performance.now as jest.Mock).mockReturnValue(1_020);
353
- await expect(
354
- axios.responseErrHandlers[0]({
355
- config,
356
- response: { status: 500 },
357
- })
358
- ).rejects.toBeDefined();
359
- });
360
- completeJourney(def.name);
361
-
362
- expect(emitMock).toHaveBeenCalledTimes(1);
363
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
364
- });
365
- });
@@ -1,327 +0,0 @@
1
- /** @jest-environment jsdom */
2
- import { useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { completeJourney, journeyStep, startJourney } from '../core';
4
- import { instrumentJquery } from '../integrations/jquery/instrument';
5
-
6
- const emitMock = jest.fn();
7
-
8
- function createJqueryMock() {
9
- const prefilters: ((o: any, orig: any, xhr: any) => void)[] = [];
10
- const completes: ((event: unknown, xhr: any, settings: any) => void)[] = [];
11
-
12
- const $ = ((_selector: Document) => ({
13
- ajaxComplete(handler: (event: unknown, xhr: any, settings: any) => void) {
14
- completes.push(handler);
15
- },
16
- })) as any;
17
-
18
- $.ajaxPrefilter = (handler: (o: any, orig: any, xhr: any) => void) => {
19
- prefilters.push(handler);
20
- };
21
- $.__prefilters = prefilters;
22
- $.__completes = completes;
23
- return $;
24
- }
25
-
26
- function simulateAjax($: any, url: string, status: number, durationMs = 10) {
27
- const xhr = { status };
28
- const settings = { url };
29
- const start = globalThis.performance.now();
30
-
31
- for (const prefilter of $.__prefilters) {
32
- prefilter(settings, settings, xhr);
33
- }
34
-
35
- (globalThis.performance.now as jest.Mock).mockReturnValue(start + durationMs);
36
-
37
- for (const complete of $.__completes) {
38
- complete({}, xhr, settings);
39
- }
40
- }
41
-
42
- describe('[journey] instrumentJquery', () => {
43
- useJourneyTestRuntime(emitMock);
44
-
45
- beforeEach(() => {
46
- jest.spyOn(globalThis.performance, 'now').mockReturnValue(1_000);
47
- });
48
-
49
- afterEach(() => {
50
- jest.restoreAllMocks();
51
- });
52
-
53
- test('is idempotent for the same $ instance', () => {
54
- const $ = createJqueryMock();
55
- instrumentJquery($);
56
- instrumentJquery($);
57
-
58
- expect($.__prefilters).toHaveLength(1);
59
- expect($.__completes).toHaveLength(1);
60
- });
61
-
62
- test('ignores ajax outside an active journey step', () => {
63
- const $ = createJqueryMock();
64
- instrumentJquery($);
65
-
66
- startJourney({
67
- name: 'jq-idle',
68
- team: 'test',
69
- group: 'test',
70
- service: 'test',
71
- timeoutMs: 0,
72
- requestTimeoutMs: 50,
73
- });
74
-
75
- simulateAjax($, '/CallScreen/BookJob', 500);
76
- completeJourney('jq-idle');
77
-
78
- expect(emitMock).toHaveBeenCalledTimes(1);
79
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
80
- });
81
-
82
- test('marks journey bad on 5xx inside a journey step', async () => {
83
- const $ = createJqueryMock();
84
- instrumentJquery($);
85
-
86
- startJourney({
87
- name: 'jq-5xx',
88
- team: 'test',
89
- group: 'test',
90
- service: 'test',
91
- timeoutMs: 0,
92
- requestTimeoutMs: 5_000,
93
- });
94
-
95
- await journeyStep('jq-5xx', 'book-job', () => {
96
- simulateAjax($, '/CallScreen/BookJob', 500, 20);
97
- });
98
-
99
- expect(emitMock).toHaveBeenCalledTimes(1);
100
- const event = emitMock.mock.calls[0][0].journey;
101
- expect(event.outcome).toBe('bad');
102
- expect(event.reason).toBe('request-error');
103
- });
104
-
105
- test('marks journey bad on slow request inside a journey step', async () => {
106
- const $ = createJqueryMock();
107
- instrumentJquery($);
108
-
109
- startJourney({
110
- name: 'jq-slow',
111
- team: 'test',
112
- group: 'test',
113
- service: 'test',
114
- timeoutMs: 0,
115
- requestTimeoutMs: 50,
116
- });
117
-
118
- await journeyStep('jq-slow', 'book-job', () => {
119
- simulateAjax($, '/CallScreen/BookJob', 200, 200);
120
- });
121
-
122
- expect(emitMock).toHaveBeenCalledTimes(1);
123
- const event = emitMock.mock.calls[0][0].journey;
124
- expect(event.outcome).toBe('bad');
125
- expect(event.reason).toBe('request-latency');
126
- });
127
-
128
- test('treats status 0 as a network/request error', async () => {
129
- const $ = createJqueryMock();
130
- instrumentJquery($);
131
-
132
- startJourney({
133
- name: 'jq-network',
134
- team: 'test',
135
- group: 'test',
136
- service: 'test',
137
- timeoutMs: 0,
138
- requestTimeoutMs: 5_000,
139
- });
140
-
141
- await journeyStep('jq-network', 'book-job', () => {
142
- simulateAjax($, '/CallScreen/BookJob', 0, 10);
143
- });
144
-
145
- expect(emitMock).toHaveBeenCalledTimes(1);
146
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
147
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
148
- });
149
-
150
- test('does not mark bad on 4xx (user/validation)', async () => {
151
- const $ = createJqueryMock();
152
- instrumentJquery($);
153
-
154
- startJourney({
155
- name: 'jq-4xx',
156
- team: 'test',
157
- group: 'test',
158
- service: 'test',
159
- timeoutMs: 0,
160
- requestTimeoutMs: 5_000,
161
- });
162
-
163
- await journeyStep('jq-4xx', 'book-job', () => {
164
- simulateAjax($, '/CallScreen/BookJob', 400, 10);
165
- });
166
- completeJourney('jq-4xx');
167
-
168
- expect(emitMock).toHaveBeenCalledTimes(1);
169
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
170
- });
171
-
172
- test('honors an explicit journeyStep already stamped on the XHR', async () => {
173
- const $ = createJqueryMock();
174
- instrumentJquery($);
175
-
176
- startJourney({
177
- name: 'jq-explicit',
178
- team: 'test',
179
- group: 'test',
180
- service: 'test',
181
- timeoutMs: 0,
182
- requestTimeoutMs: 5_000,
183
- });
184
-
185
- await journeyStep('jq-explicit', 'book-job', step => {
186
- const tagged = step.tag() as { journeyStep: unknown };
187
- const xhr: {
188
- status: number;
189
- journeyStep: unknown;
190
- journeyStart?: number;
191
- } = { status: 500, journeyStep: tagged.journeyStep };
192
- const settings = { url: '/CallScreen/BookJob' };
193
- const start = globalThis.performance.now();
194
- for (const prefilter of $.__prefilters) {
195
- prefilter(settings, settings, xhr);
196
- }
197
- // Prefilter must not clear the explicit step; it should stamp a start time.
198
- expect(xhr.journeyStep).toBe(tagged.journeyStep);
199
- expect(xhr.journeyStart).toBeDefined();
200
- (globalThis.performance.now as jest.Mock).mockReturnValue(start + 20);
201
- for (const complete of $.__completes) {
202
- complete({}, xhr, settings);
203
- }
204
- });
205
-
206
- expect(emitMock).toHaveBeenCalledTimes(1);
207
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
208
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
209
- });
210
-
211
- test('falls back to performance.now when journeyStart is missing', async () => {
212
- const $ = createJqueryMock();
213
- instrumentJquery($);
214
-
215
- startJourney({
216
- name: 'jq-no-start',
217
- team: 'test',
218
- group: 'test',
219
- service: 'test',
220
- timeoutMs: 0,
221
- requestTimeoutMs: 5_000,
222
- });
223
-
224
- await journeyStep('jq-no-start', 'book-job', step => {
225
- const tagged = step.tag() as { journeyStep: unknown };
226
- // Bypass prefilter stamping — only fire ajaxComplete with a step and no start.
227
- const xhr = { status: 200, journeyStep: tagged.journeyStep };
228
- for (const complete of $.__completes) {
229
- complete({}, xhr, { url: '/ok' });
230
- }
231
- });
232
- completeJourney('jq-no-start');
233
-
234
- expect(emitMock).toHaveBeenCalledTimes(1);
235
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
236
- });
237
-
238
- test('honors step.tag() on ajax settings without deep-merge stack overflow', async () => {
239
- const $ = createJqueryMock();
240
- instrumentJquery($);
241
-
242
- // Mimic jQuery.ajaxSetup: deep-merge plain objects only (class instances by ref).
243
- function isPlainObject(obj: unknown): obj is Record<string, unknown> {
244
- return (
245
- !!obj && typeof obj === 'object' && Object.getPrototypeOf(obj) === Object.prototype
246
- );
247
- }
248
- function deepExtend(target: Record<string, unknown>, ...sources: unknown[]) {
249
- for (const source of sources) {
250
- if (!source || typeof source !== 'object') {
251
- continue;
252
- }
253
- for (const key of Object.keys(source as object)) {
254
- // Block prototype-polluting keys (CodeQL js/prototype-pollution-utility).
255
- if (key === '__proto__' || key === 'constructor' || key === 'prototype') {
256
- continue;
257
- }
258
- const copy = (source as Record<string, unknown>)[key];
259
- if (isPlainObject(copy)) {
260
- const dest = isPlainObject(target[key])
261
- ? (target[key] as Record<string, unknown>)
262
- : {};
263
- target[key] = deepExtend(dest, copy);
264
- } else {
265
- target[key] = copy;
266
- }
267
- }
268
- }
269
- return target;
270
- }
271
-
272
- startJourney({
273
- name: 'jq-settings-tag',
274
- team: 'test',
275
- group: 'test',
276
- service: 'test',
277
- timeoutMs: 0,
278
- requestTimeoutMs: 5_000,
279
- });
280
-
281
- await journeyStep('jq-settings-tag', 'book-job', step => {
282
- const tagged = step.tag() as { journeyStep: unknown };
283
- expect(() => deepExtend({}, tagged)).not.toThrow();
284
- const settings = deepExtend({ url: '/CallScreen/BookJob' }, tagged);
285
- const xhr: { status: number; journeyStep?: unknown; journeyStart?: number } = {
286
- status: 500,
287
- };
288
- const start = globalThis.performance.now();
289
- for (const prefilter of $.__prefilters) {
290
- prefilter(settings, settings, xhr);
291
- }
292
- expect(xhr.journeyStep).toBe(tagged.journeyStep);
293
- (globalThis.performance.now as jest.Mock).mockReturnValue(start + 20);
294
- for (const complete of $.__completes) {
295
- complete({}, xhr, settings);
296
- }
297
- });
298
-
299
- expect(emitMock).toHaveBeenCalledTimes(1);
300
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('bad');
301
- expect(emitMock.mock.calls[0][0].journey.reason).toBe('request-error');
302
- });
303
-
304
- test('keys cross-origin ajax urls as host/path', async () => {
305
- const $ = createJqueryMock();
306
- instrumentJquery($);
307
-
308
- startJourney({
309
- name: 'jq-cross',
310
- team: 'test',
311
- group: 'test',
312
- service: 'test',
313
- timeoutMs: 0,
314
- requestTimeoutMs: 5_000,
315
- endpoints: [{ match: 'pdf.vendor.com/render', ignore: true }],
316
- });
317
-
318
- await journeyStep('jq-cross', 'book-job', () => {
319
- simulateAjax($, 'https://pdf.vendor.com/render', 500, 20);
320
- });
321
- completeJourney('jq-cross');
322
-
323
- // Ignored endpoint — 5xx must not close the journey as bad.
324
- expect(emitMock).toHaveBeenCalledTimes(1);
325
- expect(emitMock.mock.calls[0][0].journey.outcome).toBe('good');
326
- });
327
- });