Attribution across multi-page journeys
The campaign tag arrives on page one. The lead converts on page five. Attribution is the art of not losing the thread in between.
Attribution data has a one-page lifespan
Every attribution scheme on the web works the same way at the start: the inbound link carries query parameters. A newsletter link carries utm_source=newsletter, a Google Ads click carries gclid, a Meta ad carries fbclid. Those parameters exist in exactly one place - the URL of the first page the visitor lands on.
Browsers do not carry query strings across navigations. The visitor clicks from your landing page to /pricing, then to /contact, and the parameters are gone from the address bar and from anything a form on /contact could naively read. Unless something deliberately preserved them, the eventual submission arrives with no memory of how the visitor found you - and your Leads sheet fills with rows that look organic but are not.
What a real journey looks like
The one-page journey - ad to landing page to form on that same page - is the case every hidden-field tutorial handles, and the case real traffic rarely follows. A considered purchase looks more like: ad -> campaign landing page -> pricing -> a guide or two -> leaves -> returns two days later via a bookmark -> contact form. Five or more navigations, possibly across sessions, before the form ever renders.
Attribution that only survives the first page systematically undercounts exactly the campaigns that drive considered, higher-intent visits, and overcounts impulse conversions. The bias is invisible in the sheet - missing attribution looks identical to organic traffic - which is why so many teams distrust their own campaign columns without knowing why.
The mechanics: capture once, persist, attach at submit
The fix has three parts, and the sl.js embed implements all of them from one script tag:
<script src="https://sheetlinkforms.com/sl.js" data-token="slf_yourtoken"></script>- Capture: on every page load, the script checks the URL for the five
utm_*parameters (source,medium,campaign,term,content) and the ad click IDsgclid,wbraid,gbraid,fbclid, andmsclkid. - Persist: anything found is stored in localStorage, so it survives every navigation on your site - and later sessions in the same browser, covering the visitor who returns days after the ad click.
- Attach: when any bound form submits, the stored values ride along with the submission and land as columns next to the visitor's answers.
No hidden inputs to author, no per-template snippet that a new page can forget. The deeper dive into each click ID - including why iOS traffic often carries wbraid/gbraid instead of gclid - is in capturing click IDs in forms.
The journey's other end: page and referrer metadata
Landing attribution answers "which campaign brought them". Two metadata fields the embed sends answer the complementary question, "where did they finally convert": the submitting page's URL and its referrer travel with each submission as _sl_page and _sl_referrer.
Together the columns tell a story per row: arrived from utm_campaign=spring-launch, converted on /pricing. Aggregate a month of rows and you learn things neither end tells you alone - which campaigns convert on the landing page versus after research, and which pages on your site actually produce submissions regardless of source. That second list is where your next form belongs.
Single-page apps: the journey without navigations
SPAs bend the problem in two ways. The good news: client-side routing often preserves the query string longer, and localStorage persistence does not care how you "navigate". The bad news: forms render after initial page load, so scripts that scan the DOM once never see them - the classic reason attribution snippets silently do nothing in React or Vue apps.
sl.js runs a MutationObserver, so forms mounted later - a modal opened on click, a route rendered after data loads - are bound the same as static markup, with the same capture-persist-attach behavior. There are also sheetlink:success and sheetlink:error events bubbling from the form, with details in event.detail, when your app wants to react in code. Specifics are in the docs.
Honest limits, and reading the columns well
Client-side attribution has edges no vendor can remove, and knowing them keeps your analysis honest:
- Cross-device journeys do not stitch. Ad clicked on a phone, form filled on a laptop - localStorage is per-browser, and no client-side method can connect the two.
- Empty attribution columns are data. They are your organic and dark-social segment, not an error. Watch the ratio over time rather than chasing 100% coverage.
- Consent applies. Click IDs are pseudonymous ad identifiers; if your banner gates marketing storage, load
sl.jsunder the same policy as your other marketing tags. - Last-touch wins within the browser. A visitor who arrives via two campaigns carries the most recent capture. For most lead-gen sheets that is the pragmatic choice.
The mechanics of the whole pipeline are on how it works, and the live demo shows rows landing with their columns. Free during beta - invites at the waitlist.
FAQ
Do I need hidden fields on every form for attribution?
No. sl.js stores captured parameters in localStorage and attaches them at submit time, so form markup stays clean and no template can forget a field. Existing hidden-field setups keep working; they are just no longer the mechanism.
What if the visitor converts days after the ad click?
Captured parameters persist in localStorage, so they survive not just page navigations but later sessions in the same browser. A visitor who first arrived through an ad and returns via a bookmark still carries the campaign and click ID when they finally submit.
Which parameters are captured, exactly?
The five UTM parameters - utm_source, utm_medium, utm_campaign, utm_term, utm_content - plus the ad click IDs gclid, wbraid, gbraid, fbclid, and msclkid. The submission also carries the converting page URL and referrer as metadata.
Does this work when the form is in a modal or rendered by React?
Yes. A MutationObserver binds forms that appear after initial load - late-mounted components, modals, client-side routes - with identical behavior. Your app can also listen for sheetlink:success and sheetlink:error events to update UI state.
Why do some rows have no attribution columns filled?
Because that visitor never arrived with tagged parameters in this browser: organic search, direct traffic, dark social, or a cross-device journey. Empty columns are the correct record of that. If paid traffic specifically shows gaps, confirm auto-tagging is on in the ad platform and that sl.js loads on your landing pages.
Related guides: Capture gclid (and every other click ID) in your forms · Redirects and thank-you pages after form submit · AJAX form submission, from scratch and with sl.js