Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/event/EventFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,17 @@ class EventFactory implements IEventFactory {
return this.excludedQueryParams.has(key.toLowerCase());
}

/**
* Normalize URL paths for analytics aggregation by stripping trailing slashes
* from non-root paths. Query strings and hash fragments are preserved by
* mutating only the URL pathname.
*/
private normalizeUrlPath(url: URL): void {
if (url.pathname !== "/") {
url.pathname = url.pathname.replace(/\/+$/, "");
}
}

/**
* Strip excluded (sensitive) query parameters from a URL in place. Only the
* query string is touched; the path and hash/fragment are left as-is.
Expand All @@ -142,15 +153,16 @@ class EventFactory implements IEventFactory {
}

/**
* Return the given absolute URL with excluded query parameters removed. The
* input is returned unchanged when it is empty or cannot be parsed (e.g. an
* empty referrer).
* Return the given absolute URL with excluded query parameters removed and
* trailing slashes stripped from non-root paths. The input is returned
* unchanged when it is empty or cannot be parsed (e.g. an empty referrer).
*/
private redactUrl(href: string): string {
if (!href) return href;
try {
const url = new URL(href);
this.redactQueryParams(url);
this.normalizeUrlPath(url);
return url.href;
} catch {
return href;
Expand Down Expand Up @@ -410,14 +422,15 @@ class EventFactory implements IEventFactory {
try {
urlObj = new URL(globalThis.location.href);
this.redactQueryParams(urlObj);
this.normalizeUrlPath(urlObj);
} catch {}

if (isUndefined(pageProps.url)) {
pageProps.url = urlObj ? urlObj.href : globalThis.location.href;
}

if (isUndefined(pageProps.path)) {
pageProps.path = globalThis.location.pathname;
pageProps.path = urlObj ? urlObj.pathname : globalThis.location.pathname;
}

if (isUndefined(pageProps.hash)) {
Expand Down
27 changes: 26 additions & 1 deletion test/lib/event/PagePropertiesParsing.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,32 @@ describe("Page Event Property Parsing", () => {
expect(props.hash).to.equal("");
expect(props.query).to.equal("");
});

it("should strip trailing slashes from non-root page URL and path", async () => {
setMockLocation("https://formo.so/limit/bnb/?foo=bar#intro");

const props = await getPageProperties();
const context = await getPageContext();

expect(props.url).to.equal("https://formo.so/limit/bnb?foo=bar#intro");
expect(props.path).to.equal("/limit/bnb");
expect(props.hash).to.equal("#intro");
expect(props.query).to.equal("foo=bar");
expect(context.page_url).to.equal(
"https://formo.so/limit/bnb?foo=bar#intro"
);
});

it("should preserve the root slash when normalizing page URL and path", async () => {
setMockLocation("https://formo.so/?foo=bar#intro");

const props = await getPageProperties();
const context = await getPageContext();

expect(props.url).to.equal("https://formo.so/?foo=bar#intro");
expect(props.path).to.equal("/");
expect(context.page_url).to.equal("https://formo.so/?foo=bar#intro");
});
});

describe("Query parameter parsing", () => {
Expand Down Expand Up @@ -854,4 +880,3 @@ describe("Page Event Property Parsing", () => {
});
});
});