@dash0/sdk-web 0.14.1 → 0.16.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.
Files changed (38) hide show
  1. package/README.md +77 -6
  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/instrumentations/http/fetch.js +47 -5
  11. package/dist/modules/instrumentations/http/fetch_test.js +170 -0
  12. package/dist/modules/instrumentations/http/propagator-integration_test.js +77 -0
  13. package/dist/modules/transport/index.js +1 -3
  14. package/dist/modules/utils/otel/trace-context.js +24 -1
  15. package/dist/modules/utils/otel/trace-context_test.js +40 -0
  16. package/dist/modules/utils/rate-limit.js +1 -5
  17. package/dist/tsconfig.tsbuildinfo +1 -1
  18. package/dist/types/entrypoint/npm-package.d.ts +1 -1
  19. package/dist/types/instrumentations/http/fetch_test.d.ts +1 -0
  20. package/dist/types/instrumentations/http/propagator-integration_test.d.ts +1 -0
  21. package/dist/types/types/options.d.ts +6 -1
  22. package/dist/types/utils/otel/trace-context.d.ts +2 -1
  23. package/dist/types/utils/otel/trace-context_test.d.ts +1 -0
  24. package/dist/types/utils/rate-limit.d.ts +0 -1
  25. package/dist/types/vars.d.ts +11 -0
  26. package/package.json +1 -1
  27. package/src/api/init.ts +34 -0
  28. package/src/api/init_test.ts +130 -9
  29. package/src/entrypoint/npm-package.ts +1 -1
  30. package/src/instrumentations/http/fetch.ts +59 -6
  31. package/src/instrumentations/http/fetch_test.ts +191 -0
  32. package/src/instrumentations/http/propagator-integration_test.ts +93 -0
  33. package/src/transport/index.ts +1 -3
  34. package/src/types/options.ts +7 -1
  35. package/src/utils/otel/trace-context.ts +30 -1
  36. package/src/utils/otel/trace-context_test.ts +59 -0
  37. package/src/utils/rate-limit.ts +2 -12
  38. package/src/vars.ts +14 -0
@@ -1,5 +1,7 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
2
  import { InitOptions, InstrumentationName } from "../types/options";
3
+ import { PropagatorConfig, Vars } from "../vars";
4
+ import { init as initFun } from "./init";
3
5
 
4
6
  // Mock all the instrumentation modules
5
7
  vi.mock("../instrumentations/web-vitals", () => ({
@@ -18,21 +20,30 @@ vi.mock("../instrumentations/navigation", () => ({
18
20
  startNavigationInstrumentation: vi.fn(),
19
21
  }));
20
22
 
21
- import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
22
23
  import { startErrorInstrumentation } from "../instrumentations/errors";
23
24
  import { instrumentFetch } from "../instrumentations/http/fetch";
24
25
  import { startNavigationInstrumentation } from "../instrumentations/navigation";
26
+ import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
25
27
 
26
28
  describe("init", () => {
27
29
  const baseOptions: InitOptions = {
28
30
  serviceName: "test-service",
29
31
  endpoint: { url: "https://test-endpoint.com", authToken: "invalid" },
30
32
  };
33
+ let vars: Vars;
34
+ let init: typeof initFun;
31
35
 
32
- beforeEach(() => {
36
+ beforeEach(async () => {
33
37
  vi.clearAllMocks();
34
38
  // Reset the hasBeenInitialised flag by re-importing the module
35
39
  vi.resetModules();
40
+ // Reset vars state after module reset
41
+ // since init itself needs vars, this needs to imported again as well.
42
+ // Otherwise the tests that make assumptions on the vars fail because they use a different reference.
43
+ const { vars: importedVars } = await import("../vars");
44
+ const { init: importedInit } = await import("./init");
45
+ vars = importedVars;
46
+ init = importedInit;
36
47
  });
37
48
 
38
49
  afterEach(() => {
@@ -41,8 +52,6 @@ describe("init", () => {
41
52
 
42
53
  describe("instrumentation enablement", () => {
43
54
  it("should enable all instrumentations when enabledInstrumentations is undefined", async () => {
44
- const { init } = await import("./init");
45
-
46
55
  init({
47
56
  ...baseOptions,
48
57
  enabledInstrumentations: undefined,
@@ -69,8 +78,6 @@ describe("init", () => {
69
78
 
70
79
  instrumentations.forEach((instrumentation) => {
71
80
  it(`should enable ${instrumentation} instrumentation when present in enabledInstrumentations array`, async () => {
72
- const { init } = await import("./init");
73
-
74
81
  init({
75
82
  ...baseOptions,
76
83
  enabledInstrumentations: [instrumentation],
@@ -87,8 +94,6 @@ describe("init", () => {
87
94
  });
88
95
 
89
96
  it(`should not enable ${instrumentation} instrumentation when not present in enabledInstrumentations array`, async () => {
90
- const { init } = await import("./init");
91
-
92
97
  const otherInstrumentations = instrumentations.filter((i) => i !== instrumentation);
93
98
 
94
99
  init({
@@ -105,4 +110,120 @@ describe("init", () => {
105
110
  });
106
111
  });
107
112
  });
113
+
114
+ describe("propagator configuration", () => {
115
+ it("should set propagators configuration when provided", async () => {
116
+ const propagators: PropagatorConfig[] = [
117
+ { type: "traceparent" as const, match: [/.*\/api\/.*/] },
118
+ { type: "xray" as const, match: [/.*\.amazonaws\.com.*/] },
119
+ ];
120
+
121
+ init({
122
+ ...baseOptions,
123
+ propagators,
124
+ });
125
+
126
+ expect(vars.propagators).toEqual(propagators);
127
+ });
128
+
129
+ it("should warn when both propagators and legacy config are provided", async () => {
130
+ const spyOnWarn = vi.spyOn(console, "warn");
131
+ const propagators: PropagatorConfig[] = [{ type: "traceparent" as const, match: [/.*\/api\/.*/] }];
132
+
133
+ init({
134
+ ...baseOptions,
135
+ propagators,
136
+ propagateTraceHeadersCorsURLs: [/.*\.example\.com.*/],
137
+ });
138
+
139
+ expect(spyOnWarn).toHaveBeenCalledWith(
140
+ "Both 'propagators' and deprecated 'propagateTraceHeadersCorsURLs' were provided. Using 'propagators' configuration. Please migrate to the new 'propagators' config."
141
+ );
142
+ expect(vars.propagators).toEqual(propagators);
143
+ });
144
+
145
+ it("should convert legacy config to new format with deprecation warning", async () => {
146
+ const spyOnWarn = vi.spyOn(console, "warn");
147
+ const legacyConfig = [/.*\.example\.com.*/, /.*\.test\.com.*/];
148
+
149
+ init({
150
+ ...baseOptions,
151
+ propagateTraceHeadersCorsURLs: legacyConfig,
152
+ });
153
+
154
+ expect(spyOnWarn).toHaveBeenCalledWith(
155
+ "'propagateTraceHeadersCorsURLs' is deprecated. Please use the new 'propagators' configuration."
156
+ );
157
+
158
+ expect(vars.propagators).toEqual([
159
+ {
160
+ type: "traceparent",
161
+ match: [...legacyConfig],
162
+ },
163
+ ]);
164
+ });
165
+
166
+ it("should set default propagators when no configuration is provided", async () => {
167
+ init(baseOptions);
168
+
169
+ expect(vars.propagators).toEqual([
170
+ {
171
+ type: "traceparent",
172
+ match: [],
173
+ },
174
+ ]);
175
+ });
176
+
177
+ it("should not convert empty legacy config", async () => {
178
+ const spyOnWarn = vi.spyOn(console, "warn");
179
+
180
+ init({
181
+ ...baseOptions,
182
+ propagateTraceHeadersCorsURLs: [],
183
+ });
184
+
185
+ expect(spyOnWarn).not.toHaveBeenCalled();
186
+ expect(vars.propagators).toEqual([
187
+ {
188
+ type: "traceparent",
189
+ match: [],
190
+ },
191
+ ]);
192
+ });
193
+
194
+ it("should handle mixed pattern types in propagators", async () => {
195
+ const spyOnWarn = vi.spyOn(console, "warn");
196
+
197
+ const propagators: PropagatorConfig[] = [
198
+ {
199
+ type: "traceparent" as const,
200
+ match: [/.*\/api\/.*/],
201
+ },
202
+ {
203
+ type: "xray" as const,
204
+ match: [/.*\.amazonaws\.com.*/, /.*\.aws\.com.*/],
205
+ },
206
+ ];
207
+
208
+ init({
209
+ ...baseOptions,
210
+ propagators,
211
+ });
212
+
213
+ expect(vars.propagators).toEqual(propagators);
214
+ expect(spyOnWarn).not.toHaveBeenCalled();
215
+ });
216
+
217
+ it("should handle empty propagators array", async () => {
218
+ const spyOnWarn = vi.spyOn(console, "warn");
219
+
220
+ init({
221
+ ...baseOptions,
222
+ propagators: [],
223
+ });
224
+
225
+ expect(vars.propagators).toEqual([]);
226
+ expect(spyOnWarn).not.toHaveBeenCalled();
227
+ });
228
+ });
108
229
  });
@@ -13,7 +13,7 @@ export { reportError } from "../api/report-error";
13
13
  // Additional utility types
14
14
  export type { AttributeValueType } from "../utils/otel";
15
15
  export type { AnyValue } from "../types/otlp";
16
- export type { PageViewMeta } from "../vars";
16
+ export type { PageViewMeta, PropagatorConfig, PropagatorType } from "../vars";
17
17
  export type { UrlAttributeScrubber, UrlAttributeRecord } from "../attributes/url";
18
18
 
19
19
  export function init(opts: InitOptions): void {
@@ -3,7 +3,8 @@ import { isUrlIgnored, matchesAny } from "../../utils/ignore-rules";
3
3
  import {
4
4
  addAttribute,
5
5
  setSpanStatus,
6
- addTraceContextHttpHeaders,
6
+ addW3CTraceContextHttpHeaders,
7
+ addXRayTraceContextHttpHeaders,
7
8
  endSpan,
8
9
  errorToSpanStatus,
9
10
  Exception,
@@ -20,7 +21,7 @@ import {
20
21
  SPAN_STATUS_UNSET,
21
22
  } from "../../semantic-conventions";
22
23
  import { isSameOrigin, wrap, parseUrl } from "../../utils";
23
- import { vars } from "../../vars";
24
+ import { vars, PropagatorType } from "../../vars";
24
25
  import { httpRequestHeaderKey, httpResponseHeaderKey } from "../../utils/otel/http";
25
26
  import { sendSpan } from "../../transport";
26
27
  import { addResourceNetworkEvents, addResourceSize, HTTP_METHOD_OTHER, isWellKnownHttpMethod } from "./utils";
@@ -72,20 +73,21 @@ function wrapFetch(original: typeof fetch) {
72
73
  addAttribute(span.attributes, HTTP_REQUEST_METHOD_ORIGINAL, originalMethod);
73
74
  }
74
75
 
75
- const shouldSetCorrelationHeaders = isSameOrigin(url) || matchesAny(vars.propagateTraceHeadersCorsURLs, url);
76
+ const propagatorTypes = determinePropagatorTypes(url);
77
+ const shouldSetCorrelationHeaders = propagatorTypes.length > 0;
76
78
  if (shouldSetCorrelationHeaders) {
77
79
  if (copyOfInit?.headers) {
78
80
  // ensure we have a unified container for the headers
79
81
  copyOfInit.headers = new Headers(copyOfInit.headers);
80
- addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
82
+ addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span, propagatorTypes);
81
83
  } else if (input instanceof Request) {
82
- addTraceContextHttpHeaders(request.headers.append, request.headers, span);
84
+ addTraceContextHttpHeaders(request.headers.append, request.headers, span, propagatorTypes);
83
85
  } else {
84
86
  if (!copyOfInit) {
85
87
  copyOfInit = {};
86
88
  }
87
89
  copyOfInit.headers = new Headers();
88
- addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span);
90
+ addTraceContextHttpHeaders(copyOfInit.headers.append, copyOfInit.headers, span, propagatorTypes);
89
91
  }
90
92
  }
91
93
 
@@ -196,3 +198,54 @@ function endSpanOnError(span: InProgressSpan, error: Exception) {
196
198
  recordException(span, error);
197
199
  sendSpan(endSpan(span, errorToSpanStatus(error), undefined));
198
200
  }
201
+
202
+ function determinePropagatorTypes(url: string): PropagatorType[] {
203
+ const matchingTypes: PropagatorType[] = [];
204
+ const isUrlSameOrigin = isSameOrigin(url);
205
+
206
+ // For same-origin requests, always include traceparent + all configured propagators
207
+ if (isUrlSameOrigin) {
208
+ // Always add traceparent for same-origin requests
209
+ matchingTypes.push("traceparent");
210
+
211
+ // Add all other configured propagator types for same-origin requests
212
+ if (vars.propagators) {
213
+ for (const propagator of vars.propagators) {
214
+ if (propagator.type !== "traceparent" && !matchingTypes.includes(propagator.type)) {
215
+ matchingTypes.push(propagator.type);
216
+ }
217
+ }
218
+ }
219
+ return matchingTypes;
220
+ }
221
+
222
+ // For cross-origin requests, use new propagators config if available
223
+ if (vars.propagators) {
224
+ for (const propagator of vars.propagators) {
225
+ if (matchesAny(propagator.match, url)) {
226
+ // Avoid duplicates
227
+ if (!matchingTypes.includes(propagator.type)) {
228
+ matchingTypes.push(propagator.type);
229
+ }
230
+ }
231
+ }
232
+ return matchingTypes;
233
+ }
234
+
235
+ return [];
236
+ }
237
+
238
+ function addTraceContextHttpHeaders(
239
+ fn: (name: string, value: string) => void,
240
+ ctx: unknown,
241
+ span: InProgressSpan,
242
+ types: PropagatorType[]
243
+ ) {
244
+ for (const type of types) {
245
+ if (type === "xray") {
246
+ addXRayTraceContextHttpHeaders(fn, ctx, span);
247
+ } else {
248
+ addW3CTraceContextHttpHeaders(fn, ctx, span);
249
+ }
250
+ }
251
+ }
@@ -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
+ });
@@ -1,8 +1,7 @@
1
1
  import { newBatcher } from "./batcher";
2
2
  import { send } from "./fetch";
3
3
  import { vars } from "../vars";
4
- import { createRateLimiter } from "../utils/rate-limit";
5
- import { debug, error } from "../utils";
4
+ import { debug, error, createRateLimiter } from "../utils";
6
5
  import { ExportLogsServiceRequest, ExportTraceServiceRequest, LogRecord, Span } from "../types/otlp";
7
6
 
8
7
  const logBatcher = newBatcher<LogRecord>(sendLogs);
@@ -13,7 +12,6 @@ let rateLimiter: (() => boolean) | undefined;
13
12
  function isRateLimited() {
14
13
  if (!rateLimiter) {
15
14
  rateLimiter = createRateLimiter({
16
- maxCalls: 8096,
17
15
  maxCallsPerTenMinutes: 4096,
18
16
  maxCallsPerTenSeconds: 128,
19
17
  });
@@ -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,
@@ -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
+ }