@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,70 +1,171 @@
1
1
  import {
2
- longestEndpointMatch,
2
+ getAutoAttributeRequests as getSharedAutoAttributeRequests,
3
+ getHttpAbortedRequestsPolicy as getSharedHttpAbortedRequestsPolicy,
4
+ getHttpClientErrorRequestsPolicy as getSharedHttpClientErrorRequestsPolicy,
5
+ getSharedSlowRequests,
6
+ isJourneyDebugEnabled,
7
+ } from '../global';
8
+ import { isCanceledError } from './abort-error';
9
+ import {
10
+ resolveEndpointPolicy,
3
11
  sortEndpointsByLongestMatch,
4
- type AppRequestTimeouts,
5
- } from '../endpoint-policy';
6
- import { JourneyStepTag } from './step-tag';
7
- import type {
8
- JourneyDef,
9
- JourneyEvent,
10
- JourneySink,
11
- JourneyState,
12
- JourneyStepEvent,
13
- JourneyStepOptions,
14
- JourneyStepRef,
15
- JourneyStepTarget,
16
- Outcome,
17
- StepHandle,
18
- StepRecord,
19
- TagValue,
12
+ type SlowRequestConfig,
13
+ } from './endpoint-policy';
14
+ import {
15
+ clampMaxSteps,
16
+ DEFAULT_MAX_STEPS,
17
+ DEFAULT_JOURNEY_IDLE_MS,
18
+ finiteSlowRequestMs,
19
+ finiteTimeoutMs,
20
+ MAX_ATTRIBUTED_REQUEST_KEYS,
21
+ MAX_TAG_KEYS,
22
+ resolveJourneyTimeoutMs,
23
+ } from './limits';
24
+ import { assignRecord, emptyRecord, sanitizeJourneyEvent, setSafeAttribute } from './sanitize';
25
+ import { JourneyStepStamp, resolveStampOpts } from './step-tag';
26
+ import type { RequestPolicyConfig } from './request-policy';
27
+ import {
28
+ JOURNEY_ENGINE,
29
+ type JourneyDef,
30
+ type JourneyEngine,
31
+ type JourneyEvent,
32
+ type JourneyNameLike,
33
+ type JourneySink,
34
+ type JourneyState,
35
+ type JourneyStepEvent,
36
+ type JourneyStepOptions,
37
+ type JourneyStepTarget,
38
+ type Outcome,
39
+ type StepHandle,
40
+ type StepRecord,
41
+ type TagValue,
20
42
  } from './types';
21
43
 
22
- const DEFAULT_REQUEST_TIMEOUTS: AppRequestTimeouts = { defaultMs: 4_000, endpoints: [] };
44
+ const DEFAULT_SLOW_REQUESTS: SlowRequestConfig = { defaultMs: 4_000, endpoints: [] };
23
45
 
24
46
  const noopStep: StepHandle = {
25
- name: null,
47
+ journeyName: null,
26
48
  setAttribute() {},
27
49
  setTags() {},
28
- complete() {},
29
- fail() {},
30
- tag: () => ({}),
50
+ completeJourney() {},
51
+ failJourney() {},
52
+ stamp: () => undefined,
31
53
  };
32
54
 
33
- function toName(target: string | { readonly name: string }): string {
34
- return typeof target === 'string' ? target : target.name;
55
+ function toName(target: JourneyNameLike): string {
56
+ if (typeof target === 'string') {
57
+ return target;
58
+ }
59
+ return 'config' in target ? target.config.name : target.name;
35
60
  }
36
61
 
37
62
  /** @internal Private — not part of the public package API. */
38
63
  export interface JourneyRuntimeOptions {
39
- /** Initial sinks. Defaults to `[]` — package bootstrap installs Datadog on the default runtime. */
64
+ /**
65
+ * DevTools label for this engine copy (`__stJourney.getDebugState().runtimes`).
66
+ * Auto-assigned `runtime-N` from the current `__stJourney` registration count
67
+ * when omitted at `registerJourneyRuntime`.
68
+ */
69
+ id?: string;
70
+ /** Initial sinks. Defaults to `[]` — package bootstrap installs Datadog on the module runtime. */
40
71
  sinks?: readonly JourneySink[];
41
- requestTimeouts?: AppRequestTimeouts;
72
+ slowRequests?: SlowRequestConfig;
73
+ /** Cap recorded steps; default 50. */
74
+ maxSteps?: number;
75
+ /** Default timeout when JourneyDef.timeoutMs is omitted. 0 disables idle countdown. */
76
+ defaultJourneyIdleMs?: number;
77
+ }
78
+
79
+ /** @internal Read-only snapshot for DevTools (`__stJourney.getDebugState`; empty unless debugging). */
80
+ export interface JourneyRuntimeDebugSnapshot {
81
+ id: string;
82
+ openJourneys: readonly {
83
+ name: string;
84
+ team: string;
85
+ group: string;
86
+ service: string;
87
+ verdict: Outcome;
88
+ reason: string | null;
89
+ tags: Record<string, TagValue>;
90
+ stepsInFlight: readonly string[];
91
+ steps: readonly {
92
+ name: string;
93
+ outcome: 'good' | 'bad';
94
+ durationMs: number;
95
+ reason?: string;
96
+ attributedRequestKeys?: readonly string[];
97
+ }[];
98
+ }[];
99
+ activeStep: { journey: string; name: string } | null;
100
+ /** Which shared policy knobs this runtime overrode (false = inherit global). */
101
+ policyOverlay: {
102
+ slowRequests: boolean;
103
+ httpAbortedRequests: boolean;
104
+ httpClientErrorRequests: boolean;
105
+ autoAttributeRequests: boolean;
106
+ };
107
+ }
108
+
109
+ function freezeJourneyDef(config: JourneyDef, tags: Record<string, TagValue>): JourneyDef {
110
+ const endpoints = sortEndpointsByLongestMatch(config.endpoints);
111
+ return Object.freeze({
112
+ ...config,
113
+ timeoutMs: config.timeoutMs !== undefined ? finiteTimeoutMs(config.timeoutMs) : undefined,
114
+ stepTimeoutMs:
115
+ config.stepTimeoutMs !== undefined ? finiteTimeoutMs(config.stepTimeoutMs) : undefined,
116
+ slowRequestMs: finiteSlowRequestMs(config.slowRequestMs),
117
+ maxSteps: config.maxSteps !== undefined ? clampMaxSteps(config.maxSteps) : undefined,
118
+ endpoints: endpoints ? Object.freeze([...endpoints]) : undefined,
119
+ expected: config.expected ? Object.freeze([...config.expected]) : undefined,
120
+ tags: Object.freeze({ ...tags }),
121
+ });
42
122
  }
43
123
 
44
124
  /**
45
125
  * @internal Private — not part of the public package API.
46
126
  * Isolated journey engine (open journeys, ambient steps, sinks, request thresholds).
47
127
  * App code should keep using free helpers (`defineJourney`, `journeyStep`, …) — they
48
- * bind to the process default runtime. Prefer `resetJourney()` in tests.
128
+ * bind to the module runtime. Prefer `resetJourney()` in tests.
49
129
  */
50
130
  export interface JourneyRuntime {
51
- getRequestTimeouts(): AppRequestTimeouts;
52
- setRequestTimeouts(next: AppRequestTimeouts): void;
131
+ /** DevTools id for this engine copy. Auto `runtime-N` when registered without an id. */
132
+ readonly id: string;
133
+ getSlowRequests(): SlowRequestConfig;
134
+ setSlowRequests(next: SlowRequestConfig): void;
135
+ getMaxSteps(): number;
136
+ setMaxSteps(next: number): void;
137
+ getDefaultJourneyIdleMs(): number;
138
+ setDefaultJourneyIdleMs(next: number): void;
53
139
 
54
140
  setSinks(next: readonly JourneySink[]): void;
141
+ /** First `configureJourney({ sinks })` on this runtime wins. Returns false if already set. */
142
+ trySetConfiguredSinks(next: readonly JourneySink[]): boolean;
55
143
  addSink(sink: JourneySink): void;
56
144
  emit(payload: JourneyEvent): void;
57
145
 
146
+ setPolicyOverlay(next: {
147
+ slowRequests?: SlowRequestConfig;
148
+ httpAbortedRequests?: RequestPolicyConfig;
149
+ httpClientErrorRequests?: RequestPolicyConfig;
150
+ autoAttributeRequests?: boolean;
151
+ }): void;
152
+ getAutoAttributeRequests(): boolean;
153
+ getHttpAbortedRequestsPolicy(): RequestPolicyConfig;
154
+ getHttpClientErrorRequestsPolicy(): RequestPolicyConfig;
155
+
58
156
  getOpenJourney(name: string): JourneyState | undefined;
59
- firstOpenJourney(names: readonly string[]): string | null;
157
+ /** @internal DevTools snapshot of open journeys / ambient step. */
158
+ getDebugSnapshot(): JourneyRuntimeDebugSnapshot;
159
+ firstOpenJourneyName(names: readonly string[]): string | null;
60
160
  setJourneyTags(name: string | null, tags: Record<string, TagValue>): void;
61
- startJourney(def: JourneyDef): void;
62
- failJourneyNow(journey: JourneyState, reason: string): void;
63
- failJourney(name: string, reason: string): void;
161
+ startJourney(config: JourneyDef): void;
162
+ failJourney(journey: JourneyState, reason: string): void;
64
163
  completeJourney(name: string | null, tags?: Record<string, TagValue>): void;
65
- excludeJourney(name: string): void;
164
+ completeJourneyState(journey: JourneyState, tags?: Record<string, TagValue>): void;
165
+ excludeJourney(journey: JourneyState): void;
166
+ updateOpenJourney(config: JourneyDef): void;
66
167
 
67
- activeJourneyStep(): JourneyStepRef | null;
168
+ activeJourneyStep(): StepRecord | null;
68
169
  journeyStep<T>(
69
170
  journey: JourneyStepTarget,
70
171
  stepName: string,
@@ -79,30 +180,71 @@ export interface JourneyRuntime {
79
180
  ): Promise<T>;
80
181
 
81
182
  reportBackendRequest(
82
- step: JourneyStepRef | undefined,
183
+ step: StepRecord | undefined,
83
184
  status: number | undefined,
84
185
  durationMs: number,
85
186
  requestKey: string
86
187
  ): void;
188
+ /** True when this request key is ignored for the step's journey (never changes verdict). */
189
+ shouldIgnoreRequest(journey: JourneyState, key: string): boolean;
87
190
 
88
191
  /** Close open journeys, clear ambient steps, restore default timeouts/sinks. */
89
192
  reset(options?: JourneyRuntimeOptions): void;
90
193
  }
91
194
 
195
+ function bindJourneyEngine(
196
+ api: JourneyRuntime,
197
+ journey: JourneyState,
198
+ shouldIgnoreRequest: (target: JourneyState, key: string) => boolean
199
+ ): JourneyEngine {
200
+ return {
201
+ failJourney(reason) {
202
+ api.failJourney(journey, reason);
203
+ },
204
+ exclude() {
205
+ api.excludeJourney(journey);
206
+ },
207
+ shouldIgnoreRequest(key) {
208
+ return shouldIgnoreRequest(journey, key);
209
+ },
210
+ reportBackendRequest(step, status, durationMs, requestKey) {
211
+ api.reportBackendRequest(step, status, durationMs, requestKey);
212
+ },
213
+ getHttpAbortedRequestsPolicy() {
214
+ return api.getHttpAbortedRequestsPolicy();
215
+ },
216
+ getHttpClientErrorRequestsPolicy() {
217
+ return api.getHttpClientErrorRequestsPolicy();
218
+ },
219
+ };
220
+ }
221
+
92
222
  /**
93
223
  * @internal Private — not part of the public package API.
94
224
  * Build an isolated engine. Rarely needed — apps use the default via free helpers.
95
225
  */
96
226
  export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): JourneyRuntime {
227
+ let id = typeof options.id === 'string' && options.id !== '' ? options.id : '';
97
228
  const journeys = new Map<string, JourneyState>();
98
229
  const inFlight = new Set<StepRecord>();
99
230
  let sinks: JourneySink[] = [...(options.sinks ?? [])];
100
- let requestTimeouts: AppRequestTimeouts = {
101
- defaultMs: options.requestTimeouts?.defaultMs ?? DEFAULT_REQUEST_TIMEOUTS.defaultMs,
231
+ let sinksConfigured = false;
232
+ let overlay: {
233
+ slowRequests?: SlowRequestConfig;
234
+ httpAbortedRequests?: RequestPolicyConfig;
235
+ httpClientErrorRequests?: RequestPolicyConfig;
236
+ autoAttributeRequests?: boolean;
237
+ } = {};
238
+ let slowRequests: SlowRequestConfig = {
239
+ defaultMs: options.slowRequests?.defaultMs ?? DEFAULT_SLOW_REQUESTS.defaultMs,
102
240
  endpoints: sortEndpointsByLongestMatch(
103
- options.requestTimeouts?.endpoints ?? DEFAULT_REQUEST_TIMEOUTS.endpoints
241
+ options.slowRequests?.endpoints ?? DEFAULT_SLOW_REQUESTS.endpoints
104
242
  ),
105
243
  };
244
+ let maxSteps = clampMaxSteps(options.maxSteps ?? DEFAULT_MAX_STEPS);
245
+ let defaultJourneyIdleMs = finiteTimeoutMs(
246
+ options.defaultJourneyIdleMs ?? DEFAULT_JOURNEY_IDLE_MS
247
+ );
106
248
 
107
249
  function getInFlightSteps(journey: JourneyState): StepRecord[] {
108
250
  const out: StepRecord[] = [];
@@ -122,6 +264,18 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
122
264
  inFlight.delete(step);
123
265
  }
124
266
 
267
+ function releaseInFlightSteps(journey: JourneyState): void {
268
+ for (const step of [...inFlight]) {
269
+ if (step.journey !== journey) {
270
+ continue;
271
+ }
272
+ if (step.durationMs === 0) {
273
+ step.durationMs = Math.round(globalThis.performance.now() - step.startedAt);
274
+ }
275
+ inFlight.delete(step);
276
+ }
277
+ }
278
+
125
279
  function clearCountdown(journey: JourneyState): void {
126
280
  if (journey.timer != null) {
127
281
  clearTimeout(journey.timer);
@@ -130,9 +284,10 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
130
284
  }
131
285
 
132
286
  function emit(payload: JourneyEvent): void {
287
+ const safePayload = sanitizeJourneyEvent(payload);
133
288
  for (const sink of sinks) {
134
289
  try {
135
- sink(payload);
290
+ sink(safePayload);
136
291
  } catch {
137
292
  // One sinking throw must not block the remaining sinks.
138
293
  }
@@ -144,19 +299,16 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
144
299
  return;
145
300
  }
146
301
  journey.closed = true;
147
- journeys.delete(journey.name);
148
- clearCountdown(journey);
149
-
150
- for (const step of getInFlightSteps(journey)) {
151
- if (step.durationMs === 0) {
152
- step.durationMs = Math.round(globalThis.performance.now() - step.startedAt);
153
- }
302
+ if (journeys.get(journey.name) === journey) {
303
+ journeys.delete(journey.name);
154
304
  }
305
+ clearCountdown(journey);
306
+ releaseInFlightSteps(journey);
155
307
 
156
308
  let finalReason = journey.reason ?? reason;
157
309
  let finalOutcome = outcome;
158
310
  const durationMs = Math.round(globalThis.performance.now() - journey.startedAt);
159
- if (journey.timeoutMs > 0 && durationMs > journey.timeoutMs) {
311
+ if (journey.timeoutExplicit && journey.timeoutMs > 0 && durationMs > journey.timeoutMs) {
160
312
  finalOutcome = 'bad';
161
313
  finalReason = finalReason ?? 'journey-timeout';
162
314
  }
@@ -168,7 +320,7 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
168
320
  outcome: s.outcome,
169
321
  ...(s.reason ? { reason: s.reason } : {}),
170
322
  ...(s.httpStatus !== undefined ? { httpStatus: s.httpStatus } : {}),
171
- ...(Object.keys(s.attributes).length ? { attributes: s.attributes } : {}),
323
+ ...(Object.keys(s.attributes).length ? { attributes: { ...s.attributes } } : {}),
172
324
  }));
173
325
 
174
326
  emit({
@@ -178,48 +330,47 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
178
330
  group: journey.group,
179
331
  service: journey.service,
180
332
  outcome: finalOutcome,
181
- ...(finalOutcome === 'bad' && finalReason ? { reason: finalReason } : {}),
333
+ ...(finalReason ? { reason: finalReason } : {}),
182
334
  durationMs,
183
335
  steps,
184
336
  ...(journey.expected ? { expected: journey.expected } : {}),
185
- ...(Object.keys(journey.tags).length ? { tags: journey.tags } : {}),
337
+ ...(Object.keys(journey.tags).length ? { tags: { ...journey.tags } } : {}),
186
338
  },
187
339
  });
188
340
  }
189
341
 
190
- function armCountdown(journey: JourneyState): void {
191
- if (journey.timeoutMs <= 0) {
342
+ function armCountdown(journey: JourneyState, delayMs: number = journey.timeoutMs): void {
343
+ if (delayMs <= 0) {
192
344
  return;
193
345
  }
194
346
  journey.timer = setTimeout(() => {
195
- finish(journey, 'bad', 'journey-timeout');
196
- }, journey.timeoutMs);
347
+ if (journey.timeoutExplicit) {
348
+ finish(journey, 'bad', 'journey-timeout');
349
+ return;
350
+ }
351
+ finish(journey, journey.verdict === 'bad' ? 'bad' : 'excluded', 'journey-idle-timeout');
352
+ }, delayMs);
197
353
  }
198
354
 
199
- function resolveRequestTimeoutMs(journey: JourneyState, key: string): number {
200
- const own = longestEndpointMatch(journey.endpoints, key);
201
- if (own?.timeoutMs) {
202
- return own.timeoutMs;
203
- }
204
-
205
- const app = longestEndpointMatch(requestTimeouts.endpoints, key);
206
- if (app?.timeoutMs) {
207
- return app.timeoutMs;
208
- }
209
-
210
- if (journey.requestTimeoutMs != null) {
211
- return journey.requestTimeoutMs;
212
- }
213
-
214
- return requestTimeouts.defaultMs;
355
+ function effectiveSlowRequests(): SlowRequestConfig {
356
+ return overlay.slowRequests ?? getSharedSlowRequests() ?? slowRequests;
215
357
  }
216
358
 
217
359
  function shouldIgnoreRequest(journey: JourneyState, key: string): boolean {
218
- const own = longestEndpointMatch(journey.endpoints, key);
219
- if (own) {
220
- return own.ignore === true;
360
+ return resolveEndpointPolicy(journey, effectiveSlowRequests(), key).ignore;
361
+ }
362
+
363
+ function trackAttributedKey(step: StepRecord, requestKey: string): void {
364
+ if (!requestKey) {
365
+ return;
366
+ }
367
+ step.attributedRequestKeys ??= [];
368
+ if (step.attributedRequestKeys.length >= MAX_ATTRIBUTED_REQUEST_KEYS) {
369
+ return;
370
+ }
371
+ if (!step.attributedRequestKeys.includes(requestKey)) {
372
+ step.attributedRequestKeys.push(requestKey);
221
373
  }
222
- return longestEndpointMatch(requestTimeouts.endpoints, key)?.ignore === true;
223
374
  }
224
375
 
225
376
  function resolveJourneyName(journey: JourneyStepTarget): string | null {
@@ -227,58 +378,178 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
227
378
  return null;
228
379
  }
229
380
  if (typeof journey === 'string' || !Array.isArray(journey)) {
230
- return toName(journey as string | { readonly name: string });
381
+ return toName(journey as JourneyNameLike);
382
+ }
383
+ return api.firstOpenJourneyName(journey.map(toName));
384
+ }
385
+
386
+ function configFromTarget(target: JourneyNameLike): JourneyDef | undefined {
387
+ if (typeof target === 'string') {
388
+ return undefined;
389
+ }
390
+ if ('config' in target && target.config != null && typeof target.config === 'object') {
391
+ return target.config;
392
+ }
393
+ const rec = target as {
394
+ name?: unknown;
395
+ team?: unknown;
396
+ group?: unknown;
397
+ service?: unknown;
398
+ };
399
+ if (
400
+ typeof rec.name === 'string' &&
401
+ typeof rec.team === 'string' &&
402
+ typeof rec.group === 'string' &&
403
+ typeof rec.service === 'string'
404
+ ) {
405
+ return target as JourneyDef;
406
+ }
407
+ return undefined;
408
+ }
409
+
410
+ function maybeAutoStart(target: JourneyStepTarget): void {
411
+ if (target == null || typeof target === 'string' || Array.isArray(target)) {
412
+ return;
413
+ }
414
+ const def = configFromTarget(target as JourneyNameLike);
415
+ if (def && !api.getOpenJourney(def.name)) {
416
+ api.startJourney(def);
231
417
  }
232
- return api.firstOpenJourney(journey.map(toName));
233
418
  }
234
419
 
235
420
  function createStepHandle(journeyName: string, step: StepRecord): StepHandle {
236
421
  return {
237
- name: journeyName,
422
+ journeyName,
238
423
  setAttribute(key, value) {
239
- step.attributes[key] = value;
424
+ setSafeAttribute(step.attributes, key, value);
240
425
  },
241
426
  setTags(tags) {
242
- api.setJourneyTags(journeyName, tags);
427
+ if (step.journey.closed) {
428
+ return;
429
+ }
430
+ assignRecord(step.journey.tags, tags, MAX_TAG_KEYS);
243
431
  },
244
- complete(tags) {
245
- api.completeJourney(journeyName, tags);
432
+ completeJourney(tags) {
433
+ api.completeJourneyState(step.journey, tags);
246
434
  },
247
- fail(reason) {
248
- api.failJourney(journeyName, reason);
435
+ failJourney(reason) {
436
+ api.failJourney(step.journey, reason);
249
437
  },
250
- // Box in JourneyStepTag — raw StepRecord is circular and blows jQuery/axios deep-merge.
251
- tag: () => ({ journeyStep: new JourneyStepTag(step) }),
438
+ // Box in JourneyStepStamp — raw StepRecord is circular and blows jQuery/axios deep-merge.
439
+ stamp: scoreOrOpts => new JourneyStepStamp(step, resolveStampOpts(scoreOrOpts)),
252
440
  };
253
441
  }
254
442
 
255
443
  const api: JourneyRuntime = {
256
- getRequestTimeouts() {
257
- return requestTimeouts;
444
+ get id() {
445
+ return id;
446
+ },
447
+ set id(next: string) {
448
+ id = next;
449
+ },
450
+ getSlowRequests() {
451
+ return effectiveSlowRequests();
258
452
  },
259
453
 
260
- setRequestTimeouts(next) {
261
- requestTimeouts = {
454
+ setSlowRequests(next) {
455
+ slowRequests = {
262
456
  ...next,
263
457
  endpoints: sortEndpointsByLongestMatch(next.endpoints),
264
458
  };
265
459
  },
266
460
 
461
+ getMaxSteps() {
462
+ return maxSteps;
463
+ },
464
+
465
+ setMaxSteps(next) {
466
+ maxSteps = clampMaxSteps(next);
467
+ },
468
+
469
+ getDefaultJourneyIdleMs() {
470
+ return defaultJourneyIdleMs;
471
+ },
472
+
473
+ setDefaultJourneyIdleMs(next) {
474
+ defaultJourneyIdleMs = finiteTimeoutMs(next);
475
+ },
476
+
267
477
  setSinks(next) {
268
478
  sinks = [...next];
269
479
  },
270
480
 
481
+ trySetConfiguredSinks(next) {
482
+ if (sinksConfigured) {
483
+ return false;
484
+ }
485
+ sinks = [...next];
486
+ sinksConfigured = true;
487
+ return true;
488
+ },
489
+
271
490
  addSink(sink) {
272
491
  sinks.push(sink);
273
492
  },
274
493
 
275
494
  emit,
276
495
 
496
+ setPolicyOverlay(next) {
497
+ overlay = { ...overlay, ...next };
498
+ },
499
+
500
+ getAutoAttributeRequests() {
501
+ if (overlay.autoAttributeRequests !== undefined) {
502
+ return overlay.autoAttributeRequests;
503
+ }
504
+ return getSharedAutoAttributeRequests();
505
+ },
506
+
507
+ getHttpAbortedRequestsPolicy() {
508
+ return overlay.httpAbortedRequests ?? getSharedHttpAbortedRequestsPolicy();
509
+ },
510
+
511
+ getHttpClientErrorRequestsPolicy() {
512
+ return overlay.httpClientErrorRequests ?? getSharedHttpClientErrorRequestsPolicy();
513
+ },
514
+
277
515
  getOpenJourney(name) {
278
516
  return journeys.get(name);
279
517
  },
280
518
 
281
- firstOpenJourney(names) {
519
+ getDebugSnapshot() {
520
+ const active = api.activeJourneyStep();
521
+ return {
522
+ id: api.id,
523
+ openJourneys: [...journeys.values()].map(journey => ({
524
+ name: journey.name,
525
+ team: journey.team,
526
+ group: journey.group,
527
+ service: journey.service,
528
+ verdict: journey.verdict,
529
+ reason: journey.reason,
530
+ tags: { ...journey.tags },
531
+ stepsInFlight: getInFlightSteps(journey).map(step => step.name),
532
+ steps: journey.steps.map(step => ({
533
+ name: step.name,
534
+ outcome: step.outcome,
535
+ durationMs: step.durationMs,
536
+ ...(step.reason ? { reason: step.reason } : {}),
537
+ ...(step.attributedRequestKeys?.length
538
+ ? { attributedRequestKeys: [...step.attributedRequestKeys] }
539
+ : {}),
540
+ })),
541
+ })),
542
+ activeStep: active ? { journey: active.journey.name, name: active.name } : null,
543
+ policyOverlay: {
544
+ slowRequests: overlay.slowRequests != null,
545
+ httpAbortedRequests: overlay.httpAbortedRequests != null,
546
+ httpClientErrorRequests: overlay.httpClientErrorRequests != null,
547
+ autoAttributeRequests: overlay.autoAttributeRequests != null,
548
+ },
549
+ };
550
+ },
551
+
552
+ firstOpenJourneyName(names) {
282
553
  for (const name of names) {
283
554
  if (journeys.has(name)) {
284
555
  return name;
@@ -290,39 +561,51 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
290
561
  setJourneyTags(name, tags) {
291
562
  const journey = name ? journeys.get(name) : undefined;
292
563
  if (journey) {
293
- Object.assign(journey.tags, tags);
564
+ assignRecord(journey.tags, tags, MAX_TAG_KEYS);
294
565
  }
295
566
  },
296
567
 
297
- startJourney(def) {
298
- const existing = journeys.get(def.name);
568
+ startJourney(config) {
569
+ const existing = journeys.get(config.name);
299
570
  if (existing) {
300
571
  finish(existing, existing.verdict === 'bad' ? 'bad' : 'excluded');
301
572
  }
302
573
 
574
+ const tags = emptyRecord();
575
+ assignRecord(tags, config.tags, MAX_TAG_KEYS);
576
+ const snapshot = freezeJourneyDef(config, tags);
577
+ const timeout = resolveJourneyTimeoutMs(snapshot.timeoutMs, defaultJourneyIdleMs);
303
578
  const journey: JourneyState = {
304
- name: def.name,
305
- team: def.team,
306
- group: def.group,
307
- service: def.service,
579
+ config: snapshot,
580
+ name: snapshot.name,
581
+ team: snapshot.team,
582
+ group: snapshot.group,
583
+ service: snapshot.service,
308
584
  verdict: 'good',
309
585
  startedAt: globalThis.performance.now(),
310
- timeoutMs: def.timeoutMs ?? 0,
311
- stepTimeoutMs: def.stepTimeoutMs,
586
+ timeoutMs: timeout.ms,
587
+ timeoutExplicit: timeout.explicit,
588
+ stepTimeoutMs:
589
+ snapshot.stepTimeoutMs !== undefined
590
+ ? finiteTimeoutMs(snapshot.stepTimeoutMs)
591
+ : undefined,
312
592
  reason: null,
313
593
  steps: [],
314
- expected: def.steps ? [...def.steps] : null,
315
- tags: { ...(def.tags ?? {}) },
316
- requestTimeoutMs: def.requestTimeoutMs,
317
- endpoints: sortEndpointsByLongestMatch(def.endpoints),
594
+ expected: snapshot.expected ? [...snapshot.expected] : null,
595
+ tags,
596
+ slowRequestMs: finiteSlowRequestMs(snapshot.slowRequestMs),
597
+ endpoints: snapshot.endpoints,
318
598
  timer: null,
319
599
  closed: false,
600
+ maxSteps:
601
+ snapshot.maxSteps !== undefined ? clampMaxSteps(snapshot.maxSteps) : undefined,
320
602
  };
321
- journeys.set(def.name, journey);
603
+ journeys.set(config.name, journey);
604
+ journey[JOURNEY_ENGINE] = bindJourneyEngine(api, journey, shouldIgnoreRequest);
322
605
  armCountdown(journey);
323
606
  },
324
607
 
325
- failJourneyNow(journey, reason) {
608
+ failJourney(journey, reason) {
326
609
  if (journey.closed) {
327
610
  return;
328
611
  }
@@ -331,34 +614,81 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
331
614
  finish(journey, 'bad');
332
615
  },
333
616
 
334
- failJourney(name, reason) {
617
+ completeJourney(name, tags) {
618
+ if (!name) {
619
+ return;
620
+ }
335
621
  const journey = journeys.get(name);
336
622
  if (journey) {
337
- api.failJourneyNow(journey, reason);
623
+ api.completeJourneyState(journey, tags);
338
624
  }
339
625
  },
340
626
 
341
- completeJourney(name, tags) {
342
- if (!name) {
627
+ completeJourneyState(journey, tags) {
628
+ if (journey.closed) {
343
629
  return;
344
630
  }
345
631
  if (tags) {
346
- api.setJourneyTags(name, tags);
632
+ assignRecord(journey.tags, tags, MAX_TAG_KEYS);
347
633
  }
348
- const journey = journeys.get(name);
349
- if (journey) {
350
- finish(journey, journey.verdict === 'bad' ? 'bad' : 'good');
634
+ finish(journey, journey.verdict === 'bad' ? 'bad' : 'good');
635
+ },
636
+
637
+ excludeJourney(journey) {
638
+ if (journey.closed) {
639
+ return;
351
640
  }
641
+ finish(journey, journey.verdict === 'bad' ? 'bad' : 'excluded');
352
642
  },
353
643
 
354
- excludeJourney(name) {
355
- const journey = journeys.get(name);
356
- if (journey) {
357
- finish(journey, journey.verdict === 'bad' ? 'bad' : 'excluded');
644
+ updateOpenJourney(config) {
645
+ const journey = journeys.get(config.name);
646
+ if (!journey || journey.closed) {
647
+ return;
648
+ }
649
+ journey.team = config.team;
650
+ journey.group = config.group;
651
+ journey.service = config.service;
652
+ const timeout = resolveJourneyTimeoutMs(config.timeoutMs, defaultJourneyIdleMs);
653
+ journey.timeoutMs = timeout.ms;
654
+ journey.timeoutExplicit = timeout.explicit;
655
+ journey.stepTimeoutMs =
656
+ config.stepTimeoutMs !== undefined
657
+ ? finiteTimeoutMs(config.stepTimeoutMs)
658
+ : undefined;
659
+ journey.slowRequestMs = finiteSlowRequestMs(config.slowRequestMs);
660
+ journey.endpoints = sortEndpointsByLongestMatch(config.endpoints);
661
+ journey.expected = config.expected ? [...config.expected] : null;
662
+ journey.maxSteps =
663
+ config.maxSteps !== undefined ? clampMaxSteps(config.maxSteps) : undefined;
664
+ if (config.tags) {
665
+ assignRecord(journey.tags, config.tags, MAX_TAG_KEYS);
358
666
  }
667
+ clearCountdown(journey);
668
+ if (journey.timeoutMs <= 0) {
669
+ return;
670
+ }
671
+ const elapsedMs = globalThis.performance.now() - journey.startedAt;
672
+ const remainingMs = journey.timeoutMs - elapsedMs;
673
+ if (remainingMs <= 0) {
674
+ if (journey.timeoutExplicit) {
675
+ finish(journey, 'bad', 'journey-timeout');
676
+ return;
677
+ }
678
+ finish(
679
+ journey,
680
+ journey.verdict === 'bad' ? 'bad' : 'excluded',
681
+ 'journey-idle-timeout'
682
+ );
683
+ return;
684
+ }
685
+ armCountdown(journey, remainingMs);
359
686
  },
360
687
 
361
688
  activeJourneyStep() {
689
+ if (!api.getAutoAttributeRequests()) {
690
+ return null;
691
+ }
362
692
  if (inFlight.size !== 1) {
363
693
  return null;
364
694
  }
@@ -366,9 +696,14 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
366
696
  },
367
697
 
368
698
  async journeyStep(journey, stepName, fn, opts) {
699
+ maybeAutoStart(journey);
369
700
  const journeyName = resolveJourneyName(journey);
370
701
  const open = journeyName ? journeys.get(journeyName) : undefined;
371
702
  if (!open || !journeyName) {
703
+ if (isJourneyDebugEnabled()) {
704
+ // eslint-disable-next-line no-console -- debug-only untraced hint
705
+ console.debug(`[journey] step "${stepName}" ran untraced (no open journey)`);
706
+ }
372
707
  return fn(noopStep);
373
708
  }
374
709
 
@@ -377,10 +712,15 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
377
712
  startedAt: globalThis.performance.now(),
378
713
  durationMs: 0,
379
714
  outcome: 'good',
380
- attributes: {},
715
+ attributes: emptyRecord(),
381
716
  journey: open,
382
717
  };
383
- open.steps.push(step);
718
+ const cap = open.maxSteps ?? maxSteps;
719
+ if (open.steps.length >= cap) {
720
+ open.tags.steps_truncated = true;
721
+ } else {
722
+ open.steps.push(step);
723
+ }
384
724
 
385
725
  const handle = createStepHandle(journeyName, step);
386
726
 
@@ -395,7 +735,7 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
395
735
  step.reason ??= 'step-timeout';
396
736
  step.attributes.timeout_ms ??= stepTimeoutMs;
397
737
  step.durationMs = Math.round(globalThis.performance.now() - step.startedAt);
398
- api.failJourneyNow(open, 'step-timeout');
738
+ api.failJourney(open, 'step-timeout');
399
739
  }, stepTimeoutMs);
400
740
  }
401
741
 
@@ -403,11 +743,16 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
403
743
  try {
404
744
  return await fn(handle);
405
745
  } catch (err) {
406
- const message = err instanceof Error ? err.message : String(err);
407
- step.outcome = 'bad';
408
- step.reason ??= 'step-error';
409
- step.attributes.error ??= message;
410
- api.failJourneyNow(open, 'step-error');
746
+ if (isCanceledError(err)) {
747
+ /*
748
+ * Attributed abort is scored in onHttpComplete. Unattributed /
749
+ * ignored abort throws must not apply policy (empty requestKey).
750
+ */
751
+ } else {
752
+ step.outcome = 'bad';
753
+ step.reason ??= 'step-error';
754
+ api.failJourney(open, 'step-error');
755
+ }
411
756
  throw err;
412
757
  } finally {
413
758
  if (timeoutTimer != null) {
@@ -425,20 +770,29 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
425
770
  return api.journeyStep(journey, stepName, fn, opts);
426
771
  },
427
772
 
773
+ shouldIgnoreRequest,
774
+
428
775
  reportBackendRequest(step, status, durationMs, requestKey) {
429
- if (!step) {
776
+ if (!step?.journey) {
430
777
  return;
431
778
  }
432
779
  const journey = step.journey;
433
780
  if (journey.closed) {
434
781
  return;
435
782
  }
436
- if (shouldIgnoreRequest(journey, requestKey)) {
783
+ trackAttributedKey(step, requestKey);
784
+ const endpointPolicy = resolveEndpointPolicy(
785
+ journey,
786
+ effectiveSlowRequests(),
787
+ requestKey
788
+ );
789
+ if (endpointPolicy.ignore) {
437
790
  return;
438
791
  }
439
792
 
440
793
  const failed = status === undefined || status >= 500;
441
- const slow = durationMs > resolveRequestTimeoutMs(journey, requestKey);
794
+ const threshold = endpointPolicy.slowRequestMs;
795
+ const slow = threshold > 0 && durationMs > threshold;
442
796
  if (!failed && !slow) {
443
797
  return;
444
798
  }
@@ -451,7 +805,7 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
451
805
  step.httpStatus = status;
452
806
  }
453
807
 
454
- api.failJourneyNow(journey, category);
808
+ api.failJourney(journey, category);
455
809
  },
456
810
 
457
811
  reset(next = {}) {
@@ -461,13 +815,20 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
461
815
  }
462
816
  journeys.clear();
463
817
  inFlight.clear();
818
+ overlay = {};
819
+ sinksConfigured = false;
464
820
  sinks = [...(next.sinks ?? [])];
465
- requestTimeouts = {
466
- defaultMs: next.requestTimeouts?.defaultMs ?? DEFAULT_REQUEST_TIMEOUTS.defaultMs,
821
+ slowRequests = {
822
+ defaultMs: next.slowRequests?.defaultMs ?? DEFAULT_SLOW_REQUESTS.defaultMs,
467
823
  endpoints: sortEndpointsByLongestMatch(
468
- next.requestTimeouts?.endpoints ?? DEFAULT_REQUEST_TIMEOUTS.endpoints
824
+ next.slowRequests?.endpoints ?? DEFAULT_SLOW_REQUESTS.endpoints
469
825
  ),
470
826
  };
827
+ maxSteps =
828
+ next.maxSteps !== undefined ? clampMaxSteps(next.maxSteps) : DEFAULT_MAX_STEPS;
829
+ defaultJourneyIdleMs = finiteTimeoutMs(
830
+ next.defaultJourneyIdleMs ?? DEFAULT_JOURNEY_IDLE_MS
831
+ );
471
832
  },
472
833
  };
473
834
 
@@ -476,14 +837,76 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
476
837
 
477
838
  /**
478
839
  * @internal Private — not part of the public package API.
479
- * Process-wide engine used by free helpers. App code does not need to touch this.
840
+ * Module-scoped engine used by free helpers. App code does not need to touch this.
841
+ * Each host/MFE bundle gets its own copy when the package is duplicated.
480
842
  */
481
- export const defaultRuntime: JourneyRuntime = createJourneyRuntime();
843
+ export const moduleRuntime: JourneyRuntime = createJourneyRuntime();
844
+
845
+ const noopEngine: JourneyEngine = {
846
+ failJourney() {},
847
+ exclude() {},
848
+ shouldIgnoreRequest() {
849
+ return false;
850
+ },
851
+ reportBackendRequest() {},
852
+ getHttpAbortedRequestsPolicy() {
853
+ return 'continue';
854
+ },
855
+ getHttpClientErrorRequestsPolicy() {
856
+ return 'continue';
857
+ },
858
+ };
859
+
860
+ /**
861
+ * True when this bundle can fail/exclude/report the journey: stamped engine, or
862
+ * this runtime's own unstamped object. False for a foreign older-copy journey.
863
+ */
864
+ export function canRouteHttpToJourney(journey: JourneyState): boolean {
865
+ return (
866
+ journey[JOURNEY_ENGINE] != null || moduleRuntime.getOpenJourney(journey.name) === journey
867
+ );
868
+ }
869
+
870
+ /**
871
+ * @internal Prefer the creating engine on a journey (MFE `step.stamp()` on a host transport).
872
+ * Falls back to this bundle's module runtime only when this runtime owns the object.
873
+ * Unstamped foreign journeys (older package copies) are a no-op so a same-named
874
+ * host journey is not deleted from this map.
875
+ */
876
+ export function resolveJourneyEngine(journey: JourneyState): JourneyEngine {
877
+ const stamped = journey[JOURNEY_ENGINE];
878
+ if (stamped) {
879
+ return stamped;
880
+ }
881
+ if (moduleRuntime.getOpenJourney(journey.name) !== journey) {
882
+ return noopEngine;
883
+ }
884
+ return {
885
+ failJourney(reason) {
886
+ moduleRuntime.failJourney(journey, reason);
887
+ },
888
+ exclude() {
889
+ moduleRuntime.excludeJourney(journey);
890
+ },
891
+ shouldIgnoreRequest(key) {
892
+ return moduleRuntime.shouldIgnoreRequest(journey, key);
893
+ },
894
+ reportBackendRequest(step, status, durationMs, requestKey) {
895
+ moduleRuntime.reportBackendRequest(step, status, durationMs, requestKey);
896
+ },
897
+ getHttpAbortedRequestsPolicy() {
898
+ return moduleRuntime.getHttpAbortedRequestsPolicy();
899
+ },
900
+ getHttpClientErrorRequestsPolicy() {
901
+ return moduleRuntime.getHttpClientErrorRequestsPolicy();
902
+ },
903
+ };
904
+ }
482
905
 
483
906
  /**
484
- * Reset the default engine (open journeys, ambient steps, sinks, request thresholds).
907
+ * @internal Reset the module engine (open journeys, ambient steps, sinks, slow-request thresholds).
485
908
  * Intended for tests — call in `beforeEach` / `afterEach` instead of manual cleanup.
486
909
  */
487
910
  export function resetJourney(options?: JourneyRuntimeOptions): void {
488
- defaultRuntime.reset(options);
911
+ moduleRuntime.reset(options);
489
912
  }