@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.
- package/README.md +1 -1
- package/dist/__test-utils__/test-runtime.d.ts.map +1 -0
- package/dist/__test-utils__/test-runtime.js +54 -0
- package/dist/__test-utils__/test-runtime.js.map +1 -0
- package/dist/__tests__/core/ambient.test.d.ts.map +1 -0
- package/dist/__tests__/core/api.test.d.ts +2 -0
- package/dist/__tests__/core/api.test.d.ts.map +1 -0
- package/dist/__tests__/core/define-journey.test.d.ts.map +1 -0
- package/dist/__tests__/core/requests.test.d.ts.map +1 -0
- package/dist/__tests__/core/runtime.test.d.ts.map +1 -0
- package/dist/__tests__/core/sanitize.test.d.ts +2 -0
- package/dist/__tests__/core/sanitize.test.d.ts.map +1 -0
- package/dist/__tests__/core/step.test.d.ts.map +1 -0
- package/dist/__tests__/core/timeouts.test.d.ts.map +1 -0
- package/dist/__tests__/global.test.d.ts +3 -0
- package/dist/__tests__/global.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/axios.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/context.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/expose-app-journey.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/fetch.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/jquery.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/request-key.test.d.ts.map +1 -0
- package/dist/__tests__/integrations/scope.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/__tests__/sinks/console.test.d.ts +2 -0
- package/dist/__tests__/sinks/console.test.d.ts.map +1 -0
- package/dist/__tests__/sinks/datadog.test.d.ts.map +1 -0
- package/dist/__tests__/sinks/sinks.test.d.ts.map +1 -0
- package/dist/config.d.ts +61 -13
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +98 -29
- 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 +43 -0
- package/dist/core/api.d.ts.map +1 -0
- package/dist/core/api.js +122 -0
- package/dist/core/api.js.map +1 -0
- package/dist/{endpoint-policy.d.ts → core/endpoint-policy.d.ts} +14 -4
- package/dist/core/endpoint-policy.d.ts.map +1 -0
- package/dist/core/endpoint-policy.js +70 -0
- package/dist/core/endpoint-policy.js.map +1 -0
- package/dist/core/index.d.ts +6 -5
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +4 -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 +95 -17
- package/dist/core/runtime.d.ts.map +1 -1
- package/dist/core/runtime.js +435 -124
- 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 +31 -6
- package/dist/core/step-tag.d.ts.map +1 -1
- package/dist/core/step-tag.js +62 -9
- package/dist/core/step-tag.js.map +1 -1
- package/dist/core/types.d.ts +76 -26
- 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 +145 -0
- package/dist/global.d.ts.map +1 -0
- package/dist/global.js +326 -0
- package/dist/global.js.map +1 -0
- package/dist/index.d.ts +9 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -5
- 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 +6 -4
- 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 +10 -6
- package/dist/integrations/jquery/index.d.ts.map +1 -1
- package/dist/integrations/jquery/index.js +28 -9
- 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 +48 -9
- 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 +11 -0
- package/dist/sinks/console.d.ts.map +1 -0
- package/dist/sinks/console.js +19 -0
- package/dist/sinks/console.js.map +1 -0
- package/dist/sinks/datadog.d.ts +12 -1
- package/dist/sinks/datadog.d.ts.map +1 -1
- package/dist/sinks/datadog.js +42 -25
- package/dist/sinks/datadog.js.map +1 -1
- package/package.json +2 -2
- package/src/{__mocks__ → __test-utils__}/test-runtime.ts +15 -4
- package/src/__tests__/config.test.ts +158 -42
- package/src/__tests__/{ambient.test.ts → core/ambient.test.ts} +23 -16
- package/src/__tests__/{registry.test.ts → core/api.test.ts} +35 -30
- package/src/__tests__/{define-journey.test.ts → core/define-journey.test.ts} +33 -15
- package/src/__tests__/{requests.test.ts → core/requests.test.ts} +61 -39
- package/src/__tests__/{runtime.test.ts → core/runtime.test.ts} +84 -37
- package/src/__tests__/core/sanitize.test.ts +69 -0
- package/src/__tests__/{step.test.ts → core/step.test.ts} +146 -52
- package/src/__tests__/{timeouts.test.ts → core/timeouts.test.ts} +83 -13
- package/src/__tests__/global.test.ts +154 -0
- package/src/__tests__/hardening.test.ts +613 -0
- package/src/__tests__/integrations/axios.test.ts +616 -0
- package/src/__tests__/{context.test.tsx → integrations/context.test.tsx} +1 -1
- package/src/__tests__/{expose-app-journey.test.ts → integrations/expose-app-journey.test.ts} +11 -5
- package/src/__tests__/{fetch.test.ts → integrations/fetch.test.ts} +159 -25
- package/src/__tests__/integrations/host-mfe.test.ts +548 -0
- package/src/__tests__/integrations/jquery.test.ts +539 -0
- package/src/__tests__/{request-key.test.ts → integrations/request-key.test.ts} +1 -1
- package/src/__tests__/integrations/scope.test.tsx +286 -0
- package/src/__tests__/integrations/use-journey-step.test.tsx +43 -0
- package/src/__tests__/sinks/console.test.ts +103 -0
- package/src/__tests__/{datadog.test.ts → sinks/datadog.test.ts} +109 -5
- package/src/__tests__/{sinks.test.ts → sinks/sinks.test.ts} +13 -12
- package/src/config.ts +173 -41
- package/src/core/abort-error.ts +24 -0
- package/src/core/api.ts +159 -0
- package/src/{endpoint-policy.ts → core/endpoint-policy.ts} +46 -4
- package/src/core/index.ts +16 -17
- package/src/core/limits.ts +73 -0
- package/src/core/request-policy.ts +66 -0
- package/src/core/runtime.ts +560 -137
- package/src/core/sanitize.ts +177 -0
- package/src/core/step-tag.ts +90 -11
- package/src/core/types.ts +82 -26
- package/src/global.ts +466 -0
- package/src/index.ts +31 -18
- package/src/integrations/axios.ts +183 -45
- package/src/integrations/fetch.ts +127 -41
- package/src/integrations/http-report.ts +184 -0
- package/src/integrations/jquery/index.ts +37 -18
- package/src/integrations/jquery/instrument.ts +126 -42
- package/src/integrations/react/context.ts +4 -1
- package/src/integrations/react/scope.tsx +61 -10
- 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 +21 -0
- package/src/sinks/datadog.ts +52 -33
- package/dist/__mocks__/test-runtime.d.ts.map +0 -1
- package/dist/__tests__/ambient.test.d.ts.map +0 -1
- package/dist/__tests__/axios.test.d.ts.map +0 -1
- package/dist/__tests__/context.test.d.ts.map +0 -1
- package/dist/__tests__/datadog.test.d.ts.map +0 -1
- package/dist/__tests__/define-journey.test.d.ts.map +0 -1
- package/dist/__tests__/expose-app-journey.test.d.ts.map +0 -1
- package/dist/__tests__/fetch.test.d.ts.map +0 -1
- package/dist/__tests__/jquery.test.d.ts.map +0 -1
- package/dist/__tests__/registry.test.d.ts +0 -2
- package/dist/__tests__/registry.test.d.ts.map +0 -1
- package/dist/__tests__/request-key.test.d.ts.map +0 -1
- package/dist/__tests__/requests.test.d.ts.map +0 -1
- package/dist/__tests__/runtime.test.d.ts.map +0 -1
- package/dist/__tests__/scope.test.d.ts.map +0 -1
- package/dist/__tests__/sinks.test.d.ts.map +0 -1
- package/dist/__tests__/step.test.d.ts.map +0 -1
- package/dist/__tests__/timeouts.test.d.ts.map +0 -1
- package/dist/core/registry.d.ts +0 -50
- package/dist/core/registry.d.ts.map +0 -1
- package/dist/core/registry.js +0 -102
- package/dist/core/registry.js.map +0 -1
- package/dist/endpoint-policy.d.ts.map +0 -1
- package/dist/endpoint-policy.js +0 -39
- package/dist/endpoint-policy.js.map +0 -1
- package/src/__tests__/axios.test.ts +0 -365
- package/src/__tests__/jquery.test.ts +0 -327
- package/src/__tests__/scope.test.tsx +0 -128
- package/src/core/registry.ts +0 -162
- /package/dist/{__mocks__ → __test-utils__}/test-runtime.d.ts +0 -0
- /package/dist/__tests__/{ambient.test.d.ts → core/ambient.test.d.ts} +0 -0
- /package/dist/__tests__/{define-journey.test.d.ts → core/define-journey.test.d.ts} +0 -0
- /package/dist/__tests__/{requests.test.d.ts → core/requests.test.d.ts} +0 -0
- /package/dist/__tests__/{runtime.test.d.ts → core/runtime.test.d.ts} +0 -0
- /package/dist/__tests__/{step.test.d.ts → core/step.test.d.ts} +0 -0
- /package/dist/__tests__/{timeouts.test.d.ts → core/timeouts.test.d.ts} +0 -0
- /package/dist/__tests__/{axios.test.d.ts → integrations/axios.test.d.ts} +0 -0
- /package/dist/__tests__/{context.test.d.ts → integrations/context.test.d.ts} +0 -0
- /package/dist/__tests__/{expose-app-journey.test.d.ts → integrations/expose-app-journey.test.d.ts} +0 -0
- /package/dist/__tests__/{fetch.test.d.ts → integrations/fetch.test.d.ts} +0 -0
- /package/dist/__tests__/{jquery.test.d.ts → integrations/jquery.test.d.ts} +0 -0
- /package/dist/__tests__/{request-key.test.d.ts → integrations/request-key.test.d.ts} +0 -0
- /package/dist/__tests__/{scope.test.d.ts → integrations/scope.test.d.ts} +0 -0
- /package/dist/__tests__/{datadog.test.d.ts → sinks/datadog.test.d.ts} +0 -0
- /package/dist/__tests__/{sinks.test.d.ts → sinks/sinks.test.d.ts} +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
|
+
import { resolveStamp } from '../core';
|
|
2
|
+
import { adoptJourneyStepStamp, type JourneyStepStamp } from '../core/step-tag';
|
|
1
3
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
} from '
|
|
4
|
+
getInstrumentedAxiosSet,
|
|
5
|
+
registerInstrumentRecord,
|
|
6
|
+
type InstrumentRecord,
|
|
7
|
+
} from '../global';
|
|
8
|
+
import { isCanceledError, onHttpComplete } from './http-report';
|
|
7
9
|
import { requestKey, type JourneyTimedMeta } from './request-key';
|
|
10
|
+
import { acceptInstrumentTarget } from './soft-skip';
|
|
8
11
|
|
|
9
12
|
/**
|
|
10
13
|
* Minimal structural axios surface. No axios dependency — works across 0.31 and 1.x.
|
|
@@ -12,74 +15,209 @@ import { requestKey, type JourneyTimedMeta } from './request-key';
|
|
|
12
15
|
export interface AxiosLikeInstance {
|
|
13
16
|
defaults: { baseURL?: string };
|
|
14
17
|
interceptors: {
|
|
15
|
-
request: {
|
|
18
|
+
request: {
|
|
19
|
+
use(onFulfilled: (config: any) => any): unknown;
|
|
20
|
+
eject?(id: unknown): void;
|
|
21
|
+
};
|
|
16
22
|
response: {
|
|
17
23
|
use(onFulfilled: (response: any) => any, onRejected?: (error: any) => any): unknown;
|
|
24
|
+
eject?(id: unknown): void;
|
|
18
25
|
};
|
|
19
26
|
};
|
|
20
27
|
}
|
|
21
28
|
|
|
22
|
-
type TimedConfig = JourneyTimedMeta & { url?: string; baseURL?: string };
|
|
29
|
+
type TimedConfig = JourneyTimedMeta & { url?: string; baseURL?: string; method?: string };
|
|
30
|
+
|
|
31
|
+
const axiosStamps = new WeakMap<object, JourneyStepStamp>();
|
|
32
|
+
|
|
33
|
+
function stampFromConfig(config: TimedConfig | undefined): JourneyStepStamp | undefined {
|
|
34
|
+
if (!config) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
return axiosStamps.get(config) ?? adoptJourneyStepStamp(config.stamp);
|
|
38
|
+
}
|
|
23
39
|
|
|
24
40
|
function reportTiming(
|
|
25
41
|
instance: AxiosLikeInstance,
|
|
26
42
|
config: TimedConfig | undefined,
|
|
27
|
-
status: number | undefined
|
|
43
|
+
status: number | undefined,
|
|
44
|
+
aborted: boolean,
|
|
45
|
+
extras?: { request?: unknown; response?: unknown; error?: unknown }
|
|
28
46
|
): void {
|
|
29
|
-
|
|
30
|
-
|
|
47
|
+
if (!config || config.ignore === true) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const stamp = stampFromConfig(config);
|
|
51
|
+
if (!stamp || stamp.ignore) {
|
|
31
52
|
return;
|
|
32
53
|
}
|
|
33
54
|
const start = config.journeyStart ?? globalThis.performance.now();
|
|
34
55
|
const key = requestKey(config.url, config.baseURL, instance.defaults.baseURL);
|
|
35
|
-
|
|
56
|
+
onHttpComplete({
|
|
57
|
+
stamp,
|
|
58
|
+
status,
|
|
59
|
+
durationMs: globalThis.performance.now() - start,
|
|
60
|
+
requestKey: key,
|
|
61
|
+
aborted,
|
|
62
|
+
url: config.url,
|
|
63
|
+
method: config.method,
|
|
64
|
+
meta: config,
|
|
65
|
+
request: extras?.request ?? config,
|
|
66
|
+
response: extras?.response,
|
|
67
|
+
error: extras?.error,
|
|
68
|
+
});
|
|
36
69
|
}
|
|
37
70
|
|
|
38
|
-
|
|
71
|
+
function isAxiosLikeInstance(value: unknown): value is AxiosLikeInstance {
|
|
72
|
+
if (value == null || (typeof value !== 'object' && typeof value !== 'function')) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
const instance = value as AxiosLikeInstance;
|
|
76
|
+
return (
|
|
77
|
+
instance.defaults != null &&
|
|
78
|
+
typeof instance.defaults === 'object' &&
|
|
79
|
+
typeof instance.interceptors?.request?.use === 'function' &&
|
|
80
|
+
typeof instance.interceptors?.response?.use === 'function'
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function ejectInterceptor(
|
|
85
|
+
interceptors: { eject?: (id: unknown) => void } | undefined,
|
|
86
|
+
id: unknown
|
|
87
|
+
): boolean {
|
|
88
|
+
if (id === undefined || typeof interceptors?.eject !== 'function') {
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
interceptors.eject(id);
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
39
98
|
|
|
40
99
|
/**
|
|
41
100
|
* Wire an axios instance into the journey engine (observation-only).
|
|
42
101
|
* Call once per instance (idempotent). Register any async request interceptors
|
|
43
102
|
* (e.g. auth) before this call so this one runs first.
|
|
103
|
+
* Soft-skips when `instance` is missing or not axios-like — hosts can pass axios as-is.
|
|
104
|
+
* Returns a restore function that disposes handlers (they no-op after restore).
|
|
44
105
|
*/
|
|
45
|
-
export function instrumentAxios(instance: AxiosLikeInstance): void {
|
|
46
|
-
|
|
47
|
-
|
|
106
|
+
export function instrumentAxios(instance: AxiosLikeInstance | null | undefined): () => void {
|
|
107
|
+
const axios = acceptInstrumentTarget('Axios', instance, isAxiosLikeInstance);
|
|
108
|
+
const instrumented = getInstrumentedAxiosSet();
|
|
109
|
+
if (!axios || instrumented.has(axios as object)) {
|
|
110
|
+
return () => {};
|
|
48
111
|
}
|
|
49
|
-
instrumented.add(instance as object);
|
|
50
112
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
if (
|
|
60
|
-
|
|
113
|
+
const record: InstrumentRecord = { disposed: false };
|
|
114
|
+
let requestId: unknown;
|
|
115
|
+
let responseId: unknown;
|
|
116
|
+
let requestAdded = false;
|
|
117
|
+
let responseAdded = false;
|
|
118
|
+
|
|
119
|
+
try {
|
|
120
|
+
requestId = axios.interceptors.request.use(config => {
|
|
121
|
+
if (record.disposed) {
|
|
122
|
+
return config;
|
|
61
123
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
124
|
+
const timed = config as TimedConfig;
|
|
125
|
+
if (timed.ignore === true) {
|
|
126
|
+
return config;
|
|
127
|
+
}
|
|
128
|
+
const stamp = resolveStamp(timed.stamp);
|
|
129
|
+
if (stamp) {
|
|
130
|
+
axiosStamps.set(timed, stamp);
|
|
131
|
+
/*
|
|
132
|
+
* Keep config.stamp so later interceptors that clone/spread config
|
|
133
|
+
* still carry attribution. JourneyStepStamp.toJSON keeps this safe
|
|
134
|
+
* for AxiosError serialization.
|
|
135
|
+
*/
|
|
136
|
+
}
|
|
137
|
+
if (stamp?.ignore === true) {
|
|
138
|
+
return config;
|
|
139
|
+
}
|
|
140
|
+
if (stamp) {
|
|
141
|
+
timed.journeyStart = globalThis.performance.now();
|
|
142
|
+
}
|
|
143
|
+
return config;
|
|
144
|
+
});
|
|
145
|
+
requestAdded = true;
|
|
66
146
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
147
|
+
responseId = axios.interceptors.response.use(
|
|
148
|
+
response => {
|
|
149
|
+
if (!record.disposed) {
|
|
150
|
+
reportTiming(
|
|
151
|
+
axios,
|
|
152
|
+
response?.config as TimedConfig | undefined,
|
|
153
|
+
response?.status,
|
|
154
|
+
false,
|
|
155
|
+
{ request: response?.config, response }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
return response;
|
|
159
|
+
},
|
|
160
|
+
(error: unknown) => {
|
|
161
|
+
if (!record.disposed) {
|
|
162
|
+
const axiosError = error as {
|
|
163
|
+
config?: TimedConfig;
|
|
164
|
+
response?: { status?: number };
|
|
165
|
+
};
|
|
166
|
+
reportTiming(
|
|
167
|
+
axios,
|
|
168
|
+
axiosError?.config,
|
|
169
|
+
axiosError?.response?.status,
|
|
170
|
+
isCanceledError(error),
|
|
171
|
+
{
|
|
172
|
+
request: axiosError?.config,
|
|
173
|
+
response: axiosError?.response,
|
|
174
|
+
error,
|
|
175
|
+
}
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
/*
|
|
179
|
+
* Passthrough original reason (AxiosError or test doubles). Do not wrap —
|
|
180
|
+
* callers match on .response / AxiosError identity.
|
|
181
|
+
*/
|
|
182
|
+
return Promise.reject(error as Error);
|
|
183
|
+
}
|
|
184
|
+
);
|
|
185
|
+
responseAdded = true;
|
|
186
|
+
instrumented.add(axios as object);
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const unwound =
|
|
189
|
+
(!responseAdded || ejectInterceptor(axios.interceptors.response, responseId)) &&
|
|
190
|
+
(!requestAdded || ejectInterceptor(axios.interceptors.request, requestId));
|
|
191
|
+
if (!unwound) {
|
|
78
192
|
/*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
193
|
+
* Interceptors may still be registered and cannot be ejected. Mark disposed
|
|
194
|
+
* and keep the WeakSet slot, but register a restore so teardownJourney can
|
|
195
|
+
* unmark and a later retry can proceed.
|
|
81
196
|
*/
|
|
82
|
-
|
|
197
|
+
record.disposed = true;
|
|
198
|
+
instrumented.add(axios as object);
|
|
199
|
+
const restore = () => {
|
|
200
|
+
record.disposed = true;
|
|
201
|
+
instrumented.delete(axios as object);
|
|
202
|
+
};
|
|
203
|
+
record.restore = restore;
|
|
204
|
+
registerInstrumentRecord(record);
|
|
205
|
+
// eslint-disable-next-line no-console -- soft-fail host wiring
|
|
206
|
+
console.error('[journey] instrumentAxios failed', err);
|
|
207
|
+
return restore;
|
|
83
208
|
}
|
|
84
|
-
|
|
209
|
+
// eslint-disable-next-line no-console -- soft-fail host wiring
|
|
210
|
+
console.error('[journey] instrumentAxios failed', err);
|
|
211
|
+
return () => {};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const restore = () => {
|
|
215
|
+
record.disposed = true;
|
|
216
|
+
ejectInterceptor(axios.interceptors.request, requestId);
|
|
217
|
+
ejectInterceptor(axios.interceptors.response, responseId);
|
|
218
|
+
instrumented.delete(axios as object);
|
|
219
|
+
};
|
|
220
|
+
record.restore = restore;
|
|
221
|
+
registerInstrumentRecord(record);
|
|
222
|
+
return restore;
|
|
85
223
|
}
|
|
@@ -1,22 +1,29 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveStamp } from '../core';
|
|
2
|
+
import {
|
|
3
|
+
getInstrumentedFetchSet,
|
|
4
|
+
registerInstrumentRecord,
|
|
5
|
+
setFetchRestore,
|
|
6
|
+
type InstrumentRecord,
|
|
7
|
+
} from '../global';
|
|
8
|
+
import { isAbortError, onHttpComplete } from './http-report';
|
|
2
9
|
import { requestKey, type JourneyTimedMeta } from './request-key';
|
|
10
|
+
import { acceptInstrumentTarget } from './soft-skip';
|
|
3
11
|
|
|
4
12
|
/** Minimal structural fetch shape. */
|
|
5
|
-
|
|
13
|
+
type FetchLike = (input: any, init?: any) => Promise<{ status: number }>;
|
|
6
14
|
|
|
7
15
|
/** Holder of a `fetch` property — globalThis by default, or a scoped object. */
|
|
8
|
-
|
|
16
|
+
interface FetchTarget {
|
|
9
17
|
fetch: FetchLike;
|
|
10
18
|
}
|
|
11
19
|
|
|
12
20
|
export interface InstrumentFetchOptions {
|
|
13
21
|
/** Object whose `fetch` is wrapped. Defaults to globalThis. */
|
|
14
|
-
target?: FetchTarget;
|
|
22
|
+
target?: FetchTarget | null;
|
|
15
23
|
/** Home API base URL for request-key origin resolution. */
|
|
16
24
|
baseURL?: string;
|
|
17
25
|
}
|
|
18
26
|
|
|
19
|
-
const instrumented = new WeakSet<object>();
|
|
20
27
|
const originals = new WeakMap<object, FetchLike>();
|
|
21
28
|
|
|
22
29
|
function resolveUrl(input: any): string | undefined {
|
|
@@ -32,14 +39,47 @@ function resolveUrl(input: any): string | undefined {
|
|
|
32
39
|
return undefined;
|
|
33
40
|
}
|
|
34
41
|
|
|
42
|
+
/** Fetch defaults to GET; `init.method` overrides a Request's method. */
|
|
43
|
+
function resolveMethod(input: any, init?: any): string {
|
|
44
|
+
if (typeof init?.method === 'string' && init.method !== '') {
|
|
45
|
+
return init.method;
|
|
46
|
+
}
|
|
47
|
+
if (typeof input?.method === 'string' && input.method !== '') {
|
|
48
|
+
return input.method;
|
|
49
|
+
}
|
|
50
|
+
return 'GET';
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function isFetchTarget(value: unknown): value is FetchTarget {
|
|
54
|
+
return (
|
|
55
|
+
value != null &&
|
|
56
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
57
|
+
typeof (value as FetchTarget).fetch === 'function'
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
35
61
|
/**
|
|
36
62
|
* Wrap a target's fetch so requests inside a journeyStep are attributed and timed.
|
|
37
63
|
* Call once per target (idempotent — second call is a no-op). Returns a restore
|
|
38
64
|
* function that puts the original fetch back. Observation-only.
|
|
65
|
+
* Soft-skips when the resolved target is missing or has no callable `fetch`.
|
|
39
66
|
*/
|
|
40
|
-
export function instrumentFetch(options
|
|
41
|
-
|
|
67
|
+
export function instrumentFetch(options?: InstrumentFetchOptions | null): () => void {
|
|
68
|
+
if (options === null) {
|
|
69
|
+
acceptInstrumentTarget('Fetch', null, isFetchTarget);
|
|
70
|
+
return () => {};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const opts = options ?? {};
|
|
74
|
+
const candidate =
|
|
75
|
+
opts.target !== undefined ? opts.target : (globalThis as unknown as FetchTarget);
|
|
76
|
+
const target = acceptInstrumentTarget('Fetch', candidate, isFetchTarget);
|
|
77
|
+
if (!target) {
|
|
78
|
+
return () => {};
|
|
79
|
+
}
|
|
80
|
+
|
|
42
81
|
const key = target as object;
|
|
82
|
+
const instrumented = getInstrumentedFetchSet();
|
|
43
83
|
|
|
44
84
|
if (instrumented.has(key)) {
|
|
45
85
|
return () => {};
|
|
@@ -49,49 +89,95 @@ export function instrumentFetch(options: InstrumentFetchOptions = {}): () => voi
|
|
|
49
89
|
originals.set(key, original);
|
|
50
90
|
instrumented.add(key);
|
|
51
91
|
|
|
52
|
-
|
|
53
|
-
const wrapped: FetchLike = (input, init) => {
|
|
54
|
-
// Prefer explicit step.tag(); else soft ambient.
|
|
55
|
-
const explicit = resolveJourneyStep((init as JourneyTimedMeta | undefined)?.journeyStep);
|
|
56
|
-
const step = explicit ?? activeJourneyStep();
|
|
57
|
-
if (!step) {
|
|
58
|
-
return original.call(target, input, init);
|
|
59
|
-
}
|
|
92
|
+
const record: InstrumentRecord = { disposed: false };
|
|
60
93
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
reportBackendRequest(
|
|
76
|
-
step,
|
|
77
|
-
undefined,
|
|
78
|
-
globalThis.performance.now() - start,
|
|
79
|
-
endpointKey
|
|
80
|
-
);
|
|
81
|
-
// Passthrough original rejection reason; do not wrap (identity / AbortError).
|
|
82
|
-
return Promise.reject(error as Error);
|
|
94
|
+
try {
|
|
95
|
+
// Call through `target` — native fetch throws "Illegal invocation" with the wrong receiver.
|
|
96
|
+
const wrapped: FetchLike = (input, init) => {
|
|
97
|
+
if (record.disposed) {
|
|
98
|
+
return original.call(target, input, init);
|
|
99
|
+
}
|
|
100
|
+
// Prefer explicit step.stamp(); else auto-attribute when enabled.
|
|
101
|
+
const meta = init as JourneyTimedMeta | undefined;
|
|
102
|
+
if (meta?.ignore === true) {
|
|
103
|
+
return original.call(target, input, init);
|
|
104
|
+
}
|
|
105
|
+
const stamp = resolveStamp(meta?.stamp);
|
|
106
|
+
if (!stamp || stamp.ignore) {
|
|
107
|
+
return original.call(target, input, init);
|
|
83
108
|
}
|
|
84
|
-
);
|
|
85
|
-
};
|
|
86
109
|
|
|
87
|
-
|
|
110
|
+
const start = globalThis.performance.now();
|
|
111
|
+
const url = resolveUrl(input);
|
|
112
|
+
const method = resolveMethod(input, init);
|
|
113
|
+
const endpointKey = requestKey(url, undefined, opts.baseURL);
|
|
114
|
+
const promise = original.call(target, input, init);
|
|
115
|
+
return promise.then(
|
|
116
|
+
response => {
|
|
117
|
+
if (!record.disposed) {
|
|
118
|
+
onHttpComplete({
|
|
119
|
+
stamp,
|
|
120
|
+
status: response.status,
|
|
121
|
+
durationMs: globalThis.performance.now() - start,
|
|
122
|
+
requestKey: endpointKey,
|
|
123
|
+
url,
|
|
124
|
+
method,
|
|
125
|
+
meta,
|
|
126
|
+
request: init ?? input,
|
|
127
|
+
response,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
return response;
|
|
131
|
+
},
|
|
132
|
+
(error: unknown) => {
|
|
133
|
+
if (!record.disposed) {
|
|
134
|
+
onHttpComplete({
|
|
135
|
+
stamp,
|
|
136
|
+
status: undefined,
|
|
137
|
+
durationMs: globalThis.performance.now() - start,
|
|
138
|
+
requestKey: endpointKey,
|
|
139
|
+
aborted: isAbortError(error),
|
|
140
|
+
url,
|
|
141
|
+
method,
|
|
142
|
+
meta,
|
|
143
|
+
request: init ?? input,
|
|
144
|
+
error,
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
// Passthrough original rejection reason; do not wrap (identity / AbortError).
|
|
148
|
+
return Promise.reject(error as Error);
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
target.fetch = wrapped;
|
|
154
|
+
} catch (err) {
|
|
155
|
+
target.fetch = original;
|
|
156
|
+
instrumented.delete(key);
|
|
157
|
+
originals.delete(key);
|
|
158
|
+
// eslint-disable-next-line no-console -- soft-fail host wiring
|
|
159
|
+
console.error('[journey] instrumentFetch failed', err);
|
|
160
|
+
return () => {};
|
|
161
|
+
}
|
|
88
162
|
|
|
89
|
-
|
|
163
|
+
const restore = () => {
|
|
90
164
|
if (!instrumented.has(key)) {
|
|
91
165
|
return;
|
|
92
166
|
}
|
|
167
|
+
record.disposed = true;
|
|
93
168
|
target.fetch = originals.get(key) ?? original;
|
|
94
169
|
instrumented.delete(key);
|
|
95
170
|
originals.delete(key);
|
|
171
|
+
if (target === (globalThis as unknown as FetchTarget)) {
|
|
172
|
+
setFetchRestore(undefined);
|
|
173
|
+
}
|
|
96
174
|
};
|
|
175
|
+
|
|
176
|
+
record.restore = restore;
|
|
177
|
+
registerInstrumentRecord(record);
|
|
178
|
+
if (target === (globalThis as unknown as FetchTarget)) {
|
|
179
|
+
setFetchRestore(restore);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return restore;
|
|
97
183
|
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { MAX_ATTRIBUTED_REQUEST_KEYS } from '../core/limits';
|
|
2
|
+
import {
|
|
3
|
+
isJourneyRequestPolicy,
|
|
4
|
+
resolveRequestPolicy,
|
|
5
|
+
type JourneyHttpPolicyContext,
|
|
6
|
+
type JourneyRequestPolicy,
|
|
7
|
+
} from '../core/request-policy';
|
|
8
|
+
import { reportBackendRequest } from '../core/api';
|
|
9
|
+
import { canRouteHttpToJourney, resolveJourneyEngine } from '../core/runtime';
|
|
10
|
+
import type { JourneyStepStamp } from '../core/step-tag';
|
|
11
|
+
import type { StepRecord } from '../core/types';
|
|
12
|
+
import type { JourneyTimedMeta } from './request-key';
|
|
13
|
+
|
|
14
|
+
export interface HttpCompleteReport {
|
|
15
|
+
stamp: JourneyStepStamp | undefined;
|
|
16
|
+
status?: number;
|
|
17
|
+
durationMs: number;
|
|
18
|
+
requestKey: string;
|
|
19
|
+
aborted?: boolean;
|
|
20
|
+
url?: string;
|
|
21
|
+
method?: string;
|
|
22
|
+
/** Per-request literal overrides from JourneyTimedMeta. */
|
|
23
|
+
meta?: JourneyTimedMeta;
|
|
24
|
+
/** Transport request (axios config, fetch input/init, jQuery settings). */
|
|
25
|
+
request?: unknown;
|
|
26
|
+
/** Transport response (axios response, fetch Response, jqXHR). */
|
|
27
|
+
response?: unknown;
|
|
28
|
+
/** Rejection reason when the transport failed. */
|
|
29
|
+
error?: unknown;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function applyPolicy(
|
|
33
|
+
policy: JourneyRequestPolicy,
|
|
34
|
+
step: StepRecord,
|
|
35
|
+
reason: string,
|
|
36
|
+
status?: number
|
|
37
|
+
): void {
|
|
38
|
+
if (policy === 'continue') {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const journey = step.journey;
|
|
42
|
+
if (!journey || journey.closed) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const engine = resolveJourneyEngine(journey);
|
|
46
|
+
if (policy === 'exclude') {
|
|
47
|
+
engine.exclude();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
// 'bad'
|
|
51
|
+
step.outcome = 'bad';
|
|
52
|
+
step.reason ??= reason;
|
|
53
|
+
if (status !== undefined) {
|
|
54
|
+
step.httpStatus = status;
|
|
55
|
+
}
|
|
56
|
+
engine.failJourney(reason);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function httpScoreReason(report: HttpCompleteReport): string {
|
|
60
|
+
if (report.aborted) {
|
|
61
|
+
return 'request-aborted';
|
|
62
|
+
}
|
|
63
|
+
const status = report.status;
|
|
64
|
+
if (status !== undefined && status >= 400 && status < 500) {
|
|
65
|
+
return 'request-client-error';
|
|
66
|
+
}
|
|
67
|
+
return 'request-error';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Attribute a finished HTTP call: abort / 4xx policies, else 5xx+latency via reportBackendRequest.
|
|
72
|
+
* Ignored endpoint prefixes never change the journey verdict.
|
|
73
|
+
* 4xx with policy `'continue'` (default) still scores latency.
|
|
74
|
+
*/
|
|
75
|
+
export function onHttpComplete(report: HttpCompleteReport): void {
|
|
76
|
+
const {
|
|
77
|
+
stamp,
|
|
78
|
+
status,
|
|
79
|
+
durationMs,
|
|
80
|
+
requestKey,
|
|
81
|
+
aborted,
|
|
82
|
+
url,
|
|
83
|
+
method,
|
|
84
|
+
meta,
|
|
85
|
+
request,
|
|
86
|
+
response,
|
|
87
|
+
error,
|
|
88
|
+
} = report;
|
|
89
|
+
if (!stamp) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
const step = stamp.step;
|
|
93
|
+
if (!step?.journey) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
/*
|
|
97
|
+
* Unstamped foreign journeys (older package copies) cannot be closed from
|
|
98
|
+
* this bundle — do not write onHttpAborted / step.outcome on them.
|
|
99
|
+
*/
|
|
100
|
+
if (!canRouteHttpToJourney(step.journey)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const engine = resolveJourneyEngine(step.journey);
|
|
104
|
+
|
|
105
|
+
if (meta?.ignore === true || stamp.ignore === true) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (aborted) {
|
|
110
|
+
step.onHttpAborted = true;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (requestKey) {
|
|
114
|
+
step.attributedRequestKeys ??= [];
|
|
115
|
+
if (
|
|
116
|
+
step.attributedRequestKeys.length < MAX_ATTRIBUTED_REQUEST_KEYS &&
|
|
117
|
+
!step.attributedRequestKeys.includes(requestKey)
|
|
118
|
+
) {
|
|
119
|
+
step.attributedRequestKeys.push(requestKey);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (requestKey && engine.shouldIgnoreRequest(requestKey)) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ctx: JourneyHttpPolicyContext = {
|
|
128
|
+
requestKey,
|
|
129
|
+
status,
|
|
130
|
+
aborted: aborted === true,
|
|
131
|
+
url,
|
|
132
|
+
method,
|
|
133
|
+
journeyName: step.journey.name,
|
|
134
|
+
stepName: step.name,
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const score = stamp.score;
|
|
138
|
+
if (score) {
|
|
139
|
+
let policy: JourneyRequestPolicy | undefined;
|
|
140
|
+
try {
|
|
141
|
+
const scored = score({
|
|
142
|
+
...ctx,
|
|
143
|
+
durationMs,
|
|
144
|
+
request,
|
|
145
|
+
response,
|
|
146
|
+
error,
|
|
147
|
+
});
|
|
148
|
+
if (isJourneyRequestPolicy(scored)) {
|
|
149
|
+
policy = scored;
|
|
150
|
+
}
|
|
151
|
+
} catch {
|
|
152
|
+
policy = undefined;
|
|
153
|
+
}
|
|
154
|
+
if (policy !== undefined) {
|
|
155
|
+
applyPolicy(policy, step, httpScoreReason(report), status);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (aborted) {
|
|
161
|
+
const override = meta?.httpAbortedRequests;
|
|
162
|
+
const policy = isJourneyRequestPolicy(override)
|
|
163
|
+
? override
|
|
164
|
+
: resolveRequestPolicy(engine.getHttpAbortedRequestsPolicy(), 'continue', ctx);
|
|
165
|
+
applyPolicy(policy, step, 'request-aborted');
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (status !== undefined && status >= 400 && status < 500) {
|
|
170
|
+
const override = meta?.httpClientErrorRequests;
|
|
171
|
+
const policy = isJourneyRequestPolicy(override)
|
|
172
|
+
? override
|
|
173
|
+
: resolveRequestPolicy(engine.getHttpClientErrorRequestsPolicy(), 'continue', ctx);
|
|
174
|
+
if (policy !== 'continue') {
|
|
175
|
+
applyPolicy(policy, step, 'request-client-error', status);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
// 'continue' does not count 4xx as an error; still score latency below.
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
reportBackendRequest(step, status, durationMs, requestKey);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export { isAbortError, isCanceledError } from '../core/abort-error';
|