diff --git a/packages/tracker/integration/04_utmTracking/index.spec.ts b/packages/tracker/integration/04_utmTracking/index.spec.ts index e430495c..d40db5af 100644 --- a/packages/tracker/integration/04_utmTracking/index.spec.ts +++ b/packages/tracker/integration/04_utmTracking/index.spec.ts @@ -21,7 +21,7 @@ test("tracks UTM parameters when present in URL", async ({ page }) => { expect(params.get("sid")).toBe("utm-test-site-id"); expect(params.get("h")).toBe("http://localhost"); expect(params.get("p")).toBe("/04_utmTracking/"); - expect(params.get("r")).toBe(""); + expect(params.get("r")).toBe("google"); // utm_source is now used as referrer expect(params.get("us")).toBe("google"); // utm_source expect(params.get("um")).toBe("cpc"); // utm_medium @@ -100,3 +100,49 @@ test("handles mixed UTM and non-UTM parameters", async ({ page }) => { expect(params.has("ut")).toBe(false); expect(params.has("uco")).toBe(false); }); + +test("tracks referrer from query parameters when document.referrer is missing", async ({ + page, +}) => { + const collectRequestPromise = page.waitForRequest((request) => + request.url().includes("/collect"), + ); + + // Navigate with referrer query parameter + await page.goto( + "http://localhost:3004/04_utmTracking/?ref=external-site.com", + ); + + const request = await collectRequestPromise; + expect(request).toBeTruthy(); + + const url = request.url(); + const params = new URLSearchParams(url.split("?")[1]); + + expect(params.get("sid")).toBe("utm-test-site-id"); + expect(params.get("h")).toBe("http://localhost"); + expect(params.get("p")).toBe("/04_utmTracking/"); + expect(params.get("r")).toBe("external-site.com"); // ref parameter used as referrer +}); + +test("prioritizes referrer query parameters in correct order", async ({ + page, +}) => { + const collectRequestPromise = page.waitForRequest((request) => + request.url().includes("/collect"), + ); + + // Navigate with multiple referrer parameters - should use 'ref' (first in priority) + await page.goto( + "http://localhost:3004/04_utmTracking/?utm_source=second.com&ref=first.com&source=third.com", + ); + + const request = await collectRequestPromise; + expect(request).toBeTruthy(); + + const url = request.url(); + const params = new URLSearchParams(url.split("?")[1]); + + expect(params.get("r")).toBe("first.com"); // Should use 'ref' since it comes first in priority + expect(params.get("us")).toBe("second.com"); // utm_source still tracked as UTM parameter +}); diff --git a/packages/tracker/src/lib/__tests__/track.spec.ts b/packages/tracker/src/lib/__tests__/track.spec.ts index 77020861..75078925 100644 --- a/packages/tracker/src/lib/__tests__/track.spec.ts +++ b/packages/tracker/src/lib/__tests__/track.spec.ts @@ -279,7 +279,7 @@ describe("trackPageview", () => { expect.objectContaining({ p: "/test-path", h: "http://localhost", - r: "", + r: "google", sid: "test-site", ht: "1", us: "google", @@ -425,4 +425,235 @@ describe("trackPageview", () => { expect(callArgs).not.toHaveProperty("uco"); }); }); + + describe("referrer query parameter tracking", () => { + test("should use ref parameter when document.referrer is missing", async () => { + // Mock location with ref parameter + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?ref=external-site.com", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "external-site.com", + }), + ); + }); + + test("should use referer parameter when document.referrer is missing", async () => { + // Mock location with referer parameter + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?referer=external-site.com", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "external-site.com", + }), + ); + }); + + test("should use referrer parameter when document.referrer is missing", async () => { + // Mock location with referrer parameter + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?referrer=external-site.com", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "external-site.com", + }), + ); + }); + + test("should use source parameter when document.referrer is missing", async () => { + // Mock location with source parameter + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?source=external-site.com", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "external-site.com", + }), + ); + }); + + test("should use utm_source parameter when document.referrer is missing", async () => { + // Mock location with utm_source parameter + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?utm_source=external-site.com", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "external-site.com", + }), + ); + }); + + test("should prioritize first matching referrer parameter in order", async () => { + // Mock location with multiple referrer parameters + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?utm_source=second.com&ref=first.com", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "first.com", // Should use 'ref' since it comes first in the priority list + }), + ); + }); + + test("should prefer document.referrer over query parameters", async () => { + // Mock document.referrer and location with ref parameter + Object.defineProperty(document, "referrer", { + writable: true, + value: "https://document-referrer.com", + }); + + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?ref=query-referrer.com", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "https://document-referrer.com", // Should use document.referrer + }), + ); + }); + + test("should handle empty referrer query parameters", async () => { + // Mock location with empty referrer parameters + Object.defineProperty(window, "location", { + writable: true, + value: { + pathname: "/test-path", + search: "?ref=&referer=&referrer=&source=&utm_source=", + host: "example.com", + }, + }); + + const client = new Client({ + siteId: "test-site", + reporterUrl: "https://example.com/collect", + autoTrackPageviews: false, + }); + + await trackPageview(client); + + expect(makeRequestMock).toHaveBeenCalledTimes(1); + expect(makeRequestMock).toHaveBeenCalledWith( + "https://example.com/collect", + expect.objectContaining({ + r: "", // Should be empty when all parameters are empty + }), + ); + }); + }); }); diff --git a/packages/tracker/src/lib/track.ts b/packages/tracker/src/lib/track.ts index 47280c78..e9e7f5dc 100644 --- a/packages/tracker/src/lib/track.ts +++ b/packages/tracker/src/lib/track.ts @@ -38,15 +38,34 @@ function getCanonicalUrl() { } function getBrowserReferrer(hostname: string, referrer: string): string { - if ( - !referrer && - document.referrer && - document.referrer.indexOf(hostname) < 0 - ) { - referrer = document.referrer; + // First, check if we have an explicit referrer parameter + if (referrer) { + return getReferrer(hostname, referrer); + } + + // If no explicit referrer, check document.referrer + if (document.referrer && document.referrer.indexOf(hostname) < 0) { + return getReferrer(hostname, document.referrer); + } + + // If still no referrer, check query parameters + const urlParams = new URLSearchParams(window.location.search); + const referrerParams = [ + "ref", + "referer", + "referrer", + "source", + "utm_source", + ]; + + for (const param of referrerParams) { + const value = urlParams.get(param); + if (value) { + return getReferrer(hostname, value); + } } - return getReferrer(hostname, referrer || ""); + return getReferrer(hostname, ""); } export async function trackPageview(