GAP School Module 03 — Lead Engine Lesson 3.2

Attribution is the answer to "which of my ad campaigns is working?" Without it, you're spending money on the assumption that something is generating leads — but you can't prove which thing, and you can't stop spending on the things that aren't working.


The situation

Before Era 2, the Anchor build captured name, email, phone, and message. No UTM parameters. No referrer. No landing page. When the owner asked "is our Facebook ad campaign generating leads?" the honest answer was "we don't know." Every lead record was identical from a source perspective — a name and a phone number with no context about how they got there.

The ad budget was significant. Without attribution, there was no way to evaluate it. Pausing the Facebook campaign might cut leads in half or do nothing — there was genuinely no way to know.


What I did

UTM capture in sessionStorage on page load

UTM parameters appear in the URL when a visitor arrives from a tagged source: ?utm_source=facebook&utm_medium=cpc&utm_campaign=spring-sale. The problem: the visitor may browse through several pages before submitting a form. By the time they submit, the UTM parameters are gone from the URL. If you read UTM from the URL at form submission time, you get nothing.

The fix: capture UTM parameters into sessionStorage on the first page load, before the visitor navigates anywhere. sessionStorage persists across page navigation within the same browser tab until the tab is closed.

UTM capture — runs on every page load, writes on first entry
const utmKeys = [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content' ]; const params = new URLSearchParams(window.location.search); utmKeys.forEach(key => { // Only write if a value exists in the URL (don't overwrite with blank) if (params.has(key) && params.get(key)) { sessionStorage.setItem(key, params.get(key)); } });

Landing page capture at session entry

The landing page — the first URL the visitor saw — is different from the page where they submitted the form. If they arrived on a specific listing and submitted from the contact page, you want to know about the listing, not just the contact page.

Landing page — captured once at session start
// Only set if not already set — captures first URL of session, not current page if (!sessionStorage.getItem('landing_page')) { sessionStorage.setItem( 'landing_page', window.location.pathname + window.location.search ); }

Referrer capture

The HTTP referrer tells you which external site sent the visitor. document.referrer is only available on the first page load — subsequent page navigations have the same-origin referrer. Capture it alongside UTM at session entry.

Referrer — captured once, only if external
if (!sessionStorage.getItem('referrer') && document.referrer) { const referrerHost = new URL(document.referrer).hostname; if (referrerHost !== window.location.hostname) { sessionStorage.setItem('referrer', document.referrer); } }

Reading attribution at form submission

At form submission, the hidden fields are populated from sessionStorage before the form data is sent. This is done in JavaScript, not server-side — the server receives the values as standard POST fields.

Populate hidden attribution fields on submit
function populateAttributionFields(form) { const attributionFields = [ 'utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'landing_page', 'referrer' ]; attributionFields.forEach(field => { const input = form.querySelector(`input[name="${field}"]`); if (input) { input.value = sessionStorage.getItem(field) || ''; } }); }

PHP sanitization on the server side

Attribution data — read from POST and sanitize
function [client]_get_attribution_data( array $post_data ): array { return [ 'utm_source' => sanitize_text_field( $post_data['utm_source'] ?? '' ), 'utm_medium' => sanitize_text_field( $post_data['utm_medium'] ?? '' ), 'utm_campaign' => sanitize_text_field( $post_data['utm_campaign'] ?? '' ), 'utm_term' => sanitize_text_field( $post_data['utm_term'] ?? '' ), 'utm_content' => sanitize_text_field( $post_data['utm_content'] ?? '' ), 'referrer' => esc_url_raw( $post_data['referrer'] ?? '' ), 'landing_page' => esc_url_raw( $post_data['landing_page'] ?? '' ), ]; }

Why it matters

Attribution makes ad spend defensible. When the owner asks "is our Facebook campaign working?" the answer becomes "Facebook-sourced leads are 18% of total volume and account for 31% of closed deals in the last 90 days." That's a different conversation than "we're spending $1,200/month on Facebook and we think it's helping."

It also surfaces problems. If Google Ads is generating 40% of traffic but only 8% of leads, either the landing pages are wrong, the ad targeting is wrong, or the traffic is low-intent. Attribution makes that visible. Without it, the ad account keeps spending and nobody knows why it's underperforming.


The Anchor build

Attribution was added in Era 2 alongside the multi-touch capture surfaces. Within three months, the data showed that organic search was generating the highest-intent leads (unit inquiries converting to sales at 3×the rate of paid social), and that a specific Google Ads campaign was generating volume but almost zero conversions. The campaign was restructured around high-intent keywords. The lead quality from paid search improved measurably.

None of that analysis was possible before the attribution data was being captured.


Do this, not that

  • Capture UTM in sessionStorage, not a JavaScript variable. A JS variable is lost when the visitor navigates to a new page. sessionStorage persists across page navigation within the same tab — which is exactly the scope you need.
  • Capture landing page at session entry, not at form submission. By the time someone submits a form, they may be on the contact page. You want the first page they saw, not the last.
  • Capture referrer only once, only if external. Subsequent page navigations have the same-origin referrer. Write it once at session entry, check that the host is different from your own domain before storing.
  • Sanitize all attribution fields server-side with sanitize_text_field() or esc_url_raw(). These values come from the URL — anyone can put anything in a UTM parameter. Sanitize before storing.
  • Include hidden attribution fields on every form, even general contact. "I don't run ads on this form" is a present-state claim. Attribution data costs nothing to capture and is extremely expensive to backfill later.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.

None of this gates a single lesson. The curriculum was free before you got here and it stays that way.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.

See GAP Concierge →