@servicetitan/journey 2.1.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 (163) hide show
  1. package/README.md +1 -1
  2. package/dist/__test-utils__/test-runtime.d.ts.map +1 -1
  3. package/dist/__test-utils__/test-runtime.js +9 -2
  4. package/dist/__test-utils__/test-runtime.js.map +1 -1
  5. package/dist/__tests__/core/sanitize.test.d.ts +2 -0
  6. package/dist/__tests__/core/sanitize.test.d.ts.map +1 -0
  7. package/dist/__tests__/hardening.test.d.ts +2 -0
  8. package/dist/__tests__/hardening.test.d.ts.map +1 -0
  9. package/dist/__tests__/integrations/host-mfe.test.d.ts +2 -0
  10. package/dist/__tests__/integrations/host-mfe.test.d.ts.map +1 -0
  11. package/dist/__tests__/integrations/use-journey-step.test.d.ts +2 -0
  12. package/dist/__tests__/integrations/use-journey-step.test.d.ts.map +1 -0
  13. package/dist/config.d.ts +57 -13
  14. package/dist/config.d.ts.map +1 -1
  15. package/dist/config.js +87 -24
  16. package/dist/config.js.map +1 -1
  17. package/dist/core/abort-error.d.ts +4 -0
  18. package/dist/core/abort-error.d.ts.map +1 -0
  19. package/dist/core/abort-error.js +22 -0
  20. package/dist/core/abort-error.js.map +1 -0
  21. package/dist/core/api.d.ts +22 -9
  22. package/dist/core/api.d.ts.map +1 -1
  23. package/dist/core/api.js +77 -30
  24. package/dist/core/api.js.map +1 -1
  25. package/dist/core/endpoint-policy.d.ts +14 -4
  26. package/dist/core/endpoint-policy.d.ts.map +1 -1
  27. package/dist/core/endpoint-policy.js +32 -1
  28. package/dist/core/endpoint-policy.js.map +1 -1
  29. package/dist/core/index.d.ts +4 -3
  30. package/dist/core/index.d.ts.map +1 -1
  31. package/dist/core/index.js +3 -3
  32. package/dist/core/index.js.map +1 -1
  33. package/dist/core/limits.d.ts +29 -0
  34. package/dist/core/limits.d.ts.map +1 -0
  35. package/dist/core/limits.js +63 -0
  36. package/dist/core/limits.js.map +1 -0
  37. package/dist/core/request-policy.d.ts +39 -0
  38. package/dist/core/request-policy.d.ts.map +1 -0
  39. package/dist/core/request-policy.js +18 -0
  40. package/dist/core/request-policy.js.map +1 -0
  41. package/dist/core/runtime.d.ts +65 -15
  42. package/dist/core/runtime.d.ts.map +1 -1
  43. package/dist/core/runtime.js +403 -126
  44. package/dist/core/runtime.js.map +1 -1
  45. package/dist/core/sanitize.d.ts +23 -0
  46. package/dist/core/sanitize.d.ts.map +1 -0
  47. package/dist/core/sanitize.js +143 -0
  48. package/dist/core/sanitize.js.map +1 -0
  49. package/dist/core/step-tag.d.ts +25 -6
  50. package/dist/core/step-tag.d.ts.map +1 -1
  51. package/dist/core/step-tag.js +56 -10
  52. package/dist/core/step-tag.js.map +1 -1
  53. package/dist/core/types.d.ts +73 -25
  54. package/dist/core/types.d.ts.map +1 -1
  55. package/dist/core/types.js +4 -1
  56. package/dist/core/types.js.map +1 -1
  57. package/dist/global.d.ts +91 -13
  58. package/dist/global.d.ts.map +1 -1
  59. package/dist/global.js +235 -25
  60. package/dist/global.js.map +1 -1
  61. package/dist/index.d.ts +7 -8
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +4 -6
  64. package/dist/index.js.map +1 -1
  65. package/dist/integrations/axios.d.ts +5 -1
  66. package/dist/integrations/axios.d.ts.map +1 -1
  67. package/dist/integrations/axios.js +146 -34
  68. package/dist/integrations/axios.js.map +1 -1
  69. package/dist/integrations/fetch.d.ts +3 -2
  70. package/dist/integrations/fetch.d.ts.map +1 -1
  71. package/dist/integrations/fetch.js +105 -26
  72. package/dist/integrations/fetch.js.map +1 -1
  73. package/dist/integrations/http-report.d.ts +27 -0
  74. package/dist/integrations/http-report.d.ts.map +1 -0
  75. package/dist/integrations/http-report.js +123 -0
  76. package/dist/integrations/http-report.js.map +1 -0
  77. package/dist/integrations/jquery/index.d.ts +8 -4
  78. package/dist/integrations/jquery/index.d.ts.map +1 -1
  79. package/dist/integrations/jquery/index.js +26 -8
  80. package/dist/integrations/jquery/index.js.map +1 -1
  81. package/dist/integrations/jquery/instrument.d.ts +14 -5
  82. package/dist/integrations/jquery/instrument.d.ts.map +1 -1
  83. package/dist/integrations/jquery/instrument.js +111 -29
  84. package/dist/integrations/jquery/instrument.js.map +1 -1
  85. package/dist/integrations/react/context.d.ts +4 -1
  86. package/dist/integrations/react/context.d.ts.map +1 -1
  87. package/dist/integrations/react/context.js +4 -1
  88. package/dist/integrations/react/context.js.map +1 -1
  89. package/dist/integrations/react/scope.d.ts +14 -3
  90. package/dist/integrations/react/scope.d.ts.map +1 -1
  91. package/dist/integrations/react/scope.js +46 -8
  92. package/dist/integrations/react/scope.js.map +1 -1
  93. package/dist/integrations/react/stable-config-keys.d.ts +6 -0
  94. package/dist/integrations/react/stable-config-keys.d.ts.map +1 -0
  95. package/dist/integrations/react/stable-config-keys.js +21 -0
  96. package/dist/integrations/react/stable-config-keys.js.map +1 -0
  97. package/dist/integrations/react/use-journey-step.d.ts +11 -0
  98. package/dist/integrations/react/use-journey-step.d.ts.map +1 -0
  99. package/dist/integrations/react/use-journey-step.js +19 -0
  100. package/dist/integrations/react/use-journey-step.js.map +1 -0
  101. package/dist/integrations/request-key.d.ts +8 -3
  102. package/dist/integrations/request-key.d.ts.map +1 -1
  103. package/dist/integrations/request-key.js +10 -9
  104. package/dist/integrations/request-key.js.map +1 -1
  105. package/dist/integrations/soft-skip.d.ts +7 -0
  106. package/dist/integrations/soft-skip.d.ts.map +1 -0
  107. package/dist/integrations/soft-skip.js +30 -0
  108. package/dist/integrations/soft-skip.js.map +1 -0
  109. package/dist/sinks/console.d.ts.map +1 -1
  110. package/dist/sinks/console.js +2 -1
  111. package/dist/sinks/console.js.map +1 -1
  112. package/dist/sinks/datadog.d.ts +1 -1
  113. package/dist/sinks/datadog.d.ts.map +1 -1
  114. package/dist/sinks/datadog.js +16 -20
  115. package/dist/sinks/datadog.js.map +1 -1
  116. package/package.json +2 -2
  117. package/src/__test-utils__/test-runtime.ts +9 -2
  118. package/src/__tests__/config.test.ts +150 -31
  119. package/src/__tests__/core/ambient.test.ts +16 -10
  120. package/src/__tests__/core/api.test.ts +7 -7
  121. package/src/__tests__/core/define-journey.test.ts +28 -11
  122. package/src/__tests__/core/requests.test.ts +49 -31
  123. package/src/__tests__/core/runtime.test.ts +67 -20
  124. package/src/__tests__/core/sanitize.test.ts +69 -0
  125. package/src/__tests__/core/step.test.ts +132 -36
  126. package/src/__tests__/core/timeouts.test.ts +69 -4
  127. package/src/__tests__/global.test.ts +44 -3
  128. package/src/__tests__/hardening.test.ts +613 -0
  129. package/src/__tests__/integrations/axios.test.ts +247 -27
  130. package/src/__tests__/integrations/expose-app-journey.test.ts +9 -3
  131. package/src/__tests__/integrations/fetch.test.ts +153 -19
  132. package/src/__tests__/integrations/host-mfe.test.ts +548 -0
  133. package/src/__tests__/integrations/jquery.test.ts +209 -34
  134. package/src/__tests__/integrations/scope.test.tsx +160 -2
  135. package/src/__tests__/integrations/use-journey-step.test.tsx +43 -0
  136. package/src/__tests__/sinks/console.test.ts +9 -4
  137. package/src/__tests__/sinks/datadog.test.ts +47 -0
  138. package/src/config.ts +162 -38
  139. package/src/core/abort-error.ts +24 -0
  140. package/src/core/api.ts +83 -33
  141. package/src/core/endpoint-policy.ts +46 -4
  142. package/src/core/index.ts +12 -4
  143. package/src/core/limits.ts +73 -0
  144. package/src/core/request-policy.ts +66 -0
  145. package/src/core/runtime.ts +507 -133
  146. package/src/core/sanitize.ts +177 -0
  147. package/src/core/step-tag.ts +82 -12
  148. package/src/core/types.ts +79 -25
  149. package/src/global.ts +312 -42
  150. package/src/index.ts +17 -17
  151. package/src/integrations/axios.ts +183 -45
  152. package/src/integrations/fetch.ts +125 -39
  153. package/src/integrations/http-report.ts +184 -0
  154. package/src/integrations/jquery/index.ts +34 -10
  155. package/src/integrations/jquery/instrument.ts +126 -42
  156. package/src/integrations/react/context.ts +4 -1
  157. package/src/integrations/react/scope.tsx +59 -9
  158. package/src/integrations/react/stable-config-keys.ts +28 -0
  159. package/src/integrations/react/use-journey-step.ts +31 -0
  160. package/src/integrations/request-key.ts +18 -10
  161. package/src/integrations/soft-skip.ts +35 -0
  162. package/src/sinks/console.ts +2 -1
  163. package/src/sinks/datadog.ts +23 -29
@@ -1,48 +1,84 @@
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,
12
+ type SlowRequestConfig,
5
13
  } 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,
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 {
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;
39
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;
42
77
  }
43
78
 
44
79
  /** @internal Read-only snapshot for DevTools (`__stJourney.getDebugState`; empty unless debugging). */
45
80
  export interface JourneyRuntimeDebugSnapshot {
81
+ id: string;
46
82
  openJourneys: readonly {
47
83
  name: string;
48
84
  team: string;
@@ -57,9 +93,32 @@ export interface JourneyRuntimeDebugSnapshot {
57
93
  outcome: 'good' | 'bad';
58
94
  durationMs: number;
59
95
  reason?: string;
96
+ attributedRequestKeys?: readonly string[];
60
97
  }[];
61
98
  }[];
62
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
+ });
63
122
  }
64
123
 
65
124
  /**
@@ -69,25 +128,44 @@ export interface JourneyRuntimeDebugSnapshot {
69
128
  * bind to the module runtime. Prefer `resetJourney()` in tests.
70
129
  */
71
130
  export interface JourneyRuntime {
72
- getRequestTimeouts(): AppRequestTimeouts;
73
- 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;
74
139
 
75
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;
76
143
  addSink(sink: JourneySink): void;
77
144
  emit(payload: JourneyEvent): void;
78
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
+
79
156
  getOpenJourney(name: string): JourneyState | undefined;
80
157
  /** @internal DevTools snapshot of open journeys / ambient step. */
81
158
  getDebugSnapshot(): JourneyRuntimeDebugSnapshot;
82
- firstOpenJourney(names: readonly string[]): string | null;
159
+ firstOpenJourneyName(names: readonly string[]): string | null;
83
160
  setJourneyTags(name: string | null, tags: Record<string, TagValue>): void;
84
- startJourney(def: JourneyDef): void;
85
- failJourneyNow(journey: JourneyState, reason: string): void;
86
- failJourney(name: string, reason: string): void;
161
+ startJourney(config: JourneyDef): void;
162
+ failJourney(journey: JourneyState, reason: string): void;
87
163
  completeJourney(name: string | null, tags?: Record<string, TagValue>): void;
88
- excludeJourney(name: string): void;
164
+ completeJourneyState(journey: JourneyState, tags?: Record<string, TagValue>): void;
165
+ excludeJourney(journey: JourneyState): void;
166
+ updateOpenJourney(config: JourneyDef): void;
89
167
 
90
- activeJourneyStep(): JourneyStepRef | null;
168
+ activeJourneyStep(): StepRecord | null;
91
169
  journeyStep<T>(
92
170
  journey: JourneyStepTarget,
93
171
  stepName: string,
@@ -102,30 +180,71 @@ export interface JourneyRuntime {
102
180
  ): Promise<T>;
103
181
 
104
182
  reportBackendRequest(
105
- step: JourneyStepRef | undefined,
183
+ step: StepRecord | undefined,
106
184
  status: number | undefined,
107
185
  durationMs: number,
108
186
  requestKey: string
109
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;
110
190
 
111
191
  /** Close open journeys, clear ambient steps, restore default timeouts/sinks. */
112
192
  reset(options?: JourneyRuntimeOptions): void;
113
193
  }
114
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
+
115
222
  /**
116
223
  * @internal Private — not part of the public package API.
117
224
  * Build an isolated engine. Rarely needed — apps use the default via free helpers.
118
225
  */
119
226
  export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): JourneyRuntime {
227
+ let id = typeof options.id === 'string' && options.id !== '' ? options.id : '';
120
228
  const journeys = new Map<string, JourneyState>();
121
229
  const inFlight = new Set<StepRecord>();
122
230
  let sinks: JourneySink[] = [...(options.sinks ?? [])];
123
- let requestTimeouts: AppRequestTimeouts = {
124
- 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,
125
240
  endpoints: sortEndpointsByLongestMatch(
126
- options.requestTimeouts?.endpoints ?? DEFAULT_REQUEST_TIMEOUTS.endpoints
241
+ options.slowRequests?.endpoints ?? DEFAULT_SLOW_REQUESTS.endpoints
127
242
  ),
128
243
  };
244
+ let maxSteps = clampMaxSteps(options.maxSteps ?? DEFAULT_MAX_STEPS);
245
+ let defaultJourneyIdleMs = finiteTimeoutMs(
246
+ options.defaultJourneyIdleMs ?? DEFAULT_JOURNEY_IDLE_MS
247
+ );
129
248
 
130
249
  function getInFlightSteps(journey: JourneyState): StepRecord[] {
131
250
  const out: StepRecord[] = [];
@@ -145,6 +264,18 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
145
264
  inFlight.delete(step);
146
265
  }
147
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
+
148
279
  function clearCountdown(journey: JourneyState): void {
149
280
  if (journey.timer != null) {
150
281
  clearTimeout(journey.timer);
@@ -153,9 +284,10 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
153
284
  }
154
285
 
155
286
  function emit(payload: JourneyEvent): void {
287
+ const safePayload = sanitizeJourneyEvent(payload);
156
288
  for (const sink of sinks) {
157
289
  try {
158
- sink(payload);
290
+ sink(safePayload);
159
291
  } catch {
160
292
  // One sinking throw must not block the remaining sinks.
161
293
  }
@@ -167,19 +299,16 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
167
299
  return;
168
300
  }
169
301
  journey.closed = true;
170
- journeys.delete(journey.name);
171
- clearCountdown(journey);
172
-
173
- for (const step of getInFlightSteps(journey)) {
174
- if (step.durationMs === 0) {
175
- step.durationMs = Math.round(globalThis.performance.now() - step.startedAt);
176
- }
302
+ if (journeys.get(journey.name) === journey) {
303
+ journeys.delete(journey.name);
177
304
  }
305
+ clearCountdown(journey);
306
+ releaseInFlightSteps(journey);
178
307
 
179
308
  let finalReason = journey.reason ?? reason;
180
309
  let finalOutcome = outcome;
181
310
  const durationMs = Math.round(globalThis.performance.now() - journey.startedAt);
182
- if (journey.timeoutMs > 0 && durationMs > journey.timeoutMs) {
311
+ if (journey.timeoutExplicit && journey.timeoutMs > 0 && durationMs > journey.timeoutMs) {
183
312
  finalOutcome = 'bad';
184
313
  finalReason = finalReason ?? 'journey-timeout';
185
314
  }
@@ -191,7 +320,7 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
191
320
  outcome: s.outcome,
192
321
  ...(s.reason ? { reason: s.reason } : {}),
193
322
  ...(s.httpStatus !== undefined ? { httpStatus: s.httpStatus } : {}),
194
- ...(Object.keys(s.attributes).length ? { attributes: s.attributes } : {}),
323
+ ...(Object.keys(s.attributes).length ? { attributes: { ...s.attributes } } : {}),
195
324
  }));
196
325
 
197
326
  emit({
@@ -201,48 +330,47 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
201
330
  group: journey.group,
202
331
  service: journey.service,
203
332
  outcome: finalOutcome,
204
- ...(finalOutcome === 'bad' && finalReason ? { reason: finalReason } : {}),
333
+ ...(finalReason ? { reason: finalReason } : {}),
205
334
  durationMs,
206
335
  steps,
207
336
  ...(journey.expected ? { expected: journey.expected } : {}),
208
- ...(Object.keys(journey.tags).length ? { tags: journey.tags } : {}),
337
+ ...(Object.keys(journey.tags).length ? { tags: { ...journey.tags } } : {}),
209
338
  },
210
339
  });
211
340
  }
212
341
 
213
- function armCountdown(journey: JourneyState): void {
214
- if (journey.timeoutMs <= 0) {
342
+ function armCountdown(journey: JourneyState, delayMs: number = journey.timeoutMs): void {
343
+ if (delayMs <= 0) {
215
344
  return;
216
345
  }
217
346
  journey.timer = setTimeout(() => {
218
- finish(journey, 'bad', 'journey-timeout');
219
- }, 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);
220
353
  }
221
354
 
222
- function resolveRequestTimeoutMs(journey: JourneyState, key: string): number {
223
- const own = longestEndpointMatch(journey.endpoints, key);
224
- if (own?.timeoutMs) {
225
- return own.timeoutMs;
226
- }
227
-
228
- const app = longestEndpointMatch(requestTimeouts.endpoints, key);
229
- if (app?.timeoutMs) {
230
- return app.timeoutMs;
231
- }
232
-
233
- if (journey.requestTimeoutMs != null) {
234
- return journey.requestTimeoutMs;
235
- }
236
-
237
- return requestTimeouts.defaultMs;
355
+ function effectiveSlowRequests(): SlowRequestConfig {
356
+ return overlay.slowRequests ?? getSharedSlowRequests() ?? slowRequests;
238
357
  }
239
358
 
240
359
  function shouldIgnoreRequest(journey: JourneyState, key: string): boolean {
241
- const own = longestEndpointMatch(journey.endpoints, key);
242
- if (own) {
243
- 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);
244
373
  }
245
- return longestEndpointMatch(requestTimeouts.endpoints, key)?.ignore === true;
246
374
  }
247
375
 
248
376
  function resolveJourneyName(journey: JourneyStepTarget): string | null {
@@ -250,53 +378,140 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
250
378
  return null;
251
379
  }
252
380
  if (typeof journey === 'string' || !Array.isArray(journey)) {
253
- 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);
254
417
  }
255
- return api.firstOpenJourney(journey.map(toName));
256
418
  }
257
419
 
258
420
  function createStepHandle(journeyName: string, step: StepRecord): StepHandle {
259
421
  return {
260
- name: journeyName,
422
+ journeyName,
261
423
  setAttribute(key, value) {
262
- step.attributes[key] = value;
424
+ setSafeAttribute(step.attributes, key, value);
263
425
  },
264
426
  setTags(tags) {
265
- api.setJourneyTags(journeyName, tags);
427
+ if (step.journey.closed) {
428
+ return;
429
+ }
430
+ assignRecord(step.journey.tags, tags, MAX_TAG_KEYS);
266
431
  },
267
- complete(tags) {
268
- api.completeJourney(journeyName, tags);
432
+ completeJourney(tags) {
433
+ api.completeJourneyState(step.journey, tags);
269
434
  },
270
- fail(reason) {
271
- api.failJourney(journeyName, reason);
435
+ failJourney(reason) {
436
+ api.failJourney(step.journey, reason);
272
437
  },
273
- // Box in JourneyStepTag — raw StepRecord is circular and blows jQuery/axios deep-merge.
274
- 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)),
275
440
  };
276
441
  }
277
442
 
278
443
  const api: JourneyRuntime = {
279
- getRequestTimeouts() {
280
- return requestTimeouts;
444
+ get id() {
445
+ return id;
446
+ },
447
+ set id(next: string) {
448
+ id = next;
449
+ },
450
+ getSlowRequests() {
451
+ return effectiveSlowRequests();
281
452
  },
282
453
 
283
- setRequestTimeouts(next) {
284
- requestTimeouts = {
454
+ setSlowRequests(next) {
455
+ slowRequests = {
285
456
  ...next,
286
457
  endpoints: sortEndpointsByLongestMatch(next.endpoints),
287
458
  };
288
459
  },
289
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
+
290
477
  setSinks(next) {
291
478
  sinks = [...next];
292
479
  },
293
480
 
481
+ trySetConfiguredSinks(next) {
482
+ if (sinksConfigured) {
483
+ return false;
484
+ }
485
+ sinks = [...next];
486
+ sinksConfigured = true;
487
+ return true;
488
+ },
489
+
294
490
  addSink(sink) {
295
491
  sinks.push(sink);
296
492
  },
297
493
 
298
494
  emit,
299
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
+
300
515
  getOpenJourney(name) {
301
516
  return journeys.get(name);
302
517
  },
@@ -304,6 +519,7 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
304
519
  getDebugSnapshot() {
305
520
  const active = api.activeJourneyStep();
306
521
  return {
522
+ id: api.id,
307
523
  openJourneys: [...journeys.values()].map(journey => ({
308
524
  name: journey.name,
309
525
  team: journey.team,
@@ -318,13 +534,22 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
318
534
  outcome: step.outcome,
319
535
  durationMs: step.durationMs,
320
536
  ...(step.reason ? { reason: step.reason } : {}),
537
+ ...(step.attributedRequestKeys?.length
538
+ ? { attributedRequestKeys: [...step.attributedRequestKeys] }
539
+ : {}),
321
540
  })),
322
541
  })),
323
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
+ },
324
549
  };
325
550
  },
326
551
 
327
- firstOpenJourney(names) {
552
+ firstOpenJourneyName(names) {
328
553
  for (const name of names) {
329
554
  if (journeys.has(name)) {
330
555
  return name;
@@ -336,39 +561,51 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
336
561
  setJourneyTags(name, tags) {
337
562
  const journey = name ? journeys.get(name) : undefined;
338
563
  if (journey) {
339
- Object.assign(journey.tags, tags);
564
+ assignRecord(journey.tags, tags, MAX_TAG_KEYS);
340
565
  }
341
566
  },
342
567
 
343
- startJourney(def) {
344
- const existing = journeys.get(def.name);
568
+ startJourney(config) {
569
+ const existing = journeys.get(config.name);
345
570
  if (existing) {
346
571
  finish(existing, existing.verdict === 'bad' ? 'bad' : 'excluded');
347
572
  }
348
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);
349
578
  const journey: JourneyState = {
350
- name: def.name,
351
- team: def.team,
352
- group: def.group,
353
- service: def.service,
579
+ config: snapshot,
580
+ name: snapshot.name,
581
+ team: snapshot.team,
582
+ group: snapshot.group,
583
+ service: snapshot.service,
354
584
  verdict: 'good',
355
585
  startedAt: globalThis.performance.now(),
356
- timeoutMs: def.timeoutMs ?? 0,
357
- stepTimeoutMs: def.stepTimeoutMs,
586
+ timeoutMs: timeout.ms,
587
+ timeoutExplicit: timeout.explicit,
588
+ stepTimeoutMs:
589
+ snapshot.stepTimeoutMs !== undefined
590
+ ? finiteTimeoutMs(snapshot.stepTimeoutMs)
591
+ : undefined,
358
592
  reason: null,
359
593
  steps: [],
360
- expected: def.steps ? [...def.steps] : null,
361
- tags: { ...(def.tags ?? {}) },
362
- requestTimeoutMs: def.requestTimeoutMs,
363
- endpoints: sortEndpointsByLongestMatch(def.endpoints),
594
+ expected: snapshot.expected ? [...snapshot.expected] : null,
595
+ tags,
596
+ slowRequestMs: finiteSlowRequestMs(snapshot.slowRequestMs),
597
+ endpoints: snapshot.endpoints,
364
598
  timer: null,
365
599
  closed: false,
600
+ maxSteps:
601
+ snapshot.maxSteps !== undefined ? clampMaxSteps(snapshot.maxSteps) : undefined,
366
602
  };
367
- journeys.set(def.name, journey);
603
+ journeys.set(config.name, journey);
604
+ journey[JOURNEY_ENGINE] = bindJourneyEngine(api, journey, shouldIgnoreRequest);
368
605
  armCountdown(journey);
369
606
  },
370
607
 
371
- failJourneyNow(journey, reason) {
608
+ failJourney(journey, reason) {
372
609
  if (journey.closed) {
373
610
  return;
374
611
  }
@@ -377,34 +614,81 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
377
614
  finish(journey, 'bad');
378
615
  },
379
616
 
380
- failJourney(name, reason) {
617
+ completeJourney(name, tags) {
618
+ if (!name) {
619
+ return;
620
+ }
381
621
  const journey = journeys.get(name);
382
622
  if (journey) {
383
- api.failJourneyNow(journey, reason);
623
+ api.completeJourneyState(journey, tags);
384
624
  }
385
625
  },
386
626
 
387
- completeJourney(name, tags) {
388
- if (!name) {
627
+ completeJourneyState(journey, tags) {
628
+ if (journey.closed) {
389
629
  return;
390
630
  }
391
631
  if (tags) {
392
- api.setJourneyTags(name, tags);
632
+ assignRecord(journey.tags, tags, MAX_TAG_KEYS);
393
633
  }
394
- const journey = journeys.get(name);
395
- if (journey) {
396
- 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;
397
640
  }
641
+ finish(journey, journey.verdict === 'bad' ? 'bad' : 'excluded');
398
642
  },
399
643
 
400
- excludeJourney(name) {
401
- const journey = journeys.get(name);
402
- if (journey) {
403
- 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);
404
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);
405
686
  },
406
687
 
407
688
  activeJourneyStep() {
689
+ if (!api.getAutoAttributeRequests()) {
690
+ return null;
691
+ }
408
692
  if (inFlight.size !== 1) {
409
693
  return null;
410
694
  }
@@ -412,9 +696,14 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
412
696
  },
413
697
 
414
698
  async journeyStep(journey, stepName, fn, opts) {
699
+ maybeAutoStart(journey);
415
700
  const journeyName = resolveJourneyName(journey);
416
701
  const open = journeyName ? journeys.get(journeyName) : undefined;
417
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
+ }
418
707
  return fn(noopStep);
419
708
  }
420
709
 
@@ -423,10 +712,15 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
423
712
  startedAt: globalThis.performance.now(),
424
713
  durationMs: 0,
425
714
  outcome: 'good',
426
- attributes: {},
715
+ attributes: emptyRecord(),
427
716
  journey: open,
428
717
  };
429
- 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
+ }
430
724
 
431
725
  const handle = createStepHandle(journeyName, step);
432
726
 
@@ -441,7 +735,7 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
441
735
  step.reason ??= 'step-timeout';
442
736
  step.attributes.timeout_ms ??= stepTimeoutMs;
443
737
  step.durationMs = Math.round(globalThis.performance.now() - step.startedAt);
444
- api.failJourneyNow(open, 'step-timeout');
738
+ api.failJourney(open, 'step-timeout');
445
739
  }, stepTimeoutMs);
446
740
  }
447
741
 
@@ -449,11 +743,16 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
449
743
  try {
450
744
  return await fn(handle);
451
745
  } catch (err) {
452
- const message = err instanceof Error ? err.message : String(err);
453
- step.outcome = 'bad';
454
- step.reason ??= 'step-error';
455
- step.attributes.error ??= message;
456
- 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
+ }
457
756
  throw err;
458
757
  } finally {
459
758
  if (timeoutTimer != null) {
@@ -471,20 +770,29 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
471
770
  return api.journeyStep(journey, stepName, fn, opts);
472
771
  },
473
772
 
773
+ shouldIgnoreRequest,
774
+
474
775
  reportBackendRequest(step, status, durationMs, requestKey) {
475
- if (!step) {
776
+ if (!step?.journey) {
476
777
  return;
477
778
  }
478
779
  const journey = step.journey;
479
780
  if (journey.closed) {
480
781
  return;
481
782
  }
482
- if (shouldIgnoreRequest(journey, requestKey)) {
783
+ trackAttributedKey(step, requestKey);
784
+ const endpointPolicy = resolveEndpointPolicy(
785
+ journey,
786
+ effectiveSlowRequests(),
787
+ requestKey
788
+ );
789
+ if (endpointPolicy.ignore) {
483
790
  return;
484
791
  }
485
792
 
486
793
  const failed = status === undefined || status >= 500;
487
- const slow = durationMs > resolveRequestTimeoutMs(journey, requestKey);
794
+ const threshold = endpointPolicy.slowRequestMs;
795
+ const slow = threshold > 0 && durationMs > threshold;
488
796
  if (!failed && !slow) {
489
797
  return;
490
798
  }
@@ -497,7 +805,7 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
497
805
  step.httpStatus = status;
498
806
  }
499
807
 
500
- api.failJourneyNow(journey, category);
808
+ api.failJourney(journey, category);
501
809
  },
502
810
 
503
811
  reset(next = {}) {
@@ -507,13 +815,20 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
507
815
  }
508
816
  journeys.clear();
509
817
  inFlight.clear();
818
+ overlay = {};
819
+ sinksConfigured = false;
510
820
  sinks = [...(next.sinks ?? [])];
511
- requestTimeouts = {
512
- defaultMs: next.requestTimeouts?.defaultMs ?? DEFAULT_REQUEST_TIMEOUTS.defaultMs,
821
+ slowRequests = {
822
+ defaultMs: next.slowRequests?.defaultMs ?? DEFAULT_SLOW_REQUESTS.defaultMs,
513
823
  endpoints: sortEndpointsByLongestMatch(
514
- next.requestTimeouts?.endpoints ?? DEFAULT_REQUEST_TIMEOUTS.endpoints
824
+ next.slowRequests?.endpoints ?? DEFAULT_SLOW_REQUESTS.endpoints
515
825
  ),
516
826
  };
827
+ maxSteps =
828
+ next.maxSteps !== undefined ? clampMaxSteps(next.maxSteps) : DEFAULT_MAX_STEPS;
829
+ defaultJourneyIdleMs = finiteTimeoutMs(
830
+ next.defaultJourneyIdleMs ?? DEFAULT_JOURNEY_IDLE_MS
831
+ );
517
832
  },
518
833
  };
519
834
 
@@ -527,10 +842,69 @@ export function createJourneyRuntime(options: JourneyRuntimeOptions = {}): Journ
527
842
  */
528
843
  export const moduleRuntime: JourneyRuntime = createJourneyRuntime();
529
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
+ }
905
+
530
906
  /**
531
- * @internal Accidentally exported removing it is a breaking change, so it stays
532
- * for compatibility. Not part of the supported public API.
533
- * Reset the module engine (open journeys, ambient steps, sinks, request thresholds).
907
+ * @internal Reset the module engine (open journeys, ambient steps, sinks, slow-request thresholds).
534
908
  * Intended for tests — call in `beforeEach` / `afterEach` instead of manual cleanup.
535
909
  */
536
910
  export function resetJourney(options?: JourneyRuntimeOptions): void {