# Track your first custom events

> A step-by-step walkthrough: add a web app source, install the ClickClacks script tag, name and send Signed up and Plan upgraded with properties, check them live in Realtime, then find them in Events and chart them in an insight.

- Canonical URL: https://clickclacks.io/docs/walkthroughs/first-events
- Section: Walkthroughs
- Last updated: 2026-09-26

Acme CRM already counts pageviews on its marketing site. Now the team wants to know how many people sign up and how many upgrade. This walkthrough installs the tracker in the app, sends two well-named events, and follows them all the way into a saved chart.

## The goal {#goal}

By the end you will have `Signed up` and `Plan upgraded` arriving from `app.acme.com` with useful properties, and an insight called _Sign-ups by method_ that the whole team can open. It takes about twenty minutes, plus a deploy.

You need a ClickClacks project (the [quickstart](https://clickclacks.io/docs/quickstart.md) makes one) and a hostname you can deploy the app to.

## 1. Add a web app source {#source}

A source is one place events come from. Acme’s marketing site is already a **Website** source; the app gets its own, so its events can be told apart.

1. In the app, open **Sources** and click **Add source**.
2. Choose **Web app**. It’s the same script as a website, and you’ll call `event` and `identify` from your code.
3. Name it _Web app_ and enter its domain, `app.acme.com`.
4. If you are offered **Carry identity between** your site and the app, leave it on. It keeps a visitor who clicks from `acme.com` into the app as one person (see [Domains and identity](https://clickclacks.io/docs/domains.md)).
5. Click **Create source**.

> **What you should see**
>
> The source’s page opens with an **Install** card holding your script tag, and the status _Waiting for the first event_.

## 2. Install the script tag {#install}

Copy the tag from the Install card into the `<head>` of every page of the app. Put the small queue stub above it: the tag loads with `async`, and the stub keeps any call made before it has loaded instead of losing it.

```html title="index.html"
<script>
  window.clickclacks = window.clickclacks || function () {
    (window.clickclacks.q = window.clickclacks.q || []).push(arguments)
  }
</script>
<script async src="https://app.clickclacks.io/c.js" data-key="pk_live_3f9a1c7e5b2d4f6a8c0e1b3d" data-domains="acme.com,www.acme.com,app.acme.com"></script>
```

- `data-key` is the source’s browser key (`pk_live_…`). It is public and safe in your HTML. Copy yours from the app; the one above is an example.
- `data-domains` lists every hostname the tracker may run on, exactly as it appears in the address bar. On any other hostname the tracker doesn’t start.
- Framework-specific placement (Next.js, Nuxt, SvelteKit and more) is in [Install the tracker](https://clickclacks.io/docs/install.md#frameworks).

If you use TypeScript, declare the global once:

```ts title="global.d.ts"
// global.d.ts: once, so TypeScript knows the tracker's function.
declare global {
  interface Window {
    clickclacks: (command: string, ...args: unknown[]) => void
  }
}
export {}
```

## 3. Check the tracker runs {#running}

Deploy to a hostname in `data-domains`. `localhost` isn’t listed, so the tracker stays off there, which also keeps development traffic out of your data. Open the app, then the browser console:

```js title="console"
typeof window.clickclacks?.optOut
// 'function'  → the tracker is running
// 'undefined' → it hasn't started: see Debugging

localStorage.getItem('cci')
// 'user_4821' → the ID this browser identified with, after sign-up
```

> **What you should see**
>
> `'function'` in the console. In ClickClacks, open **Realtime**: your own visit appears in **Live activity** as a pageview within seconds, and the source’s status changes to _Receiving_.

Nothing? An ad blocker in your own browser is the usual cause. Try a private window with extensions off, then work through [Debugging](https://clickclacks.io/docs/debugging.md).

## 4. Name your events {#names}

Pageviews, clicks and scroll depth are already [autocaptured](https://clickclacks.io/docs/autocapture.md). Custom events are for the moments those can’t see. A few minutes on names now saves every report later:

- **Object and past-tense verb, in sentence case**: `Signed up`, `Plan upgraded`. Name what happened, not the button.
- **Variants go in properties**, not in the name: `Signed up` with `method: 'google'`, not `Signed up with Google`.
- **Properties in `snake_case`**, one type each. Send numbers as numbers (`5`, not `'5'`), so they can be summed and averaged.
- **No personal data** in names or properties. Use your user ID, never an email address.

Acme’s two events:

| Event | Properties | Fires when |
| --- | --- | --- |
| `Signed up` | `method` (text: `email` or `google`), `plan` (text) | The account has been created. |
| `Plan upgraded` | `from` (text), `to` (text), `seats` (number) | The new plan has been saved. |

Planning more than a handful? The [tracking plan walkthrough](https://clickclacks.io/docs/walkthroughs/tracking-plan.md) covers the whole set.

## 5. Send the events {#send}

Call `clickclacks('event', name, properties)` at the moment the thing has actually happened. For a sign-up, that is when your app knows the account exists, and it’s also the moment to call `identify` with your own user ID:

```ts title="signup.ts"
// Where your app learns the account was created,
// for example after the sign-up request succeeds.
export function onSignedUp(user: { id: string }, method: 'email' | 'google') {
  // Identify first, so the event below carries the user ID too.
  window.clickclacks('identify', user.id, { plan: 'free' })
  window.clickclacks('event', 'Signed up', { method, plan: 'free' })
}
```

```ts title="billing.ts"
// After the upgrade has been saved, not when the button is clicked.
export function onPlanUpgraded(from: string, to: string, seats: number) {
  window.clickclacks('event', 'Plan upgraded', { from, to, seats })
}
```

- The calls return nothing and never throw. A call that breaks the rules (a name over 128 characters, a name starting with `$`, properties that aren’t a plain object) is ignored silently.
- Properties are capped at 2 KB of JSON. Over that, the event still arrives, without its properties.
- Events are batched and sent every 5 seconds, when 25 are waiting, and when the page is hidden, so leaving the page right after the call doesn’t lose it.

> **Could the server send it instead?**
>
> `Plan upgraded` is really decided by your billing system, and the browser can miss it (an ad blocker, a closed tab). If your backend knows, send it from there: [Server-side tracking with Node](https://clickclacks.io/docs/walkthroughs/node.md) does exactly that. Send each event from one place only, or it counts twice.

## 6. Watch them arrive {#live}

1. Open **Realtime** in one tab and the deployed app in another.
2. Sign up with a test account.
3. In Realtime, narrow the feed to the exact event name `Signed up`.
4. Click the event to open **Event details**.

> **What you should see**
>
> `Signed up` in **Live activity** within a few seconds, and in **Event details** your `method` and `plan` properties, the source _Web app_, and the person. Because you called `identify` first, the console’s `localStorage.getItem('cci')` now returns your test user’s ID.

Want to see the request itself? In the browser’s Network tab, filter for `ingest`. A `202` with `{"accepted": n}` means it was received.

## 7. Find them in Events {#events}

Realtime covers the last 30 minutes. The Events explorer covers any range.

1. Open **Events** (under Data).
2. Under **Select event**, pick `Signed up`, and set the dates to **Today**.
3. Click a row to expand it and open **Your properties**.
4. Turn on **Live tail**, upgrade your test account, and pick `Plan upgraded` to watch it land.

> **What you should see**
>
> Your events in the table with the person and time. **Your properties** shows what you sent; **ClickClacks properties** shows what the tracker added, such as the page and browser. Above the table, **Feature adoption** charts the event’s occurrences and unique people.

If the page says nothing matches, it suggests the closest event names you do send. A typo such as `Signed Up` against `Signed up` is the usual cause.

## 8. Chart them in an insight {#insight}

1. Open **Insights** and click **New insight**.
2. Under **Series**, pick `Signed up` from **Your events** and choose **Unique people**.
3. Click **Add breakdown**, choose **Event property**, then `method`, and **Top 5**.
4. Switch the chart to **Bar** to total the whole range.
5. Name it _Sign-ups by method_ in the header and click **Save**.

> **What you should see**
>
> One bar per sign-up method. With only your test sign-up so far, that is a single bar of 1; it fills in as real people sign up. The insight now appears in the Insights list for your whole project, and its link opens the same chart for any teammate.

To see what share of sign-ups upgrade, add a second series for `Plan upgraded` and click **Add formula**: `B / A * 100`. The [Insights guide](https://clickclacks.io/docs/guides/insights.md) has the details.

## 9. Describe them in Lexicon {#lexicon}

Every picker in ClickClacks reads from Lexicon, so one description helps everyone who builds a report later. Editing needs the Manage Organization Settings permission, which Owners have.

1. Open **Lexicon** (under Data) and search for `Signed up`.
2. Add a description, such as _Fires once, when the account is created. method: email or google._
3. Turn on **Verified**, and do the same for `Plan upgraded`.
4. On the **Event properties** tab, check `seats` shows as a number. If the inferred type is wrong, set it to **Number**.

> **What you should see**
>
> A check next to both events in every event picker, with your description underneath.

## Common mistakes {#mistakes}

- **Testing on localhost.** The tracker only runs on hostnames in `data-domains`. Test on a deployed preview whose hostname you added to the source.
- **Firing on a thank-you page.** A page can be reloaded, bookmarked or skipped. Fire the event where the thing is confirmed, or send it from your server.
- **Calling before the script loads, without the stub.** `window.clickclacks` doesn’t exist yet and the call throws or is lost. Add the queue stub, or guard each call with `window.clickclacks?.(…)`.
- **Three spellings of one event.** `Signed up`, `signup` and `Sign Up` are three events in every report. If it has happened already, merge the extras in Lexicon and fix the code.
- **Numbers sent as strings.** `seats: '5'` won’t appear in the numeric property picker, so you can’t sum or average it.
- **An email address as a property.** Use the user ID. If your team needs the email in People, pass it as an `identify` trait instead.

## Next {#next}

Some of Acme’s most important moments happen on the server: workspaces created by the API, invoices paid by a webhook. Continue with [Server-side tracking with Node](https://clickclacks.io/docs/walkthroughs/node.md).

## Related {#related}

- [Custom events](https://clickclacks.io/docs/events.md): Rules, naming and examples by framework.
- [Tracker API](https://clickclacks.io/docs/tracker-api.md): Every command and script attribute.
- [Debugging](https://clickclacks.io/docs/debugging.md): When events don’t arrive.
- [Insights](https://clickclacks.io/docs/guides/insights.md): Series, breakdowns and formulas.
