@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 { reportError as reportErrorInternal } from "../instrumentations/errors/unhandled-error";
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
- reportError(e);
32
+ reportUnhandledError(e);
33
33
  ignoreNextOnErrorEvent();
34
34
  throw e;
35
35
  }
@@ -44,7 +44,7 @@ export function startOnErrorInstrumentation() {
44
44
  }
45
45
  };
46
46
  }
47
- export function reportError(error, opts) {
47
+ export function reportUnhandledError(error, opts) {
48
48
  if (!error) {
49
49
  return;
50
50
  }
@@ -1,5 +1,5 @@
1
1
  import { win } from "../../utils";
2
- import { reportError } from "./unhandled-error";
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
- reportError({
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
- reportError({
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
- reportError({
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
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";
@@ -83,16 +82,12 @@ function wrapFetch(original) {
83
82
  });
84
83
  performanceObserver.start();
85
84
  try {
86
- const response = await original(input instanceof Request ? request : input, copyOfInit);
87
- addResponseData(span, response);
88
- // We use a separate promise here because this needs to happen in parallel to application code consuming the response
89
- waitForFullResponse(response)
90
- .then(() => performanceObserver.end())
91
- .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) => {
92
88
  performanceObserver.cancel();
93
89
  endSpanOnError(span, e);
94
90
  });
95
- return response;
96
91
  }
97
92
  catch (e) {
98
93
  performanceObserver.cancel();
@@ -146,20 +141,81 @@ function addResponseData(span, response) {
146
141
  addAttribute(span.attributes, HTTP_RESPONSE_STATUS_CODE, String(status));
147
142
  tryCaptureHttpHeaders(response.headers, span, (k) => httpResponseHeaderKey(k));
148
143
  }
149
- function waitForFullResponse(response) {
150
- return new Promise((resolve) => {
151
- const clonedResponse = response.clone();
152
- const body = clonedResponse.body;
153
- if (!body)
154
- return resolve();
155
- const reader = body.getReader();
156
- const read = async () => {
157
- const { done } = await reader.read();
158
- if (done)
159
- return resolve();
160
- return read();
161
- };
162
- return read();
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
+ // 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
157
+ if (!body || !responseCanHaveBody(originalResponse)) {
158
+ onDone();
159
+ return originalResponse;
160
+ }
161
+ let cbCalled = false;
162
+ const handleDone = (fallbackEndTs) => {
163
+ if (cbCalled)
164
+ return;
165
+ onDone(fallbackEndTs);
166
+ cbCalled = true;
167
+ };
168
+ const handleError = (e) => {
169
+ if (cbCalled)
170
+ return;
171
+ onError(e);
172
+ cbCalled = true;
173
+ };
174
+ let bodyNeverCompletelyReadTimeout = setTimeout(() => handleDone(fallbackTs), readTimeoutMs);
175
+ const reader = body.getReader();
176
+ const stream = new ReadableStream({
177
+ async pull(controller) {
178
+ try {
179
+ clearTimeout(bodyNeverCompletelyReadTimeout);
180
+ const { value, done } = await reader.read();
181
+ if (done) {
182
+ reader.releaseLock();
183
+ controller.close();
184
+ handleDone();
185
+ }
186
+ else {
187
+ fallbackTs = perf.now();
188
+ bodyNeverCompletelyReadTimeout = setTimeout(() => handleDone(fallbackTs), readTimeoutMs);
189
+ controller.enqueue(value);
190
+ }
191
+ }
192
+ catch (e) {
193
+ handleError(e);
194
+ controller.error(e);
195
+ try {
196
+ reader.releaseLock();
197
+ }
198
+ catch {
199
+ // Spec reference:
200
+ // https://streams.spec.whatwg.org/#default-reader-release-lock
201
+ //
202
+ // releaseLock() only throws if called on an invalid reader
203
+ // (i.e. reader.[[stream]] is undefined, meaning the lock is already released
204
+ // or the reader was never associated). In normal use this cannot happen.
205
+ // This catch is defensive only.
206
+ }
207
+ }
208
+ },
209
+ cancel(reason) {
210
+ clearTimeout(bodyNeverCompletelyReadTimeout);
211
+ handleDone();
212
+ return reader.cancel(reason);
213
+ },
214
+ });
215
+ return new Response(stream, {
216
+ status: originalResponse.status,
217
+ statusText: originalResponse.statusText,
218
+ headers: originalResponse.headers,
163
219
  });
164
220
  }
165
221
  function endSpanOnError(span, error) {
@@ -207,3 +263,7 @@ function addTraceContextHttpHeaders(fn, ctx, span, types) {
207
263
  }
208
264
  }
209
265
  }
266
+ function responseCanHaveBody(response) {
267
+ const status = response.status;
268
+ return status >= 200 && status != 204 && status != 205 && status != 304;
269
+ }
@@ -39,6 +39,8 @@ function sendLogs(logs) {
39
39
  });
40
40
  }
41
41
  export function sendSpan(span) {
42
+ if (!span)
43
+ return;
42
44
  if (isRateLimited()) {
43
45
  debug("Transport rate limit. Will not send item.", span);
44
46
  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";
@@ -4,6 +4,7 @@ import { sessionId } from "../../api/session";
4
4
  import { generateTraceId } from "../trace-id";
5
5
  import { generateSpanId } from "../span-id";
6
6
  import { addAttribute } from "./attributes";
7
+ import { debug } from "../debug";
7
8
  export function startSpan(name) {
8
9
  const traceId = generateTraceId(sessionId);
9
10
  const spanId = generateSpanId(traceId);
@@ -23,9 +24,17 @@ export function startSpan(name) {
23
24
  status: { code: SPAN_STATUS_UNSET },
24
25
  };
25
26
  }
27
+ /**
28
+ * Ends the span by setting status and endTimeUnixNano on it. If the spand was already previously ended, this returns undefined
29
+ * to prevent the span from being ended multiple times on accident.
30
+ */
26
31
  export function endSpan(span, status, durationNano) {
27
32
  // We cast here to avoid having to instantiate a copy of the span
28
33
  const s = span;
34
+ if (s.endTimeUnixNano) {
35
+ debug("Attempting to end already ended span. Dropping...", s);
36
+ return undefined;
37
+ }
29
38
  if (status) {
30
39
  s.status = status;
31
40
  }
@@ -6,6 +6,7 @@ import { addEventListener, removeEventListener } from "./listeners";
6
6
  const TEN_MINUTES_IN_MILLIS = 1000 * 60 * 10;
7
7
  const ONE_DAY_IN_MILLIS = 1000 * 60 * 60 * 24;
8
8
  const OBSERVER_WAIT_TIME_MS = 300;
9
+ const MAX_RESOURCES_ENTRIES = 100;
9
10
  export const isResourceTimingAvailable = !!(perf && perf.getEntriesByType);
10
11
  export const isPerformanceObserverAvailable = perf && typeof win["PerformanceObserver"] === "function" && typeof perf["now"] === "function";
11
12
  export const PerformanceTimingNames = Object.freeze({
@@ -69,8 +70,11 @@ export function observeResourcePerformance(opts) {
69
70
  }
70
71
  fallbackEndNeverCalledTimerHandle = setTimeout(disposeGlobalResources, TEN_MINUTES_IN_MILLIS);
71
72
  }
72
- function onEnd() {
73
- endTime = perf.now();
73
+ function onEnd(endTs) {
74
+ // If endTime is already set, we are already terminating and should not do so again
75
+ if (endTime)
76
+ return;
77
+ endTime = endTs ?? perf.now();
74
78
  cancelFallbackEndNeverCalledTimer();
75
79
  if (!isWaitingAcceptable()) {
76
80
  return end();
@@ -101,7 +105,16 @@ export function observeResourcePerformance(opts) {
101
105
  const entry = e;
102
106
  return entry.startTime >= startTime && opts.resourceMatcher(entry);
103
107
  })
104
- .forEach((entry) => resources.push(entry));
108
+ .forEach((entry) => {
109
+ // Limit array size to prevent memory leaks on high-traffic pages
110
+ if (resources.length >= MAX_RESOURCES_ENTRIES) {
111
+ // Remove oldest entry (FIFO)
112
+ // Timings are only reported once the resource is fully recorded (i.e. the request has ended), so timings ending
113
+ // way before the `onEnd` is called are less likely to be good matches and we can ignore them.
114
+ resources.shift();
115
+ }
116
+ resources.push(entry);
117
+ });
105
118
  }
106
119
  function onVisibilityChanged() {
107
120
  if (!isWaitingAcceptable())