Custom events
Autocapture sees pageviews, clicks and scrolling. Custom events record what those can’t: a sign-up, an upgrade, an export. Each one is a single function call.
Updated
Send an event
Call clickclacks with 'event', a name, and an optional object of properties:
window.clickclacks('event', 'Signed up', {
plan: 'pro',
seats: 5,
referred: true,
})The call returns nothing and never throws. The event is queued and sent with the next batch, with the page’s anonymous ID, session ID and, if you’ve called identify, the user ID.
The tracker also adds the page as path: the pathname only, never the query string or #fragment. So a custom event splits by Page in reports just like a pageview. Pass your own path and yours is kept.
Names and properties
- The name is a string of 1 to 128 characters, and can’t start with
$, which is reserved for ClickClacks. - Properties must be a plain object, or left out. An array, a string or
nullin its place isn’t accepted. - Values can be strings, numbers, booleans,
null, arrays or nested objects. Functions andundefinedvalues are dropped. - Properties are capped at 2 KB of JSON. Over that, the event is still sent, without your properties (it keeps
path). - A call that breaks these rules is ignored silently rather than throwing, so a typo can’t break your page. Check new events arrive in Realtime.
Properties belong to the event, not to the person. A plan sent with Signed up describes that sign-up, not the person’s plan today; for that, pass traits to identify.
Naming conventions
Names are how everyone on your team finds an event in a picker, so they’re worth a minute of thought. What works well:
- Object and past-tense verb, in sentence case:
Signed up,Invoice paid,Report exported. - A small, stable set. Tens of well-named events beat hundreds of near-duplicates.
- Properties in
snake_case, with one type each:amount_centsis always a number,planalways a string. - No personal data in names or properties. Use your user ID, not an email address.
| Prefer | Over | Why |
|---|---|---|
Invoice paid | invoice_paid_4821 | IDs belong in properties. One name per kind of thing, or every invoice becomes its own event. |
Signed up | signup / SignUp / sign_up | Pick one style and keep it. Three spellings are three events in every report. |
Report exported { format: "csv" } | CSV report exported | Variants go in properties, so one insight can split by format. |
Plan changed { from: "growth", to: "pro" } | Button clicked | Name the outcome, not the widget. Clicks are already autocaptured. |
Describe each event and property once in Lexicon, under Data in the app. Every picker in ClickClacks reads from it, so your descriptions show up wherever someone builds a report.
Examples by framework
The tracker is a global function, so there’s nothing to import. Use optional chaining (?.) so a call is skipped when the tracker isn’t running, for example on a hostname outside data-domains or before it has loaded.
// A React component
export function UpgradeButton({ plan }: { plan: string }) {
return (
<button
onClick={() => window.clickclacks?.('event', 'Upgrade clicked', { plan })}
>
Upgrade
</button>
)
}
// Once, for TypeScript (for example in global.d.ts):
declare global {
interface Window {
clickclacks?: (command: string, ...args: unknown[]) => void
}
}Calling before the script loads
The tag is async, so window.clickclacks doesn’t exist until c.js has loaded, and never exists on a hostname missing from data-domains. When the tracker starts, it replays any calls waiting in window.clickclacks.q, in order, before it sends the first pageview. Define this stub above the script tag and early calls are queued instead of lost:
<script>
window.clickclacks = window.clickclacks || function () {
(window.clickclacks.q = window.clickclacks.q || []).push(arguments)
}
</script> With the stub in place you can drop the ?.. Without it, guard each call: window.clickclacks?.('event', 'Signed up').
How events are sent
- Events are batched in the browser and sent every 5 seconds, as soon as 25 are waiting, and when the page is hidden or closed.
- They go out with
fetchandkeepalive(orsendBeaconin older browsers), so a batch survives the page closing. - If a batch fails, its events are kept and retried with the next one, up to 100 waiting events, so a brief network drop doesn’t lose them.
- Requests carry no cookies or credentials.
Events for a company
If your customers are companies, the group command says which company the signed-in person is acting for. The tracker remembers it and adds it to every later event as $groups, so reports can count companies as well as people.
// Once, after sign-in
window.clickclacks('group', 'company', 'cmp_311', { name: 'Acme', plan: 'pro' })
// Every later event from this browser counts for that company
window.clickclacks('event', 'Report exported', { format: 'csv' })- The type is 1–64 lower-case letters, digits or underscores, such as
company. The ID is your own ID for the company, 1–255 characters. - A browser holds one ID per type, and at most 5 types. Pass
nullas the ID to leave a group;resetforgets them all. - The optional third argument is the company’s traits. Sending them is free.
- Events sent before the call don’t count for the company.
Companies and groups covers the model, traits and what reports do with them.
Events the browser can’t see
Some moments happen with no page open, or matter too much to leave to a browser: renewals, refunds, webhooks, background jobs. Send those from your backend with the server API or the Node SDK. They join the same people, as long as you use the same user ID you pass to identify.
Next steps
- Identify people: tie events to your user IDs.
- Companies and groups: count companies as well as people.
- Tracker API: every command in one place.
- Events and properties: the reserved
$names.