diff --git a/lib/addons/prebid/analytics.md b/lib/addons/prebid/analytics.md index b470db07..2d9d0260 100644 --- a/lib/addons/prebid/analytics.md +++ b/lib/addons/prebid/analytics.md @@ -154,7 +154,9 @@ are captured when the auction ends, since the payload is only built after | `idsMs` | Set when the targeting cache was written during this page view: ms from page start to the write. | | `om` | Per bidder request, a bitmask over `optableSources` (bit i = `optableSources[i]`). Omitted when the bidder received every source. | -`referrer` is the referring hostname only, never the full referring URL. +`referrer` is the referring hostname only, never the full referring URL, and is +omitted when there is none (a witness event with an empty property value is +rejected with a 400). `trafficSource` classifies it as one of `direct`, `internal`, `paid`, `campaign`, `email`, `organic_search`, `social` or `referral`. Both are first-touch per tab: the first non-internal result is cached in `sessionStorage` diff --git a/lib/core/traffic-source.test.ts b/lib/core/traffic-source.test.ts index 56cb5abe..28766a40 100644 --- a/lib/core/traffic-source.test.ts +++ b/lib/core/traffic-source.test.ts @@ -15,14 +15,19 @@ beforeEach(() => { }); describe("getTrafficSource - classification", () => { - it("reports direct when there is no referrer", () => { + it("reports direct without a referrer key when there is no referrer", () => { visit("https://www.site.com/"); - expect(getTrafficSource()).toEqual({ referrer: "", trafficSource: "direct" }); + expect(getTrafficSource()).toStrictEqual({ trafficSource: "direct" }); }); - it("reports direct when the referrer is not a valid URL", () => { + it("reports direct without a referrer key when the referrer is not a valid URL", () => { visit("https://www.site.com/", "not a url"); - expect(getTrafficSource()).toEqual({ referrer: "", trafficSource: "direct" }); + expect(getTrafficSource()).toStrictEqual({ trafficSource: "direct" }); + }); + + it("omits referrer for a paid visit without a referrer", () => { + visit("https://www.site.com/?gclid=abc"); + expect(getTrafficSource()).toStrictEqual({ trafficSource: "paid" }); }); it("reports internal for the same host", () => { @@ -109,7 +114,13 @@ describe("getTrafficSource - session caching", () => { getTrafficSource(); visit("https://www.site.com/b", "https://www.site.com/a"); - expect(getTrafficSource()).toEqual({ referrer: "", trafficSource: "direct" }); + expect(getTrafficSource()).toStrictEqual({ trafficSource: "direct" }); + }); + + it("omits an empty referrer cached by an earlier SDK version", () => { + sessionStorage.setItem(TRAFFIC_SOURCE_KEY, JSON.stringify({ referrer: "", trafficSource: "paid" })); + visit("https://www.site.com/b", "https://www.site.com/a"); + expect(getTrafficSource()).toStrictEqual({ trafficSource: "paid" }); }); it("replaces a cached direct with a later external source", () => { diff --git a/lib/core/traffic-source.ts b/lib/core/traffic-source.ts index 3fe26d6b..70e9acd5 100644 --- a/lib/core/traffic-source.ts +++ b/lib/core/traffic-source.ts @@ -27,7 +27,7 @@ const TWO_PART_SUFFIXES = ["co.uk", "com.au", "co.jp", "co.in", "com.br", "co.nz type TrafficSource = "direct" | "internal" | "paid" | "campaign" | "email" | "organic_search" | "social" | "referral"; type TrafficSourceProperties = { - referrer: string; + referrer?: string; trafficSource: TrafficSource; }; @@ -85,9 +85,17 @@ function classify(host: string): TrafficSource { * classification in the same tab replaces it. * * Only the referring hostname is reported, never the full referring URL, which - * can carry query strings with personal data. + * can carry query strings with personal data. With no referring hostname, + * `referrer` is omitted rather than sent empty: a witness event with an empty + * property value is rejected with a 400 and dropped whole. */ function getTrafficSource(): TrafficSourceProperties { + const properties = resolveTrafficSource(); + // Also covers a `referrer: ""` cached by an SDK version that sent it. + return properties.referrer ? properties : { trafficSource: properties.trafficSource }; +} + +function resolveTrafficSource(): TrafficSourceProperties { let cached: TrafficSourceProperties | null = null; try { const stored = window.sessionStorage?.getItem(TRAFFIC_SOURCE_KEY);