Track events
Record product events and sessions, and understand how the SDK delivers them.
The SDK records product events for the current visitor and delivers them in background batches that survive network outages, reloads and closed tabs. It uses the same event model as the mobile SDKs, so a funnel can start on your website and finish in your app. See Events for the event model and built-in events.
Track an event
await voidhash.track("upgrade_clicked", {
plan: "yearly",
source: "pricing_page",
});track() queues the event and resolves. It never waits for the network and never rejects because
Voidhash is slow or unreachable. In React, take track from useAnalytics().
- Names: use clear, stable names. Names that start with
$are reserved; Voidhash discards a custom event that uses one. - Properties: strings, numbers, booleans,
null,Dates (sent as ISO-8601), and arrays and plain objects of those, up to 10 levels deep. Values withtoJSON(), such asURL, are sent asJSON.stringifywrites them. Other values are left out: class instances, DOM elements, functions,undefined, invalid dates and circular references.
The third argument of track() and page() overrides what the SDK fills in:
| Option | Default | Use it to |
|---|---|---|
eventId | A new random ID | Give the event your own ID, so a duplicate is stored once. |
timestamp | Now | Record when the event happened, as an ISO-8601 string. |
sessionId | The current session | Attribute the event to another session. |
Automatic events
| Event | When |
|---|---|
$pageview | On load and after each client-side navigation that changes the path. See Page views. |
$pageleave | When a page view ends. See Page engagement. |
$outbound_click | When a visitor clicks a link to another origin. |
$set | When a visit carries campaign parameters, and when you set person attributes. |
$sign_out | When you call signOut(). |
Every event carries the page's origin, path and title, the referrer's origin and path, the screen
and viewport size, and $platform (web), $sdk (web), $sdk_version, $locale and
$timezone. Voidhash adds the user agent, coarse location and $environment (test for a
vh_pk_test_... key, otherwise production) when the event arrives.
Self-hosted installs store $pageview, $pageleave, $outbound_click and $sign_out only after
you turn them on under Settings → Events in Studio.
Page engagement
A $pageleave ends every page view: when the visitor moves to another page of your site, closes
the tab or leaves for another site. Switching tabs does not end it; hidden time is not counted. If
the browser discards a hidden page, the next page of your site sends its $pageleave.
| Property | Contents |
|---|---|
url_path, url_origin | The page that was left. |
duration_ms | How long the page was visible, in milliseconds. |
max_scroll_percent | Deepest scroll position, from 0 to 1. A page that does not scroll reports 1. |
lcp_ms | Largest Contentful Paint, in milliseconds. Only on the first page view of a page load. |
inp_ms | The slowest interaction, in milliseconds. Left out when none took more than 40 ms. |
cls | Cumulative Layout Shift, as Core Web Vitals counts it. |
Browsers that do not measure a Web Vital leave it out. Only the page's own scrolling counts.
An $outbound_click has one property, target_origin, such as https://stripe.com. The link's
path and query are never recorded. Links to your own subdomains count; mailto: and tel: links
do not.
capturePageLeaves defaults to capturePageViews. To turn both automatic events off:
import { createVoidhashClient } from "@voidhash/web";
const voidhash = createVoidhashClient({
analytics: { captureOutboundClicks: false, capturePageLeaves: false },
publishableKey: "vh_pk_...",
});Sessions
A session starts with the first event and ends after 30 minutes without events, as in the mobile
SDKs. All tabs of your site share it, and it survives reloads. signOut() and reset() start a new
one. getSessionId() returns the current session, or undefined without analytics consent or after
30 idle minutes.
Delivery
Events are delivered at least once and stored once:
- Batching. The queue is sent every 5 seconds (
flushIntervalMs), or at once when it holdsmaxBatchSizeevents, 20 by default. - Retries. Failed requests, and answers of 408, 425, 429 or 5xx, are retried with growing,
randomized delays of at most 37.5 seconds, for as long as it takes. Voidhash's
Retry-Afteris honored up to 30 seconds. An error that does not come from Voidhash, such as a proxy's 404, keeps the events queued. - Offline. The queue lives in
localStorageand keeps up to 1,000 events (maxQueueSize), dropping the oldest first. It takes at most about one million characters of storage across tabs; older events past that wait in memory only. - Leaving the page. On
pagehide, the SDK sends up to 64 KB of the queue in a request the browser completes after the page is gone. The rest goes out on the next visit. - Several tabs. Each tab keeps its own part of the queue. Another open tab sends a closed tab's events with its next batch.
- Outages. Requests time out after 10 seconds. After five failures in a row, the SDK pauses requests for 30–37.5 seconds, doubling up to 5 minutes while failures continue. Failures while the browser is offline do not count.
Events are dropped only when Voidhash refuses them for good (malformed or oversized), when a full queue drops its oldest, when the page closes with events held in memory, and when consent is withdrawn. Diagnostics report every drop.
To send the queue now, for example before navigating to an external checkout:
const { flushed, pending } = await voidhash.flushAnalytics();flushAnalytics() waits at most 2 seconds and sends at most 10 batches; the rest continues in the
background. It also delivers pending identify() links. You do not need to flush around
identify() or signOut(): every event keeps the ID it was recorded with.
Analytics consent
Without analytics consent ("denied" or "pending"), the SDK records no page views, events,
sessions or person attributes. Feature flags and identify() keep working. Withdrawing consent
discards queued events in every open tab; granting it records from that moment on.
analytics: { enabled: false } turns analytics off for a client, for example in internal builds.
See Privacy and consent.
Stripe revenue does not depend on consent: Voidhash records purchases from Stripe either way.
Events from your server
Your backend can send its own events, such as a server-side signup, through the Event Capture API. Send them with the visitor's distinct ID so they join the same person.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.