@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
@@ -0,0 +1,177 @@
1
+ import { isUnsafeKey, MAX_ATTRIBUTE_KEYS, MAX_TAG_KEYS, MAX_TAG_VALUE_LEN } from './limits';
2
+ import type { JourneyEvent, JourneyStepEvent, TagValue } from './types';
3
+
4
+ const SANITIZED_EVENT = Symbol.for('st.journey.sanitized');
5
+
6
+ /** Match Datadog metric-tag value rules so RUM attributes / globals join derived SLO tags. */
7
+ export function normalizeTagValue(value: string): string {
8
+ return value
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9_\-:./]/g, '_')
11
+ .replace(/_+/g, '_')
12
+ .slice(0, MAX_TAG_VALUE_LEN)
13
+ .replace(/_+$/, '');
14
+ }
15
+
16
+ export function emptyRecord(): Record<string, TagValue> {
17
+ return Object.create(null) as Record<string, TagValue>;
18
+ }
19
+
20
+ function asTagValue(value: unknown): TagValue | undefined {
21
+ if (typeof value === 'string') {
22
+ return normalizeTagValue(value);
23
+ }
24
+ if (typeof value === 'number' || typeof value === 'boolean') {
25
+ return value;
26
+ }
27
+ return undefined;
28
+ }
29
+
30
+ export interface CopyRecordOptions {
31
+ maxKeys: number;
32
+ /** When true, keys are run through {@link normalizeTagValue} (sink / wire). */
33
+ normalizeKeys?: boolean;
34
+ /** When true (default), string values are normalized. */
35
+ normalizeStringValues?: boolean;
36
+ }
37
+
38
+ /**
39
+ * Own-key copy onto a null-prototype object. Skips unsafe keys and excess keys.
40
+ */
41
+ export function copyRecord(
42
+ src: Record<string, unknown> | undefined,
43
+ options: CopyRecordOptions
44
+ ): Record<string, TagValue> {
45
+ const out = emptyRecord();
46
+ if (src == null || typeof src !== 'object') {
47
+ return out;
48
+ }
49
+ const normalizeKeys = options.normalizeKeys === true;
50
+ const normalizeStringValues = options.normalizeStringValues !== false;
51
+ for (const key of Object.keys(src)) {
52
+ if (isUnsafeKey(key)) {
53
+ continue;
54
+ }
55
+ const destKey = normalizeKeys ? normalizeTagValue(key) : key;
56
+ if (!destKey || isUnsafeKey(destKey)) {
57
+ continue;
58
+ }
59
+ const raw = src[key];
60
+ const value = normalizeStringValues
61
+ ? asTagValue(raw)
62
+ : typeof raw === 'string' || typeof raw === 'number' || typeof raw === 'boolean'
63
+ ? raw
64
+ : undefined;
65
+ if (value === undefined) {
66
+ continue;
67
+ }
68
+ if (Object.prototype.hasOwnProperty.call(out, destKey)) {
69
+ out[destKey] = value;
70
+ continue;
71
+ }
72
+ if (Object.keys(out).length >= options.maxKeys) {
73
+ continue;
74
+ }
75
+ out[destKey] = value;
76
+ }
77
+ return out;
78
+ }
79
+
80
+ /** Merge `src` into `target` (null-prototype), honouring caps and unsafe-key skips. */
81
+ export function assignRecord(
82
+ target: Record<string, TagValue>,
83
+ src: Record<string, unknown> | undefined,
84
+ maxKeys: number
85
+ ): void {
86
+ if (src == null || typeof src !== 'object') {
87
+ return;
88
+ }
89
+ let room = maxKeys - Object.keys(target).length;
90
+ for (const key of Object.keys(src)) {
91
+ if (isUnsafeKey(key)) {
92
+ continue;
93
+ }
94
+ const raw = src[key];
95
+ if (typeof raw !== 'string' && typeof raw !== 'number' && typeof raw !== 'boolean') {
96
+ continue;
97
+ }
98
+ const exists = Object.prototype.hasOwnProperty.call(target, key);
99
+ if (!exists && room <= 0) {
100
+ continue;
101
+ }
102
+ if (!exists) {
103
+ room -= 1;
104
+ }
105
+ target[key] = raw;
106
+ }
107
+ }
108
+
109
+ export function setSafeAttribute(
110
+ attributes: Record<string, TagValue>,
111
+ key: string,
112
+ value: string | number | boolean
113
+ ): void {
114
+ if (isUnsafeKey(key)) {
115
+ return;
116
+ }
117
+ const exists = Object.prototype.hasOwnProperty.call(attributes, key);
118
+ if (!exists && Object.keys(attributes).length >= MAX_ATTRIBUTE_KEYS) {
119
+ return;
120
+ }
121
+ const next = asTagValue(value);
122
+ if (next !== undefined) {
123
+ attributes[key] = next;
124
+ }
125
+ }
126
+
127
+ function sanitizeStep(step: JourneyStepEvent): JourneyStepEvent {
128
+ const name = normalizeTagValue(step.name);
129
+ return {
130
+ ...step,
131
+ name: name || step.name,
132
+ ...(step.reason ? { reason: normalizeTagValue(step.reason) } : {}),
133
+ ...(step.attributes
134
+ ? {
135
+ attributes: copyRecord(step.attributes, {
136
+ maxKeys: MAX_ATTRIBUTE_KEYS,
137
+ normalizeKeys: true,
138
+ }),
139
+ }
140
+ : {}),
141
+ };
142
+ }
143
+
144
+ /** Default-sink payload: normalized keys/names, capped maps, no unsafe keys. */
145
+ export function sanitizeJourneyEvent(event: JourneyEvent): JourneyEvent {
146
+ const j = event.journey;
147
+ const expected = j.expected?.map(name => normalizeTagValue(name)).filter(name => name !== '');
148
+ const tags = j.tags
149
+ ? copyRecord(j.tags, { maxKeys: MAX_TAG_KEYS, normalizeKeys: true })
150
+ : undefined;
151
+ const tagKeys = tags ? Object.keys(tags) : [];
152
+ const sanitized: JourneyEvent = {
153
+ journey: {
154
+ name: normalizeTagValue(j.name),
155
+ team: normalizeTagValue(j.team),
156
+ group: normalizeTagValue(j.group),
157
+ service: normalizeTagValue(j.service),
158
+ outcome: j.outcome,
159
+ ...(j.reason ? { reason: normalizeTagValue(j.reason) } : {}),
160
+ durationMs: j.durationMs,
161
+ steps: j.steps.map(sanitizeStep),
162
+ ...(expected?.length ? { expected } : {}),
163
+ ...(tagKeys.length ? { tags } : {}),
164
+ },
165
+ };
166
+ Object.defineProperty(sanitized, SANITIZED_EVENT, {
167
+ value: true,
168
+ enumerable: false,
169
+ configurable: false,
170
+ });
171
+ return sanitized;
172
+ }
173
+
174
+ /** True when payload already passed through sanitizeJourneyEvent. */
175
+ export function isSanitizedJourneyEvent(event: JourneyEvent): boolean {
176
+ return (event as unknown as Record<PropertyKey, unknown>)[SANITIZED_EVENT] === true;
177
+ }
@@ -1,24 +1,103 @@
1
- import type { JourneyStepRef, StepRecord } from './types';
1
+ import type { JourneyHttpScore, JourneyStampOptions } from './request-policy';
2
+ import type { StepRecord } from './types';
3
+
4
+ /** Cross-realm brand so duplicate package copies can unwrap stamps safely. */
5
+ export const JOURNEY_STEP_STAMP_BRAND = Symbol.for('st.journey.stamp');
2
6
 
3
7
  /**
4
- * Non-plain wrapper for {@link StepHandle.tag}.
8
+ * Non-plain wrapper for {@link StepHandle.stamp}.
5
9
  *
6
10
  * jQuery `ajaxSetup` / axios `mergeConfig` deep-merge plain objects. A raw
7
11
  * {@link StepRecord} is circular (`step.journey.steps → step`), so putting it
8
12
  * on ajax/axios options causes "Maximum call stack size exceeded". Class
9
13
  * instances are assigned by reference instead of recursed.
10
14
  */
11
- export class JourneyStepTag {
12
- constructor(readonly step: StepRecord) {}
15
+ export class JourneyStepStamp {
16
+ readonly [JOURNEY_STEP_STAMP_BRAND] = true as const;
17
+ /** Present only on explicit `step.stamp(score)` — never on auto-attribute. */
18
+ readonly score?: JourneyHttpScore;
19
+ /** Present only on explicit `step.stamp({ ignore: true })` — never on auto-attribute. */
20
+ readonly ignore?: boolean;
21
+
22
+ /**
23
+ * @internal Engine record — adapters unwrap via `resolveStamp` /
24
+ * `onHttpComplete`. Not part of the supported public surface.
25
+ */
26
+ readonly step: StepRecord;
27
+
28
+ constructor(step: StepRecord, opts?: JourneyStampOptions) {
29
+ this.step = step;
30
+ this.score = opts?.score;
31
+ this.ignore = opts?.ignore;
32
+ }
33
+
34
+ /**
35
+ * Circular by design (`step.journey.steps → step`) for mergeConfig/deep-merge
36
+ * safety. Prevent accidental `JSON.stringify` (e.g. AxiosError.toJSON() includes
37
+ * config by reference) elsewhere in the app from throwing "circular structure".
38
+ */
39
+ toJSON(): string {
40
+ return '[JourneyStepStamp]';
41
+ }
42
+ }
43
+
44
+ function isStepRecord(value: object): value is StepRecord {
45
+ const record = value as StepRecord;
46
+ return (
47
+ typeof record.name === 'string' &&
48
+ record.journey != null &&
49
+ typeof record.journey === 'object' &&
50
+ typeof (record.journey as { name?: unknown }).name === 'string'
51
+ );
52
+ }
53
+
54
+ /**
55
+ * Same-realm stamp, or a branded foreign copy with score/ignore copied.
56
+ * Does not fall back to ambient. Raw StepRecords are not stamps.
57
+ */
58
+ export function adoptJourneyStepStamp(value: unknown): JourneyStepStamp | undefined {
59
+ if (value == null || typeof value !== 'object') {
60
+ return undefined;
61
+ }
62
+ if (value instanceof JourneyStepStamp) {
63
+ return value;
64
+ }
65
+ const branded = value as {
66
+ [JOURNEY_STEP_STAMP_BRAND]?: unknown;
67
+ step?: unknown;
68
+ score?: unknown;
69
+ ignore?: unknown;
70
+ };
71
+ if (branded[JOURNEY_STEP_STAMP_BRAND] !== true || branded.step == null) {
72
+ return undefined;
73
+ }
74
+ if (typeof branded.step !== 'object' || !isStepRecord(branded.step)) {
75
+ return undefined;
76
+ }
77
+ const opts: JourneyStampOptions = {};
78
+ if (typeof branded.score === 'function') {
79
+ opts.score = branded.score as JourneyHttpScore;
80
+ }
81
+ if (branded.ignore === true) {
82
+ opts.ignore = true;
83
+ }
84
+ return new JourneyStepStamp(branded.step, opts);
85
+ }
86
+
87
+ /** @internal Unwrap a stamp to the engine record. */
88
+ export function resolveJourneyStep(value: unknown): StepRecord | undefined {
89
+ return adoptJourneyStepStamp(value)?.step;
13
90
  }
14
91
 
15
- /** Unwrap `step.tag().journeyStep` or a JourneyStepTag / StepRecord stamp. */
16
- export function resolveJourneyStep(value: unknown): JourneyStepRef | undefined {
17
- if (value instanceof JourneyStepTag) {
18
- return value.step;
92
+ /** Normalize `step.stamp(fn)` / `step.stamp({ score, ignore })`. */
93
+ export function resolveStampOpts(
94
+ scoreOrOpts?: JourneyHttpScore | JourneyStampOptions
95
+ ): JourneyStampOptions | undefined {
96
+ if (scoreOrOpts == null) {
97
+ return undefined;
19
98
  }
20
- if (value && typeof value === 'object') {
21
- return value as JourneyStepRef;
99
+ if (typeof scoreOrOpts === 'function') {
100
+ return { score: scoreOrOpts };
22
101
  }
23
- return undefined;
102
+ return scoreOrOpts;
24
103
  }
package/src/core/types.ts CHANGED
@@ -1,11 +1,17 @@
1
- import type { EndpointPolicy } from '../endpoint-policy';
1
+ import type { EndpointPolicy } from './endpoint-policy';
2
+ import type { JourneyHttpScore, JourneyStampOptions, RequestPolicyConfig } from './request-policy';
3
+ import type { JourneyStepStamp } from './step-tag';
2
4
 
3
5
  /** Arbitrary tag values a developer can attach. */
4
6
  export type TagValue = string | number | boolean;
5
7
 
6
8
  /** Config for one journey — supplied by the caller (typically `<JourneyScope>` props). */
7
9
  export interface JourneyDef {
8
- /** Journey name — lifecycle key, event field, and concurrency map key. */
10
+ /**
11
+ * Journey name — lifecycle key, event field, and concurrency map key.
12
+ * Must be unique among concurrently open journeys on the same runtime
13
+ * (a second start with the same name restarts / excludes the previous).
14
+ */
9
15
  name: string;
10
16
  /** Owner team (required tag). */
11
17
  team: string;
@@ -13,16 +19,23 @@ export interface JourneyDef {
13
19
  group: string;
14
20
  /** Business component label, e.g. "checkout" — not the Datadog RUM service. */
15
21
  service: string;
16
- /** Whole-journey budget in ms; omit / 0 to disable. */
22
+ /**
23
+ * Whole-journey timeout in ms.
24
+ * - Omit to use the runtime idle timeout default (15m unless overridden)
25
+ * - Set `0` to disable the journey countdown
26
+ * - Set `N > 0` to enforce an explicit SLO budget
27
+ */
17
28
  timeoutMs?: number;
18
29
  /** Default per-step timeout in ms; per-call option overrides. Omit / 0 to disable. */
19
30
  stepTimeoutMs?: number;
20
- /** Slow-request threshold for this journey; falls back to app-wide default. */
21
- requestTimeoutMs?: number;
31
+ /** Slow-request threshold for this journey; omit to fall through. `0` disables latency scoring. */
32
+ slowRequestMs?: number;
22
33
  /** Per-endpoint overrides for this journey (longest prefix wins over app-wide). */
23
34
  endpoints?: readonly EndpointPolicy[];
24
- /** Optional expected step names (rides as `expected` on the event). */
25
- steps?: readonly string[];
35
+ /** Optional expected happy-path step names (copied onto the event as `expected`). */
36
+ expected?: readonly string[];
37
+ /** Cap recorded steps for this journey; falls back to the runtime default (50). */
38
+ maxSteps?: number;
26
39
  /** Custom tags on every event of this journey. */
27
40
  tags?: Record<string, TagValue>;
28
41
  }
@@ -32,27 +45,31 @@ export type Outcome = 'good' | 'bad' | 'excluded';
32
45
  /** Handle passed to each step function. */
33
46
  export interface StepHandle {
34
47
  /** Resolved journey name for this step, or null if untraced. */
35
- readonly name: string | null;
48
+ readonly journeyName: string | null;
36
49
  setAttribute(key: string, value: string | number | boolean): void;
37
50
  /** Journey-level tags without closing. */
38
51
  setTags(tags: Record<string, TagValue>): void;
39
- /** Apply optional tags then completeJourney. No-op if name is null. */
40
- complete(tags?: Record<string, TagValue>): void;
41
- /** Soft business Bad without throw. No-op if name is null. */
42
- fail(reason: string): void;
52
+ /** Apply optional tags then complete the whole journey. No-op if journeyName is null. */
53
+ completeJourney(tags?: Record<string, TagValue>): void;
54
+ /** Soft business Bad without throw. Closes the whole journey. No-op if journeyName is null. */
55
+ failJourney(reason: string): void;
43
56
  /**
44
- * Bind this step to a request explicitly (spread into axios config / fetch init /
45
- * jQuery ajax settings). Needed when requests fire after an await or while steps
46
- * overlap see docs soft ambient. Untraced (noop) steps omit `journeyStep`.
57
+ * Bind this step to a request explicitly. Pass as `{ stamp: step.stamp() }` on
58
+ * axios config / fetch init / jQuery ajax settings. Needed when requests fire
59
+ * after an await, while steps overlap, or when `autoAttributeRequests` is false.
60
+ * Untraced (noop) steps return `undefined`.
61
+ *
62
+ * Pass `score` (or `{ score, ignore }`) to score or skip this stamped request.
63
+ * Auto-attributed requests never run a score function and never set ignore.
47
64
  *
48
- * The value is a non-plain {@link JourneyStepTag} so jQuery/axios deep-merge will
65
+ * The value is a non-plain {@link JourneyStepStamp} so jQuery/axios deep-merge will
49
66
  * not recurse into the circular StepRecord graph.
50
67
  */
51
- tag(): { journeyStep?: unknown };
68
+ stamp(scoreOrOpts?: JourneyHttpScore | JourneyStampOptions): JourneyStepStamp | undefined;
52
69
  }
53
70
 
54
- /** A journey name, or anything with a `name` (e.g. a JourneyHandle / JourneyDef). */
55
- export type JourneyNameLike = string | { readonly name: string };
71
+ /** A journey name, a config object, or a handle (`handle.config.name`). */
72
+ export type JourneyNameLike = string | { readonly name: string } | { readonly config: JourneyDef };
56
73
 
57
74
  /** First argument to journeyStep / journeyMountStep. */
58
75
  export type JourneyStepTarget = JourneyNameLike | null | readonly JourneyNameLike[];
@@ -63,6 +80,32 @@ export interface JourneyStepOptions {
63
80
  timeoutMs?: number;
64
81
  }
65
82
 
83
+ /**
84
+ * @internal Cross-bundle key for the engine that created a {@link JourneyState}.
85
+ * Host adapters read this off an MFE `step.stamp()` so abort/4xx/5xx close the owning copy.
86
+ */
87
+ export const JOURNEY_ENGINE = Symbol.for('st.journey.engine');
88
+
89
+ /**
90
+ * @internal Bound fail/exclude/report hooks for a journey's creating engine.
91
+ * Name lookup on another package copy cannot see this journey.
92
+ */
93
+ export interface JourneyEngine {
94
+ /** Close this journey as bad on the creating engine — not a by-name lookup. */
95
+ failJourney(reason: string): void;
96
+ exclude(): void;
97
+ shouldIgnoreRequest(key: string): boolean;
98
+ reportBackendRequest(
99
+ step: StepRecord,
100
+ status: number | undefined,
101
+ durationMs: number,
102
+ requestKey: string
103
+ ): void;
104
+ getHttpAbortedRequestsPolicy(): RequestPolicyConfig;
105
+ getHttpClientErrorRequestsPolicy(): RequestPolicyConfig;
106
+ }
107
+
108
+ /** @internal Engine open-journey record — not part of the supported public API. */
66
109
  export interface JourneyState {
67
110
  name: string;
68
111
  team: string;
@@ -71,6 +114,8 @@ export interface JourneyState {
71
114
  verdict: Outcome;
72
115
  startedAt: number;
73
116
  timeoutMs: number;
117
+ /** True when JourneyDef.timeoutMs was explicitly provided by caller. */
118
+ timeoutExplicit: boolean;
74
119
  stepTimeoutMs?: number;
75
120
  /**
76
121
  * Why the journey closed as bad, else null.
@@ -81,12 +126,17 @@ export interface JourneyState {
81
126
  steps: StepRecord[];
82
127
  expected: string[] | null;
83
128
  tags: Record<string, TagValue>;
84
- requestTimeoutMs?: number;
129
+ slowRequestMs?: number;
85
130
  endpoints?: readonly EndpointPolicy[];
86
131
  timer: ReturnType<typeof setTimeout> | null;
87
132
  closed: boolean;
133
+ maxSteps?: number;
134
+ /** Frozen snapshot taken at start — not the caller's original object. */
135
+ config: JourneyDef;
136
+ [JOURNEY_ENGINE]?: JourneyEngine;
88
137
  }
89
138
 
139
+ /** @internal Engine in-flight / completed step record — not part of the supported public API. */
90
140
  export interface StepRecord {
91
141
  name: string;
92
142
  startedAt: number;
@@ -96,11 +146,12 @@ export interface StepRecord {
96
146
  httpStatus?: number;
97
147
  attributes: Record<string, string | number | boolean>;
98
148
  journey: JourneyState;
149
+ /** True after onHttpComplete handled an abort for this step (including ignore). */
150
+ onHttpAborted?: boolean;
151
+ /** @internal Request keys attributed while this step was in flight (debug). */
152
+ attributedRequestKeys?: string[];
99
153
  }
100
154
 
101
- /** Opaque handle to a step, captured at request initiation for response attribution. */
102
- export type JourneyStepRef = StepRecord;
103
-
104
155
  export interface JourneyStepEvent {
105
156
  name: string;
106
157
  /** Offset from the journey start, ms. */
@@ -131,9 +182,14 @@ export interface JourneyEvent {
131
182
  /** Destination for a terminal journey event. */
132
183
  export type JourneySink = (event: JourneyEvent) => void;
133
184
 
134
- /** Callable journey — config plus lifecycle helpers bound to `name`. Prefer this over free helpers. */
135
- export interface JourneyHandle extends JourneyDef {
136
- /** Open this journey (restarts if already open). */
185
+ /** Callable journey — `config` plus lifecycle helpers. Prefer this over free helpers. */
186
+ export interface JourneyHandle {
187
+ /** Same config object passed to `defineJourney` / used to start. */
188
+ readonly config: JourneyDef;
189
+ /**
190
+ * Always opens a new instance. If this name is already open, the previous
191
+ * instance is finished (Bad if it had breached, else Excluded).
192
+ */
137
193
  start(tags?: Record<string, TagValue>): void;
138
194
  /**
139
195
  * Record a step. Opens the journey first when it isn't already open