Track events
Record product events and sessions, deliver them reliably, and respect analytics consent.
The SDK records product events and sends them to Voidhash for you. Every event is attributed to the current visitor, carries a session, and is delivered in background batches that survive network outages, page reloads, and closed tabs. It uses the same event model as the iOS, Android, and React Native SDKs, so a funnel can start on your website and finish in your app.
Track an event
Call track with an event name and the properties you want to attach:
await voidhash.track("upgrade_clicked", {
plan: "yearly",
source: "pricing_page",
});track() adds the event to the queue and resolves; it never waits for the network and never
rejects because Voidhash is slow or unreachable. A call made while initialize() is still running
waits for it, so you can track from the first click.
Property values can be strings, numbers, booleans, null, Dates, sent as ISO-8601 strings, and
arrays and plain objects of those, nested up to 10 levels deep. A value with a toJSON() method,
such as a URL or a date-library object, is sent as JSON.stringify would write it. Anything else
is left out: other class instances, DOM elements and events, functions, undefined, invalid dates,
values nested deeper, and references back to an enclosing object. Pass the values you need, such as a button's id,
rather than a whole element or event.
Use clear, stable event names. Names that begin with $ are reserved for Voidhash's own events, and
Voidhash discards a custom event that uses one.
In React, useAnalytics() returns the same track bound to the provider's client:
const { track } = useAnalytics();Track options
The third argument of track() and page() overrides what the SDK fills in:
| Option | Default | Use it to |
|---|---|---|
eventId | A new random ID | Give an event your own ID, so the same event sent twice 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
The SDK records these events for you:
| Event | When |
|---|---|
$pageview | On load and after every client-side navigation that changes the path. See Page views. |
$set | When a visit carries campaign parameters, and when you set person attributes. |
$sign_out | When you call signOut(). |
Every event also carries the page's origin, path, and title, the referrer's origin and path, the
screen and viewport size, and these standard properties: $platform (web), $sdk (web),
$sdk_version, $locale, and $timezone. Voidhash adds the visitor's user agent and coarse
location when the event arrives, and $environment, the environment of the publishable key: test
for a vh_pk_test_… key, otherwise production. See
Analytics events.
Self-hosted installs store $pageview and $sign_out only after they are turned on in the
project's event settings, under Settings → Events in Studio.
Edit or drop events
analytics.beforeSend sees every event before the SDK queues or stores it: page views, $set and
$sign_out events, and the events you track. Return the event, changed or not, or null to drop it:
const voidhash = createVoidhashClient({
analytics: {
beforeSend: (event) => {
if (event.event === "internal_debug") return null;
delete event.properties.coupon_code;
return event;
},
},
publishableKey: "vh_pk_…",
});The event has these fields:
| Field | Contents |
|---|---|
event | The name, such as $pageview, $set, $sign_out, or one you track. |
properties | Your properties and the SDK's $ properties. A $set event carries the person traits it writes in $set and $set_once, campaign traits such as $initial_landing_path included. |
context | The page: url_origin, url_path, page_title, referrer_origin, referrer_path, and the screen and viewport size. |
distinctId, eventId, sessionId, timestamp | Who the event belongs to, its ID, its session, and when it happened. Read only. |
- Change
propertiesandcontextin place, or return a new event. Changes to the other fields are ignored. - Pass a list to run several functions in order. The first
nulldrops the event. - A function that throws, or returns something else than an event or
null, such as the promise of anasyncfunction, drops the event, and the SDK reports aBEFORE_SEND_FAILEDdiagnostic. - A dropped event is never stored, and does not start or extend a session.
- It runs once, when the event is recorded. Events already queued are not run through it again.
- Change
$-prefixed properties only when you mean to: Voidhash reads them, for example$platformto tell visitors of your website from users of your app.
identify() sends no event: it sends the user ID and the traits you pass to it, without the page's
address. To keep a page's address out of every event, use sanitizeUrl; see
Keep private pages out of analytics.
Pause recording
pauseAnalytics() stops recording at once, until you call resumeAnalytics():
voidhash.pauseAnalytics();
voidhash.isAnalyticsPaused(); // true
voidhash.resumeAnalytics();While recording is paused, page views, campaign touches, events, and person attributes are not recorded. Unlike withdrawing consent:
- The pause is not stored. It lasts until you resume or the page unloads.
- Events queued before it are kept and still sent.
- Nothing is recorded later for the time it lasted. Resuming records nothing by itself: the next
navigation records its page view, or call
page(). getAnalyticsConsent()does not change. Feature flags andidentify()keep working.
You can call it before initialize(), so even the first page view is not recorded. To keep a page
out with a pause, pause before the navigation that opens it, for example in your router's
navigation hook, and resume only once the visitor has left it: the SDK records a page view just
after the History API call, before your router has rendered the new page. To keep pages out by their address, sanitizeUrl is simpler and has no
such timing; see Keep private pages out of analytics.
In React, useAnalytics() returns pauseAnalytics and resumeAnalytics.
Sessions
Every event carries a session ID, so paths and funnels can tell one visit from the next. A session
starts with the first event and ends after 30 minutes without any event, the same as in the mobile
SDKs. All tabs of your site share the session, and it survives reloads within those 30 minutes.
signOut() and reset() start a new one.
Read the current session when you want to correlate your own data with it:
const sessionId = voidhash.getSessionId();It is undefined while analytics consent is not granted, and after 30 minutes without events until the
next event starts a new session.
Delivery
Events are delivered at least once, and stored once, except those sent while leaving a page (see below):
- Batching. The queue is sent every 5 seconds, or in the background as soon as it holds a full
batch, 20 events by default (
analytics.maxBatchSize). - Retries. A batch that fails, or that Voidhash answers with 408, 429, or any 5xx, including
the 520–530 a CDN answers while Voidhash is unreachable, is retried with growing, randomized
delays of at most 37.5 seconds, for as long as it takes. A
Retry-Afterfrom Voidhash is honored, up to 30 seconds. - Offline. The queue lives in
localStorage, so it survives reloads and browser restarts. It keeps up to 1,000 events; past that, the oldest are dropped first. Coming back online, or returning to the tab, sends it at once. Events are recorded only with analytics consent, so the queue is never stored without it. - Your app's storage. A browser gives a site about five million characters of
localStorage, shared by your app and the SDK. The queue takes at most about a million of them across all open tabs, so a long outage cannot fill what your app needs; events past that are kept in memory until the page closes. If the browser refuses a write because storage is full, the SDK removes the copy it stored, giving the space back to your app, and keeps the queue in memory until it is sent. - Leaving the page. On
pagehide, when the visitor closes the tab or navigates away, the SDK sends up to 64 KB of the queue in a request the browser completes after the page is gone. The rest is sent on the next visit. The browser sends that request even though the page never sees the answer, so the next page does not send those events again; if the request fails after the page is gone, they are lost. They are kept for the next page instead when the browser is offline, cannot keep a request going after the page, or Voidhash was failing or asked the SDK to wait. Switching to another tab sends nothing extra; the queue keeps being sent on its schedule. - Several tabs. Each tab keeps its own part of the queue, so tabs never overwrite each other. A closed tab's events are sent by another open tab with its next batch, every 5 seconds by default.
- Duplicates. Every event has a unique ID. A batch that reaches Voidhash twice, for example because the connection dropped before the answer arrived, is stored once.
- Outages. Every request times out after 10 seconds. After five failures in a row the SDK pauses requests for 30 to 37.5 seconds, doubling up to 5 minutes while they keep failing, so an outage costs your page no extra work. The pause is shared by your site's tabs and its next pages, which start paused instead of trying again. Once Voidhash is back, a backlog is sent over a few flushes rather than all at once.
Only an event Voidhash refuses for good, such as a malformed or oversized one, is dropped, besides the oldest events of a full queue and the events discarded when consent is withdrawn. Diagnostics report every drop; see Troubleshooting.
You do not need to send the queue before the visitor leaves, even for another site such as a checkout: the SDK sends it as the page is hidden, and what it cannot send then is sent on the next visit. To send it now, for example to show that everything is delivered:
const { flushed, pending } = await voidhash.flushAnalytics();flushAnalytics() waits at most 2 seconds. When Voidhash is slower or unreachable, it resolves with
what was sent so far and what is still pending, and sending carries on in the background, so it
never holds up a navigation for long.
You do not need to flush around identify() or signOut(): every event keeps the ID it was
recorded with. flushAnalytics() also delivers identity links still waiting to be sent.
Analytics consent
When a visitor declines usage analytics, or has not decided yet, tell the SDK:
await voidhash.setAnalyticsConsent("denied"); // "granted", "denied" or "pending"
const consent = voidhash.getAnalyticsConsent();denied and pending behave the same. While consent is either of them:
- Page views, events, sessions, and person attributes set with
setPersonAttributes()are not recorded, stored, or sent.setPersonAttributesSync()writes nothing and returns the unchanged person. The first call the SDK skips is reported once as theANALYTICS_CONSENT_DENIEDdiagnostic. - Feature flags and
identify()keep working. Their requests tell Voidhash that consent is missing, so it records no analytics for them.identify()still sends the email, name, and attributes you pass to it, because purchases are attributed to the person; leave it out until consent is granted if your consent regime treats linking a visitor to your user as analytics. - A campaign the visitor arrived from while consent is
pendingis held for the browser session and sent if consent is granted in that session, unlessstoreBeforeConsentisfalse.denieddiscards it.
Withdrawing consent discards every queued event, including a batch that is being sent, in every open tab. Granting it records from that moment on; nothing skipped in the meantime is sent later.
The choice you set is stored in the browser and wins over the analyticsConsent option on later
visits. Every open tab follows it, and signOut() and reset() keep it. When a consent tool such as
Cookiebot or OneTrust keeps the visitor's answer, set analyticsConsentSource: "app" instead, so
the tool's answer always wins; see Use a consent tool.
By default, the SDK keeps its anonymous ID, cached flags, and pending identify() links in the
browser even before consent. To keep everything but the consent choice off the device until the
visitor agrees, and to connect a consent tool, see
Privacy and consent.
Revenue from Stripe does not depend on consent: Voidhash records purchases from Stripe either way. Metrics built from analytics events, such as page views and campaign conversion rates, count only visitors who granted consent.
Turn analytics off
analytics: { enabled: false } turns analytics off for a client, as if consent were denied,
without storing a choice. Use it for internal or staging builds.
Event Capture API
Your backend can send events of its own, such as a server-side signup, through the Event Capture API, authenticated with the publishable key. Send them with the visitor's distinct ID so they join the same person.