@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.
Files changed (54) hide show
  1. package/README.md +35 -515
  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/init.js +30 -0
  9. package/dist/modules/api/init_test.js +102 -6
  10. package/dist/modules/api/report-error.js +1 -1
  11. package/dist/modules/instrumentations/errors/event-handlers.js +2 -2
  12. package/dist/modules/instrumentations/errors/unhandled-error.js +1 -1
  13. package/dist/modules/instrumentations/errors/unhandled-promise-rejection.js +4 -4
  14. package/dist/modules/instrumentations/http/fetch.js +125 -28
  15. package/dist/modules/instrumentations/http/fetch_test.js +170 -0
  16. package/dist/modules/instrumentations/http/propagator-integration_test.js +77 -0
  17. package/dist/modules/transport/index.js +2 -0
  18. package/dist/modules/utils/index.js +1 -0
  19. package/dist/modules/utils/otel/span.js +9 -0
  20. package/dist/modules/utils/otel/trace-context.js +24 -1
  21. package/dist/modules/utils/otel/trace-context_test.js +40 -0
  22. package/dist/modules/utils/performance.js +16 -3
  23. package/dist/tsconfig.tsbuildinfo +1 -1
  24. package/dist/types/entrypoint/npm-package.d.ts +1 -1
  25. package/dist/types/instrumentations/errors/unhandled-error.d.ts +1 -1
  26. package/dist/types/instrumentations/http/fetch_test.d.ts +1 -0
  27. package/dist/types/instrumentations/http/propagator-integration_test.d.ts +1 -0
  28. package/dist/types/transport/index.d.ts +1 -1
  29. package/dist/types/types/options.d.ts +6 -1
  30. package/dist/types/utils/index.d.ts +1 -0
  31. package/dist/types/utils/otel/span.d.ts +5 -1
  32. package/dist/types/utils/otel/trace-context.d.ts +2 -1
  33. package/dist/types/utils/otel/trace-context_test.d.ts +1 -0
  34. package/dist/types/utils/performance.d.ts +5 -1
  35. package/dist/types/vars.d.ts +11 -0
  36. package/package.json +1 -1
  37. package/src/api/init.ts +34 -0
  38. package/src/api/init_test.ts +130 -9
  39. package/src/api/report-error.ts +1 -1
  40. package/src/entrypoint/npm-package.ts +1 -1
  41. package/src/instrumentations/errors/event-handlers.ts +2 -2
  42. package/src/instrumentations/errors/unhandled-error.ts +1 -1
  43. package/src/instrumentations/errors/unhandled-promise-rejection.ts +4 -4
  44. package/src/instrumentations/http/fetch.ts +148 -30
  45. package/src/instrumentations/http/fetch_test.ts +191 -0
  46. package/src/instrumentations/http/propagator-integration_test.ts +93 -0
  47. package/src/transport/index.ts +3 -1
  48. package/src/types/options.ts +7 -1
  49. package/src/utils/index.ts +1 -0
  50. package/src/utils/otel/span.ts +16 -1
  51. package/src/utils/otel/trace-context.ts +30 -1
  52. package/src/utils/otel/trace-context_test.ts +59 -0
  53. package/src/utils/performance.ts +21 -4
  54. package/src/vars.ts +14 -0
@@ -0,0 +1,77 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { addW3CTraceContextHttpHeaders, addXRayTraceContextHttpHeaders } from "../../utils/otel/trace-context";
3
+ describe("Multiple propagator integration", () => {
4
+ let headers;
5
+ let mockAppend;
6
+ let mockSpan;
7
+ beforeEach(() => {
8
+ headers = {};
9
+ mockAppend = (name, value) => {
10
+ headers[name] = value;
11
+ };
12
+ mockSpan = {
13
+ traceId: "4efaaf4d1e8720b39541901950019ee5",
14
+ spanId: "53995c3f42cd8ad8",
15
+ attributes: [],
16
+ };
17
+ });
18
+ it("should add both W3C traceparent and X-Ray headers when both propagators match", () => {
19
+ // Simulate what happens when both propagator types are matched
20
+ const propagatorTypes = ["traceparent", "xray"];
21
+ // Add headers for each type (this simulates addHeadersBasedOnTypes)
22
+ for (const type of propagatorTypes) {
23
+ if (type === "xray") {
24
+ addXRayTraceContextHttpHeaders(mockAppend, {}, mockSpan);
25
+ }
26
+ else if (type === "traceparent") {
27
+ addW3CTraceContextHttpHeaders(mockAppend, {}, mockSpan);
28
+ }
29
+ }
30
+ // Verify both headers were added
31
+ expect(headers).toHaveProperty("traceparent");
32
+ expect(headers).toHaveProperty("X-Amzn-Trace-Id");
33
+ // Verify header formats
34
+ expect(headers["traceparent"]).toBe("00-4efaaf4d1e8720b39541901950019ee5-53995c3f42cd8ad8-01");
35
+ expect(headers["X-Amzn-Trace-Id"]).toBe("Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=53995c3f42cd8ad8;Sampled=1");
36
+ });
37
+ it("should only add W3C header when only traceparent propagator matches", () => {
38
+ const propagatorTypes = ["traceparent"];
39
+ for (const type of propagatorTypes) {
40
+ if (type === "xray") {
41
+ addXRayTraceContextHttpHeaders(mockAppend, {}, mockSpan);
42
+ }
43
+ else if (type === "traceparent") {
44
+ addW3CTraceContextHttpHeaders(mockAppend, {}, mockSpan);
45
+ }
46
+ }
47
+ expect(headers).toHaveProperty("traceparent");
48
+ expect(headers).not.toHaveProperty("X-Amzn-Trace-Id");
49
+ });
50
+ it("should only add X-Ray header when only xray propagator matches", () => {
51
+ const propagatorTypes = ["xray"];
52
+ for (const type of propagatorTypes) {
53
+ if (type === "xray") {
54
+ addXRayTraceContextHttpHeaders(mockAppend, {}, mockSpan);
55
+ }
56
+ else if (type === "traceparent") {
57
+ addW3CTraceContextHttpHeaders(mockAppend, {}, mockSpan);
58
+ }
59
+ }
60
+ expect(headers).not.toHaveProperty("traceparent");
61
+ expect(headers).toHaveProperty("X-Amzn-Trace-Id");
62
+ });
63
+ it("should handle Headers object correctly", () => {
64
+ const headersObj = new Headers();
65
+ const propagatorTypes = ["traceparent", "xray"];
66
+ for (const type of propagatorTypes) {
67
+ if (type === "xray") {
68
+ addXRayTraceContextHttpHeaders(headersObj.append.bind(headersObj), headersObj, mockSpan);
69
+ }
70
+ else if (type === "traceparent") {
71
+ addW3CTraceContextHttpHeaders(headersObj.append.bind(headersObj), headersObj, mockSpan);
72
+ }
73
+ }
74
+ expect(headersObj.get("traceparent")).toBe("00-4efaaf4d1e8720b39541901950019ee5-53995c3f42cd8ad8-01");
75
+ expect(headersObj.get("X-Amzn-Trace-Id")).toBe("Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=53995c3f42cd8ad8;Sampled=1");
76
+ });
77
+ });
@@ -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
  }
@@ -39,7 +39,7 @@ function getTraceparentFromServerTiming(serverTimings) {
39
39
  }
40
40
  return "";
41
41
  }
42
- export function addTraceContextHttpHeaders(fn, ctx, span) {
42
+ export function addW3CTraceContextHttpHeaders(fn, ctx, span) {
43
43
  /**
44
44
  * W3C Traceparent header.
45
45
  * General format is ${version}-${trace-id}-${parent-id}-${trace-flags}
@@ -60,3 +60,26 @@ export function addTraceContextHttpHeaders(fn, ctx, span) {
60
60
  */
61
61
  fn.call(ctx, "traceparent", `00-${span.traceId}-${span.spanId}-01`);
62
62
  }
63
+ export function addXRayTraceContextHttpHeaders(fn, ctx, span) {
64
+ /**
65
+ * AWS X-Ray trace header.
66
+ * General format is Root=${trace-id};Parent=${span-id};Sampled=${sampling-flag}
67
+ *
68
+ * Converts W3C trace ID format to X-Ray format:
69
+ * W3C: 4efaaf4d1e8720b39541901950019ee5 (32 hex chars)
70
+ * X-Ray: 1-4efaaf4d-1e8720b39541901950019ee5 (1-{8chars}-{24chars})
71
+ *
72
+ * References:
73
+ * https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader
74
+ */
75
+ const xrayTraceId = convertW3CTraceIdToXRay(span.traceId);
76
+ const xrayHeader = `Root=${xrayTraceId};Parent=${span.spanId};Sampled=1`;
77
+ fn.call(ctx, "X-Amzn-Trace-Id", xrayHeader);
78
+ }
79
+ function convertW3CTraceIdToXRay(w3cTraceId) {
80
+ // X-Ray trace ID format: 1-{timestamp}-{unique-id}
81
+ // Split the 32-char W3C trace ID into 8-char timestamp and 24-char unique ID
82
+ const timestamp = w3cTraceId.substring(0, 8);
83
+ const uniqueId = w3cTraceId.substring(8, 32);
84
+ return `1-${timestamp}-${uniqueId}`;
85
+ }
@@ -0,0 +1,40 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { addXRayTraceContextHttpHeaders } from "./trace-context";
3
+ describe("X-Ray trace context", () => {
4
+ it("should generate X-Ray header with correct format", () => {
5
+ const span = {
6
+ traceId: "4efaaf4d1e8720b39541901950019ee5",
7
+ spanId: "53995c3f42cd8ad8",
8
+ };
9
+ const headers = {};
10
+ const mockAppend = (name, value) => {
11
+ headers[name] = value;
12
+ };
13
+ addXRayTraceContextHttpHeaders(mockAppend, {}, span);
14
+ expect(headers["X-Amzn-Trace-Id"]).toBe("Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=53995c3f42cd8ad8;Sampled=1");
15
+ });
16
+ it("should convert W3C trace ID to X-Ray format correctly", () => {
17
+ const span = {
18
+ traceId: "5759e988bd862e3fe1be46a994272793",
19
+ spanId: "53995c3f42cd8ad8",
20
+ };
21
+ const headers = {};
22
+ const mockAppend = (name, value) => {
23
+ headers[name] = value;
24
+ };
25
+ addXRayTraceContextHttpHeaders(mockAppend, {}, span);
26
+ expect(headers["X-Amzn-Trace-Id"]).toBe("Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1");
27
+ });
28
+ it("should handle different span IDs", () => {
29
+ const span = {
30
+ traceId: "4efaaf4d1e8720b39541901950019ee5",
31
+ spanId: "1234567890abcdef",
32
+ };
33
+ const headers = {};
34
+ const mockAppend = (name, value) => {
35
+ headers[name] = value;
36
+ };
37
+ addXRayTraceContextHttpHeaders(mockAppend, {}, span);
38
+ expect(headers["X-Amzn-Trace-Id"]).toBe("Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=1234567890abcdef;Sampled=1");
39
+ });
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())