Avoid saving same-domain referrers and resolve path-only referrers correctly
This commit is contained in:
1 parent
c5d34d10ad
commit
8efd5d7279
4 files changed
+131
-6
No files matched your search
@@ -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';
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
Vendored
+4
-2
@@ -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;
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in new issue
Block a user