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