# Custom Events

Clicktag records custom events for free: call `window.tt_event(name, metadata)` in the browser, or send one JSON `POST /events` from any backend with no API key. Event names are cut at 256 characters and metadata at 4,096.

## Browser event tracking

Record events in the browser using `window.tt_event`. To record events before `latest.js` finishes loading, declare the asynchronous buffer queue early in your document:

```html
<script>
  window.tt_event =
    window.tt_event ||
    function () {
      window.tt_event.q.push(Array.from(arguments));
    };
  window.tt_event.q = window.tt_event.q || [];
</script>
<script
  async
  src="https://clicktag.io/latest.js"
  data-hostname="example.com"
></script>
```

Always use standard function syntax rather than an arrow function so that `arguments` is captured correctly. Keep the `q` line: `latest.js` takes over `tt_event` only when it is missing or has a `q` array, and replays the queued calls in order. Any other existing `tt_event` function stays in place, so events are not sent while pageviews continue. Clicktag consumes only its own queue; an existing Simple Analytics `sa_event` queue is left untouched. To send an action to both providers, call each provider's event API separately.

### Triggering events from UI

Call `window.tt_event(name, metadata?, callback?)` inside event handlers:

```html
<button id="upgrade-btn">Upgrade Plan</button>

<script>
  document.getElementById("upgrade-btn").addEventListener("click", function () {
    if (typeof window.tt_event === "function") {
      window.tt_event("upgrade_clicked", { tier: "pro", period: "annual" });
    }
  });
</script>
```

### TypeScript definitions

If you use TypeScript, augment the global `Window` interface:

```typescript
declare global {
  interface Window {
    tt_event?: (
      name: string,
      metadata?: Record<string, string | number | boolean>,
      callback?: () => void,
    ) => void;
    tt_pageview?: (
      path?: string,
      metadata?: Record<string, string | number | boolean>,
    ) => void;
  }
}

export {};
```

## Naming conventions and normalization

Use short, descriptive event names in lowercase snake_case, such as `signup_completed` or `pricing_opened`.

Incoming event names undergo automated normalization:

- The collector truncates names exceeding 256 characters before normalization.
- Non-alphanumeric character sequences are converted to underscores.
- Leading and trailing underscores are stripped.

## Event metadata rules

Event metadata must be a plain JavaScript object:

- The collector stores at most 4,096 characters of serialized metadata JSON.
- Payloads exceeding this limit are truncated, which may result in non-parseable JSON.
- Send compact, non-sensitive strings, numbers, or booleans.
- In the browser, keys from `window.tt_metadata` (set before `latest.js` runs) are added to every pageview and event and replace event keys with the same name. A global function named by `data-metadata-collector` receives that merged metadata plus `type` and `event`, and the keys it returns override the merged metadata.

Clicktag aggregates event counts by name. Event breakdown `visitors` counts distinct nonempty visitor hashes only. Pageview breakdowns also count the `unique` flag of rows without a hash, but the event breakdown does not, so imported events add no visitors. Last seen is the newest event in the selected range. Use the [Goals API](/docs/goals#access) to measure page or event goals and ordered funnels of 2 to 6 steps. Custom metadata filtering is not supported.

Query aggregated event breakdown data using the API:

```bash
curl --fail-with-body -sS --max-time 30 \
  'https://clicktag.io/api/sites/demo.bitgate.dev/breakdown?dim=events&limit=10'
```

## Event callbacks

The optional callback runs when the event's `/simple.gif` request loads or fails; a function passed as the second argument is used as the callback. It also runs at once, without a request, when the name is not a string, number or function, or when a name function throws or returns another type. A name that is empty after cleaning, such as `"!!!"`, sends nothing and never calls the callback:

```javascript
window.tt_event("lead_form_submitted", { source: "nav" }, function () {
  console.log("Event dispatch finished");
});
```

The callback is not a success signal; it can run even when no request was sent. It does not guarantee persistence or delivery to permanent storage. Do not gate mission-critical logic on this callback.

## Automated event scripts

`latest.js` does not automatically record link clicks, downloads, or form submissions. Clicktag serves neither `/auto.js` nor `/auto-events.js`. The upstream Simple Analytics helper targets SA globals by default and does not route events to `tt_event`. Replace needed tracking with explicit `window.tt_event` calls; do not point Clicktag at `sa_event` to share that helper.

## Backend server-side events

Send events directly from backend services via `POST /events` without loading client tracking scripts:

Use the real incoming visitor user-agent, not a fabricated browser string. This example runs in a backend with `fetch` support:

```typescript
async function recordSignup(hostname: string, visitorUserAgent: string) {
  if (!hostname || !visitorUserAgent) {
    throw new Error(
      "A registered hostname and the real visitor user-agent are required",
    );
  }

  const response = await fetch("https://clicktag.io/events", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    signal: AbortSignal.timeout(10_000),
    body: JSON.stringify({
      hostname,
      type: "event",
      event: "signup_completed",
      path: "/signup",
      metadata: { plan: "starter" },
      ua: visitorUserAgent,
    }),
  });

  const result = await response.text();
  if (!response.ok) {
    throw new Error(`Event collector ${response.status}: ${result}`);
  }

  return result;
}
```

Server-side requirements and behaviors:

- Send a JSON-encoded object. Use `Content-Type: application/json` for clarity, although the handler parses JSON regardless of that header. Form-encoded bodies are not accepted.
- Payload must be a single JSON object. Batch requests are not supported.
- Always set `"type": "event"`. Omitting this field records the request as a standard pageview.
- Returns HTTP 200 with plain text `ok` on success, or HTTP 400 plain text on malformed input.
- Pass the visitor user-agent string via the `ua` field.
- Country is inferred from the supplied browser IANA `timezone`, not the request IP. Pass the end user’s timezone when available; absent, unknown, or non-geographic zones remain unknown. Client IP overrides are not accepted.
- Timestamps are assigned on server arrival. You cannot pass custom timestamps or backdate events.
- There is no accepted user-ID or client-IP override. Visitor hashes incorporate the UTC day, site, connection IP, user-agent, and a random salt that is replaced every UTC day and deleted once the day ends, so a shared server does not recreate end-user visitor identity. Bot-marked rows are excluded from normal metrics; bot classification includes the effective user-agent and the `bot` payload field.
- Assign one stable nonempty `id` per logical event. Bounded collector replay suppression and report reconciliation are not durable exactly-once delivery. Do not blindly retry after an ambiguous timeout or issue a new ID for the same event.

Review the [Collector Specification](/docs/collector) for low-level protocol details.
