# Adding Trample analytics to a website — guide for AI agents

You are building or editing a website and the owner wants Trample analytics on it. This file tells you exactly what
to add, what to avoid, and how to check it works. Trample server: **https://trample.dev** (every URL below is relative to it).

Trample is cookieless: the tracker sets no cookies and stores nothing in the browser, and the server keeps no IP
addresses. Most sites don't need a consent banner for it. Don't add one on Trample's account unless the owner asks.

## 1. Before you start: the site must exist in Trample

Events for a domain that isn't registered are rejected (`404 unknown site`). You can't register it through an API.
Ask the owner to:

1. sign in at https://trample.dev/login.html (email link or passkey), and
2. add the site's domain (e.g. `example.com`) on the dashboard.

Use the bare domain. `www.` is stripped, so `www.example.com` and `example.com` are the same site. Subdomains
(`blog.example.com`) are separate sites unless `data-domain` says otherwise (see below).

## 2. Add the script tag

Put this in the `<head>` of **every page**, once (in a shared layout/template, not per page):

```html
<script defer data-domain="example.com"
  src="https://trample.dev/js/v/VERSION/trample.js"
  integrity="sha384-HASH" crossorigin="anonymous"></script>
```

Fill in `VERSION` and `HASH` from https://trample.dev/js/manifest.json: use `versions[latest].src` (it already includes the
`/js/v/<version>/trample.js` path) and `versions[latest].integrity`. Copy them exactly. Don't compute your own hash,
and don't guess the latest version. The owner can also copy the finished snippet from the dashboard (**Tracking code**).

`https://trample.dev/js/trample.js` is an unpinned alias for the latest release. It works, but can't carry `integrity`;
prefer the pinned URL.

Rules that break tracking if ignored:

- **Load it as a plain classic `<script src>`.** Not `type="module"`, not imported into your bundle, not copied into
  your own static folder. The tracker reads its own `<script>` element to find its settings and the event endpoint.
- **Keep `integrity` and `crossorigin="anonymous"` together**, and don't edit the file. A changed file is refused by
  the browser.
- **Content-Security-Policy:** if the site sends one, allow `script-src https://trample.dev` and `connect-src https://trample.dev`.
- **Frameworks:**
  - Next.js (App Router): a plain `<script defer … />` in the root `app/layout.tsx` `<head>`, or `next/script` with
    `strategy="afterInteractive"` and the same attributes.
  - Astro / SvelteKit / Nuxt / Remix / Vite SPA: the base layout's or `index.html`'s `<head>`.
  - Static site generators (Hugo, Jekyll, Eleventy): the base layout partial.
  - Don't wrap it in a component that mounts more than once.

### Script attributes

| Attribute | Effect |
|---|---|
| `data-domain="example.com"` | The Trample site to record into. Defaults to the page's hostname. Set it explicitly. |
| `data-no-outbound` | Don't record clicks on links to other sites. |
| `data-no-downloads` | Don't record clicks on file links. |
| `data-no-engagement` | Don't record time on page and scroll depth. |
| `data-allow-local` | Also send from `localhost`. Only for testing against a self-hosted or scratch site; it pollutes real stats. |
| `data-api="https://…/api/event"` | Send events here instead of `https://trample.dev/api/event` (for a first-party proxy; see section 5). |

## 3. What is recorded with no extra code

- **Pageviews**, including single-page-app navigations through `history.pushState` and back/forward (`popstate`).
  A new pageview is counted when the **path** changes. Query-string-only changes, `replaceState` and `#hash` routing
  are not counted. If the site uses hash routing, switch the router to history mode.
- **Sources:** referrer and `utm_source` / `utm_medium` / `utm_campaign` (also `?ref=` and `?source=`) on landing
  URLs. Query strings themselves are not stored.
- **Outbound link clicks** (`Outbound Link: Click`, with `url`) and **file downloads** (`File Download`, with `url`)
  for `<a href>` elements. Use real `<a href>` links for these. Buttons that call `window.open` aren't seen.
- **Engagement:** visible time and scroll depth per page.
- Country, browser, OS and device size, derived on the server.

## 4. Custom events (goals)

Call `trample(name, options)` for things the owner cares about: signups, purchases, form submissions, key clicks.
Add the queue stub once, in `<head>` **before** the script tag, so calls made before the script loads aren't lost:

```html
<script>window.trample = window.trample || function () { (trample.q = trample.q || []).push(arguments) };</script>
```

Then, anywhere:

```js
trample('Signup', { props: { plan: 'pro', source: 'pricing-page' } });

// Navigating away right after? sendBeacon already survives unload; a callback is there if you need ordering:
trample('Download Brochure', { callback: () => { location.href = '/thanks'; } });
```

On a 404 page, call `trample('404')`. The missing path is recorded automatically.

Limits and conventions:

- **Name:** up to 120 characters. Use short, stable, human-readable names (`Signup`, `Checkout Started`). Don't put
  variable data in the name; put it in props. `pageview` and `engagement` are reserved.
- **Props:** a flat object of strings, numbers or booleans. Up to 30 keys, keys up to 300 characters, values up to
  2000 characters (stored as strings). Nested objects, arrays and `null` are dropped.
- **No personal data:** never send emails, names, user IDs, order numbers tied to a person, or free-text input. Use
  categories (`plan: 'pro'`, `cart_size: '3-5'`).
- Events count toward the account's monthly event volume. Don't fire on scroll, mousemove or timers. Engagement
  reports are already free and don't count.
- The owner doesn't need to configure goals: every event name shows up under **Goals** on the dashboard, with its
  props as breakdowns.

## 5. Sending events from a server or a proxy (optional)

The browser tracker is the normal path. Use these only when needed.

**Server-side events** (e.g. a completed payment confirmed by a webhook):

```
POST https://trample.dev/api/event
Content-Type: application/json
User-Agent: <the visitor's User-Agent, forwarded>
X-Forwarded-For: <the visitor's IP, forwarded>

{"n": "Purchase", "u": "https://example.com/checkout", "d": "example.com", "p": {"plan": "pro"}}
```

- Fields: `n` event name, `u` full page URL (required, absolute), `d` site domain, `r` referrer (optional), `w`
  viewport width in px (optional), `p` props (optional). The body may be at most 8 KB.
- Response: `202 ok` on success. Errors are `400` (missing or invalid field) and `404` (unknown site).
- **Forward the visitor's real User-Agent and IP.** Visitors are identified by a daily-salted hash of IP + User-Agent.
  Requests with an empty or bot-like User-Agent (`curl`, `node-fetch`, `python-requests`…) are silently dropped,
  with the same `202`, so a server call with default headers records nothing.
- Events past the account's plan limits are also accepted with `202` and not recorded. The owner sees why on the
  dashboard.

**First-party proxy** (so ad blockers don't drop requests): serve the script and forward `POST /api/event` from
the site's own domain to `https://trample.dev/api/event`, keeping the body and User-Agent and appending the client IP to
`X-Forwarded-For`. Then set `data-api="/your/path/event"` on the script tag. Without the forwarded IP, all visitors
collapse into one.

## 6. Verify it works

Automated browsers are ignored on purpose: the tracker sends nothing when `navigator.webdriver` is true (Playwright,
Puppeteer, Selenium, Cypress), on `localhost`, or on `file://`. **A passing headless test does not mean events were
recorded, and a headless test that sees no request doesn't mean it's broken.**

To check:

1. In a headless run, the browser console shows `[trample] ignored "pageview": automation`. That message proves the
   script loaded, parsed its settings and would have sent.
2. Confirm the script URL and integrity load without errors: no CSP violation, no "integrity" error in the console.
3. Ask the owner to open the deployed site in a normal browser. The network panel shows `POST …/api/event` → `202`,
   and the dashboard's **current visitors** count goes up within 30 seconds.

Don't "verify" by posting fake events with a browser User-Agent: they count as real visits.

The owner can exclude their own visits by running `localStorage.trample_ignore = 'true'` in the browser console on
their site.

## 7. Reading stats (optional)

Account APIs use the signed-in owner's session; there are no API keys. For read access, ask the owner to create a
**share link** for the site (dashboard → Share). Its token gives public, read-only access to:

```
GET https://trample.dev/api/shared/TOKEN/stats/aggregate                 → { range, current, previous }
GET https://trample.dev/api/shared/TOKEN/stats/timeseries                → { range, points: [{ bucket, visitors, pageviews }] }
GET https://trample.dev/api/shared/TOKEN/stats/breakdown/BY?limit=50     → { results: [{ value, visitors, … }] }
GET https://trample.dev/api/shared/TOKEN/stats/props?limit=20            → { props }   (needs f.goal)
GET https://trample.dev/api/shared/TOKEN/stats/realtime                  → { visitors }
```

- `current` and `previous` hold `visitors`, `pageviews`, `visits`, `bounce_rate` (%) and `visit_duration` (seconds).
- `BY` is one of `page`, `entry_page`, `exit_page`, `goal`, `source`, `referrer`, `utm_source`, `utm_medium`,
  `utm_campaign`, `country`, `browser`, `os`, `device`.
- **Range:** `period=day|7d|30d|month|custom` (default `7d`). With `day`/`month`, add `date=YYYY-MM-DD`. With
  `custom`, add `from=YYYY-MM-DD&to=YYYY-MM-DD` (at most two years apart). Dates are in the site's timezone.
- **Filters:** `f.<dimension>=value` (e.g. `f.page=/pricing`, `f.goal=Signup`, `f.country=US`) and
  `f.prop.<name>=value`. Use `(none)` to match an empty value (e.g. direct traffic).

A share token is a credential. Don't commit it to a public repository or put it in client-side code unless the
owner wants the stats public.

## Checklist

- [ ] The owner added the domain in Trample.
- [ ] One pinned `<script defer data-domain=… src=…/js/v/… integrity=… crossorigin="anonymous">` in the shared `<head>`.
- [ ] The queue stub sits before it, if the site calls `trample()`.
- [ ] CSP allows https://trample.dev for `script-src` and `connect-src`.
- [ ] Custom events have stable names, flat props and no personal data.
- [ ] A 404 page calls `trample('404')`.
- [ ] Single-page-app routing uses history mode, not hash routing.
- [ ] Verified via the console message in automation, and by the owner in a real browser.
