On 23 August 2026 we looked at our own enquiry table. Nineteen rows. Every single one recorded bloodstone.co.uk as the referrer. Not one from Google, not one from LinkedIn, not one honestly marked as direct.
That is not a distribution. That is a bug. Two of them, as it turned out, sitting in the same request path and producing the same wrong answer for completely different reasons. We had been writing articles for months and our own system was telling us that the only thing driving enquiries was us.
Bug one: the fallback that undid the filter
The client-side collector was written carefully. On the first pageview of a visit it read document.referrer, checked the hostname, and discarded it if it matched our own domain. That is correct behaviour. Someone moving from an article to the contact page is not a referral, and if you log it as one you fill your table with internal noise.
Then the form POSTs to our API, and the server did this:
const referrer = attribution.referrer ?? req.headers.referer;
On a same-origin form POST, req.headers.referer is your own page. It has to be. The browser is telling you which page the request came from, and the request came from your contact page. So the fallback could only ever produce a self-referral. The filtering the client did so carefully was undone one line later.
There is a second flaw stacked on the first. ?? is the nullish coalescing operator: it only falls through on null and undefined. An empty string is neither. So if the collector had stripped a same-host referrer and sent "", the fallback never fired and the field was stored as an empty string. If the collector omitted the key entirely, the fallback fired and stored our own domain. Same intent, two different wrong answers, depending on a detail nobody had thought about.
The fix is not a cleverer fallback. It is deleting the fallback and being willing to store nothing:
const clean = (v) => (typeof v === 'string' && v.trim() !== '' ? v.trim() : null);
const referrer = clean(attribution.referrer); // no header fallback, ever
A null referrer that means "we genuinely do not know" is more useful than a self-referral that means "we broke it". Real direct traffic exists. People do type your domain. You want to be able to tell that apart from a logging error, and you can only do that if the error stops producing plausible-looking data.
Bug two: sessionStorage cannot survive the gap
The attribution object was held in sessionStorage. That store is scoped to a single tab. Close the tab and it is gone. Open a second tab and it starts empty.
Here is the sequence that breaks it. Someone searches on Monday, lands on an article, reads it, closes the tab. On Thursday they think about it again, type the domain or use a bookmark, go straight to the contact page and submit. Thursday's tab has an empty store. Nothing to send. The row is written as direct, with no landing page.
That gap between finding you and buying from you is precisely where organic search lives. Paid social and cold email tend to convert in the same session because the intent was manufactured on the spot. Organic almost never does for a considered purchase. So a sessionStorage-based store systematically launders organic into direct: the channel with the longest consideration window is the one guaranteed to lose its label.
The consequence is not a reporting inconvenience. It is that the content earning your leads appears to earn nothing, someone reasonable looks at the numbers and cuts it, and the pipeline degrades a quarter later for reasons nobody connects back.
This is not a Google Analytics setting
GA4 has its own version of the problem. Sessions time out after 30 minutes of inactivity by default, and Google's documented lookback windows are 30 days for acquisition key events and 90 days for everything else. Those are real constraints worth knowing.
But none of that is what went wrong here. GA4 could have been configured perfectly and our enquiry table would still have said nineteen self-referrals, because the enquiry table is written by our form, not by Google. The data model is in the form payload. When a founder asks whether the content programme is working, they look at the CRM, not at a traffic report. Fix the CRM record.
What we changed
| Field | Before | After |
|---|---|---|
| Store | sessionStorage, dies with the tab | localStorage, 90-day first-touch window |
| Referrer | Server fell back to the HTTP Referer header | Client value only, null when absent |
| Landing page | Not recorded | landing_page, captured on first pageview |
| Submitted page | Recorded as "the page" | Kept, but stored separately |
The landing page split matters more than it sounds. The page a form is submitted on is nearly always /contact. It carries no information. The page someone arrived on is the article that did the work, and it is the only field that tells you which piece of writing to write more of.
We also write first touch once and never overwrite it, keeping last touch in separate fields. If you overwrite, every returning visitor becomes direct and you are back where you started.
The honest caveat: 90 days is not 90 days
Safari's Intelligent Tracking Prevention caps script-writable storage, including localStorage, at seven days of browser use without interaction with the site. So a 90-day first-touch window is a 90-day window on Chrome and Firefox and roughly a seven-day one on Safari and anything on iOS. We did not fix that with a client-side change and we are not going to pretend otherwise. The more durable route is writing first touch server-side as an HttpOnly cookie, which we are working through.
Treat attribution as directional. It tells you which articles are pulling and which are not. It is not accounting.
Run this test on your own data
If your CRM says most leads are direct, the question is whether it is recording where they arrived or where they submitted. Three checks, ten minutes:
- Sort your enquiry records by referrer. If any value is your own domain, you have the fallback bug.
- Look for a landing page field. If it does not exist, or it says
/contacton most rows, you are logging the wrong page. - Open devtools on your own site, go to the Application tab, and find where the attribution object lives. If it is in sessionStorage, your organic is being recorded as direct and has been for as long as the form has existed.
If all three come back clean, your traffic really might be direct, and that is worth knowing too.
We rebuilt this on our own site before we would sell it to anyone. If your form and CRM are connected by plumbing nobody has audited since it was built, that is the sort of thing workflow automation work is actually made of, and it is baked into the sites we build and manage. You can see what we have shipped on our work page, or get in touch and we will look at your enquiry table with you.
Need the site itself? We build and manage websites from £125/month — website design london, website design manchester and website design birmingham.
Need help with this?
Bloodstone Projects helps businesses implement the strategies covered in this article. Talk to us about Website Build & Manage.
Get in touch