@dash0/sdk-web 0.19.0 → 0.20.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/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/instrumentations/http/fetch.js +17 -3
- package/dist/modules/instrumentations/http/fetch_test.js +103 -0
- package/dist/modules/semantic-conventions.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/semantic-conventions.d.ts +1 -0
- package/package.json +1 -1
- package/src/instrumentations/http/fetch.ts +16 -2
- package/src/instrumentations/http/fetch_test.ts +123 -0
- package/src/semantic-conventions.ts +1 -0
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { debug, observeResourcePerformance, perf, win, setTimeout, isSameOrigin, wrap, parseUrl, clearTimeout, } from "../../utils";
|
|
2
2
|
import { isUrlIgnored, matchesAny } from "../../utils/ignore-rules";
|
|
3
3
|
import { addAttribute, setSpanStatus, addW3CTraceContextHttpHeaders, addXRayTraceContextHttpHeaders, endSpan, errorToSpanStatus, recordException, startSpan, } from "../../utils/otel";
|
|
4
|
-
import { ERROR_TYPE, HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, SPAN_STATUS_ERROR, SPAN_STATUS_UNSET, } from "../../semantic-conventions";
|
|
4
|
+
import { ERROR_TYPE, HTTP_REQUEST_METHOD, HTTP_REQUEST_METHOD_ORIGINAL, HTTP_RESPONSE_STATUS_CODE, SPAN_STATUS_ERROR, SPAN_STATUS_UNSET, WEB_REQUEST_CANCELLED, } from "../../semantic-conventions";
|
|
5
5
|
import { vars } from "../../vars";
|
|
6
6
|
import { httpRequestHeaderKey, httpResponseHeaderKey } from "../../utils/otel/http";
|
|
7
7
|
import { sendSpan } from "../../transport";
|
|
@@ -86,12 +86,22 @@ function wrapFetch(original) {
|
|
|
86
86
|
addResponseData(span, origResponse);
|
|
87
87
|
return wrapResponse(origResponse, vars.maxToleranceForResourceTimingsMillis, () => performanceObserver.end(), (e) => {
|
|
88
88
|
performanceObserver.cancel();
|
|
89
|
-
|
|
89
|
+
if (request.signal?.aborted) {
|
|
90
|
+
endSpanOnAbort(span);
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
endSpanOnError(span, e);
|
|
94
|
+
}
|
|
90
95
|
});
|
|
91
96
|
}
|
|
92
97
|
catch (e) {
|
|
93
98
|
performanceObserver.cancel();
|
|
94
|
-
|
|
99
|
+
if (request.signal?.aborted) {
|
|
100
|
+
endSpanOnAbort(span);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
endSpanOnError(span, e);
|
|
104
|
+
}
|
|
95
105
|
throw e;
|
|
96
106
|
}
|
|
97
107
|
};
|
|
@@ -224,6 +234,10 @@ function endSpanOnError(span, error) {
|
|
|
224
234
|
recordException(span, error);
|
|
225
235
|
sendSpan(endSpan(span, errorToSpanStatus(error), undefined));
|
|
226
236
|
}
|
|
237
|
+
function endSpanOnAbort(span) {
|
|
238
|
+
addAttribute(span.attributes, WEB_REQUEST_CANCELLED, true);
|
|
239
|
+
sendSpan(endSpan(span, undefined, undefined));
|
|
240
|
+
}
|
|
227
241
|
function determinePropagatorTypes(url) {
|
|
228
242
|
const matchingTypes = [];
|
|
229
243
|
const isUrlSameOrigin = isSameOrigin(url);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
2
|
import { vars } from "../../vars";
|
|
3
3
|
import { instrumentFetch } from "./fetch";
|
|
4
|
+
import { sendSpan } from "../../transport";
|
|
5
|
+
vi.mock("../../transport", () => ({
|
|
6
|
+
sendSpan: vi.fn(),
|
|
7
|
+
}));
|
|
4
8
|
describe("fetch test", () => {
|
|
5
9
|
let fetchMock;
|
|
6
10
|
beforeEach(() => {
|
|
@@ -167,4 +171,103 @@ describe("fetch test", () => {
|
|
|
167
171
|
expect(fetchHeaders.get("traceparent")).not.toBeNull();
|
|
168
172
|
expect(fetchHeaders.get("X-Amzn-Trace-Id")).toBeNull();
|
|
169
173
|
});
|
|
174
|
+
describe("aborted requests", () => {
|
|
175
|
+
const sendSpanMock = sendSpan;
|
|
176
|
+
const NativeRequest = Request;
|
|
177
|
+
const lastSpan = () => {
|
|
178
|
+
const calls = sendSpanMock.mock.calls;
|
|
179
|
+
return calls[calls.length - 1][0];
|
|
180
|
+
};
|
|
181
|
+
const hasAttribute = (span, key, expectedValue) => span.attributes.some((a) => a.key === key && JSON.stringify(a.value) === JSON.stringify(expectedValue));
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
sendSpanMock.mockClear();
|
|
184
|
+
// jsdom's Request rejects its own AbortSignal instances (known jsdom bug).
|
|
185
|
+
// Stub Request with a minimal implementation that preserves signals verbatim.
|
|
186
|
+
class FakeRequest {
|
|
187
|
+
url;
|
|
188
|
+
method;
|
|
189
|
+
headers;
|
|
190
|
+
signal;
|
|
191
|
+
constructor(input, init) {
|
|
192
|
+
if (input instanceof FakeRequest) {
|
|
193
|
+
this.url = input.url;
|
|
194
|
+
this.method = init?.method ?? input.method;
|
|
195
|
+
this.headers = new Headers(init?.headers ?? input.headers);
|
|
196
|
+
this.signal = init?.signal ?? input.signal;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
this.url = String(input);
|
|
200
|
+
this.method = init?.method ?? "GET";
|
|
201
|
+
this.headers = new Headers(init?.headers);
|
|
202
|
+
this.signal = init?.signal ?? undefined;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
vi.stubGlobal("Request", FakeRequest);
|
|
207
|
+
});
|
|
208
|
+
afterEach(() => {
|
|
209
|
+
vi.stubGlobal("Request", NativeRequest);
|
|
210
|
+
});
|
|
211
|
+
it("marks fetch as cancelled when the abort signal fires before the response arrives", async () => {
|
|
212
|
+
const controller = new AbortController();
|
|
213
|
+
fetchMock.mockImplementation((_input, init) => new Promise((_resolve, reject) => {
|
|
214
|
+
const signal = init?.signal;
|
|
215
|
+
if (signal?.aborted) {
|
|
216
|
+
reject(new DOMException("aborted", "AbortError"));
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
signal?.addEventListener("abort", () => reject(new DOMException("aborted", "AbortError")));
|
|
220
|
+
}));
|
|
221
|
+
instrumentFetch();
|
|
222
|
+
// eslint-disable-next-line no-restricted-globals
|
|
223
|
+
const pending = fetch("http://localhost:3000/api/test", { signal: controller.signal });
|
|
224
|
+
controller.abort();
|
|
225
|
+
await expect(pending).rejects.toBeInstanceOf(DOMException);
|
|
226
|
+
expect(sendSpanMock).toHaveBeenCalledTimes(1);
|
|
227
|
+
const span = lastSpan();
|
|
228
|
+
expect(span.status?.code).toBe(0);
|
|
229
|
+
expect(span.status?.message).toBeUndefined();
|
|
230
|
+
expect(hasAttribute(span, "dash0.web.request.cancelled", { boolValue: true })).toBe(true);
|
|
231
|
+
expect(span.events.find((e) => e.name === "exception")).toBeUndefined();
|
|
232
|
+
});
|
|
233
|
+
it("marks fetch as cancelled when the abort signal fires while reading the response body", async () => {
|
|
234
|
+
const controller = new AbortController();
|
|
235
|
+
const body = new ReadableStream({
|
|
236
|
+
start(streamController) {
|
|
237
|
+
streamController.enqueue(new Uint8Array([0x68, 0x69]));
|
|
238
|
+
controller.signal.addEventListener("abort", () => {
|
|
239
|
+
streamController.error(new DOMException("aborted", "AbortError"));
|
|
240
|
+
});
|
|
241
|
+
},
|
|
242
|
+
});
|
|
243
|
+
fetchMock.mockImplementation(() => Promise.resolve(new Response(body, { status: 200 })));
|
|
244
|
+
instrumentFetch();
|
|
245
|
+
// eslint-disable-next-line no-restricted-globals
|
|
246
|
+
const response = await fetch("http://localhost:3000/api/test", { signal: controller.signal });
|
|
247
|
+
const reader = response.body.getReader();
|
|
248
|
+
await reader.read();
|
|
249
|
+
controller.abort();
|
|
250
|
+
await expect(reader.read()).rejects.toBeInstanceOf(DOMException);
|
|
251
|
+
expect(sendSpanMock).toHaveBeenCalledTimes(1);
|
|
252
|
+
const span = lastSpan();
|
|
253
|
+
// status was set from the 200 response and should not be overwritten
|
|
254
|
+
expect(span.status?.code).toBe(0);
|
|
255
|
+
expect(span.status?.message).toBeUndefined();
|
|
256
|
+
expect(hasAttribute(span, "dash0.web.request.cancelled", { boolValue: true })).toBe(true);
|
|
257
|
+
expect(hasAttribute(span, "http.response.status_code", { stringValue: "200" })).toBe(true);
|
|
258
|
+
expect(span.events.find((e) => e.name === "exception")).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
it("still treats non-abort rejections as failures", async () => {
|
|
261
|
+
fetchMock.mockImplementation(() => Promise.reject(new TypeError("network down")));
|
|
262
|
+
instrumentFetch();
|
|
263
|
+
// eslint-disable-next-line no-restricted-globals
|
|
264
|
+
await expect(fetch("http://localhost:3000/api/test")).rejects.toBeInstanceOf(TypeError);
|
|
265
|
+
expect(sendSpanMock).toHaveBeenCalledTimes(1);
|
|
266
|
+
const span = lastSpan();
|
|
267
|
+
expect(span.status?.code).toBe(2);
|
|
268
|
+
expect(span.status?.message).toBe("network down");
|
|
269
|
+
expect(hasAttribute(span, "dash0.web.request.cancelled", { boolValue: true })).toBe(false);
|
|
270
|
+
expect(span.events.some((e) => e.name === "exception")).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
170
273
|
});
|
|
@@ -9,6 +9,7 @@ export const DEPLOYMENT_ID = "deployment.id";
|
|
|
9
9
|
export const EVENT_NAME = "event.name";
|
|
10
10
|
export const WEB_EVENT_TITLE = "dash0.web.event.title";
|
|
11
11
|
export const WEB_EVENT_ID = "dash0.web.event.id";
|
|
12
|
+
export const WEB_REQUEST_CANCELLED = "dash0.web.request.cancelled";
|
|
12
13
|
export const PAGE_LOAD_ID = "page.load.id";
|
|
13
14
|
export const SESSION_ID = "session.id";
|
|
14
15
|
export const USER_AGENT = "user_agent.original";
|