diff --git a/src/event/EventFactory.ts b/src/event/EventFactory.ts index 2e9f3a1..5aafc50 100644 --- a/src/event/EventFactory.ts +++ b/src/event/EventFactory.ts @@ -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. @@ -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; @@ -410,6 +422,7 @@ class EventFactory implements IEventFactory { try { urlObj = new URL(globalThis.location.href); this.redactQueryParams(urlObj); + this.normalizeUrlPath(urlObj); } catch {} if (isUndefined(pageProps.url)) { @@ -417,7 +430,7 @@ class EventFactory implements IEventFactory { } if (isUndefined(pageProps.path)) { - pageProps.path = globalThis.location.pathname; + pageProps.path = urlObj ? urlObj.pathname : globalThis.location.pathname; } if (isUndefined(pageProps.hash)) { diff --git a/test/lib/event/PagePropertiesParsing.spec.ts b/test/lib/event/PagePropertiesParsing.spec.ts index faaff93..ef4253f 100644 --- a/test/lib/event/PagePropertiesParsing.spec.ts +++ b/test/lib/event/PagePropertiesParsing.spec.ts @@ -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", () => { @@ -854,4 +880,3 @@ describe("Page Event Property Parsing", () => { }); }); }); -