# Custom events

> Track signups, checkouts, or any action with window.attrifast.track() — then turn event names into goals to measure conversion by channel.

Last updated: 2026-07-17
Canonical: https://attrifast.com/docs/custom-events

Once the [tracker is installed](/docs/install), a small global API is available for tracking anything beyond pageviews.

## Track an event

```js
window.attrifast.track('signup');
```

Call it at the moment the action actually completes — inside a click handler or after a successful API response, not on page load. Each call records one event.

The API is available after the tracker initializes (on `DOMContentLoaded`). Calls from user-interaction handlers are always safe; if you need to fire an event earlier or from framework lifecycle code, guard the call:

```js
window.attrifast?.track('signup');
```

<Callout type="warn">

`track()` accepts an optional second argument for event properties, but properties are **not queryable or visible anywhere today** — don't rely on them. If you need to segment, encode the variant into the event name itself: `signup_pro` vs `signup_free`.

</Callout>

## Manual pageviews

SPA route changes (History API) are tracked automatically — you don't need to call anything on navigation. `window.attrifast.pageview()` records a pageview manually and restarts engagement timing; use it for view changes the History API doesn't cover (e.g. a modal step you count as a page).

You can suppress the **initial** page-load pageview with `data-auto-pageview="false"` on the snippet:

```html
<script defer data-tracking-id="YOUR_TRACKING_ID" data-auto-pageview="false" src="https://api.attrifast.com/af.js"></script>
```

<Callout type="warn">

SPA route-change tracking stays on even with `data-auto-pageview="false"` — there's no fully-manual mode yet. Don't call `pageview()` on route changes, or every navigation will be counted twice.

</Callout>

## Delivery semantics

- **Fire-and-forget.** `track()` returns nothing and never throws for network problems; delivery uses `sendBeacon` with `fetch`/XHR fallbacks.
- **The collect endpoint replies `204` and drops bad data silently** — a wrong tracking ID or an unregistered domain won't error in your console. If events don't show up, check those two first.
- **Quota**: each pageview and each custom event counts as one event against your monthly plan quota (Pro: 100,000/month — engagement heartbeats are free). Over quota, requests return `429`.
- **Bots never count.** The tracker doesn't initialize for bot-like browsers (including `navigator.webdriver`), so events won't fire from Playwright/Puppeteer/Lighthouse runs — by design.

## Where events show up

Custom events surface through [goals](/docs/goals): create a goal with type "Custom event" and the exact event name, and the dashboard Goals panel shows conversions and conversion rate for it — retroactively, including events recorded before the goal existed. There's no raw event feed in the UI yet.

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| `Cannot read properties of undefined (reading 'track')` | The call ran before the tracker initialized (or in a bot-like browser where it never does). Fire from interaction handlers, or use `window.attrifast?.track(…)`. |
| Events return `204` but nothing appears | Silent server-side drop: the `data-tracking-id` doesn't match your website, or the page's domain isn't the registered one. Verify both. |
| `429` *"Subscription required…"* or *"Monthly event limit exceeded…"* | Free plan (collection requires a subscription or trial) or quota reached. Usage resets each calendar month — check it on the billing page. |
| Events fire in Playwright/Lighthouse but never show up | Bot filtering on both ends — by design. Test in a regular browser session. |
| Goal conversions look inflated | Conversions count every occurrence, not unique visitors. Fire `track()` once per completed action, not per click. |
