Avoid saving same-domain referrers and resolve path-only referrers correctly

This commit is contained in:
Mike Cao committed 2026-08-03 23:03:52 -07:00
1 parent c5d34d10ad
commit 8efd5d7279
4 files changed
+131 -6

No files matched your search

+102
View File
@@ -369,6 +369,108 @@ describe('event url and referrer parsing', () => {
});
});
test('does not save referrer domain when it matches the hostname', async () => {
await callPOST({
type: 'event',
payload: {
website: WEBSITE_ID,
hostname: 'example.com',
url: '/products',
referrer: 'https://www.example.com/prev?a=1',
},
});
const arg = saveEventMock.mock.calls[0][0] as Record<string, any>;
expect(arg).toMatchObject({
referrerPath: '/prev',
referrerQuery: 'a=1',
});
expect(arg.referrerDomain).toBeUndefined();
});
test('does not save referrer domain for a path-only referrer', async () => {
await callPOST({
type: 'event',
payload: {
website: WEBSITE_ID,
hostname: 'example.com',
url: '/products',
referrer: '/prev?a=1',
},
});
const arg = saveEventMock.mock.calls[0][0] as Record<string, any>;
expect(arg).toMatchObject({
referrerPath: '/prev',
referrerQuery: 'a=1',
});
expect(arg.referrerDomain).toBeUndefined();
});
test('saves referrer domain for a lookalike domain that only shares a prefix', async () => {
await callPOST({
type: 'event',
payload: {
website: WEBSITE_ID,
hostname: 'example.com',
url: '/products',
referrer: 'https://example.com.br/page',
},
});
expect(saveEventMock.mock.calls[0][0]).toMatchObject({
referrerDomain: 'example.com.br',
referrerPath: '/page',
});
});
test('resolves a path-only referrer against the url domain when hostname is missing', async () => {
await callPOST({
type: 'event',
payload: {
website: WEBSITE_ID,
url: 'https://example.com/products',
referrer: '/prev',
},
});
const arg = saveEventMock.mock.calls[0][0] as Record<string, any>;
expect(arg).toMatchObject({ referrerPath: '/prev' });
expect(arg.referrerDomain).toBeUndefined();
});
test('does not save referrer domain when hostname differs only by case', async () => {
await callPOST({
type: 'event',
payload: {
website: WEBSITE_ID,
hostname: 'EXAMPLE.com',
url: '/products',
referrer: 'https://example.com/prev',
},
});
const arg = saveEventMock.mock.calls[0][0] as Record<string, any>;
expect(arg).toMatchObject({ referrerPath: '/prev' });
expect(arg.referrerDomain).toBeUndefined();
});
test('does not save referrer domain when hostname includes a port', async () => {
await callPOST({
type: 'event',
payload: {
website: WEBSITE_ID,
hostname: 'example.com:8443',
url: '/products',
referrer: 'https://example.com:8443/prev',
},
});
const arg = saveEventMock.mock.calls[0][0] as Record<string, any>;
expect(arg).toMatchObject({ referrerPath: '/prev' });
expect(arg.referrerDomain).toBeUndefined();
});
test('REMOVE_TRAILING_SLASH strips a trailing slash from the url path', async () => {
process.env.REMOVE_TRAILING_SLASH = '1';
+18 -2
View File
@@ -192,7 +192,7 @@ export async function POST(request: Request) {
let urlPath =
currentUrl.pathname === '/undefined' ? '' : currentUrl.pathname + currentUrl.hash;
const urlQuery = currentUrl.search.substring(1);
const urlDomain = currentUrl.hostname.replace(/^www./, '');
const urlDomain = currentUrl.hostname.replace(/^www\./, '');
let referrerPath: string;
let referrerQuery: string;
@@ -218,11 +218,27 @@ export async function POST(request: Request) {
}
if (referrer) {
const referrerUrl = new URL(referrer, base);
// Canonicalize the event domain (lowercase, punycode, no port) so it
// compares correctly against the parsed referrer hostname
let eventDomain = urlDomain;
if (hostname) {
try {
eventDomain = new URL(`https://${hostname}`).hostname.replace(/^www\./, '');
} catch {
eventDomain = hostname.replace(/^www\./, '');
}
}
// Resolve path-only referrers against the event's domain, not the localhost fallback
const referrerUrl = new URL(referrer, eventDomain ? `https://${eventDomain}` : base);
referrerPath = referrerUrl.pathname;
referrerQuery = referrerUrl.search.substring(1);
referrerDomain = referrerUrl.hostname.replace(/^www\./, '');
// Never save the referrer domain for self-referrals
if (referrerDomain === eventDomain) {
referrerDomain = undefined;
}
}
const eventType = linkId
+4 -2
View File
@@ -19,8 +19,10 @@ export type TrackedProperties = {
/**
* Page referrer
*
* @description extracted from `document.referrer`
* @example 'https://analytics.umami.is/docs/getting-started'
* @description extracted from `document.referrer` on page load, or the previous
* in-app URL on client-side navigation. Same-origin referrers are sent as
* root-relative paths so the site's own domain is never saved as a referrer.
* @example 'https://analytics.umami.is/docs/getting-started', '/docs/getting-started'
*/
referrer?: string;
/**
+7 -2
View File
@@ -274,6 +274,11 @@ type MetricEntry = PerformanceEntry & {
}
};
// Strip the origin from same-origin referrers so the referrer domain
// is never saved when it matches the current hostname
const stripOrigin = (url: string): string =>
url === origin || url?.startsWith(origin + '/') ? url.slice(origin.length) : url;
const getPayload = () => ({
website,
screen,
@@ -281,7 +286,7 @@ type MetricEntry = PerformanceEntry & {
title: document.title,
hostname,
url: currentUrl,
referrer: currentRef,
referrer: stripOrigin(currentRef),
tag,
id: identity ? identity : undefined,
});
@@ -631,7 +636,7 @@ type MetricEntry = PerformanceEntry & {
}
let currentUrl = normalize(href);
let currentRef = normalize(referrer.startsWith(origin) ? '' : referrer);
let currentRef = normalize(referrer);
let initialized = false;
let disabled = false;