@dash0/sdk-web 0.13.5 → 0.14.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 +21 -0
- 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 +19 -4
- package/dist/modules/api/init_test.js +86 -0
- package/dist/modules/attributes/url.js +22 -12
- package/dist/modules/attributes/url_test.js +326 -0
- package/dist/modules/types/options.js +1 -0
- package/dist/modules/utils/fn.js +3 -0
- package/dist/modules/vars.js +2 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types/api/init.d.ts +1 -30
- package/dist/types/api/init_test.d.ts +1 -0
- package/dist/types/attributes/url.d.ts +10 -0
- package/dist/types/attributes/url_test.d.ts +1 -0
- package/dist/types/entrypoint/npm-package.d.ts +5 -1
- package/dist/types/types/options.d.ts +35 -0
- package/dist/types/utils/fn.d.ts +1 -0
- package/dist/types/vars.d.ts +10 -0
- package/package.json +1 -1
- package/src/api/init.ts +25 -52
- package/src/api/init_test.ts +108 -0
- package/src/attributes/url.ts +34 -12
- package/src/attributes/url_test.ts +417 -0
- package/src/entrypoint/npm-package.ts +8 -1
- package/src/types/options.ts +56 -0
- package/src/utils/fn.ts +4 -0
- package/src/vars.ts +13 -0
package/dist/modules/api/init.js
CHANGED
|
@@ -37,16 +37,25 @@ export function init(opts) {
|
|
|
37
37
|
"maxWaitForResourceTimingsMillis",
|
|
38
38
|
"maxToleranceForResourceTimingsMillis",
|
|
39
39
|
"headersToCapture",
|
|
40
|
+
"urlAttributeScrubber",
|
|
40
41
|
"pageViewInstrumentation",
|
|
41
42
|
])));
|
|
42
43
|
initializeResourceAttributes(opts);
|
|
43
44
|
initializeSignalAttributes(opts);
|
|
44
45
|
initializeTabId();
|
|
45
46
|
trackSessions(opts.sessionInactivityTimeoutMillis, opts.sessionTerminationTimeoutMillis);
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
47
|
+
if (isInstrumentationEnabled("@dash0/navigation", opts)) {
|
|
48
|
+
startNavigationInstrumentation();
|
|
49
|
+
}
|
|
50
|
+
if (isInstrumentationEnabled("@dash0/web-vitals", opts)) {
|
|
51
|
+
startWebVitalsInstrumentation();
|
|
52
|
+
}
|
|
53
|
+
if (isInstrumentationEnabled("@dash0/error", opts)) {
|
|
54
|
+
startErrorInstrumentation();
|
|
55
|
+
}
|
|
56
|
+
if (isInstrumentationEnabled("@dash0/fetch", opts)) {
|
|
57
|
+
instrumentFetch();
|
|
58
|
+
}
|
|
50
59
|
hasBeenInitialised = true;
|
|
51
60
|
}
|
|
52
61
|
function initializeResourceAttributes(opts) {
|
|
@@ -128,3 +137,9 @@ function detectDeploymentId(opts) {
|
|
|
128
137
|
return undefined;
|
|
129
138
|
}
|
|
130
139
|
}
|
|
140
|
+
function isInstrumentationEnabled(name, opts) {
|
|
141
|
+
const instrumentations = opts.enabledInstrumentations;
|
|
142
|
+
if (!instrumentations)
|
|
143
|
+
return true;
|
|
144
|
+
return instrumentations.includes(name);
|
|
145
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
// Mock all the instrumentation modules
|
|
3
|
+
vi.mock("../instrumentations/web-vitals", () => ({
|
|
4
|
+
startWebVitalsInstrumentation: vi.fn(),
|
|
5
|
+
}));
|
|
6
|
+
vi.mock("../instrumentations/errors", () => ({
|
|
7
|
+
startErrorInstrumentation: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("../instrumentations/http/fetch", () => ({
|
|
10
|
+
instrumentFetch: vi.fn(),
|
|
11
|
+
}));
|
|
12
|
+
vi.mock("../instrumentations/navigation", () => ({
|
|
13
|
+
startNavigationInstrumentation: vi.fn(),
|
|
14
|
+
}));
|
|
15
|
+
import { startWebVitalsInstrumentation } from "../instrumentations/web-vitals";
|
|
16
|
+
import { startErrorInstrumentation } from "../instrumentations/errors";
|
|
17
|
+
import { instrumentFetch } from "../instrumentations/http/fetch";
|
|
18
|
+
import { startNavigationInstrumentation } from "../instrumentations/navigation";
|
|
19
|
+
describe("init", () => {
|
|
20
|
+
const baseOptions = {
|
|
21
|
+
serviceName: "test-service",
|
|
22
|
+
endpoint: { url: "https://test-endpoint.com", authToken: "invalid" },
|
|
23
|
+
};
|
|
24
|
+
beforeEach(() => {
|
|
25
|
+
vi.clearAllMocks();
|
|
26
|
+
// Reset the hasBeenInitialised flag by re-importing the module
|
|
27
|
+
vi.resetModules();
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => {
|
|
30
|
+
vi.clearAllMocks();
|
|
31
|
+
});
|
|
32
|
+
describe("instrumentation enablement", () => {
|
|
33
|
+
it("should enable all instrumentations when enabledInstrumentations is undefined", async () => {
|
|
34
|
+
const { init } = await import("./init");
|
|
35
|
+
init({
|
|
36
|
+
...baseOptions,
|
|
37
|
+
enabledInstrumentations: undefined,
|
|
38
|
+
});
|
|
39
|
+
expect(startNavigationInstrumentation).toHaveBeenCalled();
|
|
40
|
+
expect(startWebVitalsInstrumentation).toHaveBeenCalled();
|
|
41
|
+
expect(startErrorInstrumentation).toHaveBeenCalled();
|
|
42
|
+
expect(instrumentFetch).toHaveBeenCalled();
|
|
43
|
+
});
|
|
44
|
+
const instrumentations = [
|
|
45
|
+
"@dash0/navigation",
|
|
46
|
+
"@dash0/web-vitals",
|
|
47
|
+
"@dash0/error",
|
|
48
|
+
"@dash0/fetch",
|
|
49
|
+
];
|
|
50
|
+
const instrumentationMocks = {
|
|
51
|
+
"@dash0/navigation": startNavigationInstrumentation,
|
|
52
|
+
"@dash0/web-vitals": startWebVitalsInstrumentation,
|
|
53
|
+
"@dash0/error": startErrorInstrumentation,
|
|
54
|
+
"@dash0/fetch": instrumentFetch,
|
|
55
|
+
};
|
|
56
|
+
instrumentations.forEach((instrumentation) => {
|
|
57
|
+
it(`should enable ${instrumentation} instrumentation when present in enabledInstrumentations array`, async () => {
|
|
58
|
+
const { init } = await import("./init");
|
|
59
|
+
init({
|
|
60
|
+
...baseOptions,
|
|
61
|
+
enabledInstrumentations: [instrumentation],
|
|
62
|
+
});
|
|
63
|
+
expect(instrumentationMocks[instrumentation]).toHaveBeenCalled();
|
|
64
|
+
// Check that other instrumentations are not called
|
|
65
|
+
Object.entries(instrumentationMocks).forEach(([name, mock]) => {
|
|
66
|
+
if (name !== instrumentation) {
|
|
67
|
+
expect(mock).not.toHaveBeenCalled();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
it(`should not enable ${instrumentation} instrumentation when not present in enabledInstrumentations array`, async () => {
|
|
72
|
+
const { init } = await import("./init");
|
|
73
|
+
const otherInstrumentations = instrumentations.filter((i) => i !== instrumentation);
|
|
74
|
+
init({
|
|
75
|
+
...baseOptions,
|
|
76
|
+
enabledInstrumentations: otherInstrumentations,
|
|
77
|
+
});
|
|
78
|
+
expect(instrumentationMocks[instrumentation]).not.toHaveBeenCalled();
|
|
79
|
+
// Check that other instrumentations are called
|
|
80
|
+
otherInstrumentations.forEach((name) => {
|
|
81
|
+
expect(instrumentationMocks[name]).toHaveBeenCalled();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { addAttribute, withPrefix } from "../utils/otel";
|
|
2
2
|
import { URL_DOMAIN, URL_FRAGMENT, URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME } from "../semantic-conventions";
|
|
3
|
-
import { parseUrl } from "../utils";
|
|
3
|
+
import { identity, parseUrl } from "../utils";
|
|
4
|
+
import { vars } from "../vars";
|
|
4
5
|
export function addUrlAttributes(attributes, url, prefix) {
|
|
5
6
|
const applyPrefix = withPrefix(prefix);
|
|
6
7
|
try {
|
|
@@ -9,18 +10,27 @@ export function addUrlAttributes(attributes, url, prefix) {
|
|
|
9
10
|
parsed.username = "REDACTED";
|
|
10
11
|
if (parsed.password)
|
|
11
12
|
parsed.password = "REDACTED";
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
13
|
+
const attrs = vars.urlAttributeScrubber({
|
|
14
|
+
[URL_FULL]: parsed.href,
|
|
15
|
+
[URL_PATH]: parsed.pathname,
|
|
16
|
+
[URL_DOMAIN]: parsed.hostname,
|
|
17
|
+
[URL_SCHEME]: parsed.protocol.replace(":", ""),
|
|
18
|
+
[URL_FRAGMENT]: parsed.hash ? parsed.hash.replace("#", "") : undefined,
|
|
19
|
+
[URL_QUERY]: parsed.search ? parsed.search.replace("?", "") : undefined,
|
|
20
|
+
});
|
|
21
|
+
Object.entries(attrs).forEach(([key, value]) => {
|
|
22
|
+
if (value !== undefined) {
|
|
23
|
+
addAttribute(attributes, applyPrefix(key), value);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
22
26
|
}
|
|
23
27
|
catch (_e) {
|
|
24
|
-
|
|
28
|
+
// This is fallback handling in case the url failed to parse or the user defined attribute scrubber behaved unexpectedly
|
|
29
|
+
// If the user did not attempt scrubbing we'll supply the full url for debugging purposes, otherwise we drop all url
|
|
30
|
+
// attributes
|
|
31
|
+
if (vars.urlAttributeScrubber === identity) {
|
|
32
|
+
// `identity` is the default assignment for this option
|
|
33
|
+
addAttribute(attributes, applyPrefix(URL_FULL), String(url));
|
|
34
|
+
}
|
|
25
35
|
}
|
|
26
36
|
}
|
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
import { expect, describe, it, beforeEach, afterEach } from "vitest";
|
|
2
|
+
import { addUrlAttributes } from "./url";
|
|
3
|
+
import { vars } from "../vars";
|
|
4
|
+
import { identity } from "../utils/fn";
|
|
5
|
+
import { URL_DOMAIN, URL_FRAGMENT, URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME } from "../semantic-conventions";
|
|
6
|
+
describe("addUrlAttributes", () => {
|
|
7
|
+
let attributes;
|
|
8
|
+
let originalScrubber;
|
|
9
|
+
beforeEach(() => {
|
|
10
|
+
attributes = [];
|
|
11
|
+
originalScrubber = vars.urlAttributeScrubber;
|
|
12
|
+
vars.urlAttributeScrubber = identity;
|
|
13
|
+
});
|
|
14
|
+
afterEach(() => {
|
|
15
|
+
vars.urlAttributeScrubber = originalScrubber;
|
|
16
|
+
});
|
|
17
|
+
describe("URL parsing and attribute extraction", () => {
|
|
18
|
+
it("extracts all URL components for complete URL", () => {
|
|
19
|
+
const url = "https://example.com:8080/path/to/resource?param1=value1¶m2=value2#section1";
|
|
20
|
+
addUrlAttributes(attributes, url);
|
|
21
|
+
expect(attributes).toEqual([
|
|
22
|
+
{
|
|
23
|
+
key: URL_FULL,
|
|
24
|
+
value: { stringValue: "https://example.com:8080/path/to/resource?param1=value1¶m2=value2#section1" },
|
|
25
|
+
},
|
|
26
|
+
{ key: URL_PATH, value: { stringValue: "/path/to/resource" } },
|
|
27
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
28
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
29
|
+
{ key: URL_FRAGMENT, value: { stringValue: "section1" } },
|
|
30
|
+
{ key: URL_QUERY, value: { stringValue: "param1=value1¶m2=value2" } },
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
it("handles URL with only required components", () => {
|
|
34
|
+
const url = "https://example.com";
|
|
35
|
+
addUrlAttributes(attributes, url);
|
|
36
|
+
expect(attributes).toEqual([
|
|
37
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com/" } },
|
|
38
|
+
{ key: URL_PATH, value: { stringValue: "/" } },
|
|
39
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
40
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
41
|
+
]);
|
|
42
|
+
});
|
|
43
|
+
it("handles URL with path but no query or fragment", () => {
|
|
44
|
+
const url = "http://example.com/some/path";
|
|
45
|
+
addUrlAttributes(attributes, url);
|
|
46
|
+
expect(attributes).toEqual([
|
|
47
|
+
{ key: URL_FULL, value: { stringValue: "http://example.com/some/path" } },
|
|
48
|
+
{ key: URL_PATH, value: { stringValue: "/some/path" } },
|
|
49
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
50
|
+
{ key: URL_SCHEME, value: { stringValue: "http" } },
|
|
51
|
+
]);
|
|
52
|
+
});
|
|
53
|
+
it("handles URL with query but no fragment", () => {
|
|
54
|
+
const url = "https://example.com/path?query=test";
|
|
55
|
+
addUrlAttributes(attributes, url);
|
|
56
|
+
expect(attributes).toEqual([
|
|
57
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com/path?query=test" } },
|
|
58
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
59
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
60
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
61
|
+
{ key: URL_QUERY, value: { stringValue: "query=test" } },
|
|
62
|
+
]);
|
|
63
|
+
});
|
|
64
|
+
it("handles URL with fragment but no query", () => {
|
|
65
|
+
const url = "https://example.com/path#section";
|
|
66
|
+
addUrlAttributes(attributes, url);
|
|
67
|
+
expect(attributes).toEqual([
|
|
68
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com/path#section" } },
|
|
69
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
70
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
71
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
72
|
+
{ key: URL_FRAGMENT, value: { stringValue: "section" } },
|
|
73
|
+
]);
|
|
74
|
+
});
|
|
75
|
+
it("accepts URL object as input", () => {
|
|
76
|
+
const urlObject = new URL("https://example.com/path?query=test#fragment");
|
|
77
|
+
addUrlAttributes(attributes, urlObject);
|
|
78
|
+
expect(attributes).toEqual([
|
|
79
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com/path?query=test#fragment" } },
|
|
80
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
81
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
82
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
83
|
+
{ key: URL_FRAGMENT, value: { stringValue: "fragment" } },
|
|
84
|
+
{ key: URL_QUERY, value: { stringValue: "query=test" } },
|
|
85
|
+
]);
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
describe("credential redaction functionality", () => {
|
|
89
|
+
it("redacts username and password from URL", () => {
|
|
90
|
+
const url = "https://user:pass@example.com/path";
|
|
91
|
+
addUrlAttributes(attributes, url);
|
|
92
|
+
expect(attributes).toEqual([
|
|
93
|
+
{ key: URL_FULL, value: { stringValue: "https://REDACTED:REDACTED@example.com/path" } },
|
|
94
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
95
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
96
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
97
|
+
]);
|
|
98
|
+
});
|
|
99
|
+
it("redacts username when password is not present", () => {
|
|
100
|
+
const url = "https://user@example.com/path";
|
|
101
|
+
addUrlAttributes(attributes, url);
|
|
102
|
+
expect(attributes).toEqual([
|
|
103
|
+
{ key: URL_FULL, value: { stringValue: "https://REDACTED@example.com/path" } },
|
|
104
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
105
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
106
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
107
|
+
]);
|
|
108
|
+
});
|
|
109
|
+
it("handles special characters in credentials", () => {
|
|
110
|
+
const url = "https://user%40domain:p%40ssw0rd@example.com/path";
|
|
111
|
+
addUrlAttributes(attributes, url);
|
|
112
|
+
expect(attributes).toEqual([
|
|
113
|
+
{ key: URL_FULL, value: { stringValue: "https://REDACTED:REDACTED@example.com/path" } },
|
|
114
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
115
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
116
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
117
|
+
]);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
describe("URL attribute scrubber integration", () => {
|
|
121
|
+
it("applies custom scrubber to URL attributes", () => {
|
|
122
|
+
const customScrubber = (attrs) => ({
|
|
123
|
+
...attrs,
|
|
124
|
+
[URL_PATH]: "REDACTED",
|
|
125
|
+
[URL_QUERY]: undefined,
|
|
126
|
+
});
|
|
127
|
+
vars.urlAttributeScrubber = customScrubber;
|
|
128
|
+
const url = "https://example.com/sensitive/path?secret=value#fragment";
|
|
129
|
+
addUrlAttributes(attributes, url);
|
|
130
|
+
expect(attributes).toEqual([
|
|
131
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com/sensitive/path?secret=value#fragment" } },
|
|
132
|
+
{ key: URL_PATH, value: { stringValue: "REDACTED" } },
|
|
133
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
134
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
135
|
+
{ key: URL_FRAGMENT, value: { stringValue: "fragment" } },
|
|
136
|
+
]);
|
|
137
|
+
});
|
|
138
|
+
it("applies scrubber that removes all optional attributes", () => {
|
|
139
|
+
const restrictiveScrubber = (attrs) => ({
|
|
140
|
+
[URL_FULL]: attrs[URL_FULL],
|
|
141
|
+
});
|
|
142
|
+
vars.urlAttributeScrubber = restrictiveScrubber;
|
|
143
|
+
const url = "https://example.com/path?query=test#fragment";
|
|
144
|
+
addUrlAttributes(attributes, url);
|
|
145
|
+
expect(attributes).toEqual([
|
|
146
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com/path?query=test#fragment" } },
|
|
147
|
+
]);
|
|
148
|
+
});
|
|
149
|
+
it("handles scrubber that throws an error", () => {
|
|
150
|
+
const errorScrubber = () => {
|
|
151
|
+
throw new Error("Scrubber error");
|
|
152
|
+
};
|
|
153
|
+
vars.urlAttributeScrubber = errorScrubber;
|
|
154
|
+
const url = "https://example.com/path";
|
|
155
|
+
addUrlAttributes(attributes, url);
|
|
156
|
+
expect(attributes).toHaveLength(0);
|
|
157
|
+
});
|
|
158
|
+
it("applies identity scrubber correctly", () => {
|
|
159
|
+
vars.urlAttributeScrubber = identity;
|
|
160
|
+
const url = "https://example.com/path?query=test";
|
|
161
|
+
addUrlAttributes(attributes, url);
|
|
162
|
+
expect(attributes).toHaveLength(5);
|
|
163
|
+
expect(attributes.find((attr) => attr.key === URL_FULL)?.value).toEqual({
|
|
164
|
+
stringValue: "https://example.com/path?query=test",
|
|
165
|
+
});
|
|
166
|
+
expect(attributes.find((attr) => attr.key === URL_PATH)?.value).toEqual({ stringValue: "/path" });
|
|
167
|
+
expect(attributes.find((attr) => attr.key === URL_DOMAIN)?.value).toEqual({ stringValue: "example.com" });
|
|
168
|
+
expect(attributes.find((attr) => attr.key === URL_SCHEME)?.value).toEqual({ stringValue: "https" });
|
|
169
|
+
expect(attributes.find((attr) => attr.key === URL_QUERY)?.value).toEqual({ stringValue: "query=test" });
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
describe("error handling for invalid URLs", () => {
|
|
173
|
+
it("handles invalid URL with identity scrubber by adding full URL", () => {
|
|
174
|
+
vars.urlAttributeScrubber = identity;
|
|
175
|
+
const invalidUrl = "not-a-valid-url";
|
|
176
|
+
addUrlAttributes(attributes, invalidUrl);
|
|
177
|
+
// Invalid URLs still parse but create fallback attributes
|
|
178
|
+
expect(attributes.length).toBeGreaterThan(0);
|
|
179
|
+
// The actual behavior might be different based on the parsing logic
|
|
180
|
+
const fullAttr = attributes.find((attr) => attr.key === URL_FULL);
|
|
181
|
+
expect(fullAttr).toBeDefined();
|
|
182
|
+
});
|
|
183
|
+
it("handles invalid URL with custom scrubber by dropping attributes", () => {
|
|
184
|
+
const customScrubber = (attrs) => attrs;
|
|
185
|
+
vars.urlAttributeScrubber = customScrubber;
|
|
186
|
+
const invalidUrl = "not-a-valid-url";
|
|
187
|
+
addUrlAttributes(attributes, invalidUrl);
|
|
188
|
+
// With custom scrubber, invalid URLs still get parsed with fallback
|
|
189
|
+
expect(attributes.length).toBeGreaterThan(0);
|
|
190
|
+
});
|
|
191
|
+
it("handles empty string URL with identity scrubber", () => {
|
|
192
|
+
vars.urlAttributeScrubber = identity;
|
|
193
|
+
const emptyUrl = "";
|
|
194
|
+
addUrlAttributes(attributes, emptyUrl);
|
|
195
|
+
// Empty URLs still parse but create fallback attributes
|
|
196
|
+
expect(attributes.length).toBeGreaterThan(0);
|
|
197
|
+
expect(attributes.find((attr) => attr.key === URL_FULL)?.value).toEqual({
|
|
198
|
+
stringValue: "http://localhost:3000/",
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
it("handles relative URL that can be parsed with base", () => {
|
|
202
|
+
// This should work if there's a document.baseURI or location.href available
|
|
203
|
+
const relativeUrl = "/relative/path?query=test";
|
|
204
|
+
// This might throw or might work depending on environment
|
|
205
|
+
addUrlAttributes(attributes, relativeUrl);
|
|
206
|
+
// We expect either parsed attributes or fallback behavior
|
|
207
|
+
expect(attributes.length).toBeGreaterThanOrEqual(0);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
describe("prefix functionality for attributes", () => {
|
|
211
|
+
it("applies string prefix to attribute keys", () => {
|
|
212
|
+
const url = "https://example.com/path";
|
|
213
|
+
const prefix = "page";
|
|
214
|
+
addUrlAttributes(attributes, url, prefix);
|
|
215
|
+
expect(attributes).toEqual([
|
|
216
|
+
{ key: "page.url.full", value: { stringValue: "https://example.com/path" } },
|
|
217
|
+
{ key: "page.url.path", value: { stringValue: "/path" } },
|
|
218
|
+
{ key: "page.url.domain", value: { stringValue: "example.com" } },
|
|
219
|
+
{ key: "page.url.scheme", value: { stringValue: "https" } },
|
|
220
|
+
]);
|
|
221
|
+
});
|
|
222
|
+
it("applies array prefix to attribute keys", () => {
|
|
223
|
+
const url = "https://example.com/path?query=test";
|
|
224
|
+
const prefix = ["http", "request"];
|
|
225
|
+
addUrlAttributes(attributes, url, prefix);
|
|
226
|
+
expect(attributes).toEqual([
|
|
227
|
+
{ key: "http.request.url.full", value: { stringValue: "https://example.com/path?query=test" } },
|
|
228
|
+
{ key: "http.request.url.path", value: { stringValue: "/path" } },
|
|
229
|
+
{ key: "http.request.url.domain", value: { stringValue: "example.com" } },
|
|
230
|
+
{ key: "http.request.url.scheme", value: { stringValue: "https" } },
|
|
231
|
+
{ key: "http.request.url.query", value: { stringValue: "query=test" } },
|
|
232
|
+
]);
|
|
233
|
+
});
|
|
234
|
+
it("handles empty string prefix", () => {
|
|
235
|
+
const url = "https://example.com/path";
|
|
236
|
+
const prefix = "";
|
|
237
|
+
addUrlAttributes(attributes, url, prefix);
|
|
238
|
+
expect(attributes).toEqual([
|
|
239
|
+
{ key: "url.full", value: { stringValue: "https://example.com/path" } },
|
|
240
|
+
{ key: "url.path", value: { stringValue: "/path" } },
|
|
241
|
+
{ key: "url.domain", value: { stringValue: "example.com" } },
|
|
242
|
+
{ key: "url.scheme", value: { stringValue: "https" } },
|
|
243
|
+
]);
|
|
244
|
+
});
|
|
245
|
+
it("handles undefined prefix", () => {
|
|
246
|
+
const url = "https://example.com/path";
|
|
247
|
+
addUrlAttributes(attributes, url, undefined);
|
|
248
|
+
expect(attributes).toEqual([
|
|
249
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com/path" } },
|
|
250
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
251
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
252
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
253
|
+
]);
|
|
254
|
+
});
|
|
255
|
+
it("applies prefix with error handling fallback", () => {
|
|
256
|
+
vars.urlAttributeScrubber = identity;
|
|
257
|
+
const invalidUrl = "invalid-url";
|
|
258
|
+
const prefix = "page";
|
|
259
|
+
addUrlAttributes(attributes, invalidUrl, prefix);
|
|
260
|
+
// With prefix and identity scrubber, fallback should include prefix
|
|
261
|
+
expect(attributes.length).toBeGreaterThan(0);
|
|
262
|
+
// The actual behavior might be different based on the parsing logic
|
|
263
|
+
const fullAttr = attributes.find((attr) => attr.key === "page.url.full");
|
|
264
|
+
expect(fullAttr).toBeDefined();
|
|
265
|
+
});
|
|
266
|
+
});
|
|
267
|
+
describe("edge cases and corner cases", () => {
|
|
268
|
+
it("handles URL with port number", () => {
|
|
269
|
+
const url = "https://example.com:8080/path";
|
|
270
|
+
addUrlAttributes(attributes, url);
|
|
271
|
+
expect(attributes).toEqual([
|
|
272
|
+
{ key: URL_FULL, value: { stringValue: "https://example.com:8080/path" } },
|
|
273
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
274
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
275
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
276
|
+
]);
|
|
277
|
+
});
|
|
278
|
+
it("handles URL with IP address", () => {
|
|
279
|
+
const url = "http://192.168.1.1:8080/api";
|
|
280
|
+
addUrlAttributes(attributes, url);
|
|
281
|
+
expect(attributes).toEqual([
|
|
282
|
+
{ key: URL_FULL, value: { stringValue: "http://192.168.1.1:8080/api" } },
|
|
283
|
+
{ key: URL_PATH, value: { stringValue: "/api" } },
|
|
284
|
+
{ key: URL_DOMAIN, value: { stringValue: "192.168.1.1" } },
|
|
285
|
+
{ key: URL_SCHEME, value: { stringValue: "http" } },
|
|
286
|
+
]);
|
|
287
|
+
});
|
|
288
|
+
it("handles URL with IPv6 address", () => {
|
|
289
|
+
const url = "http://[::1]:8080/path";
|
|
290
|
+
addUrlAttributes(attributes, url);
|
|
291
|
+
expect(attributes).toEqual([
|
|
292
|
+
{ key: URL_FULL, value: { stringValue: "http://[::1]:8080/path" } },
|
|
293
|
+
{ key: URL_PATH, value: { stringValue: "/path" } },
|
|
294
|
+
{ key: URL_DOMAIN, value: { stringValue: "[::1]" } },
|
|
295
|
+
{ key: URL_SCHEME, value: { stringValue: "http" } },
|
|
296
|
+
]);
|
|
297
|
+
});
|
|
298
|
+
it("handles URL with encoded characters", () => {
|
|
299
|
+
const url = "https://example.com/path%20with%20spaces?query=value%20with%20spaces#fragment%20with%20spaces";
|
|
300
|
+
addUrlAttributes(attributes, url);
|
|
301
|
+
expect(attributes).toEqual([
|
|
302
|
+
{
|
|
303
|
+
key: URL_FULL,
|
|
304
|
+
value: {
|
|
305
|
+
stringValue: "https://example.com/path%20with%20spaces?query=value%20with%20spaces#fragment%20with%20spaces",
|
|
306
|
+
},
|
|
307
|
+
},
|
|
308
|
+
{ key: URL_PATH, value: { stringValue: "/path%20with%20spaces" } },
|
|
309
|
+
{ key: URL_DOMAIN, value: { stringValue: "example.com" } },
|
|
310
|
+
{ key: URL_SCHEME, value: { stringValue: "https" } },
|
|
311
|
+
{ key: URL_FRAGMENT, value: { stringValue: "fragment%20with%20spaces" } },
|
|
312
|
+
{ key: URL_QUERY, value: { stringValue: "query=value%20with%20spaces" } },
|
|
313
|
+
]);
|
|
314
|
+
});
|
|
315
|
+
it("handles different protocol schemes", () => {
|
|
316
|
+
const protocols = ["ftp", "ws", "wss", "file"];
|
|
317
|
+
protocols.forEach((protocol) => {
|
|
318
|
+
attributes = []; // Reset attributes for each test
|
|
319
|
+
const url = `${protocol}://example.com/path`;
|
|
320
|
+
addUrlAttributes(attributes, url);
|
|
321
|
+
const schemeAttr = attributes.find((attr) => attr.key === URL_SCHEME);
|
|
322
|
+
expect(schemeAttr?.value).toEqual({ stringValue: protocol });
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/modules/utils/fn.js
CHANGED
package/dist/modules/vars.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { identity } from "./utils";
|
|
1
2
|
export const vars = {
|
|
2
3
|
endpoints: [],
|
|
3
4
|
resource: {
|
|
@@ -17,6 +18,7 @@ export const vars = {
|
|
|
17
18
|
maxWaitForResourceTimingsMillis: 10000,
|
|
18
19
|
maxToleranceForResourceTimingsMillis: 50,
|
|
19
20
|
headersToCapture: [],
|
|
21
|
+
urlAttributeScrubber: identity,
|
|
20
22
|
pageViewInstrumentation: {
|
|
21
23
|
trackVirtualPageViews: true,
|
|
22
24
|
includeParts: [],
|