From 0c0f8135672e6e9deabd8774c77236a4a4a4df68 Mon Sep 17 00:00:00 2001 From: Altay Date: Fri, 9 Oct 2026 15:53:07 +0300 Subject: [PATCH] fix: send only the referrer origin with page views A referrer's path and query can carry search terms, such as a search engine's ?q= or put.io's own /search?phrase=, so page_viewed now keeps only the referrer's origin. --- README.md | 4 ++++ src/client.spec.ts | 24 ++++++++++++++++++++++++ src/client.ts | 14 +++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 3313eec..0ab944b 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,10 @@ pas.alias({ id: "42", hash: "signed-user-hash" }); pas.pageView(); ``` +`pageView` sends the page's origin and path, its `utm_source`, `utm_medium`, +and `utm_campaign` values, and only the origin of the referrer. Nothing else +from either URL is sent, so search terms in them stay out of PAS. + Requests queued for retry are stored in a cookie and replayed on the next initialization. The queue keeps at most the 20 most recent requests within 3000 bytes of percent-encoded JSON: a request too large on its own is diff --git a/src/client.spec.ts b/src/client.spec.ts index 66d2a45..95308bd 100644 --- a/src/client.spec.ts +++ b/src/client.spec.ts @@ -159,6 +159,30 @@ describe("Client", () => { }); }); + it.each([ + ["https://www.google.com/search?q=secret+movie#top", "https://www.google.com"], + ["https://torrents.example/search/secret-movie/1/", "https://torrents.example"], + ["https://app.put.io/search?phrase=secret%20movie", "https://app.put.io"], + ["https://app.put.io/files?search=secret+movie", "https://app.put.io"], + [ + "android-app://com.google.android.googlequicksearchbox/https/www.google.com", + "android-app://com.google.android.googlequicksearchbox", + ], + ["", ""], + ])("sends only the referrer origin for pageView: %s", (referrer, expected) => { + vi.spyOn(document, "referrer", "get").mockReturnValueOnce(referrer); + client.pageView(); + + expect(mockAPI.post).toHaveBeenCalledWith("/events", { + events: [ + expect.objectContaining({ + properties: expect.objectContaining({ referrer: expected }), + }), + ], + }); + expect(JSON.stringify(vi.mocked(mockAPI.post).mock.calls)).not.toContain("secret"); + }); + it("clears cached user state", () => { client.alias({ id: 7, hash: "user_hash" }); client.clear(); diff --git a/src/client.ts b/src/client.ts index 170f7e4..5d584ba 100644 --- a/src/client.ts +++ b/src/client.ts @@ -13,6 +13,18 @@ const defaultConfig = { export type Config = typeof defaultConfig; +// A referrer's path, query, and fragment can carry search terms, so PAS only +// gets where the visit came from, never which page. `URL#origin` is "null" for +// app referrers such as android-app://, so build it from protocol and host. +const referrerOrigin = (referrer: string) => { + try { + const { host, protocol } = new URL(referrer); + return `${protocol}//${host}`; + } catch { + return ""; + } +}; + export const createClientFactoryWithDependencies = ( cacheFactory: typeof createCache, @@ -71,7 +83,7 @@ export const createClientFactoryWithDependencies = return track("page_viewed", { domain: origin, path: pathname, - referrer: document.referrer, + referrer: referrerOrigin(document.referrer), utm_source, utm_medium, utm_campaign,