# Event tracking recipes

> Copy-paste event tracking for signups, checkout funnels, file downloads, outbound links, 404 pages, scroll milestones, video, A/B tests and user plans.

## Signup

On the page after a successful signup (or in your signup success handler):

```js
privatus.track('Signup', { plan: 'free', method: 'email' })
```

Better still, send it from the server once the account exists, so blocked
scripts and double submits don't matter. See
[server-side examples](/docs/server-side/examples).

Then create an **Event** goal named `Signup`.

## Checkout funnel

```js
privatus.track('Checkout started', { items: cart.items.length })
// …
privatus.track('Payment details entered')
// on the confirmation page:
privatus.track('Purchase', { revenue: order.total, currency: order.currency, items: order.items.length })
```

Build a [funnel](/docs/features/funnels) with the three events as steps.
Never send order ids, emails or addresses.

## File downloads

Automatically, with the `auto` module (`File Download` events with a `file`
property):

```html
<script defer src="https://privatusanalytics.com/js/pa.js" data-site="pa_YOURSITEID" data-modules="engage,auto"></script>
```

Or for a single link:

```html
<a href="/files/price-list.pdf" data-privatus-event="Price list download">Price list (PDF)</a>
```

## Outbound links

The `auto` module sends `Outbound Link` with `domain` and `url`. Create an
**Outbound click** goal for a specific domain, e.g. your app's signup page
on another domain.

## 404 pages

With the `auto` module, add this to your 404 template:

```html
<meta name="privatus-404" content="true">
```

Then look at the `404` event's `path` property to find broken links, or
use the **Not found** tab of the Pages panel. Without `auto`:

```html
<script>
  window.addEventListener('load', function () {
    privatus.track('404', { path: location.pathname });
  });
</script>
```

## Scroll milestones

The `engage` module already reports scroll depth for every page. For an
event at a specific point (e.g. the end of an article):

```js
const end = document.querySelector('#article-end')
new IntersectionObserver((entries, observer) => {
  if (entries.some((e) => e.isIntersecting)) {
    privatus.track('Article finished', { section: 'blog' })
    observer.disconnect()
  }
}).observe(end)
```

Or create an **Engagement** goal (scroll ≥ 75% on `/blog/*`) with no code.

## Video

HTML5 video:

```js
document.querySelectorAll('video').forEach((video) => {
  const name = video.dataset.name || video.currentSrc.split('/').pop()
  video.addEventListener('play', () => privatus.track('Video play', { video: name }), { once: true })
  video.addEventListener('ended', () => privatus.track('Video complete', { video: name }))
})
```

YouTube embeds need the YouTube IFrame API: listen to `onStateChange` and
track `YT.PlayerState.PLAYING` and `ENDED` the same way.

## Experiments (A/B tests)

Send the variant as a property on every hit, so every metric can be split
by variant:

```js
privatus.props({ experiment: 'pricing_2026_09', variant: variant })
```

with [manual mode](/docs/tracker/manual-mode) if the variant isn't known
until after load. Then break down any goal by `prop:variant`. The
[Experiments report](/docs/features/experiments) calculates conversion
rates per variant and their statistical significance.

## Logged-in state or plan

```js
privatus.props({ logged_in: true, plan: 'business' })
```

Never send a user id: it would make visitors identifiable and defeat the
cookieless design.
