Get started
Custom Events
Track conversions, button clicks, and custom application workflows using browser calls or backend requests.
Browser event tracking
Record events in the browser using window.sa_event. To record events before latest.js finishes loading, declare the asynchronous buffer queue early in your document:
<script>
window.sa_event =
window.sa_event ||
function () {
(window.sa_event.q = window.sa_event.q || []).push(Array.from(arguments));
};
</script>
<script
async
src="https://analytics.bitgate.dev/latest.js"
data-hostname="example.com"
></script>Always use standard function syntax rather than an arrow function so that arguments is captured correctly.
Triggering events from UI
Call window.sa_event(name, metadata?, callback?) inside event handlers:
<button id="upgrade-btn">Upgrade Plan</button>
<script>
document.getElementById("upgrade-btn").addEventListener("click", function () {
if (typeof window.sa_event === "function") {
window.sa_event("upgrade_clicked", { tier: "pro", period: "annual" });
}
});
</script>TypeScript definitions
If you use TypeScript, augment the global Window interface:
declare global {
interface Window {
sa_event?: (
name: string,
metadata?: Record<string, string | number | boolean>,
callback?: () => void,
) => void;
sa_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.
Totallytics aggregates event counts and unique visitor totals by event name. The API does not provide custom metadata filtering, funnel analysis, or custom goal query endpoints.
Query aggregated event breakdown data using the API:
curl --fail-with-body -sS --max-time 30 \
'https://analytics.bitgate.dev/api/sites/demo.bitgate.dev/breakdown?dim=events&limit=10'Event callbacks
The optional callback runs on image load/error and can also run immediately after a local validation failure, without a request:
window.sa_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 record link clicks, file downloads, or form submissions automatically. Companion scripts like /auto.js or /auto-events.js are not supported. If migrating from older scripts, attach explicit window.sa_event triggers.
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:
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://analytics.bitgate.dev/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/jsonfor 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
okon success, or HTTP 400 plain text on malformed input. - Pass the visitor user-agent string via the
uafield. - Country and geolocation are computed from the incoming HTTP connection IP. Server-side dispatches reflect the geographic location of your server, not the end user. 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 deployment salt, 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
botpayload field. - There is no idempotency key. Do not automatically retry an event POST after an ambiguous timeout: a retry can record the event twice.
Review the Collector Specification for low-level protocol details.