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, arrays, nested objects, and Dates, sent
as ISO-8601 strings. Functions and undefined values are left out. Use clear, stable event names;
names that begin with $ are reserved for Voidhash's own events.
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, $timezone, and $environment. Voidhash adds the visitor's user agent
and coarse location when the event arrives.
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.
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:
- 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 failed batch is retried with growing, randomized delays of up to 30 seconds, for as
long as it takes.
Retry-Afterfrom Voidhash is honored. - 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. - 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. 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 within about 15 seconds.
- 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 repeated failures the SDK waits before trying again, so an outage costs your page no extra work.
Only an event Voidhash refuses for good, such as a malformed one, is dropped. Diagnostics report every drop; see Troubleshooting.
To send the queue now, for example before you navigate to another site:
const { flushed, pending } = await voidhash.flushAnalytics();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:
- Not recorded, stored, or sent: page views, events, person attributes set with
setPersonAttributes(), and sessions.setPersonAttributesSync()writes nothing and returns the unchanged person. The first call the SDK skips is reported once as theANALYTICS_CONSENT_DENIEDdiagnostic. - Stored in the browser: the anonymous ID, the consent choice, the last evaluated feature flags, and identity links that are waiting to be delivered.
- Sent to Voidhash: feature flag requests, and
identify()links with the email, name, and attributes you pass toidentify(), because purchases are attributed to the person. Every request says that consent is missing, so Voidhash records no analytics for it. - Campaigns: a campaign the visitor arrived from while consent is
pendingis held insessionStoragefor the browser session and sent if consent is granted in that session.denieddiscards it.
Leave identify() out until consent is granted if your consent regime treats linking a visitor to
your user as analytics.
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.
Connect a cookie banner
Start the client with consent pending and pass every answer from your consent tool to the SDK.
Most consent tools call you back when the visitor answers and when a stored answer is loaded on a
later visit:
import { createVoidhashClient } from "@voidhash/web";
const voidhash = createVoidhashClient({
analyticsConsent: "pending",
publishableKey: "vh_pk_…",
});
void voidhash.initialize();
// Call this from your consent tool's callback.
export function onConsentChange(analyticsAllowed: boolean) {
void voidhash.setAnalyticsConsent(analyticsAllowed ? "granted" : "denied");
}In React, useAnalytics() exposes consent and setAnalyticsConsent, and re-renders when consent
changes in any tab:
const { consent, setAnalyticsConsent } = useAnalytics();
if (consent !== "pending") return null;
return (
<ConsentBanner
onAccept={() => void setAnalyticsConsent("granted")}
onDecline={() => void setAnalyticsConsent("denied")}
/>
);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.