@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,191 @@
1
+ import { expect, vi, beforeEach, afterEach } from "vitest";
2
+ import { vars } from "../../vars";
3
+ import { instrumentFetch } from "./fetch";
4
+
5
+ describe("fetch test", () => {
6
+ let fetchMock: any;
7
+
8
+ beforeEach(() => {
9
+ fetchMock = vi.fn(() => ({
10
+ ok: false,
11
+ headers: new Headers(),
12
+ status: 200,
13
+ clone: () => ({
14
+ body: null,
15
+ }),
16
+ }));
17
+ vi.stubGlobal("fetch", fetchMock);
18
+ vi.stubGlobal("location", { origin: "http://localhost:3000" });
19
+ });
20
+
21
+ afterEach(() => {
22
+ vi.resetAllMocks();
23
+ vars.propagators = undefined;
24
+ });
25
+
26
+ it("should inject traceparent header for cross-origin requests", async () => {
27
+ vars.propagators = [
28
+ {
29
+ type: "traceparent",
30
+ match: [new RegExp("http://foo.bar/")],
31
+ },
32
+ ];
33
+ instrumentFetch();
34
+ // eslint-disable-next-line no-restricted-globals
35
+ await fetch("http://foo.bar/foo");
36
+
37
+ expect(fetchMock).toHaveBeenCalledOnce();
38
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
39
+ expect(fetchHeaders.get("traceparent")).not.toBeNull();
40
+ });
41
+
42
+ it("should inject xray header for cross-origin requests", async () => {
43
+ vars.propagators = [
44
+ {
45
+ type: "xray",
46
+ match: [new RegExp("http://foo.bar/")],
47
+ },
48
+ ];
49
+ instrumentFetch();
50
+ // eslint-disable-next-line no-restricted-globals
51
+ await fetch("http://foo.bar/foo");
52
+
53
+ expect(fetchMock).toHaveBeenCalledOnce();
54
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
55
+ expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
56
+ });
57
+
58
+ it("should inject both headers for cross-origin requests when both match", async () => {
59
+ vars.propagators = [
60
+ {
61
+ type: "traceparent",
62
+ match: [new RegExp("http://foo.bar/")],
63
+ },
64
+ {
65
+ type: "xray",
66
+ match: [new RegExp("http://foo.bar/")],
67
+ },
68
+ ];
69
+ instrumentFetch();
70
+ // eslint-disable-next-line no-restricted-globals
71
+ await fetch("http://foo.bar/foo");
72
+
73
+ expect(fetchMock).toHaveBeenCalledOnce();
74
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
75
+ expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
76
+ expect(fetchHeaders.get("traceparent")).not.toBeNull();
77
+ });
78
+
79
+ it("should inject no headers for non-matching cross-origin requests", async () => {
80
+ vars.propagators = [];
81
+ instrumentFetch();
82
+ // eslint-disable-next-line no-restricted-globals
83
+ await fetch("http://foo.bar/foo");
84
+
85
+ expect(fetchMock).toHaveBeenCalledOnce();
86
+ const expectedHeaders = undefined;
87
+ expect(fetchMock).toHaveBeenCalledWith("http://foo.bar/foo", expectedHeaders);
88
+ });
89
+
90
+ // New same-origin behavior tests
91
+ it("should inject all configured propagator headers for same-origin requests", async () => {
92
+ vars.propagators = [
93
+ {
94
+ type: "traceparent",
95
+ match: [new RegExp("http://foo.bar/")], // Doesn't match same-origin
96
+ },
97
+ {
98
+ type: "xray",
99
+ match: [new RegExp("http://baz.com/")], // Doesn't match same-origin
100
+ },
101
+ ];
102
+ instrumentFetch();
103
+ // Same-origin request
104
+ // eslint-disable-next-line no-restricted-globals
105
+ await fetch("http://localhost:3000/api/test");
106
+
107
+ expect(fetchMock).toHaveBeenCalledOnce();
108
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
109
+ // Both headers should be present for same-origin, regardless of match patterns
110
+ expect(fetchHeaders.get("traceparent")).not.toBeNull();
111
+ expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
112
+ });
113
+
114
+ it("should inject only traceparent for same-origin when only traceparent propagator configured", async () => {
115
+ vars.propagators = [
116
+ {
117
+ type: "traceparent",
118
+ match: [new RegExp("http://foo.bar/")],
119
+ },
120
+ ];
121
+ instrumentFetch();
122
+ // Same-origin request
123
+ // eslint-disable-next-line no-restricted-globals
124
+ await fetch("http://localhost:3000/api/test");
125
+
126
+ expect(fetchMock).toHaveBeenCalledOnce();
127
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
128
+ expect(fetchHeaders.get("traceparent")).not.toBeNull();
129
+ expect(fetchHeaders.get("X-Amzn-Trace-Id")).toBeNull();
130
+ });
131
+
132
+ it("should inject both traceparent and xray for same-origin when only xray propagator configured", async () => {
133
+ vars.propagators = [
134
+ {
135
+ type: "xray",
136
+ match: [new RegExp("http://foo.bar/")],
137
+ },
138
+ ];
139
+ instrumentFetch();
140
+ // Same-origin request
141
+ // eslint-disable-next-line no-restricted-globals
142
+ await fetch("http://localhost:3000/api/test");
143
+
144
+ expect(fetchMock).toHaveBeenCalledOnce();
145
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
146
+ // Same-origin always gets traceparent + all configured propagator types
147
+ expect(fetchHeaders.get("traceparent")).not.toBeNull();
148
+ expect(fetchHeaders.get("X-Amzn-Trace-Id")).not.toBeNull();
149
+ });
150
+
151
+ it("should inject traceparent for same-origin when default traceparent propagator configured", async () => {
152
+ vars.propagators = [
153
+ {
154
+ type: "traceparent",
155
+ match: [], // Empty match array - matches no cross-origin URLs but same-origin gets all propagators
156
+ },
157
+ ];
158
+ instrumentFetch();
159
+ // Same-origin request
160
+ // eslint-disable-next-line no-restricted-globals
161
+ await fetch("http://localhost:3000/api/test");
162
+
163
+ expect(fetchMock).toHaveBeenCalledOnce();
164
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
165
+ // Same-origin gets all configured propagator types
166
+ expect(fetchHeaders.get("traceparent")).not.toBeNull();
167
+ expect(fetchHeaders.get("X-Amzn-Trace-Id")).toBeNull();
168
+ });
169
+
170
+ it("should follow cross-origin pattern matching for non-same-origin requests", async () => {
171
+ vars.propagators = [
172
+ {
173
+ type: "traceparent",
174
+ match: [new RegExp("http://foo.bar/")],
175
+ },
176
+ {
177
+ type: "xray",
178
+ match: [new RegExp("http://baz.com/")],
179
+ },
180
+ ];
181
+ instrumentFetch();
182
+ // Cross-origin request that matches only traceparent pattern
183
+ // eslint-disable-next-line no-restricted-globals
184
+ await fetch("http://foo.bar/api");
185
+
186
+ expect(fetchMock).toHaveBeenCalledOnce();
187
+ const fetchHeaders = (fetchMock.mock.calls[0]!.at(1)! as { headers: Headers }).headers;
188
+ expect(fetchHeaders.get("traceparent")).not.toBeNull();
189
+ expect(fetchHeaders.get("X-Amzn-Trace-Id")).toBeNull();
190
+ });
191
+ });
@@ -0,0 +1,93 @@
1
+ import { describe, it, expect, beforeEach } from "vitest";
2
+ import { addW3CTraceContextHttpHeaders, addXRayTraceContextHttpHeaders } from "../../utils/otel/trace-context";
3
+ import { InProgressSpan } from "../../utils/otel/span";
4
+
5
+ describe("Multiple propagator integration", () => {
6
+ let headers: Record<string, string>;
7
+ let mockAppend: (name: string, value: string) => void;
8
+ let mockSpan: InProgressSpan;
9
+
10
+ beforeEach(() => {
11
+ headers = {};
12
+ mockAppend = (name: string, value: string) => {
13
+ headers[name] = value;
14
+ };
15
+ mockSpan = {
16
+ traceId: "4efaaf4d1e8720b39541901950019ee5",
17
+ spanId: "53995c3f42cd8ad8",
18
+ attributes: [],
19
+ } as unknown as InProgressSpan;
20
+ });
21
+
22
+ it("should add both W3C traceparent and X-Ray headers when both propagators match", () => {
23
+ // Simulate what happens when both propagator types are matched
24
+ const propagatorTypes = ["traceparent", "xray"];
25
+
26
+ // Add headers for each type (this simulates addHeadersBasedOnTypes)
27
+ for (const type of propagatorTypes) {
28
+ if (type === "xray") {
29
+ addXRayTraceContextHttpHeaders(mockAppend, {}, mockSpan);
30
+ } else if (type === "traceparent") {
31
+ addW3CTraceContextHttpHeaders(mockAppend, {}, mockSpan);
32
+ }
33
+ }
34
+
35
+ // Verify both headers were added
36
+ expect(headers).toHaveProperty("traceparent");
37
+ expect(headers).toHaveProperty("X-Amzn-Trace-Id");
38
+
39
+ // Verify header formats
40
+ expect(headers["traceparent"]).toBe("00-4efaaf4d1e8720b39541901950019ee5-53995c3f42cd8ad8-01");
41
+ expect(headers["X-Amzn-Trace-Id"]).toBe(
42
+ "Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=53995c3f42cd8ad8;Sampled=1"
43
+ );
44
+ });
45
+
46
+ it("should only add W3C header when only traceparent propagator matches", () => {
47
+ const propagatorTypes = ["traceparent"];
48
+
49
+ for (const type of propagatorTypes) {
50
+ if (type === "xray") {
51
+ addXRayTraceContextHttpHeaders(mockAppend, {}, mockSpan);
52
+ } else if (type === "traceparent") {
53
+ addW3CTraceContextHttpHeaders(mockAppend, {}, mockSpan);
54
+ }
55
+ }
56
+
57
+ expect(headers).toHaveProperty("traceparent");
58
+ expect(headers).not.toHaveProperty("X-Amzn-Trace-Id");
59
+ });
60
+
61
+ it("should only add X-Ray header when only xray propagator matches", () => {
62
+ const propagatorTypes = ["xray"];
63
+
64
+ for (const type of propagatorTypes) {
65
+ if (type === "xray") {
66
+ addXRayTraceContextHttpHeaders(mockAppend, {}, mockSpan);
67
+ } else if (type === "traceparent") {
68
+ addW3CTraceContextHttpHeaders(mockAppend, {}, mockSpan);
69
+ }
70
+ }
71
+
72
+ expect(headers).not.toHaveProperty("traceparent");
73
+ expect(headers).toHaveProperty("X-Amzn-Trace-Id");
74
+ });
75
+
76
+ it("should handle Headers object correctly", () => {
77
+ const headersObj = new Headers();
78
+ const propagatorTypes = ["traceparent", "xray"];
79
+
80
+ for (const type of propagatorTypes) {
81
+ if (type === "xray") {
82
+ addXRayTraceContextHttpHeaders(headersObj.append.bind(headersObj), headersObj, mockSpan);
83
+ } else if (type === "traceparent") {
84
+ addW3CTraceContextHttpHeaders(headersObj.append.bind(headersObj), headersObj, mockSpan);
85
+ }
86
+ }
87
+
88
+ expect(headersObj.get("traceparent")).toBe("00-4efaaf4d1e8720b39541901950019ee5-53995c3f42cd8ad8-01");
89
+ expect(headersObj.get("X-Amzn-Trace-Id")).toBe(
90
+ "Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=53995c3f42cd8ad8;Sampled=1"
91
+ );
92
+ });
93
+ });
@@ -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;
@@ -1,6 +1,6 @@
1
1
  import { AttributeValueType } from "../utils/otel";
2
2
  import { AnyValue } from "./otlp";
3
- import { Endpoint, Vars } from "../vars";
3
+ import { Endpoint, Vars, PropagatorConfig } from "../vars";
4
4
 
5
5
  export type InstrumentationName = "@dash0/navigation" | "@dash0/web-vitals" | "@dash0/error" | "@dash0/fetch";
6
6
 
@@ -39,6 +39,12 @@ export type InitOptions = {
39
39
  * to be expired. Also think of cache time-to-live configuration options.
40
40
  */
41
41
  sessionTerminationTimeoutMillis?: number;
42
+
43
+ /**
44
+ * Configure trace context propagators for different URL patterns.
45
+ * Each propagator defines which header type to send for matching URLs.
46
+ */
47
+ propagators?: PropagatorConfig[];
42
48
  } & Partial<
43
49
  Pick<
44
50
  Vars,
@@ -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
  }
@@ -59,7 +59,7 @@ function getTraceparentFromServerTiming(serverTimings: readonly PerformanceServe
59
59
  return "";
60
60
  }
61
61
 
62
- export function addTraceContextHttpHeaders(
62
+ export function addW3CTraceContextHttpHeaders(
63
63
  fn: (name: string, value: string) => void,
64
64
  ctx: unknown,
65
65
  span: InProgressSpan
@@ -84,3 +84,32 @@ export function addTraceContextHttpHeaders(
84
84
  */
85
85
  fn.call(ctx, "traceparent", `00-${span.traceId}-${span.spanId}-01`);
86
86
  }
87
+
88
+ export function addXRayTraceContextHttpHeaders(
89
+ fn: (name: string, value: string) => void,
90
+ ctx: unknown,
91
+ span: InProgressSpan
92
+ ) {
93
+ /**
94
+ * AWS X-Ray trace header.
95
+ * General format is Root=${trace-id};Parent=${span-id};Sampled=${sampling-flag}
96
+ *
97
+ * Converts W3C trace ID format to X-Ray format:
98
+ * W3C: 4efaaf4d1e8720b39541901950019ee5 (32 hex chars)
99
+ * X-Ray: 1-4efaaf4d-1e8720b39541901950019ee5 (1-{8chars}-{24chars})
100
+ *
101
+ * References:
102
+ * https://docs.aws.amazon.com/xray/latest/devguide/xray-concepts.html#xray-concepts-tracingheader
103
+ */
104
+ const xrayTraceId = convertW3CTraceIdToXRay(span.traceId);
105
+ const xrayHeader = `Root=${xrayTraceId};Parent=${span.spanId};Sampled=1`;
106
+ fn.call(ctx, "X-Amzn-Trace-Id", xrayHeader);
107
+ }
108
+
109
+ function convertW3CTraceIdToXRay(w3cTraceId: string): string {
110
+ // X-Ray trace ID format: 1-{timestamp}-{unique-id}
111
+ // Split the 32-char W3C trace ID into 8-char timestamp and 24-char unique ID
112
+ const timestamp = w3cTraceId.substring(0, 8);
113
+ const uniqueId = w3cTraceId.substring(8, 32);
114
+ return `1-${timestamp}-${uniqueId}`;
115
+ }
@@ -0,0 +1,59 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { addXRayTraceContextHttpHeaders } from "./trace-context";
3
+ import { InProgressSpan } from "./span";
4
+
5
+ describe("X-Ray trace context", () => {
6
+ it("should generate X-Ray header with correct format", () => {
7
+ const span = {
8
+ traceId: "4efaaf4d1e8720b39541901950019ee5",
9
+ spanId: "53995c3f42cd8ad8",
10
+ } as InProgressSpan;
11
+
12
+ const headers: Record<string, string> = {};
13
+ const mockAppend = (name: string, value: string) => {
14
+ headers[name] = value;
15
+ };
16
+
17
+ addXRayTraceContextHttpHeaders(mockAppend, {}, span);
18
+
19
+ expect(headers["X-Amzn-Trace-Id"]).toBe(
20
+ "Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=53995c3f42cd8ad8;Sampled=1"
21
+ );
22
+ });
23
+
24
+ it("should convert W3C trace ID to X-Ray format correctly", () => {
25
+ const span = {
26
+ traceId: "5759e988bd862e3fe1be46a994272793",
27
+ spanId: "53995c3f42cd8ad8",
28
+ } as InProgressSpan;
29
+
30
+ const headers: Record<string, string> = {};
31
+ const mockAppend = (name: string, value: string) => {
32
+ headers[name] = value;
33
+ };
34
+
35
+ addXRayTraceContextHttpHeaders(mockAppend, {}, span);
36
+
37
+ expect(headers["X-Amzn-Trace-Id"]).toBe(
38
+ "Root=1-5759e988-bd862e3fe1be46a994272793;Parent=53995c3f42cd8ad8;Sampled=1"
39
+ );
40
+ });
41
+
42
+ it("should handle different span IDs", () => {
43
+ const span = {
44
+ traceId: "4efaaf4d1e8720b39541901950019ee5",
45
+ spanId: "1234567890abcdef",
46
+ } as InProgressSpan;
47
+
48
+ const headers: Record<string, string> = {};
49
+ const mockAppend = (name: string, value: string) => {
50
+ headers[name] = value;
51
+ };
52
+
53
+ addXRayTraceContextHttpHeaders(mockAppend, {}, span);
54
+
55
+ expect(headers["X-Amzn-Trace-Id"]).toBe(
56
+ "Root=1-4efaaf4d-1e8720b39541901950019ee5;Parent=1234567890abcdef;Sampled=1"
57
+ );
58
+ });
59
+ });
@@ -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() {
package/src/vars.ts CHANGED
@@ -3,6 +3,13 @@ import { AnyValue, InstrumentationScope, KeyValue, Resource } from "./types/otlp
3
3
  import { UrlAttributeScrubber } from "./attributes";
4
4
  import { identity } from "./utils";
5
5
 
6
+ export type PropagatorType = "traceparent" | "xray";
7
+
8
+ export type PropagatorConfig = {
9
+ type: PropagatorType;
10
+ match: RegExp[];
11
+ };
12
+
6
13
  export type Endpoint = {
7
14
  /**
8
15
  * OTLP HTTP URL excluding the /v1/* prefix
@@ -108,9 +115,16 @@ export type Vars = {
108
115
  */
109
116
  wrapTimers: boolean;
110
117
 
118
+ /**
119
+ * Configure trace context propagators for different URL patterns.
120
+ * Each propagator defines which header type to send for matching URLs.
121
+ */
122
+ propagators?: PropagatorConfig[];
123
+
111
124
  /**
112
125
  * An array of URL regular expressions
113
126
  * for which trace context headers should be sent across origins by http client instrumentations.
127
+ * @deprecated Use propagators instead
114
128
  */
115
129
  propagateTraceHeadersCorsURLs: RegExp[];
116
130