Repository navigation
analytics: report referrer and traffic source in both analytics versions - #348
MO-Thibault wants to merge 3 commits into
Conversation
Adds two fields to the auction payload built by lib/addons/prebid/analytics.ts and lib/addons/prototypes/analytics.js: - referrer: the referring hostname only, never the full referring URL, which can carry query strings with personal data. - trafficSource: direct, internal, paid, campaign, email, organic_search, social or referral, derived from the referrer plus the campaign and click-id parameters on the landing URL. Both are first-touch per tab. document.referrer becomes the site's own hostname after the first internal click, so the first non-internal result is cached in sessionStorage under optableTrafficSource and reused for the rest of the session. Without that, every auction after the first pageview would report internal. The logic lives in lib/core/traffic-source.ts for the typed path and is inlined in the prototype file, which is self-contained and imports no modules. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
| // self-contained and pulls in no modules. | ||
| const TRAFFIC_SOURCE_KEY = "optableTrafficSource"; | ||
| const SEARCH_SITES = ["google", "bing", "yahoo", "duckduckgo", "ecosia", "baidu", "yandex", "qwant", "startpage"]; | ||
| const SOCIAL_SITES = [ |
There was a problem hiding this comment.
why duplicated? Can't we use the logic in one place traffic-source.ts?
There was a problem hiding this comment.
Fixed in 9e729f0. It now imports getTrafficSource from lib/core/traffic-source. I had assumed this file was self-contained, but bundles import it from lib/dist through webpack and tsc emits core/traffic-source.js next to it, so the import resolves the same way as in the prebid analytics.
| @@ -0,0 +1,105 @@ | |||
| const TRAFFIC_SOURCE_KEY = "optableTrafficSource"; | |||
There was a problem hiding this comment.
Add test suite for this file
There was a problem hiding this comment.
Added lib/core/traffic-source.test.ts in 9e729f0: 19 tests covering every category, the internal-before-UTM order, subdomain matching, and the session caching rules. 100% line coverage on the file.
| const referrer = referrerHost(); | ||
| const properties: TrafficSourceProperties = { referrer, trafficSource: classify(referrer) }; | ||
|
|
||
| if (properties.trafficSource !== "internal") { |
There was a problem hiding this comment.
Should this blokc on direct too?
There was a problem hiding this comment.
Not blocked, but changed in 9e729f0. Blocking it would make every page after a direct landing report internal, since nothing would be cached. Instead direct is still cached, but a later non-internal source in the same tab replaces it (GA4 behaves this way too). Every other cached source stays first-touch.
| if (medium === "email") return "email"; | ||
| if (params.has("utm_source")) return "campaign"; | ||
| if (!host) return "direct"; | ||
| if (host === window.location.hostname) return "internal"; |
There was a problem hiding this comment.
Prefer this I think. Host exact match will lead to a lot of referal false positives
if (siteName(host) === siteName(window.location.hostname)) return "internal";
There was a problem hiding this comment.
Agreed, done in 9e729f0. news.site.com on www.site.com is now internal.
| if (medium === "email") return "email"; | ||
| if (params.has("utm_source")) return "campaign"; | ||
| if (!host) return "direct"; | ||
| if (host === window.location.hostname) return "internal"; |
There was a problem hiding this comment.
Flagged by claude I think relevant. We do utm check before internal so internal referal will get marked as campaign. Should move internal check to top
There was a problem hiding this comment.
Good catch, moved to the top in 9e729f0. An internal link with UTM params now reports internal.
- prototypes/analytics.js imports getTrafficSource from lib/core instead of carrying an inlined copy. Consumers import it from lib/dist through a bundler, where core/traffic-source.js is emitted alongside it. - Internal is checked first, so UTM-tagged links between the site's own pages are no longer reported as campaign. - Internal matches on the registrable name, so another subdomain of the same site is internal rather than referral. - A cached direct is replaced by a later non-internal source in the same tab. - Add traffic-source.test.ts. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Adds two fields to the auction payload built by both analytics implementations, so we can tell where a session's traffic came from alongside the bid data we already collect.
referrer— the referring hostname only.trafficSource— one ofdirect,internal,paid,campaign,email,organic_search,social,referral.Touched in both places:
lib/core/traffic-source.tsgetTrafficSource()returns{ referrer, trafficSource }lib/addons/prebid/analytics.tswitnessDatalib/addons/prototypes/analytics.jslib/addons/prebid/analytics.mdlib/core/traffic-source.test.tsMotivation and design notes
Four decisions worth a reviewer's attention:
First-touch per tab.
document.referrerbecomes the site's own hostname after the first internal click. Classifying on every event would reportinternalfor every auction past the first pageview, which is useless. The first non-internal result is cached insessionStorageunderoptableTrafficSourceand reused for the rest of the session. A cacheddirectis the exception: it is the weakest signal (in-app browsers andno-referrerpolicies land there too), so a later non-internal result in the same tab replaces it.Internal is checked first, on the registrable name. A referrer is
internalwhen its registrable name matches the page's, sonews.site.comarriving onwww.site.comis internal rather thanreferral. The check runs before the UTM and click-id checks, so UTM-tagged links between the site's own pages are not reported ascampaign.Hostname, not full URL. Only the referring hostname is reported. Full referring URLs can carry query strings with personal data. This also costs nothing in practice, since browsers default to
strict-origin-when-cross-originand usually only give us the origin anyway.Named
trafficSource, notsource. The same payload already carriesoptableSources, meaning Optable data sources. Two fields calledsourceandoptableSourcesin one payload would be read as related when they are not.Both analytics files share the one helper. Bundles import
lib/dist/addons/prototypes/analyticsthrough webpack, and tsc emitslib/dist/core/traffic-source.jsalongside it, so the import resolves the same way it does for the prebid analytics.Known limits
siteName()approximates the registrable domain with a short list of two-part suffixes rather than the public suffix list. Good enough to separategoogle.co.ukfromco, not exact.Referrer-Policy: no-referrerproduce an empty referrer, so some real social traffic lands indirectunless the link carries a click id. Expected, not a bug, but it matters when reading the numbers.site.comandsite.orgcount as the same site. Acceptable for a same-brand property, but worth knowing.Test plan
pnpm build-lib(tsc) clean.pnpm exec jest: 488 tests across 29 suites pass, including the 82 existing prebid analytics tests and the 19 newtraffic-sourcetests (100% line coverage ontraffic-source.ts).pnpm exec prettier --checkclean on all changed files.🤖 Generated with Claude Code