@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
package/src/config.ts CHANGED
@@ -1,10 +1,15 @@
1
- import { defaultRuntime } from './core/runtime';
2
- import { setJourneySinks, type JourneySink } from './core/registry';
1
+ import { moduleRuntime } from './core/runtime';
2
+ import type { JourneySink } from './core/types';
3
+ import type { RequestPolicyConfig } from './core/request-policy';
3
4
  import {
4
- sortEndpointsByLongestMatch,
5
- type AppRequestTimeouts,
6
- type EndpointPolicy,
7
- } from './endpoint-policy';
5
+ areSharedPoliciesLocked,
6
+ getSharedSlowRequests,
7
+ isJourneyEnabled,
8
+ registerJourneyRuntime,
9
+ seedSharedPolicies,
10
+ warnIfSlowRequestsUnset,
11
+ } from './global';
12
+ import { sortEndpointsByLongestMatch, type SlowRequestConfig } from './core/endpoint-policy';
8
13
  import { instrumentAxios, type AxiosLikeInstance } from './integrations/axios';
9
14
  import { instrumentFetch, type InstrumentFetchOptions } from './integrations/fetch';
10
15
  import {
@@ -13,33 +18,73 @@ import {
13
18
  type JqueryStatic,
14
19
  type WindowWithAppJourney,
15
20
  } from './integrations/jquery';
21
+ import { resolveGlobalJquery } from './integrations/jquery/instrument';
22
+ import { sendToConsole } from './sinks/console';
16
23
  import { sendToDatadog } from './sinks/datadog';
17
24
 
18
- export { longestEndpointMatch, sortEndpointsByLongestMatch } from './endpoint-policy';
19
- export type { AppRequestTimeouts, EndpointPolicy };
20
-
21
25
  /** Optional app-wide / per-bundle configuration. */
22
26
  export interface JourneyConfig {
23
- /** App-wide request-timeout fallback. Defaults to { defaultMs: 4000, endpoints: [] }. */
24
- requestTimeouts?: AppRequestTimeouts;
27
+ /**
28
+ * App-wide slow-request thresholds (shared on `__stJourney` for all package copies).
29
+ * Package fallback remains `{ defaultMs: 4000, endpoints: [] }` until set.
30
+ * Desktop / KO hosts should set `defaultMs` and ignore noisy endpoint prefixes.
31
+ */
32
+ slowRequests?: SlowRequestConfig;
33
+ /**
34
+ * How attributed aborted/cancelled HTTP counts toward the open journey.
35
+ * Shared on `__stJourney`. Literal or `(ctx) => …`. Default `'continue'`.
36
+ */
37
+ httpAbortedRequests?: RequestPolicyConfig;
38
+ /**
39
+ * How attributed 4xx responses count toward the open journey.
40
+ * Shared on `__stJourney`. Literal or `(ctx) => …`. Default `'continue'` (no verdict change).
41
+ */
42
+ httpClientErrorRequests?: RequestPolicyConfig;
43
+ /**
44
+ * When true (default), untagged instrumented HTTP is attributed to the sole
45
+ * in-flight step (if exactly one). When false, only explicit `step.stamp()` attributes.
46
+ * Prefer false on busy desktop / shared jQuery shells with background traffic.
47
+ * Shared on `__stJourney`.
48
+ */
49
+ autoAttributeRequests?: boolean;
50
+ /**
51
+ * Default cap on recorded steps per journey (default 50). Per-journey
52
+ * `JourneyDef.maxSteps` overrides. Excess steps are dropped and
53
+ * `tags.steps_truncated` is set.
54
+ */
55
+ maxSteps?: number;
56
+ /**
57
+ * Applied when a journey omits `timeoutMs`.
58
+ * Default is 15m and closes as `excluded` with `journey-idle-timeout`.
59
+ * Pass `0` to disable this idle timeout.
60
+ */
61
+ defaultJourneyIdleMs?: number;
25
62
  /**
26
63
  * Replace the sink list used when a journey finishes.
27
64
  * Only applied when provided — omit to leave the current list unchanged.
28
- * Package default is `[sendToDatadog]`; pass `[]` to disable emission.
65
+ * Package default is `[sendToDatadog, sendToConsole]`; pass `[]` to disable emission.
66
+ * `sendToConsole` is a no-op unless `sessionStorage['st:journey:debug'] === 'true'`.
67
+ * First `sinks` on this bundle wins; a later `configureJourney({ sinks })` is ignored.
29
68
  */
30
69
  sinks?: readonly JourneySink[];
31
70
  /**
32
71
  * Wire this bundle's axios instance (host or MFE). Same as `instrumentAxios(axios)`.
33
72
  * Register any async request interceptors (e.g. auth) before `configureJourney`.
73
+ * Soft-skips when axios is missing / not axios-like — safe to pass a host instance that may be absent.
34
74
  */
35
- axios?: AxiosLikeInstance;
75
+ axios?: AxiosLikeInstance | null;
36
76
  /**
37
77
  * Wire fetch. `true` wraps `globalThis.fetch`; pass options for a scoped target / baseURL.
38
- * Same as `instrumentFetch(...)`.
78
+ * Same as `instrumentFetch(...)`. Soft-skips when the target has no callable `fetch`.
79
+ * Prefer host-only for `globalThis.fetch` — MFEs inherit the host wrapper.
39
80
  */
40
- fetch?: true | InstrumentFetchOptions;
41
- /** Wire this bundle's jQuery static. Same as `instrumentJquery($)`. */
42
- jquery?: JqueryStatic;
81
+ fetch?: true | InstrumentFetchOptions | null;
82
+ /**
83
+ * Wire jQuery. Pass a `$` instance, or `true` to use `globalThis.$` / `globalThis.jQuery`.
84
+ * `false` skips wiring. Soft-skips when `$` is missing / not real jQuery.
85
+ * Prefer host-only for shared `$`.
86
+ */
87
+ jquery?: boolean | JqueryStatic | null;
43
88
  /**
44
89
  * Publish `window.App.Journey` for Knockout / legacy hosts.
45
90
  * `true` uses the real window; pass a target object in tests.
@@ -48,40 +93,127 @@ export interface JourneyConfig {
48
93
  }
49
94
 
50
95
  /*
51
- * Default emission target. Kept as a module side effect so any import of this file
52
- * (see index.ts `import './config'`) installs Datadog. configureJourney only replaces
53
- * sinks when `sinks` is passed explicitly.
96
+ * Default emission targets + `__stJourney` bag. Kept as a module side effect so any import
97
+ * of this file (see index.ts `import './config'`) installs Datadog, console, and
98
+ * `__stJourney`. configureJourney only replaces sinks when `sinks` is passed explicitly.
54
99
  */
55
- setJourneySinks([sendToDatadog]);
100
+ moduleRuntime.setSinks([sendToDatadog, sendToConsole]);
101
+ registerJourneyRuntime(moduleRuntime);
56
102
 
57
103
  /**
58
104
  * Apply bundle options: request thresholds, sinks, and/or HTTP / legacy wiring.
59
105
  * Optional — call once at startup. Pass this bundle's own axios / `$` / fetch target.
106
+ * Policy knobs (`slowRequests`, `httpAbortedRequests`, `httpClientErrorRequests`,
107
+ * `autoAttributeRequests`) are stored on shared `__stJourney` so MFEs inherit host settings.
108
+ * No-ops when `isJourneyEnabled()` is false (host default or force flag).
109
+ * Never throws: missing/invalid config and failures soft-skip (warn / error to console).
60
110
  */
61
111
  export function configureJourney(config: JourneyConfig): void {
62
- if (config.requestTimeouts !== undefined) {
63
- defaultRuntime.setRequestTimeouts({
64
- ...config.requestTimeouts,
65
- endpoints: sortEndpointsByLongestMatch(config.requestTimeouts.endpoints),
66
- });
67
- }
68
- if (config.sinks !== undefined) {
69
- setJourneySinks(config.sinks);
70
- }
71
- if (config.axios !== undefined) {
72
- instrumentAxios(config.axios);
112
+ if (config == null || typeof config !== 'object') {
113
+ // eslint-disable-next-line no-console -- soft-skip missing host config
114
+ console.warn('[journey] configureJourney: skipped — missing');
115
+ return;
73
116
  }
74
- if (config.fetch !== undefined) {
75
- instrumentFetch(config.fetch === true ? {} : config.fetch);
117
+ if (!isJourneyEnabled()) {
118
+ // eslint-disable-next-line no-console -- soft-skip when host default and force are off
119
+ console.warn(
120
+ '[journey] configureJourney: skipped — disabled (call setJourneyDefaultEnabled(true) or __stJourney.enable(), then configureJourney again / reload)'
121
+ );
122
+ return;
76
123
  }
77
- if (config.jquery !== undefined) {
78
- instrumentJquery(config.jquery);
79
- }
80
- if (config.exposeAppJourney) {
81
- exposeAppJourney(config.exposeAppJourney === true ? undefined : config.exposeAppJourney);
124
+
125
+ try {
126
+ const overlay: {
127
+ slowRequests?: SlowRequestConfig;
128
+ httpAbortedRequests?: RequestPolicyConfig;
129
+ httpClientErrorRequests?: RequestPolicyConfig;
130
+ autoAttributeRequests?: boolean;
131
+ } = {};
132
+
133
+ const policyPatch: Parameters<typeof seedSharedPolicies>[0] = {};
134
+ let hasPolicyKnob = false;
135
+
136
+ if (config.slowRequests !== undefined) {
137
+ const sorted = {
138
+ ...config.slowRequests,
139
+ endpoints: sortEndpointsByLongestMatch(config.slowRequests.endpoints),
140
+ };
141
+ policyPatch.slowRequests = sorted;
142
+ policyPatch.slowRequestsConfigured = true;
143
+ overlay.slowRequests = sorted;
144
+ hasPolicyKnob = true;
145
+ }
146
+ if (config.httpAbortedRequests !== undefined) {
147
+ policyPatch.httpAbortedRequests = config.httpAbortedRequests;
148
+ overlay.httpAbortedRequests = config.httpAbortedRequests;
149
+ hasPolicyKnob = true;
150
+ }
151
+ if (config.httpClientErrorRequests !== undefined) {
152
+ policyPatch.httpClientErrorRequests = config.httpClientErrorRequests;
153
+ overlay.httpClientErrorRequests = config.httpClientErrorRequests;
154
+ hasPolicyKnob = true;
155
+ }
156
+ if (config.autoAttributeRequests !== undefined) {
157
+ policyPatch.autoAttributeRequests = config.autoAttributeRequests;
158
+ overlay.autoAttributeRequests = config.autoAttributeRequests;
159
+ hasPolicyKnob = true;
160
+ }
161
+
162
+ if (hasPolicyKnob) {
163
+ if (!areSharedPoliciesLocked()) {
164
+ seedSharedPolicies(policyPatch);
165
+ }
166
+ moduleRuntime.setPolicyOverlay(overlay);
167
+ }
168
+ if (config.maxSteps !== undefined) {
169
+ moduleRuntime.setMaxSteps(config.maxSteps);
170
+ }
171
+ if (config.defaultJourneyIdleMs !== undefined) {
172
+ moduleRuntime.setDefaultJourneyIdleMs(config.defaultJourneyIdleMs);
173
+ }
174
+ if (config.sinks !== undefined) {
175
+ if (!moduleRuntime.trySetConfiguredSinks(config.sinks)) {
176
+ // eslint-disable-next-line no-console -- first-writer sinks
177
+ console.warn(
178
+ '[journey] configureJourney: sinks already set on this runtime — ignoring'
179
+ );
180
+ }
181
+ }
182
+
183
+ const wiringTransport =
184
+ config.axios !== undefined ||
185
+ config.fetch !== undefined ||
186
+ (config.jquery !== undefined && config.jquery !== false);
187
+ if (wiringTransport) {
188
+ warnIfSlowRequestsUnset();
189
+ }
190
+
191
+ if (config.axios !== undefined) {
192
+ instrumentAxios(config.axios);
193
+ }
194
+ if (config.fetch !== undefined) {
195
+ instrumentFetch(config.fetch === true ? {} : config.fetch);
196
+ }
197
+ if (config.jquery === true) {
198
+ instrumentJquery(resolveGlobalJquery());
199
+ } else if (config.jquery !== undefined && config.jquery !== false) {
200
+ instrumentJquery(config.jquery);
201
+ }
202
+ if (config.exposeAppJourney) {
203
+ exposeAppJourney(
204
+ config.exposeAppJourney === true ? undefined : config.exposeAppJourney
205
+ );
206
+ }
207
+ } catch (err) {
208
+ // eslint-disable-next-line no-console -- soft-fail host wiring
209
+ console.error('[journey] configureJourney failed', err);
82
210
  }
83
211
  }
84
212
 
85
- export function getRequestTimeouts(): AppRequestTimeouts {
86
- return defaultRuntime.getRequestTimeouts();
213
+ /**
214
+ * @internal Read the module runtime's app-wide slow-request config.
215
+ * Prefer `configureJourney({ slowRequests })` to set thresholds. Test / introspection only.
216
+ */
217
+ export function getSlowRequests(): SlowRequestConfig {
218
+ return getSharedSlowRequests() ?? moduleRuntime.getSlowRequests();
87
219
  }
@@ -0,0 +1,24 @@
1
+ export function isAbortError(error: unknown): boolean {
2
+ if (error == null || typeof error !== 'object') {
3
+ return false;
4
+ }
5
+ const err = error as { name?: string; code?: string | number };
6
+ if (err.name === 'AbortError' || err.name === 'CanceledError') {
7
+ return true;
8
+ }
9
+ return err.code === 'ERR_CANCELED';
10
+ }
11
+
12
+ /** Axios cancel token, CanceledError, AbortError, or ERR_CANCELED. */
13
+ export function isCanceledError(error: unknown): boolean {
14
+ if (isAbortError(error)) {
15
+ return true;
16
+ }
17
+ if (error == null || typeof error !== 'object') {
18
+ return false;
19
+ }
20
+ /* eslint-disable @typescript-eslint/naming-convention -- axios Cancel token wire format */
21
+ const err = error as { __CANCEL__?: boolean };
22
+ return err.__CANCEL__ === true;
23
+ /* eslint-enable @typescript-eslint/naming-convention */
24
+ }
@@ -0,0 +1,159 @@
1
+ import { moduleRuntime, resolveJourneyEngine } from './runtime';
2
+ import { adoptJourneyStepStamp, JourneyStepStamp } from './step-tag';
3
+ import type {
4
+ JourneyDef,
5
+ JourneyHandle,
6
+ JourneyStepOptions,
7
+ JourneyStepTarget,
8
+ StepHandle,
9
+ StepRecord,
10
+ TagValue,
11
+ } from './types';
12
+
13
+ let ambientStamp: JourneyStepStamp | null = null;
14
+ let ambientStep: StepRecord | null = null;
15
+
16
+ /**
17
+ * Unique in-flight step as a stamp-ready {@link JourneyStepStamp}, or null when
18
+ * auto-attribute is disabled, or when 0 or 2+ steps are in flight app-wide.
19
+ * Ambient stamps never carry `score` or `ignore`. Untagged traffic is attributed
20
+ * to that lone step even if unrelated — prefer `step.stamp()` when the page has
21
+ * background instrumented requests. Disable via
22
+ * `configureJourney({ autoAttributeRequests: false })`.
23
+ */
24
+ export function singleInFlightJourneyStep(): JourneyStepStamp | null {
25
+ if (ambientStep?.journey.closed) {
26
+ ambientStep = null;
27
+ ambientStamp = null;
28
+ }
29
+ const activeStep = moduleRuntime.activeJourneyStep();
30
+ if (!activeStep) {
31
+ ambientStamp = null;
32
+ ambientStep = null;
33
+ return null;
34
+ }
35
+ if (ambientStep === activeStep && ambientStamp) {
36
+ return ambientStamp;
37
+ }
38
+ ambientStep = activeStep;
39
+ ambientStamp = new JourneyStepStamp(activeStep);
40
+ return ambientStamp;
41
+ }
42
+
43
+ /** @internal Clear cached ambient stamp when runtime state changes. */
44
+ export function invalidateAmbientStamp(step?: StepRecord): void {
45
+ if (!step || step === ambientStep) {
46
+ ambientStamp = null;
47
+ ambientStep = null;
48
+ }
49
+ }
50
+
51
+ /**
52
+ * Prefer an explicit `step.stamp()` value; else the ambient in-flight stamp.
53
+ * Same-realm stamps are returned as-is (identity + score + ignore). Branded
54
+ * foreign copies are adopted into a local stamp. Raw StepRecords are not stamps.
55
+ */
56
+ export function resolveStamp(explicit?: unknown): JourneyStepStamp | undefined {
57
+ const fromExplicit = adoptJourneyStepStamp(explicit);
58
+ if (fromExplicit) {
59
+ return fromExplicit;
60
+ }
61
+ return singleInFlightJourneyStep() ?? undefined;
62
+ }
63
+
64
+ /**
65
+ * Record a finished backend request against a captured step.
66
+ * 5xx / network / slow (over threshold) mark Bad; 4xx is not counted. Fail-fast on breach.
67
+ * @internal Prefer {@link onHttpComplete} from adapters.
68
+ */
69
+ export function reportBackendRequest(
70
+ step: StepRecord | undefined,
71
+ status: number | undefined,
72
+ durationMs: number,
73
+ requestKey: string
74
+ ): void {
75
+ if (!step?.journey) {
76
+ return;
77
+ }
78
+ resolveJourneyEngine(step.journey).reportBackendRequest(step, status, durationMs, requestKey);
79
+ }
80
+
81
+ /**
82
+ * Run one step of a named journey. Runs untraced (noop handle) when no matching journey is open.
83
+ * `journey` may be a name, null, an ordered candidate list, a def, or a handle.
84
+ * A def or handle auto-starts when that journey is not already open; a bare name does not.
85
+ * Prefer `defineJourney(...).step` when you own the journey config.
86
+ */
87
+ export async function journeyStep<T>(
88
+ journey: JourneyStepTarget,
89
+ stepName: string,
90
+ fn: (step: StepHandle) => T | Promise<T>,
91
+ opts?: JourneyStepOptions
92
+ ): Promise<T> {
93
+ return moduleRuntime.journeyStep(journey, stepName, fn, opts);
94
+ }
95
+
96
+ /** Merge custom tags onto an open journey. No-op if not open. Prefer `handle.setTags`. */
97
+ export function setJourneyTags(name: string | null, tags: Record<string, TagValue>): void {
98
+ moduleRuntime.setJourneyTags(name, tags);
99
+ }
100
+
101
+ /** Mark a named journey Bad for a custom reason (fail-fast). Prefer `handle.fail`. */
102
+ export function failJourney(name: string, reason: string): void {
103
+ const journey = moduleRuntime.getOpenJourney(name);
104
+ if (journey) {
105
+ moduleRuntime.failJourney(journey, reason);
106
+ }
107
+ }
108
+
109
+ /** Successful end. Outcome stays Bad if a breach already occurred. No-op when name is null. Prefer `handle.complete`. */
110
+ export function completeJourney(name: string | null, tags?: Record<string, TagValue>): void {
111
+ moduleRuntime.completeJourney(name, tags);
112
+ }
113
+
114
+ /** User left before finishing. Stays Bad if a breach already occurred (breach-anchored). Prefer `handle.exclude`. */
115
+ export function excludeJourney(name: string): void {
116
+ const journey = moduleRuntime.getOpenJourney(name);
117
+ if (journey) {
118
+ moduleRuntime.excludeJourney(journey);
119
+ }
120
+ }
121
+
122
+ /** Turn a journey config into a handle with bound lifecycle methods. Preferred public API. */
123
+ export function defineJourney(config: JourneyDef): JourneyHandle {
124
+ return {
125
+ config,
126
+ start(tags) {
127
+ moduleRuntime.startJourney(config);
128
+ if (tags) {
129
+ moduleRuntime.setJourneyTags(config.name, tags);
130
+ }
131
+ },
132
+ step(stepName, fn, opts) {
133
+ if (!moduleRuntime.getOpenJourney(config.name)) {
134
+ moduleRuntime.startJourney(config);
135
+ }
136
+ return journeyStep(config.name, stepName, fn, opts);
137
+ },
138
+ async mountStep(stepName, fn, opts) {
139
+ // Defer so a parent <JourneyScope> opens first (child effects run before parent).
140
+ await Promise.resolve();
141
+ if (!moduleRuntime.getOpenJourney(config.name)) {
142
+ moduleRuntime.startJourney(config);
143
+ }
144
+ return journeyStep(config.name, stepName, fn, opts);
145
+ },
146
+ complete(tags) {
147
+ completeJourney(config.name, tags);
148
+ },
149
+ fail(reason) {
150
+ failJourney(config.name, reason);
151
+ },
152
+ exclude() {
153
+ excludeJourney(config.name);
154
+ },
155
+ setTags(tags) {
156
+ setJourneyTags(config.name, tags);
157
+ },
158
+ };
159
+ }
@@ -1,3 +1,5 @@
1
+ import type { JourneyState } from './types';
2
+
1
3
  /** Per-endpoint request policy (app-wide or per-journey). Longest matching prefix wins. */
2
4
  export interface EndpointPolicy {
3
5
  /**
@@ -7,20 +9,25 @@ export interface EndpointPolicy {
7
9
  * `match` already supplies the boundary.
8
10
  */
9
11
  match: string;
10
- /** Custom slow-request threshold in ms; omit / 0 falls through. */
11
- timeoutMs?: number;
12
- /** When true, errors and slow responses never mark the journey Bad. */
12
+ /** Custom slow-request threshold in ms; omit to fall through. `0` disables latency scoring for this prefix. */
13
+ slowRequestMs?: number;
14
+ /** When true, this endpoint never changes the journey verdict (errors, latency, aborts, 4xx). */
13
15
  ignore?: boolean;
14
16
  }
15
17
 
16
18
  /** App-wide slow-request threshold fallback (not an axios abort). */
17
- export interface AppRequestTimeouts {
19
+ export interface SlowRequestConfig {
18
20
  /** Fallback threshold when no endpoint or journey override applies. */
19
21
  defaultMs: number;
20
22
  /** App-wide endpoint overrides; journey-specific ones belong on JourneyDef.endpoints. */
21
23
  endpoints?: readonly EndpointPolicy[];
22
24
  }
23
25
 
26
+ export interface ResolvedEndpointPolicy {
27
+ ignore: boolean;
28
+ slowRequestMs: number;
29
+ }
30
+
24
31
  /** Copy sorted longest-match-first so lookup can return on the first hit. */
25
32
  export function sortEndpointsByLongestMatch(
26
33
  endpoints: readonly EndpointPolicy[] | undefined
@@ -64,3 +71,38 @@ export function longestEndpointMatch(
64
71
  }
65
72
  return undefined;
66
73
  }
74
+
75
+ /**
76
+ * Resolve ignore + latency threshold with longest-prefix matching.
77
+ * Journey overrides win over app defaults for slow thresholds.
78
+ */
79
+ export function resolveEndpointPolicy(
80
+ journey: JourneyState,
81
+ appSlow: SlowRequestConfig,
82
+ requestKey: string
83
+ ): ResolvedEndpointPolicy {
84
+ const own = longestEndpointMatch(journey.endpoints, requestKey);
85
+ if (own?.ignore === true) {
86
+ return { ignore: true, slowRequestMs: 0 };
87
+ }
88
+
89
+ const app = longestEndpointMatch(appSlow.endpoints, requestKey);
90
+ if (own) {
91
+ return {
92
+ ignore: false,
93
+ slowRequestMs:
94
+ own.slowRequestMs ??
95
+ app?.slowRequestMs ??
96
+ journey.slowRequestMs ??
97
+ appSlow.defaultMs,
98
+ };
99
+ }
100
+ if (app?.ignore === true) {
101
+ return { ignore: true, slowRequestMs: 0 };
102
+ }
103
+
104
+ return {
105
+ ignore: false,
106
+ slowRequestMs: app?.slowRequestMs ?? journey.slowRequestMs ?? appSlow.defaultMs,
107
+ };
108
+ }
package/src/core/index.ts CHANGED
@@ -1,39 +1,38 @@
1
- /** Core engine barrel — free helpers over the default runtime, plus handles and types. */
1
+ /** Core barrel — public free helpers, handles, and types. Engine lives in `./runtime`. */
2
2
 
3
3
  export {
4
- activeJourneyStep,
5
- addJourneySink,
6
4
  completeJourney,
7
5
  defineJourney,
8
- emitJourneyEvent,
9
6
  excludeJourney,
10
7
  failJourney,
11
- firstOpenJourney,
12
- journeyMountStep,
13
8
  journeyStep,
14
9
  reportBackendRequest,
15
- setJourneySinks,
10
+ resolveStamp,
16
11
  setJourneyTags,
17
- startJourney,
18
- type JourneyHandle,
19
- type JourneySink,
20
- } from './registry';
12
+ singleInFlightJourneyStep,
13
+ } from './api';
21
14
  export {
22
- createJourneyRuntime,
23
- defaultRuntime,
15
+ // @internal Test helper — not a supported consumer API.
24
16
  resetJourney,
25
- type JourneyRuntime,
26
- type JourneyRuntimeOptions,
27
17
  } from './runtime';
28
18
  export type {
29
19
  JourneyDef,
30
20
  JourneyEvent,
21
+ JourneyHandle,
31
22
  JourneyNameLike,
23
+ JourneySink,
32
24
  JourneyStepEvent,
33
25
  JourneyStepOptions,
34
- JourneyStepRef,
35
26
  JourneyStepTarget,
36
27
  StepHandle,
37
28
  TagValue,
38
29
  } from './types';
39
- export { JourneyStepTag, resolveJourneyStep } from './step-tag';
30
+ export { JourneyStepStamp } from './step-tag';
31
+ export type {
32
+ JourneyHttpPolicyContext,
33
+ JourneyHttpScore,
34
+ JourneyHttpScoreContext,
35
+ JourneyRequestPolicy,
36
+ JourneyStampOptions,
37
+ RequestPolicyConfig,
38
+ } from './request-policy';
@@ -0,0 +1,73 @@
1
+ /** Payload / timer bounds shared by the engine and default sinks. */
2
+
3
+ export const DEFAULT_MAX_STEPS = 50;
4
+ export const MAX_STEPS_CAP = 200;
5
+ export const MAX_TAG_KEYS = 32;
6
+ export const MAX_ATTRIBUTE_KEYS = 64;
7
+ export const MAX_ATTRIBUTED_REQUEST_KEYS = 50;
8
+ export const MAX_TAG_VALUE_LEN = 200;
9
+ export const DEFAULT_JOURNEY_IDLE_MS = 15 * 60 * 1_000;
10
+
11
+ const DENIED_KEYS = new Set([
12
+ '__proto__',
13
+ 'constructor',
14
+ 'prototype',
15
+ 'authorization',
16
+ 'cookie',
17
+ 'set-cookie',
18
+ 'password',
19
+ 'token',
20
+ 'secret',
21
+ 'ssn',
22
+ ]);
23
+
24
+ /** True for prototype-polluting keys and a small secret-name deny-list (case-insensitive). */
25
+ export function isUnsafeKey(key: string): boolean {
26
+ return DENIED_KEYS.has(key.toLowerCase());
27
+ }
28
+
29
+ /**
30
+ * Finite integer ≥ 1, else `fallback`. Used for maxSteps-like caps.
31
+ * Non-finite / ≤ 0 / non-numbers return `fallback`.
32
+ */
33
+ export function finitePositiveInt(n: unknown, fallback: number): number {
34
+ if (typeof n !== 'number' || !Number.isFinite(n) || n <= 0) {
35
+ return fallback;
36
+ }
37
+ return Math.floor(n);
38
+ }
39
+
40
+ /**
41
+ * Journey/step timeout: finite ≥ 0 keeps the value (floored); anything else is 0 (disabled).
42
+ */
43
+ export function finiteTimeoutMs(n: unknown): number {
44
+ if (typeof n !== 'number' || !Number.isFinite(n) || n < 0) {
45
+ return 0;
46
+ }
47
+ return Math.floor(n);
48
+ }
49
+
50
+ /** Clamp recorded-step cap to 1..MAX_STEPS_CAP; invalid → DEFAULT_MAX_STEPS. */
51
+ export function clampMaxSteps(n: unknown): number {
52
+ const value = finitePositiveInt(n, DEFAULT_MAX_STEPS);
53
+ return Math.min(value, MAX_STEPS_CAP);
54
+ }
55
+
56
+ /** Finite ≥ 0 slow-request threshold, or undefined to fall through. */
57
+ export function finiteSlowRequestMs(n: unknown): number | undefined {
58
+ if (typeof n !== 'number' || !Number.isFinite(n) || n < 0) {
59
+ return undefined;
60
+ }
61
+ return Math.floor(n);
62
+ }
63
+
64
+ /** Explicit 0 disables; omitted uses idle default; finite N keeps caller timeout. */
65
+ export function resolveJourneyTimeoutMs(
66
+ timeoutMs: number | undefined,
67
+ idleDefaultMs: number = DEFAULT_JOURNEY_IDLE_MS
68
+ ): { ms: number; explicit: boolean } {
69
+ if (timeoutMs === undefined) {
70
+ return { ms: finiteTimeoutMs(idleDefaultMs), explicit: false };
71
+ }
72
+ return { ms: finiteTimeoutMs(timeoutMs), explicit: true };
73
+ }