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

# Full funnel tracking

The feature that makes everything else work: **following one person from the first time they saw your brand to the day they churned**, across devices, sessions, browsers, and payment events.

This page explains the identity model — the mechanism by which an anonymous pageview becomes a named customer. Understanding it helps when you're debugging "why isn't this user showing up in my funnel?" or deciding when to call `identify`.

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

FunnelFizz stitches three kinds of identifiers together:

1. **Visitor ID** (`_fn_vid` cookie) — assigned on first pageview. Permanent per browser. UUID.
2. **User ID** — your own internal ID for the user (however your app names them). Assigned when you call `funnelfizz('identify', { userId })`.
3. **Stripe customer ID** — `cus_...`. Assigned by Stripe when checkout happens.

The goal is to link these three together so that a session that started anonymous on mobile can be tied to the desktop purchase three weeks later.

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

```
Day 1: anonymous visitor lands on your site.

       → _fn_vid cookie = "vid_abc123" (new UUID)

       → _fn_sid session = "sess_def456"

       → pageview event recorded, attributed to vid_abc123



Day 2: same visitor, signs up for a trial.

       → app JS calls: funnelfizz('identify', { userId: 'user_789' })

       → VisitorIdentity row written: vid_abc123 → user_789

       → all prior events re-attributed to user_789



Day 3: user checks out on Stripe.

       → Stripe fires checkout.session.completed webhook

       → FunnelFizz matches customer.email to user_789

       → writes journey event: stage=CUSTOMER, visitor=vid_abc123, user=user_789

       → stripe_customer_id = "cus_xyz" stored on the profile



Day 30: same user, different device (new browser, no cookie).

       → anonymous pageview → _fn_vid cookie = "vid_new999"

       → pageview recorded attributed to vid_new999

       → NO stitch yet — FunnelFizz doesn't know this is user_789



Day 30 later: user logs in on the new device.

       → app JS calls: funnelfizz('identify', { userId: 'user_789' })

       → VisitorIdentity row written: vid_new999 → user_789

       → BOTH vid_abc123 and vid_new999 now point to user_789

       → all their events, on both devices, roll up to the same profile
```

## Keys and cookies[​](#keys-and-cookies "Direct link to Keys and cookies")

| Name      | Storage               | Lifetime     | Purpose                           |
| --------- | --------------------- | ------------ | --------------------------------- |
| `_fn_vid` | Cookie + localStorage | 365 days     | Permanent visitor ID              |
| `_fn_sid` | sessionStorage        | Tab lifetime | Session ID for grouping pageviews |

Neither cookie is shared across domains. If your product is split across `marketing.example.com` and `app.example.com`, enable cross-subdomain cookies by setting `cookieDomain: '.example.com'` when you install tracking (see [Features → Providers](https://docs.funnelfizz.com/features/providers.md) for multi-domain setups).

## The `identify` call[​](#the-identify-call "Direct link to the-identify-call")

The single most important call in your codebase:

```
funnelfizz('identify', {

  userId: 'user_789',          // your internal user ID — required

  email: 'sam@example.com',    // optional but strongly recommended

  createdAt: '2026-04-22T...', // optional — helps with cohort analysis

  plan: 'pro',                 // optional custom trait

});
```

**Where to call it:**

* Right after signup (immediately — even before the first trial starts).
* Every page load after login (idempotent, safe to call repeatedly).
* After any auth state change (on every re-auth, OAuth callback, etc.).

**What *not* to pass:**

* Passwords, tokens, credit card info — anything sensitive.
* PII you haven't gotten consent to process (in GDPR jurisdictions).

## Cross-device identity[​](#cross-device-identity "Direct link to Cross-device identity")

When the same `userId` is seen on two different `_fn_vid` cookies, FunnelFizz merges them — all events roll up to the same user profile, regardless of which device fired them. This is automatic; no extra config.

The reverse isn't true: we don't guess that two anonymous visitors on different devices are the same person. If you never call `identify`, anonymous sessions stay separate. This is intentional — cross-device guessing without identity signals is how creepy tracking happens.

## Email handoff (from email campaigns)[​](#email-handoff-from-email-campaigns "Direct link to Email handoff (from email campaigns)")

When FunnelFizz sends an email on your behalf, it embeds a signed profile token into every tracked link:

```
https://your-site.com/pricing?fn_ph=<signed-token>
```

When the recipient clicks, the tracking script reads the token on first event, verifies the signature server-side, and stitches the recipient's profile to the landing visitor cookie — so you can track "this trial user clicked our day-5 email and visited pricing," even if they opened the email on a totally new device.

The token is stripped from the URL via `history.replaceState` immediately so it doesn't leak to downstream analytics or referrer headers.

## Journey events[​](#journey-events "Direct link to Journey events")

Every stage entry and conversion writes a **journey event**:

```
{

  "funnelId": "funnel_abc",

  "visitorId": "vid_123",

  "userId": "user_789",

  "stageType": "TRIAL",

  "eventType": "entered",

  "fromStage": "CONSIDERATION",

  "metadata": { "method": "stripe_trial", "productId": "prod_xyz" },

  "createdAt": "2026-04-22T12:34:56Z"

}
```

Unique constraints per (funnel, visitor, stage, eventType) guarantee that a visitor can't enter the same stage twice in the same funnel. Churn events are the exception — a customer can churn only once per funnel (unique on `funnel × visitor × stageType = CUSTOMER × eventType = "churned"`).

## Full journey view[​](#full-journey-view "Direct link to Full journey view")

On any profile's detail page, you'll see a chronological timeline:

```
2026-04-01 12:04   pageview          /                  Reddit

2026-04-01 12:05   pageview          /pricing            

2026-04-02 09:22   identify          user_789           sam@example.com

2026-04-02 09:23   signup            (custom event)

2026-04-05 14:10   stripe_checkout   cus_xyz → prod_pro  $9/mo trial

2026-04-12 22:30   subscription_active                   paid

2026-05-12 22:30   subscription_canceled                 churned
```

This is the raw source of truth. Dashboard metrics are aggregations; the journey is the trace.

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

* No third-party tracking. FunnelFizz doesn't sell data to ad networks.
* `navigator.doNotTrack` is respected — if the visitor has DNT set, no events are sent.
* GDPR / CCPA: you're responsible for consent. If you're using a consent management platform (CMP), defer loading the FunnelFizz snippet until the user grants "analytics" consent.
* All tokens in email links are short-lived and single-scoped.
* Profile tokens in the URL are HMAC-signed and verified server-side; an attacker can't guess another user's token.

See [funnelfizz.com/privacy](https://funnelfizz.com/privacy) for the full policy.

***

**Next:** browse the [Features](https://docs.funnelfizz.com/features/providers.md) section for the full details on each feature.
