Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 47 additions & 1 deletion packages/tracker/integration/04_utmTracking/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
});
233 changes: 232 additions & 1 deletion packages/tracker/src/lib/__tests__/track.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ describe("trackPageview", () => {
expect.objectContaining({
p: "/test-path",
h: "http://localhost",
r: "",
r: "google",
sid: "test-site",
ht: "1",
us: "google",
Expand Down Expand Up @@ -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
}),
);
});
});
});
33 changes: 26 additions & 7 deletions packages/tracker/src/lib/track.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Loading