Clicktag docs

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 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 for low-level protocol details.