From 9ce1d7062bc096840f246d03ebc437fa6469efc8 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Wed, 28 Oct 2026 14:12:46 -0400 Subject: [PATCH 1/2] analytics: omit an empty referrer instead of sending "" getTrafficSource() returned referrer: "" when there is no referring hostname (direct traffic, no-referrer policies, in-app browsers). Both analytics add-ons spread it into the witness properties, and the edge rejects an empty property value (MinPropertyVLen = 1) with a 400, so the whole optable.prebid.auction event is dropped. A cached direct is reused across internal navigation, so every auction of a direct session is lost. Seen in production after the ascendeum bundle moved to 0.65.0: ~16% of its witness calls returned 400, all from osdk=web-v0.65.0. referrer is now left out when empty, including a "" cached in sessionStorage by a version that sent it. trafficSource is unchanged. --- lib/addons/prebid/analytics.md | 3 ++- lib/core/traffic-source.test.ts | 21 ++++++++++++++++----- lib/core/traffic-source.ts | 12 ++++++++++-- 3 files changed, 28 insertions(+), 8 deletions(-) diff --git a/lib/addons/prebid/analytics.md b/lib/addons/prebid/analytics.md index b470db07..5961aca3 100644 --- a/lib/addons/prebid/analytics.md +++ b/lib/addons/prebid/analytics.md @@ -154,7 +154,8 @@ 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 (the edge rejects an empty property value 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..49fc4ebf 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: the edge rejects an empty + * witness property value with a 400 and drops the whole event. */ 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); From 68538321f4abf1d83a41b0d8c168177401508416 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Laroche Date: Wed, 28 Oct 2026 14:20:06 -0400 Subject: [PATCH 2/2] analytics: reword empty referrer comments --- lib/addons/prebid/analytics.md | 3 ++- lib/core/traffic-source.ts | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/addons/prebid/analytics.md b/lib/addons/prebid/analytics.md index 5961aca3..2d9d0260 100644 --- a/lib/addons/prebid/analytics.md +++ b/lib/addons/prebid/analytics.md @@ -155,7 +155,8 @@ are captured when the auction ends, since the payload is only built after | `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, and is -omitted when there is none (the edge rejects an empty property value with a 400). +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.ts b/lib/core/traffic-source.ts index 49fc4ebf..70e9acd5 100644 --- a/lib/core/traffic-source.ts +++ b/lib/core/traffic-source.ts @@ -86,8 +86,8 @@ function classify(host: string): TrafficSource { * * Only the referring hostname is reported, never the full referring URL, which * can carry query strings with personal data. With no referring hostname, - * `referrer` is omitted rather than sent empty: the edge rejects an empty - * witness property value with a 400 and drops the whole event. + * `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();