@dash0/sdk-web 0.15.0 → 0.16.1
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 +35 -515
- package/dist/dash0.iife.js +1 -1
- package/dist/dash0.iife.js.map +1 -1
- package/dist/dash0.js +1 -1
- package/dist/dash0.js.map +1 -1
- package/dist/dash0.umd.cjs +1 -1
- package/dist/dash0.umd.cjs.map +1 -1
- package/dist/modules/api/init.js +30 -0
- package/dist/modules/api/init_test.js +102 -6
- package/dist/modules/api/report-error.js +1 -1
- package/dist/modules/instrumentations/errors/event-handlers.js +2 -2
- package/dist/modules/instrumentations/errors/unhandled-error.js +1 -1
- package/dist/modules/instrumentations/errors/unhandled-promise-rejection.js +4 -4
- package/dist/modules/instrumentations/http/fetch.js +125 -28
- package/dist/modules/instrumentations/http/fetch_test.js +170 -0
- package/dist/modules/instrumentations/http/propagator-integration_test.js +77 -0
- package/dist/modules/transport/index.js +2 -0
- package/dist/modules/utils/index.js +1 -0
- package/dist/modules/utils/otel/span.js +9 -0
- package/dist/modules/utils/otel/trace-context.js +24 -1
- package/dist/modules/utils/otel/trace-context_test.js +40 -0
- package/dist/modules/utils/performance.js +16 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/entrypoint/npm-package.d.ts +1 -1
- package/dist/types/instrumentations/errors/unhandled-error.d.ts +1 -1
- package/dist/types/instrumentations/http/fetch_test.d.ts +1 -0
- package/dist/types/instrumentations/http/propagator-integration_test.d.ts +1 -0
- package/dist/types/transport/index.d.ts +1 -1
- package/dist/types/types/options.d.ts +6 -1
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/otel/span.d.ts +5 -1
- package/dist/types/utils/otel/trace-context.d.ts +2 -1
- package/dist/types/utils/otel/trace-context_test.d.ts +1 -0
- package/dist/types/utils/performance.d.ts +5 -1
- package/dist/types/vars.d.ts +11 -0
- package/package.json +1 -1
- package/src/api/init.ts +34 -0
- package/src/api/init_test.ts +130 -9
- package/src/api/report-error.ts +1 -1
- package/src/entrypoint/npm-package.ts +1 -1
- package/src/instrumentations/errors/event-handlers.ts +2 -2
- package/src/instrumentations/errors/unhandled-error.ts +1 -1
- package/src/instrumentations/errors/unhandled-promise-rejection.ts +4 -4
- package/src/instrumentations/http/fetch.ts +148 -30
- package/src/instrumentations/http/fetch_test.ts +191 -0
- package/src/instrumentations/http/propagator-integration_test.ts +93 -0
- package/src/transport/index.ts +3 -1
- package/src/types/options.ts +7 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/otel/span.ts +16 -1
- package/src/utils/otel/trace-context.ts +30 -1
- package/src/utils/otel/trace-context_test.ts +59 -0
- package/src/utils/performance.ts +21 -4
- package/src/vars.ts +14 -0
package/dist/modules/api/init.js
CHANGED
|
@@ -40,6 +40,7 @@ export function init(opts) {
|
|
|
40
40
|
"urlAttributeScrubber",
|
|
41
41
|
"pageViewInstrumentation",
|
|
42
42
|
])));
|
|
43
|
+
initializePropagators(opts);
|
|
43
44
|
initializeResourceAttributes(opts);
|
|
44
45
|
initializeSignalAttributes(opts);
|
|
45
46
|
initializeTabId();
|
|
@@ -137,6 +138,35 @@ function detectDeploymentId(opts) {
|
|
|
137
138
|
return undefined;
|
|
138
139
|
}
|
|
139
140
|
}
|
|
141
|
+
function initializePropagators(opts) {
|
|
142
|
+
if (opts.propagators) {
|
|
143
|
+
if (opts.propagateTraceHeadersCorsURLs) {
|
|
144
|
+
warn("Both 'propagators' and deprecated 'propagateTraceHeadersCorsURLs' were provided. Using 'propagators' configuration. Please migrate to the new 'propagators' config.");
|
|
145
|
+
}
|
|
146
|
+
vars.propagators = opts.propagators;
|
|
147
|
+
}
|
|
148
|
+
// Handle legacy configuration
|
|
149
|
+
else if (opts.propagateTraceHeadersCorsURLs && opts.propagateTraceHeadersCorsURLs.length > 0) {
|
|
150
|
+
warn("'propagateTraceHeadersCorsURLs' is deprecated. Please use the new 'propagators' configuration.");
|
|
151
|
+
// Convert legacy config to new format - only include cross-origin URLs since same-origin is automatic
|
|
152
|
+
vars.propagators = [
|
|
153
|
+
{
|
|
154
|
+
type: "traceparent",
|
|
155
|
+
match: [...opts.propagateTraceHeadersCorsURLs],
|
|
156
|
+
},
|
|
157
|
+
];
|
|
158
|
+
}
|
|
159
|
+
// Default configuration - traceparent with empty match array
|
|
160
|
+
// Same-origin requests get ALL configured propagators, so this ensures traceparent for same-origin
|
|
161
|
+
else {
|
|
162
|
+
vars.propagators = [
|
|
163
|
+
{
|
|
164
|
+
type: "traceparent",
|
|
165
|
+
match: [],
|
|
166
|
+
},
|
|
167
|
+
];
|
|
168
|
+
}
|
|
169
|
+
}
|
|
140
170
|
function isInstrumentationEnabled(name, opts) {
|
|
141
171
|
const instrumentations = opts.enabledInstrumentations;
|
|
142
172
|
if (!instrumentations)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
2
|
// Mock all the instrumentation modules
|
|
3
3
|
vi.mock("../instrumentations/web-vitals", () => ({
|
|
4
4
|
startWebVitalsInstrumentation: vi.fn(),
|
|
@@ -12,26 +12,34 @@ vi.mock("../instrumentations/http/fetch", () => ({
|
|
|
12
12
|
vi.mock("../instrumentations/navigation", () => ({
|
|
13
13
|
startNavigationInstrumentation: vi.fn(),
|
|
14
14
|
}));
|
|
15
|
-
import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
|
|
16
15
|
import { startErrorInstrumentation } from "../instrumentations/errors";
|
|
17
16
|
import { instrumentFetch } from "../instrumentations/http/fetch";
|
|
18
17
|
import { startNavigationInstrumentation } from "../instrumentations/navigation";
|
|
18
|
+
import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
|
|
19
19
|
describe("init", () => {
|
|
20
20
|
const baseOptions = {
|
|
21
21
|
serviceName: "test-service",
|
|
22
22
|
endpoint: { url: "https://test-endpoint.com", authToken: "invalid" },
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
let vars;
|
|
25
|
+
let init;
|
|
26
|
+
beforeEach(async () => {
|
|
25
27
|
vi.clearAllMocks();
|
|
26
28
|
// Reset the hasBeenInitialised flag by re-importing the module
|
|
27
29
|
vi.resetModules();
|
|
30
|
+
// Reset vars state after module reset
|
|
31
|
+
// since init itself needs vars, this needs to imported again as well.
|
|
32
|
+
// Otherwise the tests that make assumptions on the vars fail because they use a different reference.
|
|
33
|
+
const { vars: importedVars } = await import("../vars");
|
|
34
|
+
const { init: importedInit } = await import("./init");
|
|
35
|
+
vars = importedVars;
|
|
36
|
+
init = importedInit;
|
|
28
37
|
});
|
|
29
38
|
afterEach(() => {
|
|
30
39
|
vi.clearAllMocks();
|
|
31
40
|
});
|
|
32
41
|
describe("instrumentation enablement", () => {
|
|
33
42
|
it("should enable all instrumentations when enabledInstrumentations is undefined", async () => {
|
|
34
|
-
const { init } = await import("./init");
|
|
35
43
|
init({
|
|
36
44
|
...baseOptions,
|
|
37
45
|
enabledInstrumentations: undefined,
|
|
@@ -55,7 +63,6 @@ describe("init", () => {
|
|
|
55
63
|
};
|
|
56
64
|
instrumentations.forEach((instrumentation) => {
|
|
57
65
|
it(`should enable ${instrumentation} instrumentation when present in enabledInstrumentations array`, async () => {
|
|
58
|
-
const { init } = await import("./init");
|
|
59
66
|
init({
|
|
60
67
|
...baseOptions,
|
|
61
68
|
enabledInstrumentations: [instrumentation],
|
|
@@ -69,7 +76,6 @@ describe("init", () => {
|
|
|
69
76
|
});
|
|
70
77
|
});
|
|
71
78
|
it(`should not enable ${instrumentation} instrumentation when not present in enabledInstrumentations array`, async () => {
|
|
72
|
-
const { init } = await import("./init");
|
|
73
79
|
const otherInstrumentations = instrumentations.filter((i) => i !== instrumentation);
|
|
74
80
|
init({
|
|
75
81
|
...baseOptions,
|
|
@@ -83,4 +89,94 @@ describe("init", () => {
|
|
|
83
89
|
});
|
|
84
90
|
});
|
|
85
91
|
});
|
|
92
|
+
describe("propagator configuration", () => {
|
|
93
|
+
it("should set propagators configuration when provided", async () => {
|
|
94
|
+
const propagators = [
|
|
95
|
+
{ type: "traceparent", match: [/.*\/api\/.*/] },
|
|
96
|
+
{ type: "xray", match: [/.*\.amazonaws\.com.*/] },
|
|
97
|
+
];
|
|
98
|
+
init({
|
|
99
|
+
...baseOptions,
|
|
100
|
+
propagators,
|
|
101
|
+
});
|
|
102
|
+
expect(vars.propagators).toEqual(propagators);
|
|
103
|
+
});
|
|
104
|
+
it("should warn when both propagators and legacy config are provided", async () => {
|
|
105
|
+
const spyOnWarn = vi.spyOn(console, "warn");
|
|
106
|
+
const propagators = [{ type: "traceparent", match: [/.*\/api\/.*/] }];
|
|
107
|
+
init({
|
|
108
|
+
...baseOptions,
|
|
109
|
+
propagators,
|
|
110
|
+
propagateTraceHeadersCorsURLs: [/.*\.example\.com.*/],
|
|
111
|
+
});
|
|
112
|
+
expect(spyOnWarn).toHaveBeenCalledWith("Both 'propagators' and deprecated 'propagateTraceHeadersCorsURLs' were provided. Using 'propagators' configuration. Please migrate to the new 'propagators' config.");
|
|
113
|
+
expect(vars.propagators).toEqual(propagators);
|
|
114
|
+
});
|
|
115
|
+
it("should convert legacy config to new format with deprecation warning", async () => {
|
|
116
|
+
const spyOnWarn = vi.spyOn(console, "warn");
|
|
117
|
+
const legacyConfig = [/.*\.example\.com.*/, /.*\.test\.com.*/];
|
|
118
|
+
init({
|
|
119
|
+
...baseOptions,
|
|
120
|
+
propagateTraceHeadersCorsURLs: legacyConfig,
|
|
121
|
+
});
|
|
122
|
+
expect(spyOnWarn).toHaveBeenCalledWith("'propagateTraceHeadersCorsURLs' is deprecated. Please use the new 'propagators' configuration.");
|
|
123
|
+
expect(vars.propagators).toEqual([
|
|
124
|
+
{
|
|
125
|
+
type: "traceparent",
|
|
126
|
+
match: [...legacyConfig],
|
|
127
|
+
},
|
|
128
|
+
]);
|
|
129
|
+
});
|
|
130
|
+
it("should set default propagators when no configuration is provided", async () => {
|
|
131
|
+
init(baseOptions);
|
|
132
|
+
expect(vars.propagators).toEqual([
|
|
133
|
+
{
|
|
134
|
+
type: "traceparent",
|
|
135
|
+
match: [],
|
|
136
|
+
},
|
|
137
|
+
]);
|
|
138
|
+
});
|
|
139
|
+
it("should not convert empty legacy config", async () => {
|
|
140
|
+
const spyOnWarn = vi.spyOn(console, "warn");
|
|
141
|
+
init({
|
|
142
|
+
...baseOptions,
|
|
143
|
+
propagateTraceHeadersCorsURLs: [],
|
|
144
|
+
});
|
|
145
|
+
expect(spyOnWarn).not.toHaveBeenCalled();
|
|
146
|
+
expect(vars.propagators).toEqual([
|
|
147
|
+
{
|
|
148
|
+
type: "traceparent",
|
|
149
|
+
match: [],
|
|
150
|
+
},
|
|
151
|
+
]);
|
|
152
|
+
});
|
|
153
|
+
it("should handle mixed pattern types in propagators", async () => {
|
|
154
|
+
const spyOnWarn = vi.spyOn(console, "warn");
|
|
155
|
+
const propagators = [
|
|
156
|
+
{
|
|
157
|
+
type: "traceparent",
|
|
158
|
+
match: [/.*\/api\/.*/],
|
|
159
|
+
},
|
|
160
|
+
{
|
|
161
|
+
type: "xray",
|
|
162
|
+
match: [/.*\.amazonaws\.com.*/, /.*\.aws\.com.*/],
|
|
163
|
+
},
|
|
164
|
+
];
|
|
165
|
+
init({
|
|
166
|
+
...baseOptions,
|
|
167
|
+
propagators,
|
|
168
|
+
});
|
|
169
|
+
expect(vars.propagators).toEqual(propagators);
|
|
170
|
+
expect(spyOnWarn).not.toHaveBeenCalled();
|
|
171
|
+
});
|
|
172
|
+
it("should handle empty propagators array", async () => {
|
|
173
|
+
const spyOnWarn = vi.spyOn(console, "warn");
|
|
174
|
+
init({
|
|
175
|
+
...baseOptions,
|
|
176
|
+
propagators: [],
|
|
177
|
+
});
|
|
178
|
+
expect(vars.propagators).toEqual([]);
|
|
179
|
+
expect(spyOnWarn).not.toHaveBeenCalled();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
86
182
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { reportUnhandledError as reportErrorInternal } from "../instrumentations/errors/unhandled-error";
|
|
2
2
|
export function reportError(error, opts) {
|
|
3
3
|
reportErrorInternal(error, opts);
|
|
4
4
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { vars } from "../../vars";
|
|
2
2
|
import { win } from "../../utils";
|
|
3
3
|
import { addWrappedDomEventListener, popWrappedDomEventListener, } from "./async-function-wrapping";
|
|
4
|
-
import { ignoreNextOnErrorEvent } from "./unhandled-error";
|
|
4
|
+
import { ignoreNextOnErrorEvent, reportUnhandledError } from "./unhandled-error";
|
|
5
5
|
export function startEventHandlerInstrumentation() {
|
|
6
6
|
if (vars.wrapEventHandlers) {
|
|
7
7
|
wrapEventTarget(win?.EventTarget);
|
|
@@ -29,7 +29,7 @@ function wrapEventTarget(EventTarget) {
|
|
|
29
29
|
return fn.apply(this, arguments);
|
|
30
30
|
}
|
|
31
31
|
catch (e) {
|
|
32
|
-
|
|
32
|
+
reportUnhandledError(e);
|
|
33
33
|
ignoreNextOnErrorEvent();
|
|
34
34
|
throw e;
|
|
35
35
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { win } from "../../utils";
|
|
2
|
-
import {
|
|
2
|
+
import { reportUnhandledError } from "./unhandled-error";
|
|
3
3
|
const MESSAGE_PREFIX = "Unhandled promise rejection: ";
|
|
4
4
|
const STACK_UNAVAILABLE_MESSAGE = "<unavailable because Promise wasn't rejected with an Error object>";
|
|
5
5
|
export function startUnhandledRejectionInstrumentation() {
|
|
@@ -9,19 +9,19 @@ export function startUnhandledRejectionInstrumentation() {
|
|
|
9
9
|
}
|
|
10
10
|
export function onUnhandledRejection(event) {
|
|
11
11
|
if (event.reason == null) {
|
|
12
|
-
|
|
12
|
+
reportUnhandledError({
|
|
13
13
|
message: MESSAGE_PREFIX + "<no reason defined>",
|
|
14
14
|
stack: STACK_UNAVAILABLE_MESSAGE,
|
|
15
15
|
});
|
|
16
16
|
}
|
|
17
17
|
else if (typeof event.reason.message === "string") {
|
|
18
|
-
|
|
18
|
+
reportUnhandledError({
|
|
19
19
|
message: MESSAGE_PREFIX + event.reason.message,
|
|
20
20
|
stack: typeof event.reason.stack === "string" ? event.reason.stack : STACK_UNAVAILABLE_MESSAGE,
|
|
21
21
|
});
|
|
22
22
|
}
|
|
23
23
|
else if (typeof event.reason !== "object") {
|
|
24
|
-
|
|
24
|
+
reportUnhandledError({
|
|
25
25
|
message: MESSAGE_PREFIX + event.reason,
|
|
26
26
|
stack: STACK_UNAVAILABLE_MESSAGE,
|
|
27
27
|
});
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { debug, observeResourcePerformance, win } from "../../utils";
|
|
1
|
+
import { debug, observeResourcePerformance, perf, win, setTimeout, isSameOrigin, wrap, parseUrl } from "../../utils";
|
|
2
2
|
import { isUrlIgnored, matchesAny } from "../../utils/ignore-rules";
|
|
3
|
-
import { addAttribute, setSpanStatus,
|
|
3
|
+
import { addAttribute, setSpanStatus, addW3CTraceContextHttpHeaders, addXRayTraceContextHttpHeaders, endSpan, errorToSpanStatus, recordException, startSpan, } from "../../utils/otel";
|
|
4
4
|
import { ERROR_TYPE, HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, SPAN_STATUS_ERROR, SPAN_STATUS_UNSET, } from "../../semantic-conventions";
|
|
5
|
-
import { isSameOrigin, wrap, parseUrl } from "../../utils";
|
|
6
5
|
import { vars } from "../../vars";
|
|
7
6
|
import { httpRequestHeaderKey, httpResponseHeaderKey } from "../../utils/otel/http";
|
|
8
7
|
import { sendSpan } from "../../transport";
|
|
@@ -47,22 +46,23 @@ function wrapFetch(original) {
|
|
|
47
46
|
if (!isWellKnownMethod) {
|
|
48
47
|
addAttribute(span.attributes, HTTP_REQUEST_METHOD_ORIGINAL, originalMethod);
|
|
49
48
|
}
|
|
50
|
-
const
|
|
49
|
+
const propagatorTypes = determinePropagatorTypes(url);
|
|
50
|
+
const shouldSetCorrelationHeaders = propagatorTypes.length > 0;
|
|
51
51
|
if (shouldSetCorrelationHeaders) {
|
|
52
52
|
if (copyOfInit?.headers) {
|
|
53
53
|
// ensure we have a unified container for the headers
|
|
54
54
|
copyOfInit.headers = new Headers(copyOfInit.headers);
|
|
55
|
-
addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
|
|
55
|
+
addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span, propagatorTypes);
|
|
56
56
|
}
|
|
57
57
|
else if (input instanceof Request) {
|
|
58
|
-
addTraceContextHttpHeaders(request.headers.append, request.headers, span);
|
|
58
|
+
addTraceContextHttpHeaders(request.headers.append, request.headers, span, propagatorTypes);
|
|
59
59
|
}
|
|
60
60
|
else {
|
|
61
61
|
if (!copyOfInit) {
|
|
62
62
|
copyOfInit = {};
|
|
63
63
|
}
|
|
64
64
|
copyOfInit.headers = new Headers();
|
|
65
|
-
addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
|
|
65
|
+
addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span, propagatorTypes);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
tryCaptureHttpHeaders(request.headers, span, (k) => httpRequestHeaderKey(k));
|
|
@@ -82,16 +82,12 @@ function wrapFetch(original) {
|
|
|
82
82
|
});
|
|
83
83
|
performanceObserver.start();
|
|
84
84
|
try {
|
|
85
|
-
const
|
|
86
|
-
addResponseData(span,
|
|
87
|
-
|
|
88
|
-
waitForFullResponse(response)
|
|
89
|
-
.then(() => performanceObserver.end())
|
|
90
|
-
.catch((e) => {
|
|
85
|
+
const origResponse = await original(input instanceof Request ? request : input, copyOfInit);
|
|
86
|
+
addResponseData(span, origResponse);
|
|
87
|
+
return wrapResponse(origResponse, vars.maxToleranceForResourceTimingsMillis, () => performanceObserver.end(), (e) => {
|
|
91
88
|
performanceObserver.cancel();
|
|
92
89
|
endSpanOnError(span, e);
|
|
93
90
|
});
|
|
94
|
-
return response;
|
|
95
91
|
}
|
|
96
92
|
catch (e) {
|
|
97
93
|
performanceObserver.cancel();
|
|
@@ -145,23 +141,124 @@ function addResponseData(span, response) {
|
|
|
145
141
|
addAttribute(span.attributes, HTTP_RESPONSE_STATUS_CODE, String(status));
|
|
146
142
|
tryCaptureHttpHeaders(response.headers, span, (k) => httpResponseHeaderKey(k));
|
|
147
143
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
144
|
+
/**
|
|
145
|
+
* Wraps the response to be able to detect when it is fully read
|
|
146
|
+
* @param originalResponse
|
|
147
|
+
* @param readTimeoutMs Timeout applied between reading of response chunks, if exceeded the response is considered abandoned and onDone is called.
|
|
148
|
+
* @param onDone Called when the response is completely read or with "fallbackEndTs" when reading timed out.
|
|
149
|
+
* @param onError
|
|
150
|
+
*/
|
|
151
|
+
function wrapResponse(originalResponse, readTimeoutMs, onDone, onError) {
|
|
152
|
+
// When the response was wrapped (i.e. first available to js) we use this to replace or find the actual end timestamp
|
|
153
|
+
// in case the response body is never read by js
|
|
154
|
+
let fallbackTs = perf.now();
|
|
155
|
+
const body = originalResponse.body;
|
|
156
|
+
if (!body) {
|
|
157
|
+
onDone();
|
|
158
|
+
return originalResponse;
|
|
159
|
+
}
|
|
160
|
+
let cbCalled = false;
|
|
161
|
+
const handleDone = (fallbackEndTs) => {
|
|
162
|
+
if (cbCalled)
|
|
163
|
+
return;
|
|
164
|
+
onDone(fallbackEndTs);
|
|
165
|
+
cbCalled = true;
|
|
166
|
+
};
|
|
167
|
+
const handleError = (e) => {
|
|
168
|
+
if (cbCalled)
|
|
169
|
+
return;
|
|
170
|
+
onError(e);
|
|
171
|
+
cbCalled = true;
|
|
172
|
+
};
|
|
173
|
+
let bodyNeverCompletelyReadTimeout = setTimeout(() => handleDone(fallbackTs), readTimeoutMs);
|
|
174
|
+
const reader = body.getReader();
|
|
175
|
+
const stream = new ReadableStream({
|
|
176
|
+
async pull(controller) {
|
|
177
|
+
try {
|
|
178
|
+
clearTimeout(bodyNeverCompletelyReadTimeout);
|
|
179
|
+
const { value, done } = await reader.read();
|
|
180
|
+
if (done) {
|
|
181
|
+
reader.releaseLock();
|
|
182
|
+
controller.close();
|
|
183
|
+
handleDone();
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
fallbackTs = perf.now();
|
|
187
|
+
bodyNeverCompletelyReadTimeout = setTimeout(() => handleDone(fallbackTs), readTimeoutMs);
|
|
188
|
+
controller.enqueue(value);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
handleError(e);
|
|
193
|
+
controller.error(e);
|
|
194
|
+
try {
|
|
195
|
+
reader.releaseLock();
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Spec reference:
|
|
199
|
+
// https://streams.spec.whatwg.org/#default-reader-release-lock
|
|
200
|
+
//
|
|
201
|
+
// releaseLock() only throws if called on an invalid reader
|
|
202
|
+
// (i.e. reader.[[stream]] is undefined, meaning the lock is already released
|
|
203
|
+
// or the reader was never associated). In normal use this cannot happen.
|
|
204
|
+
// This catch is defensive only.
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
cancel(reason) {
|
|
209
|
+
clearTimeout(bodyNeverCompletelyReadTimeout);
|
|
210
|
+
handleDone();
|
|
211
|
+
return reader.cancel(reason);
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
return new Response(stream, {
|
|
215
|
+
status: originalResponse.status,
|
|
216
|
+
statusText: originalResponse.statusText,
|
|
217
|
+
headers: originalResponse.headers,
|
|
162
218
|
});
|
|
163
219
|
}
|
|
164
220
|
function endSpanOnError(span, error) {
|
|
165
221
|
recordException(span, error);
|
|
166
222
|
sendSpan(endSpan(span, errorToSpanStatus(error), undefined));
|
|
167
223
|
}
|
|
224
|
+
function determinePropagatorTypes(url) {
|
|
225
|
+
const matchingTypes = [];
|
|
226
|
+
const isUrlSameOrigin = isSameOrigin(url);
|
|
227
|
+
// For same-origin requests, always include traceparent + all configured propagators
|
|
228
|
+
if (isUrlSameOrigin) {
|
|
229
|
+
// Always add traceparent for same-origin requests
|
|
230
|
+
matchingTypes.push("traceparent");
|
|
231
|
+
// Add all other configured propagator types for same-origin requests
|
|
232
|
+
if (vars.propagators) {
|
|
233
|
+
for (const propagator of vars.propagators) {
|
|
234
|
+
if (propagator.type !== "traceparent" && !matchingTypes.includes(propagator.type)) {
|
|
235
|
+
matchingTypes.push(propagator.type);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return matchingTypes;
|
|
240
|
+
}
|
|
241
|
+
// For cross-origin requests, use new propagators config if available
|
|
242
|
+
if (vars.propagators) {
|
|
243
|
+
for (const propagator of vars.propagators) {
|
|
244
|
+
if (matchesAny(propagator.match, url)) {
|
|
245
|
+
// Avoid duplicates
|
|
246
|
+
if (!matchingTypes.includes(propagator.type)) {
|
|
247
|
+
matchingTypes.push(propagator.type);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return matchingTypes;
|
|
252
|
+
}
|
|
253
|
+
return [];
|
|
254
|
+
}
|
|
255
|
+
function addTraceContextHttpHeaders(fn, ctx, span, types) {
|
|
256
|
+
for (const type of types) {
|
|
257
|
+
if (type === "xray") {
|
|
258
|
+
addXRayTraceContextHttpHeaders(fn, ctx, span);
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
addW3CTraceContextHttpHeaders(fn, ctx, span);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { vars } from "../../vars";
|
|
3
|
+
import { instrumentFetch } from "./fetch";
|
|
4
|
+
describe("fetch test", () => {
|
|
5
|
+
let fetchMock;
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
fetchMock = vi.fn(() => ({
|
|
8
|
+
ok: false,
|
|
9
|
+
headers: new Headers(),
|
|
10
|
+
status: 200,
|
|
11
|
+
clone: () => ({
|
|
12
|
+
body: null,
|
|
13
|
+
}),
|
|
14
|
+
}));
|
|
15
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
16
|
+
vi.stubGlobal("location", { origin: "http://localhost:3000" });
|
|
17
|
+
});
|
|
18
|
+
afterEach(() => {
|
|
19
|
+
vi.resetAllMocks();
|
|
20
|
+
vars.propagators = undefined;
|
|
21
|
+
});
|
|
22
|
+
it("should inject traceparent header for cross-origin requests", async () => {
|
|
23
|
+
vars.propagators = [
|
|
24
|
+
{
|
|
25
|
+
type: "traceparent",
|
|
26
|
+
match: [new RegExp("http://foo.bar/")],
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
instrumentFetch();
|
|
30
|
+
// eslint-disable-next-line no-restricted-globals
|
|
31
|
+
await fetch("http://foo.bar/foo");
|
|
32
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
33
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
34
|
+
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
35
|
+
});
|
|
36
|
+
it("should inject xray header for cross-origin requests", async () => {
|
|
37
|
+
vars.propagators = [
|
|
38
|
+
{
|
|
39
|
+
type: "xray",
|
|
40
|
+
match: [new RegExp("http://foo.bar/")],
|
|
41
|
+
},
|
|
42
|
+
];
|
|
43
|
+
instrumentFetch();
|
|
44
|
+
// eslint-disable-next-line no-restricted-globals
|
|
45
|
+
await fetch("http://foo.bar/foo");
|
|
46
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
47
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
48
|
+
expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
|
|
49
|
+
});
|
|
50
|
+
it("should inject both headers for cross-origin requests when both match", async () => {
|
|
51
|
+
vars.propagators = [
|
|
52
|
+
{
|
|
53
|
+
type: "traceparent",
|
|
54
|
+
match: [new RegExp("http://foo.bar/")],
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
type: "xray",
|
|
58
|
+
match: [new RegExp("http://foo.bar/")],
|
|
59
|
+
},
|
|
60
|
+
];
|
|
61
|
+
instrumentFetch();
|
|
62
|
+
// eslint-disable-next-line no-restricted-globals
|
|
63
|
+
await fetch("http://foo.bar/foo");
|
|
64
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
65
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
66
|
+
expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
|
|
67
|
+
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
68
|
+
});
|
|
69
|
+
it("should inject no headers for non-matching cross-origin requests", async () => {
|
|
70
|
+
vars.propagators = [];
|
|
71
|
+
instrumentFetch();
|
|
72
|
+
// eslint-disable-next-line no-restricted-globals
|
|
73
|
+
await fetch("http://foo.bar/foo");
|
|
74
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
75
|
+
const expectedHeaders = undefined;
|
|
76
|
+
expect(fetchMock).toHaveBeenCalledWith("http://foo.bar/foo", expectedHeaders);
|
|
77
|
+
});
|
|
78
|
+
// New same-origin behavior tests
|
|
79
|
+
it("should inject all configured propagator headers for same-origin requests", async () => {
|
|
80
|
+
vars.propagators = [
|
|
81
|
+
{
|
|
82
|
+
type: "traceparent",
|
|
83
|
+
match: [new RegExp("http://foo.bar/")], // Doesn't match same-origin
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
type: "xray",
|
|
87
|
+
match: [new RegExp("http://baz.com/")], // Doesn't match same-origin
|
|
88
|
+
},
|
|
89
|
+
];
|
|
90
|
+
instrumentFetch();
|
|
91
|
+
// Same-origin request
|
|
92
|
+
// eslint-disable-next-line no-restricted-globals
|
|
93
|
+
await fetch("http://localhost:3000/api/test");
|
|
94
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
95
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
96
|
+
// Both headers should be present for same-origin, regardless of match patterns
|
|
97
|
+
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
98
|
+
expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
|
|
99
|
+
});
|
|
100
|
+
it("should inject only traceparent for same-origin when only traceparent propagator configured", async () => {
|
|
101
|
+
vars.propagators = [
|
|
102
|
+
{
|
|
103
|
+
type: "traceparent",
|
|
104
|
+
match: [new RegExp("http://foo.bar/")],
|
|
105
|
+
},
|
|
106
|
+
];
|
|
107
|
+
instrumentFetch();
|
|
108
|
+
// Same-origin request
|
|
109
|
+
// eslint-disable-next-line no-restricted-globals
|
|
110
|
+
await fetch("http://localhost:3000/api/test");
|
|
111
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
112
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
113
|
+
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
114
|
+
expect(fetchHeaders.get("X-Amzn-Trace-Id")).toBeNull();
|
|
115
|
+
});
|
|
116
|
+
it("should inject both traceparent and xray for same-origin when only xray propagator configured", async () => {
|
|
117
|
+
vars.propagators = [
|
|
118
|
+
{
|
|
119
|
+
type: "xray",
|
|
120
|
+
match: [new RegExp("http://foo.bar/")],
|
|
121
|
+
},
|
|
122
|
+
];
|
|
123
|
+
instrumentFetch();
|
|
124
|
+
// Same-origin request
|
|
125
|
+
// eslint-disable-next-line no-restricted-globals
|
|
126
|
+
await fetch("http://localhost:3000/api/test");
|
|
127
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
128
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
129
|
+
// Same-origin always gets traceparent + all configured propagator types
|
|
130
|
+
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
131
|
+
expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
|
|
132
|
+
});
|
|
133
|
+
it("should inject traceparent for same-origin when default traceparent propagator configured", async () => {
|
|
134
|
+
vars.propagators = [
|
|
135
|
+
{
|
|
136
|
+
type: "traceparent",
|
|
137
|
+
match: [], // Empty match array - matches no cross-origin URLs but same-origin gets all propagators
|
|
138
|
+
},
|
|
139
|
+
];
|
|
140
|
+
instrumentFetch();
|
|
141
|
+
// Same-origin request
|
|
142
|
+
// eslint-disable-next-line no-restricted-globals
|
|
143
|
+
await fetch("http://localhost:3000/api/test");
|
|
144
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
145
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
146
|
+
// Same-origin gets all configured propagator types
|
|
147
|
+
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
148
|
+
expect(fetchHeaders.get("X-Amzn-Trace-Id")).toBeNull();
|
|
149
|
+
});
|
|
150
|
+
it("should follow cross-origin pattern matching for non-same-origin requests", async () => {
|
|
151
|
+
vars.propagators = [
|
|
152
|
+
{
|
|
153
|
+
type: "traceparent",
|
|
154
|
+
match: [new RegExp("http://foo.bar/")],
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
type: "xray",
|
|
158
|
+
match: [new RegExp("http://baz.com/")],
|
|
159
|
+
},
|
|
160
|
+
];
|
|
161
|
+
instrumentFetch();
|
|
162
|
+
// Cross-origin request that matches only traceparent pattern
|
|
163
|
+
// eslint-disable-next-line no-restricted-globals
|
|
164
|
+
await fetch("http://foo.bar/api");
|
|
165
|
+
expect(fetchMock).toHaveBeenCalledOnce();
|
|
166
|
+
const fetchHeaders = fetchMock.mock.calls[0].at(1).headers;
|
|
167
|
+
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
168
|
+
expect(fetchHeaders.get("X-Amzn-Trace-Id")).toBeNull();
|
|
169
|
+
});
|
|
170
|
+
});
|