From 319a09cdb5f08b7973e03f758f79a79448a2ca57 Mon Sep 17 00:00:00 2001 From: NoiceHax Date: Thu, 13 Aug 2026 22:43:49 +0530 Subject: [PATCH] fix(send): keep the root path when REMOVE_TRAILING_SLASH is enabled The trailing-slash regex also matched the leading slash of the root URL, so with REMOVE_TRAILING_SLASH set every home page view was stored with an empty url_path: the home page vanished from the paths/entry/exit reports and could not be selected as a funnel step. Skip the match at the start of the path so "/" and "/#hash" are preserved while "/blog/" and "/blog/#hash" are still trimmed. Closes #4152 --- src/app/api/send/route.test.ts | 33 +++++++++++++++++++++++++++++++++ src/app/api/send/route.ts | 3 ++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/src/app/api/send/route.test.ts b/src/app/api/send/route.test.ts index 1b1d3b75a..9733c0da2 100644 --- a/src/app/api/send/route.test.ts +++ b/src/app/api/send/route.test.ts @@ -482,6 +482,39 @@ describe('event url and referrer parsing', () => { expect(saveEventMock.mock.calls[0][0]).toMatchObject({ urlPath: '/blog' }); }); + test('REMOVE_TRAILING_SLASH keeps the root path as "/"', async () => { + process.env.REMOVE_TRAILING_SLASH = '1'; + + await callPOST({ + type: 'event', + payload: { website: WEBSITE_ID, hostname: 'example.com', url: '/' }, + }); + + expect(saveEventMock.mock.calls[0][0]).toMatchObject({ urlPath: '/' }); + }); + + test('REMOVE_TRAILING_SLASH keeps the root path when the url has a hash', async () => { + process.env.REMOVE_TRAILING_SLASH = '1'; + + await callPOST({ + type: 'event', + payload: { website: WEBSITE_ID, hostname: 'example.com', url: '/#hero' }, + }); + + expect(saveEventMock.mock.calls[0][0]).toMatchObject({ urlPath: '/#hero' }); + }); + + test('REMOVE_TRAILING_SLASH strips a trailing slash before the hash', async () => { + process.env.REMOVE_TRAILING_SLASH = '1'; + + await callPOST({ + type: 'event', + payload: { website: WEBSITE_ID, hostname: 'example.com', url: '/blog/#hero' }, + }); + + expect(saveEventMock.mock.calls[0][0]).toMatchObject({ urlPath: '/blog#hero' }); + }); + test('maps a "/undefined" pathname to an empty url path', async () => { await callPOST({ type: 'event', diff --git a/src/app/api/send/route.ts b/src/app/api/send/route.ts index 6da12076c..cd5d42634 100644 --- a/src/app/api/send/route.ts +++ b/src/app/api/send/route.ts @@ -214,7 +214,8 @@ export async function POST(request: Request) { const twclid = currentUrl.searchParams.get('twclid'); if (process.env.REMOVE_TRAILING_SLASH) { - urlPath = urlPath.replace(/\/(?=(#.*)?$)/, ''); + // Never strip the root slash, otherwise the home page is saved with an empty path + urlPath = urlPath.replace(/(?!^)\/(?=(#.*)?$)/, ''); } if (referrer) {