@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,13 +1,11 @@
1
1
  /** @jest-environment jsdom */
2
- import { baseJourneyDef, lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
2
  import {
4
- completeJourney,
5
- failJourney,
6
- journeyMountStep,
7
- journeyStep,
8
- resolveJourneyStep,
9
- startJourney,
10
- } from '../core';
3
+ baseJourneyDef,
4
+ lastJourney,
5
+ useJourneyTestRuntime,
6
+ } from '../../__test-utils__/test-runtime';
7
+ import { completeJourney, failJourney, JourneyStepStamp, journeyStep } from '../../core';
8
+ import { moduleRuntime } from '../../core/runtime';
11
9
 
12
10
  const emitMock = jest.fn();
13
11
  const baseDef = { ...baseJourneyDef, name: 'step-api' };
@@ -18,10 +16,11 @@ describe('[journey] journeyStep', () => {
18
16
 
19
17
  describe('with a candidate list', () => {
20
18
  beforeEach(() => {
21
- startJourney(baseDef);
19
+ moduleRuntime.startJourney(baseDef);
22
20
  });
23
21
 
24
- const subject = () => journeyStep(['missing', baseDef.name], 'do-work', step => step.name);
22
+ const subject = () =>
23
+ journeyStep(['missing', baseDef.name], 'do-work', step => step.journeyName);
25
24
 
26
25
  test('resolves the first open name', async () => {
27
26
  await expect(subject()).resolves.toBe(baseDef.name);
@@ -30,12 +29,12 @@ describe('[journey] journeyStep', () => {
30
29
 
31
30
  describe('with multiple open candidates', () => {
32
31
  beforeEach(() => {
33
- startJourney(otherDef);
34
- startJourney(baseDef);
32
+ moduleRuntime.startJourney(otherDef);
33
+ moduleRuntime.startJourney(baseDef);
35
34
  });
36
35
 
37
36
  const subject = () =>
38
- journeyStep([baseDef.name, otherDef.name], 'do-work', step => step.name);
37
+ journeyStep([baseDef.name, otherDef.name], 'do-work', step => step.journeyName);
39
38
 
40
39
  test('prefers the first name in order', async () => {
41
40
  await expect(subject()).resolves.toBe(baseDef.name);
@@ -45,9 +44,9 @@ describe('[journey] journeyStep', () => {
45
44
  describe('with no matching open journey', () => {
46
45
  const subject = () =>
47
46
  journeyStep(['missing-a', 'missing-b'], 'do-work', step => {
48
- step.complete({ a: 1 });
49
- step.fail('noop');
50
- return step.name;
47
+ step.completeJourney({ a: 1 });
48
+ step.failJourney('noop');
49
+ return step.journeyName;
51
50
  });
52
51
 
53
52
  test('runs untraced', async () => {
@@ -56,25 +55,34 @@ describe('[journey] journeyStep', () => {
56
55
  });
57
56
  });
58
57
 
58
+ describe('with a bare name and nothing open', () => {
59
+ test('does not auto-start', async () => {
60
+ await journeyStep(baseDef.name, 'do-work', step => {
61
+ expect(step.journeyName).toBeNull();
62
+ expect(step.stamp()).toBeUndefined();
63
+ });
64
+ expect(emitMock).not.toHaveBeenCalled();
65
+ });
66
+ });
67
+
59
68
  describe('with a null target', () => {
60
- const subject = () =>
61
- journeyStep(null, 'untraced', step => step.tag() as Record<string, unknown>);
69
+ const subject = () => journeyStep(null, 'untraced', step => step.stamp());
62
70
 
63
- test('returns an empty tag object', async () => {
64
- await expect(subject()).resolves.toEqual({});
71
+ test('returns undefined', async () => {
72
+ await expect(subject()).resolves.toBeUndefined();
65
73
  });
66
74
  });
67
75
 
68
76
  describe('with an open journey', () => {
69
77
  beforeEach(() => {
70
- startJourney(baseDef);
78
+ moduleRuntime.startJourney(baseDef);
71
79
  });
72
80
 
73
81
  describe('when the step completes with tags', () => {
74
82
  const subject = () =>
75
83
  journeyStep(baseDef.name, 'do-work', step => {
76
84
  step.setAttribute('x', 1);
77
- step.complete({ 'payment.id': 42 });
85
+ step.completeJourney({ 'payment.id': 42 });
78
86
  });
79
87
 
80
88
  test('emits good with tags and attributes', async () => {
@@ -105,29 +113,63 @@ describe('[journey] journeyStep', () => {
105
113
  });
106
114
  });
107
115
 
108
- describe('when step.tag is called', () => {
109
- let tagged: Record<string, unknown> | null;
116
+ describe('when setAttribute uses unsafe or overflowing keys', () => {
117
+ test('skips prototype keys and caps the map', async () => {
118
+ await journeyStep(baseDef.name, 'attrs', step => {
119
+ step.setAttribute('__proto__', 'nope');
120
+ step.setAttribute('token', 'secret');
121
+ step.setAttribute('ok', 1);
122
+ for (let i = 0; i < 70; i += 1) {
123
+ step.setAttribute(`k${i}`, i);
124
+ }
125
+ });
126
+ completeJourney(baseDef.name);
127
+
128
+ const attributes = lastJourney(emitMock).steps[0].attributes as Record<
129
+ string,
130
+ unknown
131
+ >;
132
+ expect(Object.prototype.hasOwnProperty.call(attributes, '__proto__')).toBe(false);
133
+ expect(attributes.token).toBeUndefined();
134
+ expect(attributes.ok).toBe(1);
135
+ expect(Object.keys(attributes).length).toBe(64);
136
+ expect(({} as { polluted?: boolean }).polluted).toBeUndefined();
137
+ });
138
+ });
139
+
140
+ describe('when step.stamp is called', () => {
141
+ let tagged: JourneyStepStamp | undefined;
110
142
 
111
143
  beforeEach(() => {
112
- tagged = null;
144
+ tagged = undefined;
113
145
  });
114
146
 
115
147
  const subject = () =>
116
148
  journeyStep(baseDef.name, 'do-work', step => {
117
- tagged = step.tag() as Record<string, unknown>;
149
+ tagged = step.stamp();
118
150
  });
119
151
 
120
- test('binds journeyStep', async () => {
152
+ test('binds stamp', async () => {
121
153
  await subject();
122
154
 
123
- expect(tagged).toHaveProperty('journeyStep');
155
+ expect(tagged).toBeInstanceOf(JourneyStepStamp);
156
+ });
157
+ });
158
+
159
+ describe('when step.stamp is called with a score function', () => {
160
+ test('stores score on the stamp', async () => {
161
+ await journeyStep(baseDef.name, 'do-work', step => {
162
+ const score = () => 'exclude' as const;
163
+ const stamp = step.stamp(score);
164
+ expect(stamp?.score).toBe(score);
165
+ });
124
166
  });
125
167
  });
126
168
 
127
- describe('when step.fail is called', () => {
169
+ describe('when step.failJourney is called', () => {
128
170
  const subject = () =>
129
171
  journeyStep(baseDef.name, 'do-work', step => {
130
- step.fail('empty-catalog');
172
+ step.failJourney('empty-catalog');
131
173
  });
132
174
 
133
175
  test('marks bad without throwing', async () => {
@@ -147,7 +189,7 @@ describe('[journey] journeyStep', () => {
147
189
  test('marks step-error and rethrows', async () => {
148
190
  await expect(subject()).rejects.toThrow('boom');
149
191
  expect(lastJourney(emitMock).reason).toBe('step-error');
150
- expect(lastJourney(emitMock).steps[0].attributes).toEqual({ error: 'boom' });
192
+ expect(lastJourney(emitMock).steps[0].attributes).toBeUndefined();
151
193
  });
152
194
  });
153
195
 
@@ -158,27 +200,22 @@ describe('[journey] journeyStep', () => {
158
200
  throw 'string-boom';
159
201
  });
160
202
 
161
- test('stringifies the error attribute', async () => {
203
+ test('does not stringify the throw onto attributes', async () => {
162
204
  await expect(subject()).rejects.toBe('string-boom');
163
- expect(lastJourney(emitMock).steps[0].attributes).toEqual({
164
- error: 'string-boom',
165
- });
205
+ expect(lastJourney(emitMock).steps[0].attributes).toBeUndefined();
166
206
  });
167
207
  });
168
208
 
169
209
  describe('when error is pre-set on the step', () => {
170
210
  const subject = () =>
171
211
  journeyStep(baseDef.name, 'do-work', step => {
172
- const ref = resolveJourneyStep(
173
- (step.tag() as { journeyStep: unknown }).journeyStep
174
- )!;
175
- ref.attributes.error = 'already';
212
+ step.setAttribute('phase', 'load');
176
213
  throw new Error('boom');
177
214
  });
178
215
 
179
- test('preserves the first error attribute', async () => {
216
+ test('keeps developer attributes', async () => {
180
217
  await expect(subject()).rejects.toThrow('boom');
181
- expect(lastJourney(emitMock).steps[0].attributes).toEqual({ error: 'already' });
218
+ expect(lastJourney(emitMock).steps[0].attributes).toEqual({ phase: 'load' });
182
219
  });
183
220
  });
184
221
 
@@ -186,7 +223,7 @@ describe('[journey] journeyStep', () => {
186
223
  beforeEach(() => {
187
224
  // Parent already opened baseDef; restart with expected steps.
188
225
  emitMock.mockClear();
189
- startJourney({ ...baseDef, steps: ['load', 'save'] });
226
+ moduleRuntime.startJourney({ ...baseDef, expected: ['load', 'save'] });
190
227
  emitMock.mockClear();
191
228
  });
192
229
 
@@ -197,7 +234,7 @@ describe('[journey] journeyStep', () => {
197
234
  expect(emitMock).not.toHaveBeenCalled();
198
235
 
199
236
  await journeyStep(baseDef.name, 'save', step => {
200
- step.complete({ done: true });
237
+ step.completeJourney({ done: true });
201
238
  });
202
239
 
203
240
  expect(emitMock).toHaveBeenCalledTimes(1);
@@ -212,8 +249,8 @@ describe('[journey] journeyStep', () => {
212
249
  describe('with a JourneyDef-like object target', () => {
213
250
  const subject = () =>
214
251
  journeyStep(baseDef, 'via-def', step => {
215
- step.complete();
216
- return step.name;
252
+ step.completeJourney();
253
+ return step.journeyName;
217
254
  });
218
255
 
219
256
  test('resolves the def name', async () => {
@@ -238,13 +275,13 @@ describe('[journey] journeyStep', () => {
238
275
  describe('journeyMountStep', () => {
239
276
  describe('with candidates', () => {
240
277
  beforeEach(() => {
241
- startJourney(baseDef);
278
+ moduleRuntime.startJourney(baseDef);
242
279
  });
243
280
 
244
281
  const subject = () =>
245
- journeyMountStep(['missing', baseDef.name], 'mount-load', step => {
246
- step.complete();
247
- return step.name;
282
+ moduleRuntime.journeyMountStep(['missing', baseDef.name], 'mount-load', step => {
283
+ step.completeJourney();
284
+ return step.journeyName;
248
285
  });
249
286
 
250
287
  test('resolves the open candidate', async () => {
@@ -260,10 +297,10 @@ describe('[journey] journeyStep', () => {
260
297
  });
261
298
 
262
299
  test('defers until after a sync startJourney', async () => {
263
- const mount = journeyMountStep(baseDef.name, 'boot', () => {
300
+ const mount = moduleRuntime.journeyMountStep(baseDef.name, 'boot', () => {
264
301
  order.push('mount-step');
265
302
  });
266
- startJourney(baseDef);
303
+ moduleRuntime.startJourney(baseDef);
267
304
  order.push('started');
268
305
  await mount;
269
306
  completeJourney(baseDef.name);
@@ -276,7 +313,7 @@ describe('[journey] journeyStep', () => {
276
313
  describe('when fail-fast happens mid-step', () => {
277
314
  beforeEach(() => {
278
315
  jest.spyOn(globalThis.performance, 'now').mockReturnValue(1_000);
279
- startJourney(baseDef);
316
+ moduleRuntime.startJourney(baseDef);
280
317
  });
281
318
 
282
319
  afterEach(() => {
@@ -302,4 +339,61 @@ describe('[journey] journeyStep', () => {
302
339
  await stepPromise;
303
340
  });
304
341
  });
342
+
343
+ describe('when the same name is restarted while a step is in flight', () => {
344
+ test('completeJourney on the old step does not close the new instance', async () => {
345
+ moduleRuntime.startJourney(baseDef);
346
+ let resolveWork!: () => void;
347
+ const hang = new Promise<void>(resolve => {
348
+ resolveWork = resolve;
349
+ });
350
+ const stepPromise = journeyStep(baseDef.name, 'slow', async step => {
351
+ await hang;
352
+ step.completeJourney();
353
+ });
354
+ await Promise.resolve();
355
+ moduleRuntime.startJourney(baseDef);
356
+ expect(lastJourney(emitMock).outcome).toBe('excluded');
357
+ emitMock.mockClear();
358
+
359
+ resolveWork();
360
+ await stepPromise;
361
+ expect(emitMock).not.toHaveBeenCalled();
362
+
363
+ completeJourney(baseDef.name);
364
+ expect(lastJourney(emitMock).outcome).toBe('good');
365
+ expect(lastJourney(emitMock).steps).toHaveLength(0);
366
+ });
367
+ });
368
+
369
+ describe('when maxSteps is exceeded', () => {
370
+ test('stops recording steps and sets steps_truncated', async () => {
371
+ moduleRuntime.startJourney({ ...baseDef, maxSteps: 2 });
372
+ await journeyStep(baseDef.name, 'a', () => {});
373
+ await journeyStep(baseDef.name, 'b', () => {});
374
+ await journeyStep(baseDef.name, 'c', () => {});
375
+ completeJourney(baseDef.name);
376
+
377
+ expect(lastJourney(emitMock).steps.map((s: { name: string }) => s.name)).toEqual([
378
+ 'a',
379
+ 'b',
380
+ ]);
381
+ expect(lastJourney(emitMock).tags?.steps_truncated).toBe(true);
382
+ });
383
+ });
384
+
385
+ describe('when the started config is frozen', () => {
386
+ test('rejects mutation of the snapshot', () => {
387
+ const config = { ...baseDef, name: 'frozen-def' };
388
+ moduleRuntime.startJourney(config);
389
+ const snapshot = moduleRuntime.getOpenJourney('frozen-def')!.config;
390
+ expect(Object.isFrozen(snapshot)).toBe(true);
391
+ expect(snapshot).not.toBe(config);
392
+ expect(() => {
393
+ (snapshot as { name: string }).name = 'hacked';
394
+ }).toThrow();
395
+ expect(snapshot.name).toBe('frozen-def');
396
+ completeJourney('frozen-def');
397
+ });
398
+ });
305
399
  });
@@ -1,6 +1,12 @@
1
1
  /** @jest-environment jsdom */
2
- import { baseJourneyDef, lastJourney, useJourneyTestRuntime } from '../__mocks__/test-runtime';
3
- import { completeJourney, journeyStep, resolveJourneyStep, startJourney } from '../core';
2
+ import {
3
+ baseJourneyDef,
4
+ lastJourney,
5
+ useJourneyTestRuntime,
6
+ } from '../../__test-utils__/test-runtime';
7
+ import type { JourneyStepStamp } from '../../core';
8
+ import { completeJourney, journeyStep } from '../../core';
9
+ import { moduleRuntime } from '../../core/runtime';
4
10
 
5
11
  const emitMock = jest.fn();
6
12
  const baseDef = { ...baseJourneyDef, name: 'timeout-api' };
@@ -18,7 +24,7 @@ describe('[journey] timeouts', () => {
18
24
 
19
25
  describe('with a journey timeoutMs', () => {
20
26
  beforeEach(() => {
21
- startJourney({ ...baseDef, timeoutMs: 50 });
27
+ moduleRuntime.startJourney({ ...baseDef, timeoutMs: 50 });
22
28
  });
23
29
 
24
30
  const subject = () => jest.advanceTimersByTimeAsync(50);
@@ -33,7 +39,7 @@ describe('[journey] timeouts', () => {
33
39
 
34
40
  describe('with timeoutMs 0', () => {
35
41
  beforeEach(() => {
36
- startJourney({ ...baseDef, timeoutMs: 0 });
42
+ moduleRuntime.startJourney({ ...baseDef, timeoutMs: 0 });
37
43
  });
38
44
 
39
45
  const subject = () => jest.advanceTimersByTimeAsync(60_000);
@@ -47,12 +53,30 @@ describe('[journey] timeouts', () => {
47
53
  });
48
54
  });
49
55
 
56
+ describe('without timeoutMs', () => {
57
+ beforeEach(() => {
58
+ moduleRuntime.startJourney({
59
+ name: 'timeout-idle',
60
+ team: baseDef.team,
61
+ group: baseDef.group,
62
+ service: baseDef.service,
63
+ });
64
+ });
65
+
66
+ test('closes excluded with journey-idle-timeout after default idle window', async () => {
67
+ await jest.advanceTimersByTimeAsync(15 * 60 * 1_000);
68
+
69
+ expect(lastJourney(emitMock).outcome).toBe('excluded');
70
+ expect(lastJourney(emitMock).reason).toBe('journey-idle-timeout');
71
+ });
72
+ });
73
+
50
74
  describe('with a step timeoutMs', () => {
51
75
  let resolveWork!: () => void;
52
76
  let stepPromise: Promise<unknown>;
53
77
 
54
78
  beforeEach(() => {
55
- startJourney({ ...baseDef, stepTimeoutMs: 40 });
79
+ moduleRuntime.startJourney({ ...baseDef, stepTimeoutMs: 40 });
56
80
  const hang = new Promise<void>(resolve => {
57
81
  resolveWork = resolve;
58
82
  });
@@ -81,7 +105,7 @@ describe('[journey] timeouts', () => {
81
105
  let stepPromise: Promise<unknown>;
82
106
 
83
107
  beforeEach(() => {
84
- startJourney({ ...baseDef, stepTimeoutMs: 5_000 });
108
+ moduleRuntime.startJourney({ ...baseDef, stepTimeoutMs: 5_000 });
85
109
  const hang = new Promise<void>(resolve => {
86
110
  resolveWork = resolve;
87
111
  });
@@ -114,7 +138,7 @@ describe('[journey] timeouts', () => {
114
138
  let stepPromise: Promise<unknown>;
115
139
 
116
140
  beforeEach(() => {
117
- startJourney({ ...baseDef, stepTimeoutMs: 40 });
141
+ moduleRuntime.startJourney({ ...baseDef, stepTimeoutMs: 40 });
118
142
  const hang = new Promise<void>(resolve => {
119
143
  resolveWork = resolve;
120
144
  });
@@ -144,14 +168,12 @@ describe('[journey] timeouts', () => {
144
168
  let stepPromise: Promise<unknown>;
145
169
 
146
170
  beforeEach(() => {
147
- startJourney({ ...baseDef, stepTimeoutMs: 40 });
171
+ moduleRuntime.startJourney({ ...baseDef, stepTimeoutMs: 40 });
148
172
  const hang = new Promise<void>(resolve => {
149
173
  resolveWork = resolve;
150
174
  });
151
175
  stepPromise = journeyStep(baseDef.name, 'slow', async step => {
152
- const ref = resolveJourneyStep(
153
- (step.tag() as { journeyStep: unknown }).journeyStep
154
- )!;
176
+ const ref = (step.stamp() as JourneyStepStamp).step;
155
177
  ref.reason = 'pre-existing';
156
178
  ref.attributes.timeout_ms = 999;
157
179
  await hang;
@@ -176,7 +198,7 @@ describe('[journey] timeouts', () => {
176
198
  describe('when clearTimeout is bypassed after complete', () => {
177
199
  beforeEach(() => {
178
200
  jest.spyOn(globalThis, 'clearTimeout').mockImplementation(() => {});
179
- startJourney({ ...baseDef, timeoutMs: 50 });
201
+ moduleRuntime.startJourney({ ...baseDef, timeoutMs: 50 });
180
202
  completeJourney(baseDef.name);
181
203
  emitMock.mockClear();
182
204
  });
@@ -194,12 +216,40 @@ describe('[journey] timeouts', () => {
194
216
  });
195
217
  });
196
218
 
219
+ describe('when a journey timeout closes a hung step', () => {
220
+ let releaseWork!: () => void;
221
+ let stepPromise: Promise<unknown>;
222
+
223
+ beforeEach(() => {
224
+ moduleRuntime.startJourney({ ...baseDef, timeoutMs: 50 });
225
+ const hang = new Promise<void>(resolve => {
226
+ releaseWork = resolve;
227
+ });
228
+ stepPromise = journeyStep(baseDef.name, 'slow', async () => {
229
+ await hang;
230
+ });
231
+ });
232
+
233
+ afterEach(async () => {
234
+ releaseWork();
235
+ await stepPromise;
236
+ });
237
+
238
+ test('releases inFlight state before the step settles', async () => {
239
+ await jest.advanceTimersByTimeAsync(50);
240
+
241
+ expect(moduleRuntime.activeJourneyStep()).toBeNull();
242
+ expect(lastJourney(emitMock).outcome).toBe('bad');
243
+ expect(lastJourney(emitMock).reason).toBe('journey-timeout');
244
+ });
245
+ });
246
+
197
247
  describe('with stepTimeoutMs 0', () => {
198
248
  let resolveWork!: () => void;
199
249
  let stepPromise: Promise<unknown>;
200
250
 
201
251
  beforeEach(() => {
202
- startJourney({ ...baseDef, stepTimeoutMs: 0 });
252
+ moduleRuntime.startJourney({ ...baseDef, stepTimeoutMs: 0 });
203
253
  const hang = new Promise<void>(resolve => {
204
254
  resolveWork = resolve;
205
255
  });
@@ -223,4 +273,24 @@ describe('[journey] timeouts', () => {
223
273
  expect(lastJourney(emitMock).outcome).toBe('good');
224
274
  });
225
275
  });
276
+
277
+ describe('with non-finite timeoutMs', () => {
278
+ test('Infinity disables the countdown', async () => {
279
+ moduleRuntime.startJourney({ ...baseDef, timeoutMs: Infinity });
280
+ await jest.advanceTimersByTimeAsync(60_000);
281
+
282
+ expect(emitMock).not.toHaveBeenCalled();
283
+ completeJourney(baseDef.name);
284
+ expect(lastJourney(emitMock).outcome).toBe('good');
285
+ });
286
+
287
+ test('NaN disables the countdown', async () => {
288
+ moduleRuntime.startJourney({ ...baseDef, timeoutMs: Number.NaN });
289
+ await jest.advanceTimersByTimeAsync(60_000);
290
+
291
+ expect(emitMock).not.toHaveBeenCalled();
292
+ completeJourney(baseDef.name);
293
+ expect(lastJourney(emitMock).outcome).toBe('good');
294
+ });
295
+ });
226
296
  });
@@ -0,0 +1,154 @@
1
+ /** @jest-environment jsdom */
2
+ import '../config';
3
+ import { defineJourney, resetJourney } from '../core';
4
+ import { moduleRuntime } from '../core/runtime';
5
+ import {
6
+ JOURNEY_DEBUG_STORAGE_KEY,
7
+ JOURNEY_ENABLED_STORAGE_KEY,
8
+ ST_JOURNEY_GLOBAL_KEY,
9
+ ST_JOURNEY_SCHEMA_VERSION,
10
+ disableJourney,
11
+ enableJourney,
12
+ isJourneyEnabled,
13
+ setJourneyDefaultEnabled,
14
+ type StJourneyGlobal,
15
+ } from '../global';
16
+ import { setJourneyRum } from '../sinks/datadog';
17
+
18
+ function getStJourney(): StJourneyGlobal {
19
+ return (globalThis as Record<string, unknown>)[ST_JOURNEY_GLOBAL_KEY] as StJourneyGlobal;
20
+ }
21
+
22
+ describe('[journey] __stJourney global', () => {
23
+ beforeEach(() => {
24
+ localStorage.clear();
25
+ sessionStorage.clear();
26
+ setJourneyDefaultEnabled(false);
27
+ delete getStJourney().rum;
28
+ delete (globalThis as Record<string, unknown>).DD_RUM;
29
+ resetJourney({ sinks: [] });
30
+ });
31
+
32
+ afterEach(() => {
33
+ localStorage.clear();
34
+ sessionStorage.clear();
35
+ setJourneyDefaultEnabled(false);
36
+ delete getStJourney().rum;
37
+ delete (globalThis as Record<string, unknown>).DD_RUM;
38
+ resetJourney({ sinks: [] });
39
+ });
40
+
41
+ test('installs __stJourney on package load', () => {
42
+ expect(getStJourney()).toEqual(
43
+ expect.objectContaining({
44
+ schemaVersion: ST_JOURNEY_SCHEMA_VERSION,
45
+ enable: expect.any(Function),
46
+ disable: expect.any(Function),
47
+ isEnabled: expect.any(Function),
48
+ enableDebugging: expect.any(Function),
49
+ disableDebugging: expect.any(Function),
50
+ isDebugEnabled: expect.any(Function),
51
+ getDebugState: expect.any(Function),
52
+ })
53
+ );
54
+ });
55
+
56
+ test('enable / disable toggle force-enable localStorage', () => {
57
+ expect(isJourneyEnabled()).toBe(false);
58
+
59
+ enableJourney();
60
+ expect(localStorage.getItem(JOURNEY_ENABLED_STORAGE_KEY)).toBe('true');
61
+ expect(isJourneyEnabled()).toBe(true);
62
+ expect(getStJourney().isEnabled()).toBe(true);
63
+
64
+ disableJourney();
65
+ expect(localStorage.getItem(JOURNEY_ENABLED_STORAGE_KEY)).toBeNull();
66
+ expect(isJourneyEnabled()).toBe(false);
67
+ });
68
+
69
+ test('isJourneyEnabled is true when host default is on', () => {
70
+ expect(isJourneyEnabled()).toBe(false);
71
+
72
+ setJourneyDefaultEnabled(true);
73
+ expect(isJourneyEnabled()).toBe(true);
74
+ expect(getStJourney().isEnabled()).toBe(true);
75
+
76
+ setJourneyDefaultEnabled(false);
77
+ expect(isJourneyEnabled()).toBe(false);
78
+ });
79
+
80
+ test('isJourneyEnabled is true when either host default or force flag is on', () => {
81
+ setJourneyDefaultEnabled(true);
82
+ enableJourney();
83
+ expect(isJourneyEnabled()).toBe(true);
84
+
85
+ setJourneyDefaultEnabled(false);
86
+ expect(isJourneyEnabled()).toBe(true);
87
+
88
+ disableJourney();
89
+ expect(isJourneyEnabled()).toBe(false);
90
+
91
+ setJourneyDefaultEnabled(true);
92
+ expect(isJourneyEnabled()).toBe(true);
93
+ });
94
+
95
+ test('host default is shared on __stJourney for cross-bundle reads', () => {
96
+ setJourneyDefaultEnabled(true);
97
+ expect(getStJourney().defaultEnabled).toBe(true);
98
+ expect(isJourneyEnabled()).toBe(true);
99
+ });
100
+
101
+ test('enableDebugging / disableDebugging toggle console-debug sessionStorage', () => {
102
+ const st = getStJourney();
103
+
104
+ st.enableDebugging();
105
+ expect(sessionStorage.getItem(JOURNEY_DEBUG_STORAGE_KEY)).toBe('true');
106
+ expect(st.isDebugEnabled()).toBe(true);
107
+
108
+ st.disableDebugging();
109
+ expect(sessionStorage.getItem(JOURNEY_DEBUG_STORAGE_KEY)).toBeNull();
110
+ expect(st.isDebugEnabled()).toBe(false);
111
+ });
112
+
113
+ test('getDebugState omits runtimes until debugging is enabled', () => {
114
+ enableJourney();
115
+ const journey = defineJourney({
116
+ name: 'Book Job',
117
+ team: 'jbce',
118
+ group: 'call-booking',
119
+ service: 'call-screen',
120
+ });
121
+ journey.start();
122
+
123
+ expect(getStJourney().getDebugState()).toEqual(
124
+ expect.objectContaining({
125
+ enabled: true,
126
+ debug: false,
127
+ runtimes: [],
128
+ })
129
+ );
130
+
131
+ getStJourney().enableDebugging();
132
+ const on = getStJourney().getDebugState();
133
+ expect(on.debug).toBe(true);
134
+ expect(on.runtimes.some(r => r.id === moduleRuntime.id)).toBe(true);
135
+ expect(on.runtimes.some(r => r.openJourneys.some(j => j.name === 'Book Job'))).toBe(true);
136
+
137
+ getStJourney().disableDebugging();
138
+ expect(getStJourney().getDebugState().runtimes).toEqual([]);
139
+ });
140
+
141
+ test('getDebugState.hasRum reflects DD_RUM or the fallback bridge', () => {
142
+ getStJourney().enableDebugging();
143
+
144
+ expect(getStJourney().getDebugState().hasRum).toBe(false);
145
+
146
+ (globalThis as Record<string, unknown>).DD_RUM = { addAction: jest.fn() };
147
+ expect(getStJourney().getDebugState().hasRum).toBe(true);
148
+
149
+ delete (globalThis as Record<string, unknown>).DD_RUM;
150
+ jest.spyOn(console, 'warn').mockImplementation(() => undefined);
151
+ setJourneyRum({ addAction: jest.fn() });
152
+ expect(getStJourney().getDebugState().hasRum).toBe(true);
153
+ });
154
+ });