VoidhashDocs
DevelopersHelp centerGuidesAPI reference
Dashboard
Get started
Introduction
Quickstart
How Voidhash works
Glossary
Versions and releases
Concepts
Teams, projects and environments
API keys
Products and perks
Offers
Subscription lifecycle
People and identity
Events
Features
Paywalls in your app
Feature flags and A/B tests
Paywall components
Push notifications
Ad attribution in your app
Client SDKs
React Native
Swift
Kotlin
Web
Your backend
Node.js
PHP
Go
Rust
Other languages
Payment sources
Forward store notifications
Developer tools
CLI
MCP server
Query analytics with VoidQL
Web SDK
Web quickstart
Script tag
Identify users
Evaluate feature flags
Track
Track events
Page views and attribution
Privacy
Privacy and consent
Browser storage
Connect a consent tool
Keep data out of analytics
Revenue and attribution
Sell on the web
Stripe revenue
Web-to-app handoff
Delivery
First-party proxy
Tracking domain
Reference
Web SDK reference
Errors
Troubleshooting

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 with toJSON(), such as URL, are sent as JSON.stringify writes 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:

OptionDefaultUse it to
eventIdA new random IDGive the event your own ID, so a duplicate is stored once.
timestampNowRecord when the event happened, as an ISO-8601 string.
sessionIdThe current sessionAttribute the event to another session.

Automatic events

EventWhen
$pageviewOn load and after each client-side navigation that changes the path. See Page views.
$pageleaveWhen a page view ends. See Page engagement.
$outbound_clickWhen a visitor clicks a link to another origin.
$setWhen a visit carries campaign parameters, and when you set person attributes.
$sign_outWhen 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 projects

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.

PropertyContents
url_path, url_originThe page that was left.
duration_msHow long the page was visible, in milliseconds.
max_scroll_percentDeepest scroll position, from 0 to 1. A page that does not scroll reports 1.
lcp_msLargest Contentful Paint, in milliseconds. Only on the first page view of a page load.
inp_msThe slowest interaction, in milliseconds. Left out when none took more than 40 ms.
clsCumulative 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 holds maxBatchSize events, 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-After is 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 localStorage and 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

  • Keep data out of analytics
  • Record page views and campaign attribution
  • Check your data in Studio

Something wrong or missing on this page? Tell us by email or on Discord.

PreviousEvaluate feature flagsNextPage views and attribution

On this page

Track an eventAutomatic eventsPage engagementSessionsDeliveryAnalytics consentEvents from your serverNext steps