@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.
- package/README.md +35 -515
- package/dist/dash0.iife.js +1 -1
- package/dist/dash0.iife.js.map +1 -1
- package/dist/dash0.js +1 -1
- package/dist/dash0.js.map +1 -1
- package/dist/dash0.umd.cjs +1 -1
- package/dist/dash0.umd.cjs.map +1 -1
- package/dist/modules/api/init.js +30 -0
- package/dist/modules/api/init_test.js +102 -6
- package/dist/modules/api/report-error.js +1 -1
- package/dist/modules/instrumentations/errors/event-handlers.js +2 -2
- package/dist/modules/instrumentations/errors/unhandled-error.js +1 -1
- package/dist/modules/instrumentations/errors/unhandled-promise-rejection.js +4 -4
- package/dist/modules/instrumentations/http/fetch.js +125 -28
- package/dist/modules/instrumentations/http/fetch_test.js +170 -0
- package/dist/modules/instrumentations/http/propagator-integration_test.js +77 -0
- package/dist/modules/transport/index.js +2 -0
- package/dist/modules/utils/index.js +1 -0
- package/dist/modules/utils/otel/span.js +9 -0
- package/dist/modules/utils/otel/trace-context.js +24 -1
- package/dist/modules/utils/otel/trace-context_test.js +40 -0
- package/dist/modules/utils/performance.js +16 -3
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/entrypoint/npm-package.d.ts +1 -1
- package/dist/types/instrumentations/errors/unhandled-error.d.ts +1 -1
- package/dist/types/instrumentations/http/fetch_test.d.ts +1 -0
- package/dist/types/instrumentations/http/propagator-integration_test.d.ts +1 -0
- package/dist/types/transport/index.d.ts +1 -1
- package/dist/types/types/options.d.ts +6 -1
- package/dist/types/utils/index.d.ts +1 -0
- package/dist/types/utils/otel/span.d.ts +5 -1
- package/dist/types/utils/otel/trace-context.d.ts +2 -1
- package/dist/types/utils/otel/trace-context_test.d.ts +1 -0
- package/dist/types/utils/performance.d.ts +5 -1
- package/dist/types/vars.d.ts +11 -0
- package/package.json +1 -1
- package/src/api/init.ts +34 -0
- package/src/api/init_test.ts +130 -9
- package/src/api/report-error.ts +1 -1
- package/src/entrypoint/npm-package.ts +1 -1
- package/src/instrumentations/errors/event-handlers.ts +2 -2
- package/src/instrumentations/errors/unhandled-error.ts +1 -1
- package/src/instrumentations/errors/unhandled-promise-rejection.ts +4 -4
- package/src/instrumentations/http/fetch.ts +148 -30
- package/src/instrumentations/http/fetch_test.ts +191 -0
- package/src/instrumentations/http/propagator-integration_test.ts +93 -0
- package/src/transport/index.ts +3 -1
- package/src/types/options.ts +7 -1
- package/src/utils/index.ts +1 -0
- package/src/utils/otel/span.ts +16 -1
- package/src/utils/otel/trace-context.ts +30 -1
- package/src/utils/otel/trace-context_test.ts +59 -0
- package/src/utils/performance.ts +21 -4
- 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
|
+
});
|
package/src/transport/index.ts
CHANGED
|
@@ -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;
|
package/src/types/options.ts
CHANGED
|
@@ -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,
|
package/src/utils/index.ts
CHANGED
package/src/utils/otel/span.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
+
});
|
package/src/utils/performance.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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) =>
|
|
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
|
|