Copy as markdown[View .md](https://docs.funnelfizz.com/getting-started/install-tracking "View the raw markdown for this page")[Open in Claude](https://claude.ai/new?q=Read%20https%3A%2F%2Fdocs.funnelfizz.com%2Fgetting-started%2Finstall-tracking.md%20and%20help%20me%20with%20this%20FunnelFizz%20topic%3A%20Install%20tracking "Open this page in Claude with context")[Open in ChatGPT](https://chat.openai.com/?q=Read%20https%3A%2F%2Fdocs.funnelfizz.com%2Fgetting-started%2Finstall-tracking.md%20and%20help%20me%20with%20this%20FunnelFizz%20topic%3A%20Install%20tracking "Open this page in ChatGPT with context")

# Install tracking

The tracking snippet reports pageviews, custom events, scroll depth, and time-on-page. Everything downstream — CONSIDERATION counts, splits by UTM/device/country, email identity — depends on it.

:::tip The two-step install

1. **Paste the snippet** (this page, sections 1–4) — gives you anonymous traffic, country/device/UTM splits, and engagement metrics.
2. **Add one `identify()` call** (section 5) — links anonymous visitors to their email when they sign up, so first-touch attribution survives all the way to the paid purchase. Without this, FunnelFizz can't tell that the customer who paid today first visited 30 days ago via Twitter.

Both steps are necessary to get the value FunnelFizz is built for. :::

## 1. Grab the snippet[​](#1-grab-the-snippet "Direct link to 1. Grab the snippet")

The onboarding wizard (or **Settings → Tracking sites**) gives you a snippet:

```
<!-- FunnelFizz tracking -->

<meta name="funnelfizz-verify" content="YOUR_TOKEN" />

<script>

(function(w,d,t){

  w._fn = w._fn || [];

  w.funnelfizz = function(){ w._fn.push(arguments) };

  var s = d.createElement('script');

  s.async = 1;

  s.src = 'https://funnelfizz.com/track.js?t=' + t;

  d.head.appendChild(s);

})(window, document, 'YOUR_TOKEN');

</script>
```

`YOUR_TOKEN` is unique to your site.

## 2. Paste it into `<head>`[​](#2-paste-it-into-head "Direct link to 2-paste-it-into-head")

As high in `<head>` as possible, before other scripts.

Next.js (App Router)

```
import Script from 'next/script';



export default function RootLayout({ children }) {

  return (

    <html>

      <head>

        <meta name="funnelfizz-verify" content="YOUR_TOKEN" />

      </head>

      <body>

        <Script id="funnelfizz" strategy="afterInteractive">

          {`(function(w,d,t){w._fn=w._fn||[];w.funnelfizz=function(){w._fn.push(arguments)};var s=d.createElement('script');s.async=1;s.src='https://funnelfizz.com/track.js?t='+t;d.head.appendChild(s)})(window,document,'YOUR_TOKEN');`}

        </Script>

        {children}

      </body>

    </html>

  );

}
```

Astro / Remix / SvelteKit

In your root layout, add the meta + script tags inside `<head>`. All three frameworks support raw HTML in layouts.

WordPress

Use any "Insert Headers and Footers" plugin → paste into the **Scripts in Header** box. Or edit `header.php` inside `<head>`.

Webflow / Framer / Squarespace / Shopify

Site-wide **Custom Code → Head**. Paste both tags. Publish.

Plain HTML

Paste into `<head>` of every page (or a shared include).

## 3. Multiple sites?[​](#3-multiple-sites "Direct link to 3. Multiple sites?")

**Paste the same snippet into each one** (your main site, app, docs, blog).

To track the same visitor as one person across `example.com` and `app.example.com`, the cookie has to be set at the apex.

**For flat 2-label apex domains** (`example.com`, `myapp.io`, `acme.dev`), the script auto-detects the apex and sets the cookie domain to `.example.com` automatically — no flag needed.

**For multi-label TLDs** (`example.co.uk`, `mysite.com.au`), the auto-detect plays it safe and stays host-only. Set `data-cookie-domain` explicitly:

```
<script

  src="https://funnelfizz.com/track.js?t=YOUR_TOKEN"

  data-cookie-domain=".example.co.uk"

  async

></script>
```

If you want subdomains tracked **separately**, set `data-cookie-domain="host"` to opt out of auto-detect — each origin gets its own visitor ID.

## 4. Verify[​](#4-verify "Direct link to 4. Verify")

Visit your live site. The wizard auto-advances within \~5s once it sees the first event. Verification is host-agnostic, any URL with the snippet flips it to verified.

If nothing happens:

* View source, confirm `funnelfizz-verify` and `track.js` are actually on the page.
* Confirm `YOUR_TOKEN` was replaced with the real token in both the `<meta>` and `<script>`.
* DevTools → Network → filter `tracking`, you should see a POST to `funnelfizz.com/api/tracking/…`.
* Disable ad-blockers / privacy extensions for the test.

## 5. Identify your users — the line that makes attribution actually work[​](#5-identify-your-users--the-line-that-makes-attribution-actually-work "Direct link to 5. Identify your users — the line that makes attribution actually work")

The snippet alone gives you anonymous traffic stats. To get the thing FunnelFizz is built for — **"this paying customer first visited via Twitter 30 days ago"** — you need ONE more line of code at the moment you first capture an email.

### Why one line is needed[​](#why-one-line-is-needed "Direct link to Why one line is needed")

The cookie identifies the **browser**, not the **person**. When Stripe (or your billing system, or your signup webhook) tells FunnelFizz "Jane just paid", the system has Jane's email but no link from `jane@acme.com` to the anonymous visitor record from 30 days ago. Without that link, you get accurate anonymous traffic *and* accurate revenue numbers — but no chain between them.

`identify()` is what builds the link. You only need to call it **once per session**, the first time you have the user's email or userId.

### The line[​](#the-line "Direct link to The line")

```
// Anywhere you collect an email — signup form, waitlist, free-tier registration, lead capture:

funnelfizz('identify', { email: 'sam@example.com' });



// If you also have an internal user ID (after auth), include it:

funnelfizz('identify', { userId: 'user_123', email: 'sam@example.com' });
```

That's it. From this point, FunnelFizz knows `visitorId → email`, and any future Stripe payment, email click, or app event with that email merges back into the original visitor record — preserving `firstSource`, `firstUtm`, `firstReferrer`, and the full session history.

### Where to put it[​](#where-to-put-it "Direct link to Where to put it")

| Scenario                                      | Where to call `identify()`                                                      |
| --------------------------------------------- | ------------------------------------------------------------------------------- |
| Signup form on your marketing site            | Right after a successful submit, with the email the user typed                  |
| Free-tier signup with no card                 | After the auth handler returns, with `userId` + `email`                         |
| Email-gated content (waitlist, free download) | At submit, before the redirect / thank-you                                      |
| OAuth login (Google, GitHub)                  | In your post-login callback, with the resolved user info                        |
| App page that requires auth                   | At app boot, on every authenticated page (idempotent — safe to call repeatedly) |

The earlier in the funnel you call it, the more of the journey gets attributed. Calling it at signup is enough; calling it on every authenticated page is even safer.

### Don't have an email until checkout?[​](#dont-have-an-email-until-checkout "Direct link to Don't have an email until checkout?")

If your only email-capture point is Stripe Checkout (no signup form, no lead capture before payment), pass the visitor ID through Stripe instead — the webhook handler will match it back:

```
// Read the visitor ID directly from the cookie set by the tracker.

function getCookie(name) {

  const m = document.cookie.match(new RegExp('(?:^|; )' + name + '=([^;]*)'));

  return m ? decodeURIComponent(m[1]) : null;

}



const visitorId = getCookie('_fn_vid');



const session = await stripe.checkout.sessions.create({

  // ... your normal session config ...

  client_reference_id: visitorId,

});
```

FunnelFizz's Stripe webhook handler picks up `client_reference_id` and stitches the customer back to the visitor automatically.

## 6. Custom events[​](#6-custom-events "Direct link to 6. Custom events")

Once loaded, `window.funnelfizz()` is global. Fire events from anywhere:

```
funnelfizz('event', 'signup');

funnelfizz('event', 'form_submit', { formId: 'pricing-contact' });
```

Or mark any element as a goal:

```
<a href="/signup" data-event="signup_click">Start free trial</a>
```

Events are filterable in splits and conversions. See [Tutorials → Tracking & custom events](https://docs.funnelfizz.com/tutorials/tracking-custom-events.md).

## 7. What the script sends[​](#7-what-the-script-sends "Direct link to 7. What the script sends")

Events sent automatically:

| Event             | When                                           |
| ----------------- | ---------------------------------------------- |
| `pageview`        | Every page load + SPA route changes            |
| `leave`           | Page unload, with timeOnPage + scrollDepth     |
| `scroll`          | 25 / 50 / 75 / 90% milestones                  |
| `goal`            | Click on any `[data-event]` element            |
| `outbound_click`  | Click on a link that leaves the tracked origin |
| `form_start`      | First focus on a `<form>` field                |
| `form_submit`     | `<form>` submit                                |
| `stripe_checkout` | Click on a link to a `checkout.stripe.com` URL |
| `identify`        | Your `funnelfizz('identify', …)` calls         |

### First-class typed events[​](#first-class-typed-events "Direct link to First-class typed events")

These names are recognized by the tracking ingest and route through stage-progression logic — fire them by name (don't translate to underscores or other variants):

| Fire                                  | What it does                                                                                               |
| ------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
| `funnelfizz('event', 'signup')`       | Records a signup; advances the funnel link to **CONSIDERATION** if the profile isn't already further along |
| `funnelfizz('event', 'trial_signup')` | Advances the funnel link to **TRIAL** (non-Stripe trials)                                                  |
| `funnelfizz('event', 'trial_start')`  | Same as `trial_signup` — alias                                                                             |
| `funnelfizz('identify', { email })`   | Bridges the visitor to a profile and unlocks PII visibility on the funnel                                  |

Anything else you fire via `funnelfizz('event', name, props)` lands as a `custom` event with `props.goal = name`. Custom events show on the profile timeline, can drive split conditions, and are queryable from MCP.

Events batch every 3s; `sendBeacon` on unload prevents drops.

## Privacy[​](#privacy "Direct link to Privacy")

* Honors `navigator.doNotTrack`, DNT visitors send nothing.
* No cross-site tracking, no fingerprinting, no data sale.
* For GDPR/CCPA, defer the snippet until your CMP grants "analytics" consent.

***

**Next:** [Connect Stripe →](https://docs.funnelfizz.com/getting-started/connect-stripe.md)
