@dash0/sdk-web 0.16.0 → 0.16.2

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.
Files changed (32) hide show
  1. package/README.md +32 -583
  2. package/dist/dash0.iife.js +1 -1
  3. package/dist/dash0.iife.js.map +1 -1
  4. package/dist/dash0.js +1 -1
  5. package/dist/dash0.js.map +1 -1
  6. package/dist/dash0.umd.cjs +1 -1
  7. package/dist/dash0.umd.cjs.map +1 -1
  8. package/dist/modules/api/report-error.js +1 -1
  9. package/dist/modules/instrumentations/errors/event-handlers.js +2 -2
  10. package/dist/modules/instrumentations/errors/unhandled-error.js +1 -1
  11. package/dist/modules/instrumentations/errors/unhandled-promise-rejection.js +4 -4
  12. package/dist/modules/instrumentations/http/fetch.js +83 -23
  13. package/dist/modules/transport/index.js +2 -0
  14. package/dist/modules/utils/index.js +1 -0
  15. package/dist/modules/utils/otel/span.js +9 -0
  16. package/dist/modules/utils/performance.js +16 -3
  17. package/dist/tsconfig.tsbuildinfo +1 -1
  18. package/dist/types/instrumentations/errors/unhandled-error.d.ts +1 -1
  19. package/dist/types/transport/index.d.ts +1 -1
  20. package/dist/types/utils/index.d.ts +1 -0
  21. package/dist/types/utils/otel/span.d.ts +5 -1
  22. package/dist/types/utils/performance.d.ts +5 -1
  23. package/package.json +1 -1
  24. package/src/api/report-error.ts +1 -1
  25. package/src/instrumentations/errors/event-handlers.ts +2 -2
  26. package/src/instrumentations/errors/unhandled-error.ts +1 -1
  27. package/src/instrumentations/errors/unhandled-promise-rejection.ts +4 -4
  28. package/src/instrumentations/http/fetch.ts +95 -24
  29. package/src/transport/index.ts +3 -1
  30. package/src/utils/index.ts +1 -0
  31. package/src/utils/otel/span.ts +16 -1
  32. package/src/utils/performance.ts +21 -4
@@ -1,4 +1,4 @@
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
3
  import {
4
4
  addAttribute,
@@ -20,7 +20,6 @@ import {
20
20
  SPAN_STATUS_ERROR,
21
21
  SPAN_STATUS_UNSET,
22
22
  } from "../../semantic-conventions";
23
- import { isSameOrigin, wrap, parseUrl } from "../../utils";
24
23
  import { vars, PropagatorType } from "../../vars";
25
24
  import { httpRequestHeaderKey, httpResponseHeaderKey } from "../../utils/otel/http";
26
25
  import { sendSpan } from "../../transport";
@@ -111,18 +110,18 @@ function wrapFetch(original: typeof fetch) {
111
110
 
112
111
  performanceObserver.start();
113
112
  try {
114
- const response = await original(input instanceof Request ? request : input, copyOfInit);
115
- addResponseData(span, response);
113
+ const origResponse = await original(input instanceof Request ? request : input, copyOfInit);
114
+ addResponseData(span, origResponse);
116
115
 
117
- // We use a separate promise here because this needs to happen in parallel to application code consuming the response
118
- waitForFullResponse(response)
119
- .then(() => performanceObserver.end())
120
- .catch((e) => {
116
+ return wrapResponse(
117
+ origResponse,
118
+ vars.maxToleranceForResourceTimingsMillis,
119
+ () => performanceObserver.end(),
120
+ (e) => {
121
121
  performanceObserver.cancel();
122
- endSpanOnError(span, e as Exception);
123
- });
124
-
125
- return response;
122
+ endSpanOnError(span, e);
123
+ }
124
+ );
126
125
  } catch (e) {
127
126
  performanceObserver.cancel();
128
127
  endSpanOnError(span, e as Exception);
@@ -177,20 +176,87 @@ function addResponseData(span: InProgressSpan, response: Response) {
177
176
  tryCaptureHttpHeaders(response.headers, span, (k) => httpResponseHeaderKey(k));
178
177
  }
179
178
 
180
- function waitForFullResponse(response: Response): Promise<void> {
181
- return new Promise((resolve) => {
182
- const clonedResponse = response.clone();
183
- const body = clonedResponse.body;
179
+ /**
180
+ * Wraps the response to be able to detect when it is fully read
181
+ * @param originalResponse
182
+ * @param readTimeoutMs Timeout applied between reading of response chunks, if exceeded the response is considered abandoned and onDone is called.
183
+ * @param onDone Called when the response is completely read or with "fallbackEndTs" when reading timed out.
184
+ * @param onError
185
+ */
186
+ function wrapResponse(
187
+ originalResponse: Response,
188
+ readTimeoutMs: number,
189
+ onDone: (fallbackEndTs?: number) => void,
190
+ onError: (e: Exception) => void
191
+ ): Response {
192
+ // When the response was wrapped (i.e. first available to js) we use this to replace or find the actual end timestamp
193
+ // in case the response body is never read by js
194
+ let fallbackTs: number = perf.now();
195
+ const body = originalResponse.body;
196
+
197
+ // For some reason browsers return a response body on responses that can't be constructed with one. In that case we can't wrap the body stream
198
+ if (!body || !responseCanHaveBody(originalResponse)) {
199
+ onDone();
200
+ return originalResponse;
201
+ }
202
+
203
+ let cbCalled: boolean = false;
204
+ const handleDone = (fallbackEndTs?: number) => {
205
+ if (cbCalled) return;
206
+ onDone(fallbackEndTs);
207
+ cbCalled = true;
208
+ };
209
+ const handleError = (e: Exception) => {
210
+ if (cbCalled) return;
211
+ onError(e);
212
+ cbCalled = true;
213
+ };
214
+
215
+ let bodyNeverCompletelyReadTimeout = setTimeout(() => handleDone(fallbackTs), readTimeoutMs);
184
216
 
185
- if (!body) return resolve();
217
+ const reader = body.getReader();
218
+ const stream = new ReadableStream({
219
+ async pull(controller) {
220
+ try {
221
+ clearTimeout(bodyNeverCompletelyReadTimeout);
222
+ const { value, done } = await reader.read();
223
+ if (done) {
224
+ reader.releaseLock();
225
+ controller.close();
226
+ handleDone();
227
+ } else {
228
+ fallbackTs = perf.now();
229
+ bodyNeverCompletelyReadTimeout = setTimeout(() => handleDone(fallbackTs), readTimeoutMs);
230
+ controller.enqueue(value);
231
+ }
232
+ } catch (e) {
233
+ handleError(e as Exception);
234
+ controller.error(e);
235
+
236
+ try {
237
+ reader.releaseLock();
238
+ } catch {
239
+ // Spec reference:
240
+ // https://streams.spec.whatwg.org/#default-reader-release-lock
241
+ //
242
+ // releaseLock() only throws if called on an invalid reader
243
+ // (i.e. reader.[[stream]] is undefined, meaning the lock is already released
244
+ // or the reader was never associated). In normal use this cannot happen.
245
+ // This catch is defensive only.
246
+ }
247
+ }
248
+ },
249
+ cancel(reason) {
250
+ clearTimeout(bodyNeverCompletelyReadTimeout);
251
+ handleDone();
252
+ return reader.cancel(reason);
253
+ },
254
+ });
186
255
 
187
- const reader = body.getReader();
188
- const read = async () => {
189
- const { done } = await reader.read();
190
- if (done) return resolve();
191
- return read();
192
- };
193
- return read();
256
+ return new Response(stream, {
257
+ status: originalResponse.status,
258
+ statusText: originalResponse.statusText,
259
+ headers: originalResponse.headers,
194
260
  });
195
261
  }
196
262
 
@@ -249,3 +315,8 @@ function addTraceContextHttpHeaders(
249
315
  }
250
316
  }
251
317
  }
318
+
319
+ function responseCanHaveBody(response: Response) {
320
+ const status = response.status;
321
+ return status >= 200 && status != 204 && status != 205 && status != 304;
322
+ }
@@ -47,7 +47,9 @@ function sendLogs(logs: LogRecord[]): void {
47
47
  });
48
48
  }
49
49
 
50
- export function sendSpan(span: Span): void {
50
+ export function sendSpan(span: Span | undefined): void {
51
+ if (!span) return;
52
+
51
53
  if (isRateLimited()) {
52
54
  debug("Transport rate limit. Will not send item.", span);
53
55
  return;
@@ -8,6 +8,7 @@ export * from "./local-storage";
8
8
  export * from "./performance";
9
9
  export * from "./rate-limit";
10
10
  export * from "./time";
11
+ export * from "./timers";
11
12
  export * from "./constants";
12
13
  export * from "./math";
13
14
  export * from "./origin";
@@ -5,6 +5,7 @@ import { generateTraceId } from "../trace-id";
5
5
  import { generateSpanId } from "../span-id";
6
6
  import { addAttribute } from "./attributes";
7
7
  import { KeyValue, Span, SpanStatus } from "../../types/otlp";
8
+ import { debug } from "../debug";
8
9
 
9
10
  export type InProgressSpan = Omit<Span, "endTimeUnixNano">;
10
11
 
@@ -30,9 +31,23 @@ export function startSpan(name: string): InProgressSpan {
30
31
  };
31
32
  }
32
33
 
33
- export function endSpan(span: InProgressSpan, status: SpanStatus | undefined, durationNano: number | undefined): Span {
34
+ /**
35
+ * Ends the span by setting status and endTimeUnixNano on it. If the spand was already previously ended, this returns undefined
36
+ * to prevent the span from being ended multiple times on accident.
37
+ */
38
+ export function endSpan(
39
+ span: InProgressSpan,
40
+ status: SpanStatus | undefined,
41
+ durationNano: number | undefined
42
+ ): Span | undefined {
34
43
  // We cast here to avoid having to instantiate a copy of the span
35
44
  const s = span as Span;
45
+
46
+ if (s.endTimeUnixNano) {
47
+ debug("Attempting to end already ended span. Dropping...", s);
48
+ return undefined;
49
+ }
50
+
36
51
  if (status) {
37
52
  s.status = status;
38
53
  }
@@ -7,6 +7,7 @@ import { addEventListener, removeEventListener } from "./listeners";
7
7
  const TEN_MINUTES_IN_MILLIS = 1000 * 60 * 10;
8
8
  const ONE_DAY_IN_MILLIS = 1000 * 60 * 60 * 24;
9
9
  const OBSERVER_WAIT_TIME_MS = 300;
10
+ const MAX_RESOURCES_ENTRIES = 100;
10
11
 
11
12
  export const isResourceTimingAvailable = !!(perf && perf.getEntriesByType);
12
13
  export const isPerformanceObserverAvailable =
@@ -54,7 +55,11 @@ type ObserveResourcePerformanceOptions = {
54
55
 
55
56
  type PerformanceObservationContoller = {
56
57
  start: () => void;
57
- end: () => void;
58
+ /**
59
+ * Ends performance observation and starts matching the observed resources.
60
+ * @param endTs End performance timing timestamp to use for the end of the resource, instead of the current time when end is called. Useful when calling end from a timeout handler
61
+ */
62
+ end: (endTs?: number) => void;
58
63
  cancel: () => void;
59
64
  };
60
65
 
@@ -99,8 +104,11 @@ export function observeResourcePerformance(opts: ObserveResourcePerformanceOptio
99
104
  fallbackEndNeverCalledTimerHandle = setTimeout(disposeGlobalResources, TEN_MINUTES_IN_MILLIS);
100
105
  }
101
106
 
102
- function onEnd() {
103
- endTime = perf.now();
107
+ function onEnd(endTs?: number) {
108
+ // If endTime is already set, we are already terminating and should not do so again
109
+ if (endTime) return;
110
+
111
+ endTime = endTs ?? perf.now();
104
112
  cancelFallbackEndNeverCalledTimer();
105
113
 
106
114
  if (!isWaitingAcceptable()) {
@@ -136,7 +144,16 @@ export function observeResourcePerformance(opts: ObserveResourcePerformanceOptio
136
144
  const entry = e as PerformanceResourceTiming;
137
145
  return entry.startTime >= startTime && opts.resourceMatcher(entry);
138
146
  })
139
- .forEach((entry) => resources.push(entry as PerformanceResourceTiming));
147
+ .forEach((entry) => {
148
+ // Limit array size to prevent memory leaks on high-traffic pages
149
+ if (resources.length >= MAX_RESOURCES_ENTRIES) {
150
+ // Remove oldest entry (FIFO)
151
+ // Timings are only reported once the resource is fully recorded (i.e. the request has ended), so timings ending
152
+ // way before the `onEnd` is called are less likely to be good matches and we can ignore them.
153
+ resources.shift();
154
+ }
155
+ resources.push(entry as PerformanceResourceTiming);
156
+ });
140
157
  }
141
158
 
142
159
  function onVisibilityChanged() {