@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.
- package/README.md +1 -1
- package/dist/__test-utils__/test-runtime.d.ts.map +1 -1
- package/dist/__test-utils__/test-runtime.js +9 -2
- package/dist/__test-utils__/test-runtime.js.map +1 -1
- package/dist/__tests__/core/sanitize.test.d.ts +2 -0
- package/dist/__tests__/core/sanitize.test.d.ts.map +1 -0
- package/dist/__tests__/hardening.test.d.ts +2 -0
- package/dist/__tests__/hardening.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/host-mfe.test.d.ts +2 -0
- package/dist/__tests__/integrations/host-mfe.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/use-journey-step.test.d.ts +2 -0
- package/dist/__tests__/integrations/use-journey-step.test.d.ts.map +1 -0
- package/dist/config.d.ts +57 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +87 -24
- package/dist/config.js.map +1 -1
- package/dist/core/abort-error.d.ts +4 -0
- package/dist/core/abort-error.d.ts.map +1 -0
- package/dist/core/abort-error.js +22 -0
- package/dist/core/abort-error.js.map +1 -0
- package/dist/core/api.d.ts +22 -9
- package/dist/core/api.d.ts.map +1 -1
- package/dist/core/api.js +77 -30
- package/dist/core/api.js.map +1 -1
- package/dist/core/endpoint-policy.d.ts +14 -4
- package/dist/core/endpoint-policy.d.ts.map +1 -1
- package/dist/core/endpoint-policy.js +32 -1
- package/dist/core/endpoint-policy.js.map +1 -1
- package/dist/core/index.d.ts +4 -3
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +3 -3
- package/dist/core/index.js.map +1 -1
- package/dist/core/limits.d.ts +29 -0
- package/dist/core/limits.d.ts.map +1 -0
- package/dist/core/limits.js +63 -0
- package/dist/core/limits.js.map +1 -0
- package/dist/core/request-policy.d.ts +39 -0
- package/dist/core/request-policy.d.ts.map +1 -0
- package/dist/core/request-policy.js +18 -0
- package/dist/core/request-policy.js.map +1 -0
- package/dist/core/runtime.d.ts +65 -15
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +403 -126
- package/dist/core/runtime.js.map +1 -1
- package/dist/core/sanitize.d.ts +23 -0
- package/dist/core/sanitize.d.ts.map +1 -0
- package/dist/core/sanitize.js +143 -0
- package/dist/core/sanitize.js.map +1 -0
- package/dist/core/step-tag.d.ts +25 -6
- package/dist/core/step-tag.d.ts.map +1 -1
- package/dist/core/step-tag.js +56 -10
- package/dist/core/step-tag.js.map +1 -1
- package/dist/core/types.d.ts +73 -25
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js +4 -1
- package/dist/core/types.js.map +1 -1
- package/dist/global.d.ts +91 -13
- package/dist/global.d.ts.map +1 -1
- package/dist/global.js +235 -25
- package/dist/global.js.map +1 -1
- package/dist/index.d.ts +7 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -6
- package/dist/index.js.map +1 -1
- package/dist/integrations/axios.d.ts +5 -1
- package/dist/integrations/axios.d.ts.map +1 -1
- package/dist/integrations/axios.js +146 -34
- package/dist/integrations/axios.js.map +1 -1
- package/dist/integrations/fetch.d.ts +3 -2
- package/dist/integrations/fetch.d.ts.map +1 -1
- package/dist/integrations/fetch.js +105 -26
- package/dist/integrations/fetch.js.map +1 -1
- package/dist/integrations/http-report.d.ts +27 -0
- package/dist/integrations/http-report.d.ts.map +1 -0
- package/dist/integrations/http-report.js +123 -0
- package/dist/integrations/http-report.js.map +1 -0
- package/dist/integrations/jquery/index.d.ts +8 -4
- package/dist/integrations/jquery/index.d.ts.map +1 -1
- package/dist/integrations/jquery/index.js +26 -8
- package/dist/integrations/jquery/index.js.map +1 -1
- package/dist/integrations/jquery/instrument.d.ts +14 -5
- package/dist/integrations/jquery/instrument.d.ts.map +1 -1
- package/dist/integrations/jquery/instrument.js +111 -29
- package/dist/integrations/jquery/instrument.js.map +1 -1
- package/dist/integrations/react/context.d.ts +4 -1
- package/dist/integrations/react/context.d.ts.map +1 -1
- package/dist/integrations/react/context.js +4 -1
- package/dist/integrations/react/context.js.map +1 -1
- package/dist/integrations/react/scope.d.ts +14 -3
- package/dist/integrations/react/scope.d.ts.map +1 -1
- package/dist/integrations/react/scope.js +46 -8
- package/dist/integrations/react/scope.js.map +1 -1
- package/dist/integrations/react/stable-config-keys.d.ts +6 -0
- package/dist/integrations/react/stable-config-keys.d.ts.map +1 -0
- package/dist/integrations/react/stable-config-keys.js +21 -0
- package/dist/integrations/react/stable-config-keys.js.map +1 -0
- package/dist/integrations/react/use-journey-step.d.ts +11 -0
- package/dist/integrations/react/use-journey-step.d.ts.map +1 -0
- package/dist/integrations/react/use-journey-step.js +19 -0
- package/dist/integrations/react/use-journey-step.js.map +1 -0
- package/dist/integrations/request-key.d.ts +8 -3
- package/dist/integrations/request-key.d.ts.map +1 -1
- package/dist/integrations/request-key.js +10 -9
- package/dist/integrations/request-key.js.map +1 -1
- package/dist/integrations/soft-skip.d.ts +7 -0
- package/dist/integrations/soft-skip.d.ts.map +1 -0
- package/dist/integrations/soft-skip.js +30 -0
- package/dist/integrations/soft-skip.js.map +1 -0
- package/dist/sinks/console.d.ts.map +1 -1
- package/dist/sinks/console.js +2 -1
- package/dist/sinks/console.js.map +1 -1
- package/dist/sinks/datadog.d.ts +1 -1
- package/dist/sinks/datadog.d.ts.map +1 -1
- package/dist/sinks/datadog.js +16 -20
- package/dist/sinks/datadog.js.map +1 -1
- package/package.json +2 -2
- package/src/__test-utils__/test-runtime.ts +9 -2
- package/src/__tests__/config.test.ts +150 -31
- package/src/__tests__/core/ambient.test.ts +16 -10
- package/src/__tests__/core/api.test.ts +7 -7
- package/src/__tests__/core/define-journey.test.ts +28 -11
- package/src/__tests__/core/requests.test.ts +49 -31
- package/src/__tests__/core/runtime.test.ts +67 -20
- package/src/__tests__/core/sanitize.test.ts +69 -0
- package/src/__tests__/core/step.test.ts +132 -36
- package/src/__tests__/core/timeouts.test.ts +69 -4
- package/src/__tests__/global.test.ts +44 -3
- package/src/__tests__/hardening.test.ts +613 -0
- package/src/__tests__/integrations/axios.test.ts +247 -27
- package/src/__tests__/integrations/expose-app-journey.test.ts +9 -3
- package/src/__tests__/integrations/fetch.test.ts +153 -19
- package/src/__tests__/integrations/host-mfe.test.ts +548 -0
- package/src/__tests__/integrations/jquery.test.ts +209 -34
- package/src/__tests__/integrations/scope.test.tsx +160 -2
- package/src/__tests__/integrations/use-journey-step.test.tsx +43 -0
- package/src/__tests__/sinks/console.test.ts +9 -4
- package/src/__tests__/sinks/datadog.test.ts +47 -0
- package/src/config.ts +162 -38
- package/src/core/abort-error.ts +24 -0
- package/src/core/api.ts +83 -33
- package/src/core/endpoint-policy.ts +46 -4
- package/src/core/index.ts +12 -4
- package/src/core/limits.ts +73 -0
- package/src/core/request-policy.ts +66 -0
- package/src/core/runtime.ts +507 -133
- package/src/core/sanitize.ts +177 -0
- package/src/core/step-tag.ts +82 -12
- package/src/core/types.ts +79 -25
- package/src/global.ts +312 -42
- package/src/index.ts +17 -17
- package/src/integrations/axios.ts +183 -45
- package/src/integrations/fetch.ts +125 -39
- package/src/integrations/http-report.ts +184 -0
- package/src/integrations/jquery/index.ts +34 -10
- package/src/integrations/jquery/instrument.ts +126 -42
- package/src/integrations/react/context.ts +4 -1
- package/src/integrations/react/scope.tsx +59 -9
- package/src/integrations/react/stable-config-keys.ts +28 -0
- package/src/integrations/react/use-journey-step.ts +31 -0
- package/src/integrations/request-key.ts +18 -10
- package/src/integrations/soft-skip.ts +35 -0
- package/src/sinks/console.ts +2 -1
- package/src/sinks/datadog.ts +23 -29
|
@@ -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
|
+
}
|
package/src/core/step-tag.ts
CHANGED
|
@@ -1,15 +1,35 @@
|
|
|
1
|
-
import type {
|
|
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.
|
|
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
|
|
12
|
-
|
|
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
|
+
}
|
|
13
33
|
|
|
14
34
|
/**
|
|
15
35
|
* Circular by design (`step.journey.steps → step`) for mergeConfig/deep-merge
|
|
@@ -17,17 +37,67 @@ export class JourneyStepTag {
|
|
|
17
37
|
* config by reference) elsewhere in the app from throwing "circular structure".
|
|
18
38
|
*/
|
|
19
39
|
toJSON(): string {
|
|
20
|
-
return '[
|
|
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;
|
|
21
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;
|
|
22
90
|
}
|
|
23
91
|
|
|
24
|
-
/**
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
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;
|
|
28
98
|
}
|
|
29
|
-
if (
|
|
30
|
-
return
|
|
99
|
+
if (typeof scoreOrOpts === 'function') {
|
|
100
|
+
return { score: scoreOrOpts };
|
|
31
101
|
}
|
|
32
|
-
return
|
|
102
|
+
return scoreOrOpts;
|
|
33
103
|
}
|
package/src/core/types.ts
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
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
|
-
/**
|
|
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
|
-
/**
|
|
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;
|
|
21
|
-
|
|
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 (
|
|
25
|
-
|
|
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
|
|
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
|
|
40
|
-
|
|
41
|
-
/** Soft business Bad without throw. No-op if
|
|
42
|
-
|
|
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
|
|
45
|
-
* jQuery ajax settings
|
|
46
|
-
*
|
|
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
|
|
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
|
-
|
|
68
|
+
stamp(scoreOrOpts?: JourneyHttpScore | JourneyStampOptions): JourneyStepStamp | undefined;
|
|
52
69
|
}
|
|
53
70
|
|
|
54
|
-
/** A journey name,
|
|
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,31 @@ 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
|
+
|
|
66
108
|
/** @internal Engine open-journey record — not part of the supported public API. */
|
|
67
109
|
export interface JourneyState {
|
|
68
110
|
name: string;
|
|
@@ -72,6 +114,8 @@ export interface JourneyState {
|
|
|
72
114
|
verdict: Outcome;
|
|
73
115
|
startedAt: number;
|
|
74
116
|
timeoutMs: number;
|
|
117
|
+
/** True when JourneyDef.timeoutMs was explicitly provided by caller. */
|
|
118
|
+
timeoutExplicit: boolean;
|
|
75
119
|
stepTimeoutMs?: number;
|
|
76
120
|
/**
|
|
77
121
|
* Why the journey closed as bad, else null.
|
|
@@ -82,10 +126,14 @@ export interface JourneyState {
|
|
|
82
126
|
steps: StepRecord[];
|
|
83
127
|
expected: string[] | null;
|
|
84
128
|
tags: Record<string, TagValue>;
|
|
85
|
-
|
|
129
|
+
slowRequestMs?: number;
|
|
86
130
|
endpoints?: readonly EndpointPolicy[];
|
|
87
131
|
timer: ReturnType<typeof setTimeout> | null;
|
|
88
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;
|
|
89
137
|
}
|
|
90
138
|
|
|
91
139
|
/** @internal Engine in-flight / completed step record — not part of the supported public API. */
|
|
@@ -98,11 +146,12 @@ export interface StepRecord {
|
|
|
98
146
|
httpStatus?: number;
|
|
99
147
|
attributes: Record<string, string | number | boolean>;
|
|
100
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[];
|
|
101
153
|
}
|
|
102
154
|
|
|
103
|
-
/** Opaque handle to a step, captured at request initiation for response attribution. */
|
|
104
|
-
export type JourneyStepRef = StepRecord;
|
|
105
|
-
|
|
106
155
|
export interface JourneyStepEvent {
|
|
107
156
|
name: string;
|
|
108
157
|
/** Offset from the journey start, ms. */
|
|
@@ -133,9 +182,14 @@ export interface JourneyEvent {
|
|
|
133
182
|
/** Destination for a terminal journey event. */
|
|
134
183
|
export type JourneySink = (event: JourneyEvent) => void;
|
|
135
184
|
|
|
136
|
-
/** Callable journey — config plus lifecycle helpers
|
|
137
|
-
export interface JourneyHandle
|
|
138
|
-
/**
|
|
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
|
+
*/
|
|
139
193
|
start(tags?: Record<string, TagValue>): void;
|
|
140
194
|
/**
|
|
141
195
|
* Record a step. Opens the journey first when it isn't already open
|