Skip to content
TraceLead

Installing the snippet

The script tag, hidden fields, consent call, page API and failure behaviour.

Overview: First-touch attribution, Google Ads lead tracking

1. Add the tag

Place this before </body> (or in <head> with async) on every page of a registered domain:

<script async src="https://app.tracelead.io/snippet/v1/tracelead.js" data-site="tl_YOUR_SITE_ID"></script>
  • data-site — the public site ID from the dashboard. It is an identifier, not a secret.
  • data-api (optional) — API origin; defaults to the origin the script was loaded from.
  • data-debug="1" (optional) — logs diagnostics snapshots to the console. Never logs values or URLs.
  • Use a classic <script> tag (not type="module"): the snippet reads its options from the executing script element.

2. Add hidden fields to your form

The snippet only fills inputs that already exist and match a configured selector (default input[name="tl_<field>"]). Add the ones you want to receive:

<input type="hidden" name="tl_utm_source">
<input type="hidden" name="tl_utm_medium">
<input type="hidden" name="tl_utm_campaign">
<input type="hidden" name="tl_utm_term">
<input type="hidden" name="tl_utm_content">
<input type="hidden" name="tl_gclid">
<input type="hidden" name="tl_msclkid">
<input type="hidden" name="tl_channel">
<input type="hidden" name="tl_source">
<input type="hidden" name="tl_medium">
<input type="hidden" name="tl_referrer_host">
<input type="hidden" name="tl_landing_page">
<input type="hidden" name="tl_first_touch_at">

Fields: utm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_id, the click ids gclid, gbraid, wbraid (Google Ads), msclkid (Microsoft Ads), ttclid (TikTok Ads), li_fat_id (LinkedIn Ads), twclid (X Ads) and fbclid (Meta), plus referrer_host, landing_page, channel, source, medium and first_touch_at. Only hidden, text and search inputs are filled; forms mounted later (SPAs, embeds) are filled when they appear. Sites created before snippet 1.2.0 need a mapping added in the dashboard for ttclid, li_fat_id and twclid (Site → Attribution settings → Add mapping).

Optional: the last touch

First touch is the product: a visitor's first campaign visit is what the fields above carry, and nothing later overwrites it. If you also want the most recent campaign visit, turn on Also keep the last touch in the site's attribution settings. The snippet then keeps a second record (tl_lt_<site id>) and fills the last_* counterparts of every field above (last_channel, last_source, last_medium, last_utm_*, last_gclid and the other click ids, last_referrer_host, last_landing_page, last_touch_at):

<input type="hidden" name="tl_last_channel">
<input type="hidden" name="tl_last_utm_campaign">
<input type="hidden" name="tl_last_gclid">
<input type="hidden" name="tl_last_touch_at">

Rules: any visit that arrives with a UTM, click id or external referrer becomes the last touch; a direct visit (typed URL, bookmark, internal link) never replaces it; until a later campaign visit happens the last touch equals the first touch. Turning the setting on adds the last_* mappings with default selectors; remove the ones you do not need. The dashboard and the lead beacons stay first-touch only (last_* keys are ignored server-side).

Optional: visits before a lead

Turn on Count visits before a lead in the site's attribution settings to fill two more fields: sessions_before_lead (visits so far, a visit being separated from the next by 30 idle minutes) and days_to_lead (whole days since the first touch). After consent the snippet keeps a small record beside the first touch (tl_ec_<site id>) holding only the count and the time of the last visit; it expires with the first touch and is deleted on withdrawal. The two numbers go with lead events to your dashboard, which shows the median per channel.

<input type="hidden" name="tl_sessions_before_lead">
<input type="hidden" name="tl_days_to_lead">

Optional: Google Analytics client id

Map the ga_client_id field (for example <input type="hidden" name="tl_ga_client_id"> plus a mapping row, or data-tracelead="ga_client_id") to copy your own GA4 client id from the _ga cookie into the form, so your CRM can join leads with GA4. The snippet reads _ga only when this field is mapped, only after consent, and never sends the value to TraceLead.

3. Signal consent

In the default explicit consent mode nothing is stored until your page says so. Call this from your consent manager once analytics/marketing storage is permitted, and again on later pages (the snippet re-reads consent on every page):

<script>
window.tracelead = window.tracelead || function () { (window.tracelead.q = window.tracelead.q || []).push(arguments); };
window.tracelead('consent', true);   // or false to withdraw and delete the stored touch
</script>

The stub above may be placed before the snippet tag; queued calls are replayed. Sites configured with assumed consent store immediately (the deployer is responsible for the legal basis).

Page API

Call Effect
tracelead('consent', true|false) Grant or withdraw storage consent. Withdrawal deletes the stored first touch and clears filled fields.
tracelead('ready', cb) cb(state) once the config has been fetched (or failed).
tracelead('touch', cb) cb(fields | null, state) now and whenever the touch changes. fields is the flattened map above.
tracelead('lead', formId) Reports a lead for a form the snippet cannot see, such as a form inside an iframe from another site (see HubSpot forms). Sent only after consent and when the site collects leads, with the form id (trimmed to 100 characters) and the stored attribution; repeated reports for the same id within 2 seconds count once. Do not call it for forms on your own page: the snippet already counts those.
tracelead('contact', kind) Reports a contact click for a click-to-call button that is not a link; kind is phone, email, sms or whatsapp. Sent only after consent and when the site records contact clicks; each kind counts once per page. Links (tel:, mailto:, sms:, wa.me) are counted without it.
tracelead('diagnostics', cb) / tracelead.diagnostics() Operational snapshot: version, site id, enabled/reason, consent mode and state, config version, storage mechanism (cookie/local/none), per-mapping match counts, collect flags and beacon count. No values, URLs or PII.
tracelead.version Snippet version string.

Behaviour on failure

The snippet never throws into your page and never blocks a form. If the config cannot be fetched (network error, 4xx/5xx, timeout after 5 s, invalid JSON), the site is paused, the domain is not registered, or the browser lacks fetch, it stays inert and diagnostics().reason explains why (config_unavailable, site_paused, subscription_inactive, domain_not_allowed, not_found, missing_site_id, unsupported_browser, error).

What is sent to TraceLead

Only after consent, and only when the site's collection settings allow it (collect.sessions / collect.leads in the config), the snippet sends small beacons to POST <api>/api/collect (see docs/collect-api.md, ADR-021):

  • Pageview on load and on SPA navigation (pushState/replaceState/popstate): a random visitor id, a random session id (renewed after 30 minutes idle), the time, the pathname only and the first-touch attribution fields listed above.
  • Lead when a form containing one of the mapped inputs is submitted: the same, plus the form's id or name attribute. Field values are never sent, and the beacon never delays or blocks the submission (sendBeacon, with fetch(keepalive) as fallback).

The visitor id is stored beside the touch as tl_vid_<site id> (cookie or localStorage) and deleted when consent is withdrawn. Nothing is minted or sent before consent. Allow connect-src to the API origin in your CSP.

Storage

First touch is kept in a first-party cookie tl_ft_<site id> (and, after consent, the visitor id in tl_vid_<site id> and, only when the site keeps a last touch, tl_lt_<site id>) (SameSite=Lax, Secure on https, shared with subdomains when the browser allows, expiring after the configured lifetime, default 90 days) with localStorage as a fallback. See ADR-016.