LionsHead Analytics GroupLionsHead Analytics Group
All Insights

Field Notes

355 links, 1 phone number, 0 attribution

Huey LouisAugust 21, 20267 min read

Ask a marketing team which channel paid for last month's sales and you will usually get a confident answer. Ask them to show you the row it came from and the confidence goes somewhere else.

A health agency I worked with this year could not answer it at all, and they were not being careless about it. They had Google Analytics 4 (GA4) firing on every page. They had a Meta Pixel. They were spending real money on paid search. The instrumentation was installed and the instrumentation was useless, because of a detail nobody had thought of as a measurement decision.

They had 1 phone number.

The collapse

177 pages. 355 links carrying a tel: href. All of them the same number.

Every advertisement, every landing page, every organic article, every direct visit funnelled into that 1 number. So the chain ran fine right up until the moment it mattered:

Visitor
Session
Call click
Call
Lead
Policy

Everything to the left of Call is the website, which they could see. Everything to the right is a phone system and a dialer, which they could not. The number is where those 2 halves are supposed to join, and a single shared number is the one arrangement that guarantees they cannot.

The source was not lost in the database. It was destroyed at the moment somebody picked up the phone, which is earlier than any analytics tool can reach.

That is worth being precise about, because it changes what the job is. This was not an analytics gap. GA4 was working exactly as designed. It was a measurement design fault, sitting in the markup of 355 links, and no amount of extra tracking code installed on top would have touched it.

The wrong turn, first

The engagement brief said the client was on a drag and drop website builder and wanted to migrate to a Node hosting product so they could add forms and tracking.

I scraped the live site before writing anything, because a brief describes what somebody believes and a response header describes what is true. What came back was a server rendered React application on a modern host, with a headless content system behind it and a project id sitting in every page payload. The proposed migration solved a problem that did not exist and would have moved a working deployment onto a beta product for no reason.

I killed that line item on day 1. It was the cheapest thing I did all engagement and probably the most valuable.

The real problem underneath was not technical. The client held an editor login. Somebody else held the repository, the deployment, and the content dataset. They could reword a heading. They could not add a route, add a table, read a log, or export their own content in any structured form. The people who could wire the analytics to the business were not the people paying for it.

A rebuild here is forced by an access problem, not an engineering one. I want that on the record because it is the honest reason, and because it decided the shape of everything that followed. Every design decision answers the same question. What happens the next time we lose access to something.

The part that actually matters

Here is the thing I keep coming back to on instrumentation work.

The tracking code is cheap. The beacon that stamps a cookie and posts an event is a few hundred lines, and you can write it any afternoon. What is expensive is everything the beacon refers to, because those are the decisions that cannot be reversed once traffic starts flowing through them.

DecisionCost to change after launch
The beacon that collects eventsAn afternoon
A report queryAn afternoon
A dashboard layoutAn afternoon
What a day means in a reportEvery historical comparison
Which touch earns the saleEvery historical comparison
The name of a cookieThe entire existing audience

So the schema, the cookie names, and the module paths were fixed first, and the collection layer was left deliberately unwritten. That ordering felt backwards to describe at the time. It is the right way round.

6 decisions, made before anything was collected

The cookie name is permanent, so it was chosen first. lh_vid for the browser at 2 years, lh_sid for the visit at 30 minutes sliding. A cookie name is genuinely the one thing you cannot change later. Rename it and every returning visitor becomes a brand new one, which silently resets attribution for the whole existing audience. Nothing errors. No alert fires. The numbers simply start over and look like growth.

SameSite is lax, not strict. Strict sounds like the safer answer and it drops the cookie on inbound advertisement clicks, which is exactly the traffic being paid for and measured. The safer sounding option would have blinded the system to the only visitors it existed to track.

First touch and last touch are both stored. First touch lives on the visitor, last touch lives on the session. Which one earns the sale is a business judgment, not a query detail, and I did not want it buried inside a report where somebody could quietly change it. Store both, report both, and let the argument happen out loud where people can see the 2 numbers side by side.

A day is pinned to a business timezone. Storage is coordinated universal time (UTC) regardless. The pin decides what a date means when one gets derived from a timestamp. Without it a report grouped by day splits the working day across 2 UTC dates, so the Monday in the report is not the Monday anyone worked, and every day over day comparison is quietly wrong by a few hours of traffic.

The funnel counts people, not paperwork. A lead can hold more than 1 policy over time, a rejected application followed by a good one. Counting policies makes a single persistent customer look like 2 conversions. The query takes the earliest surviving policy per lead and nothing else. It also excludes any policy the dialer has stopped returning, because one that vanished from the source system is one that was deleted, and counting it reports business that no longer exists.

The report counts what it could not attribute. This is the one I would push hardest on anywhere else.

SELECT count(*)                                 AS total,
       count(*) FILTER (WHERE lead_id IS NULL)  AS unattributed
  FROM calls WHERE started_at >= ?

Every attribution report is a story about the traffic it managed to trace. The traffic it lost does not appear anywhere, so the report always looks complete. Putting the unattributed count next to the attributed one on the same screen means the blind spot has a size, and a blind spot with a size is something somebody eventually fixes. Without that column the number just quietly shrinks and everybody congratulates the channel that happened to survive.

What was left unbuilt on purpose

The schema is live. The reporting queries are live. 13 admin pages are live. The client side collection is not.

/api/track, /api/call/click and the attribution modules exist as reserved paths with the names fixed and nothing inside them. Every phone link on the site already renders with a data-lh-call-location attribute and an empty click handler waiting for the beacon.

That let the entire reporting interface be built, reviewed and argued about against real query shapes before a single event was collected, using a fabricated fixture set with a banner on every page saying so. By the time the beacon gets written, every question it has to answer has already been asked.

176
Pages archived before any code was written, plus 59 images
155
Education articles recovered into flat files the client controls
528
Pages publishing a relay number that reached nothing, 1 wrong digit

That last one was not an analytics finding. It came out of the same archive pass, and fixing 1 digit was the highest value change in the engagement.

What changed

Before, they had a site somebody else controlled, analytics that counted pageviews, and a phone number that erased the source of every call it received.

Now the chain is designed end to end and instrumented from the lead forward. The call leg is the piece still to come, and it is the one that turns a phone call from an untraceable event into the measured conversion the schema is already shaped to hold.

None of that was the hard part. The hard part was working out which decisions had to be right on day 1, and being willing to leave the obvious visible work unwritten until they were.

The build itself, the layers, the migrations, the sync guards and the compliance mechanisms, is on the work page.

Keep reading

Engage

If this is the conversation, let's have it properly.

Most of these notes started inside an actual engagement. If you recognize your firm in one, the fastest move is 1 proven system pointed at your operation, live in 2 weeks.