UNPKG

@dash0/sdk-web

Version:

Dash0's Web SDK to collect telemetry from end-users' web browsers

327 lines (326 loc) 17.5 kB
import { expect, describe, it, beforeEach, afterEach } from "vitest"; import { addUrlAttributes } from "./url"; import { vars } from "../vars"; import { identity } from "../utils/fn"; import { URL_DOMAIN, URL_FRAGMENT, URL_FULL, URL_PATH, URL_QUERY, URL_SCHEME } from "../semantic-conventions"; describe("addUrlAttributes", () => { let attributes; let originalScrubber; beforeEach(() => { attributes = []; originalScrubber = vars.urlAttributeScrubber; vars.urlAttributeScrubber = identity; }); afterEach(() => { vars.urlAttributeScrubber = originalScrubber; }); describe("URL parsing and attribute extraction", () => { it("extracts all URL components for complete URL", () => { const url = "https://example.com:8080/path/to/resource?param1=value1&param2=value2#section1"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com:8080/path/to/resource?param1=value1&param2=value2#section1" }, }, { key: URL_PATH, value: { stringValue: "/path/to/resource" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, { key: URL_FRAGMENT, value: { stringValue: "section1" } }, { key: URL_QUERY, value: { stringValue: "param1=value1&param2=value2" } }, ]); }); it("handles URL with only required components", () => { const url = "https://example.com"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/" } }, { key: URL_PATH, value: { stringValue: "/" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, ]); }); it("handles URL with path but no query or fragment", () => { const url = "http://example.com/some/path"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "http://example.com/some/path" } }, { key: URL_PATH, value: { stringValue: "/some/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "http" } }, ]); }); it("handles URL with query but no fragment", () => { const url = "https://example.com/path?query=test"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/path?query=test" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, { key: URL_QUERY, value: { stringValue: "query=test" } }, ]); }); it("handles URL with fragment but no query", () => { const url = "https://example.com/path#section"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/path#section" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, { key: URL_FRAGMENT, value: { stringValue: "section" } }, ]); }); it("accepts URL object as input", () => { const urlObject = new URL("https://example.com/path?query=test#fragment"); addUrlAttributes(attributes, urlObject); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/path?query=test#fragment" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, { key: URL_FRAGMENT, value: { stringValue: "fragment" } }, { key: URL_QUERY, value: { stringValue: "query=test" } }, ]); }); }); describe("credential redaction functionality", () => { it("redacts username and password from URL", () => { const url = "https://user:pass@example.com/path"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://REDACTED:REDACTED@example.com/path" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, ]); }); it("redacts username when password is not present", () => { const url = "https://user@example.com/path"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://REDACTED@example.com/path" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, ]); }); it("handles special characters in credentials", () => { const url = "https://user%40domain:p%40ssw0rd@example.com/path"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://REDACTED:REDACTED@example.com/path" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, ]); }); }); describe("URL attribute scrubber integration", () => { it("applies custom scrubber to URL attributes", () => { const customScrubber = (attrs) => ({ ...attrs, [URL_PATH]: "REDACTED", [URL_QUERY]: undefined, }); vars.urlAttributeScrubber = customScrubber; const url = "https://example.com/sensitive/path?secret=value#fragment"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/sensitive/path?secret=value#fragment" } }, { key: URL_PATH, value: { stringValue: "REDACTED" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, { key: URL_FRAGMENT, value: { stringValue: "fragment" } }, ]); }); it("applies scrubber that removes all optional attributes", () => { const restrictiveScrubber = (attrs) => ({ [URL_FULL]: attrs[URL_FULL], }); vars.urlAttributeScrubber = restrictiveScrubber; const url = "https://example.com/path?query=test#fragment"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/path?query=test#fragment" } }, ]); }); it("handles scrubber that throws an error", () => { const errorScrubber = () => { throw new Error("Scrubber error"); }; vars.urlAttributeScrubber = errorScrubber; const url = "https://example.com/path"; addUrlAttributes(attributes, url); expect(attributes).toHaveLength(0); }); it("applies identity scrubber correctly", () => { vars.urlAttributeScrubber = identity; const url = "https://example.com/path?query=test"; addUrlAttributes(attributes, url); expect(attributes).toHaveLength(5); expect(attributes.find((attr) => attr.key === URL_FULL)?.value).toEqual({ stringValue: "https://example.com/path?query=test", }); expect(attributes.find((attr) => attr.key === URL_PATH)?.value).toEqual({ stringValue: "/path" }); expect(attributes.find((attr) => attr.key === URL_DOMAIN)?.value).toEqual({ stringValue: "example.com" }); expect(attributes.find((attr) => attr.key === URL_SCHEME)?.value).toEqual({ stringValue: "https" }); expect(attributes.find((attr) => attr.key === URL_QUERY)?.value).toEqual({ stringValue: "query=test" }); }); }); describe("error handling for invalid URLs", () => { it("handles invalid URL with identity scrubber by adding full URL", () => { vars.urlAttributeScrubber = identity; const invalidUrl = "not-a-valid-url"; addUrlAttributes(attributes, invalidUrl); // Invalid URLs still parse but create fallback attributes expect(attributes.length).toBeGreaterThan(0); // The actual behavior might be different based on the parsing logic const fullAttr = attributes.find((attr) => attr.key === URL_FULL); expect(fullAttr).toBeDefined(); }); it("handles invalid URL with custom scrubber by dropping attributes", () => { const customScrubber = (attrs) => attrs; vars.urlAttributeScrubber = customScrubber; const invalidUrl = "not-a-valid-url"; addUrlAttributes(attributes, invalidUrl); // With custom scrubber, invalid URLs still get parsed with fallback expect(attributes.length).toBeGreaterThan(0); }); it("handles empty string URL with identity scrubber", () => { vars.urlAttributeScrubber = identity; const emptyUrl = ""; addUrlAttributes(attributes, emptyUrl); // Empty URLs still parse but create fallback attributes expect(attributes.length).toBeGreaterThan(0); expect(attributes.find((attr) => attr.key === URL_FULL)?.value).toEqual({ stringValue: "http://localhost:3000/", }); }); it("handles relative URL that can be parsed with base", () => { // This should work if there's a document.baseURI or location.href available const relativeUrl = "/relative/path?query=test"; // This might throw or might work depending on environment addUrlAttributes(attributes, relativeUrl); // We expect either parsed attributes or fallback behavior expect(attributes.length).toBeGreaterThanOrEqual(0); }); }); describe("prefix functionality for attributes", () => { it("applies string prefix to attribute keys", () => { const url = "https://example.com/path"; const prefix = "page"; addUrlAttributes(attributes, url, prefix); expect(attributes).toEqual([ { key: "page.url.full", value: { stringValue: "https://example.com/path" } }, { key: "page.url.path", value: { stringValue: "/path" } }, { key: "page.url.domain", value: { stringValue: "example.com" } }, { key: "page.url.scheme", value: { stringValue: "https" } }, ]); }); it("applies array prefix to attribute keys", () => { const url = "https://example.com/path?query=test"; const prefix = ["http", "request"]; addUrlAttributes(attributes, url, prefix); expect(attributes).toEqual([ { key: "http.request.url.full", value: { stringValue: "https://example.com/path?query=test" } }, { key: "http.request.url.path", value: { stringValue: "/path" } }, { key: "http.request.url.domain", value: { stringValue: "example.com" } }, { key: "http.request.url.scheme", value: { stringValue: "https" } }, { key: "http.request.url.query", value: { stringValue: "query=test" } }, ]); }); it("handles empty string prefix", () => { const url = "https://example.com/path"; const prefix = ""; addUrlAttributes(attributes, url, prefix); expect(attributes).toEqual([ { key: "url.full", value: { stringValue: "https://example.com/path" } }, { key: "url.path", value: { stringValue: "/path" } }, { key: "url.domain", value: { stringValue: "example.com" } }, { key: "url.scheme", value: { stringValue: "https" } }, ]); }); it("handles undefined prefix", () => { const url = "https://example.com/path"; addUrlAttributes(attributes, url, undefined); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/path" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, ]); }); it("applies prefix with error handling fallback", () => { vars.urlAttributeScrubber = identity; const invalidUrl = "invalid-url"; const prefix = "page"; addUrlAttributes(attributes, invalidUrl, prefix); // With prefix and identity scrubber, fallback should include prefix expect(attributes.length).toBeGreaterThan(0); // The actual behavior might be different based on the parsing logic const fullAttr = attributes.find((attr) => attr.key === "page.url.full"); expect(fullAttr).toBeDefined(); }); }); describe("edge cases and corner cases", () => { it("handles URL with port number", () => { const url = "https://example.com:8080/path"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com:8080/path" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, ]); }); it("handles URL with IP address", () => { const url = "http://192.168.1.1:8080/api"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "http://192.168.1.1:8080/api" } }, { key: URL_PATH, value: { stringValue: "/api" } }, { key: URL_DOMAIN, value: { stringValue: "192.168.1.1" } }, { key: URL_SCHEME, value: { stringValue: "http" } }, ]); }); it("handles URL with IPv6 address", () => { const url = "http://[::1]:8080/path"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "http://[::1]:8080/path" } }, { key: URL_PATH, value: { stringValue: "/path" } }, { key: URL_DOMAIN, value: { stringValue: "[::1]" } }, { key: URL_SCHEME, value: { stringValue: "http" } }, ]); }); it("handles URL with encoded characters", () => { const url = "https://example.com/path%20with%20spaces?query=value%20with%20spaces#fragment%20with%20spaces"; addUrlAttributes(attributes, url); expect(attributes).toEqual([ { key: URL_FULL, value: { stringValue: "https://example.com/path%20with%20spaces?query=value%20with%20spaces#fragment%20with%20spaces", }, }, { key: URL_PATH, value: { stringValue: "/path%20with%20spaces" } }, { key: URL_DOMAIN, value: { stringValue: "example.com" } }, { key: URL_SCHEME, value: { stringValue: "https" } }, { key: URL_FRAGMENT, value: { stringValue: "fragment%20with%20spaces" } }, { key: URL_QUERY, value: { stringValue: "query=value%20with%20spaces" } }, ]); }); it("handles different protocol schemes", () => { const protocols = ["ftp", "ws", "wss", "file"]; protocols.forEach((protocol) => { attributes = []; // Reset attributes for each test const url = `${protocol}://example.com/path`; addUrlAttributes(attributes, url); const schemeAttr = attributes.find((attr) => attr.key === URL_SCHEME); expect(schemeAttr?.value).toEqual({ stringValue: protocol }); }); }); }); });