Track your first custom events
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.
Updated
The 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 makes one) and a hostname you can deploy the app to.
1. Add a web app 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.
- In the app, open Sources and click Add source.
- Choose Web app. It’s the same script as a website, and you’ll call
eventandidentifyfrom your code. - Name it Web app and enter its domain,
app.acme.com. - If you are offered Carry identity between your site and the app, leave it on. It keeps a visitor who clicks from
acme.cominto the app as one person (see Domains and identity). - Click Create source.
2. Install the script tag
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.
<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-keyis 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-domainslists 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.
If you use TypeScript, declare the global once:
// 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
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:
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-upNothing? An ad blocker in your own browser is the usual cause. Try a private window with extensions off, then work through Debugging.
4. Name your events
Pageviews, clicks and scroll depth are already autocaptured. 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 upwithmethod: 'google', notSigned 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 covers the whole set.
5. Send the events
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:
// 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' })
}// 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.
6. Watch them arrive
- Open Realtime in one tab and the deployed app in another.
- Sign up with a test account.
- In Realtime, narrow the feed to the exact event name
Signed up. - Click the event to open Event details.
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
Realtime covers the last 30 minutes. The Events explorer covers any range.
- Open Events (under Data).
- Under Select event, pick
Signed up, and set the dates to Today. - Click a row to expand it and open Your properties.
- Turn on Live tail, upgrade your test account, and pick
Plan upgradedto watch it land.
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
- Open Insights and click New insight.
- Under Series, pick
Signed upfrom Your events and choose Unique people. - Click Add breakdown, choose Event property, then
method, and Top 5. - Switch the chart to Bar to total the whole range.
- Name it Sign-ups by method in the header and click Save.
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 has the details.
9. Describe them in 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.
- Open Lexicon (under Data) and search for
Signed up. - Add a description, such as Fires once, when the account is created. method: email or google.
- Turn on Verified, and do the same for
Plan upgraded. - On the Event properties tab, check
seatsshows as a number. If the inferred type is wrong, set it to Number.
Common 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.clickclacksdoesn’t exist yet and the call throws or is lost. Add the queue stub, or guard each call withwindow.clickclacks?.(…). - Three spellings of one event.
Signed up,signupandSign Upare 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
identifytrait instead.
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.