---
title: "Custom events"
description: "Configure, name, track, and verify custom analytics events."
documentation: "https://tracwell.app/docs/events"
markdown: "https://tracwell.app/docs/events.md"
---

# Custom events

> Configure, name, track, and verify custom analytics events.

For backend-confirmed actions, use [server events](https://tracwell.app/docs/server-events.md) with the originating visitor/session identity.

## 1. Configure the client

Initialize Tracwell once in browser-only code. Add delivery callbacks while integrating so failures remain visible.

```ts
import { createTracwell } from "tracwell";

const analytics = createTracwell({
  collectionMode: "product",
  projectKey: "tw_live_...",
  onDelivery(report) {
    console.log("Tracwell delivery", report.status, report.eventIds);
  },
  onError(error) {
    console.error("Tracwell error", error.code, error.issues);
  },
});
```

Use `private` mode for privacy-preserving website analytics. Use `product` mode for persistent anonymous identity, sessions, or `identify()`.

## 2. Name the outcome

Name events after completed outcomes such as `signup_completed`, `checkout_started`, or `report_exported`. Keep names stable and put changing values in properties.

| Rule | Limit |
| --- | --- |
| First character | A letter |
| Allowed characters | Letters, numbers, periods, underscores, colons, and hyphens |
| Length | 1–100 characters |
| Reserved names | `identify`, `page_view` |

Prefer `plan_selected` with a `plan` property over dynamic names such as `starter_plan_selected`.

## 3. Track after success

Record the successful product result, not only the click that started it.

```ts
async function handleSignup() {
  const account = await createAccount();

  analytics.track("signup_completed", {
    account_type: account.type,
    plan: account.plan,
  });
}
```

`track()` returns an event ID when the event enters the delivery queue. It returns `undefined` when collection is blocked or validation fails.

```ts
const eventId = analytics.track("signup_completed", {
  plan: "starter",
  source: "pricing",
});

if (!eventId) {
  // Collection is blocked or the event did not pass validation.
}
```

## 4. Add useful properties

Properties become filters and breakdowns. Send JSON-compatible strings, finite numbers, booleans, null, arrays, and nested objects.

```ts
analytics.track("report_exported", {
  format: "csv",
  filters: {
    country: "IN",
    range: "last_30_days",
  },
  included_sections: ["overview", "acquisition"],
  row_count: 1842,
});
```

| Property limit | Value |
| --- | --- |
| Keys across an event | 100 |
| Nesting | 4 levels |
| Array items | 50 |
| Key length | 128 characters |
| String length | 1,024 characters |
| Serialized event size | 16 KiB |

Never send passwords, authentication tokens, payment-card data, health data, secrets, or unnecessary personal data.

## 5. Verify the event

1. Open **Events → Live stream** in Tracwell.
2. Complete the real product outcome.
3. Call `await analytics.flush()` when immediate delivery is useful during testing.
4. Confirm the event name, project, page, and properties.

`accepted` means the collector returned a matching acceptance receipt. `handed_off` means the browser handed the batch to `sendBeacon` while leaving; it is not server acceptance confirmation.

## Script build

```js
window.tracwell?.track("signup_completed", {
  plan: "starter",
  source: "pricing",
});
```

## Common mistakes

- Tracking intent instead of the completed outcome.
- Encoding changing values in dynamic event names.
- Tracking the same outcome from a button and an effect.
- Manually tracking standard History API page changes.
- Sending sensitive or unnecessary personal data.
