Page views and attribution
Record page views and sessions, and attribute visitors and revenue to campaigns and ads.
The web SDK records which pages visitors see and which campaign or ad brought them to your site. Combined with your Stripe payments, this tells you the trials, conversions, revenue and refunds each campaign, ad set and ad produced.
Page views
The SDK records a $pageview event when the page loads and after every client-side navigation
that changes the path, through history.pushState, history.replaceState or the back and forward
buttons. Changes to the hash do not count, and a navigation that only changes the query string is
not a new page view.
This covers single-page apps without any router code: Next.js (App Router and Pages Router), React
Router, TanStack Router, Vue Router, SvelteKit, and any router built on the History API. A router
that keeps the route in the hash, such as Vue Router's hash mode or React Router's
createHashRouter, records only the first page view, and the hash is not recorded; call
page() with the route's name on its route changes.
Every event carries the page's origin, path and title, the referrer's origin and path, and the screen and viewport size. The query string is not recorded, except for the campaign parameters below.
To record page views yourself, turn the automatic ones off and call page():
const voidhash = createVoidhashClient({
analytics: { capturePageViews: false },
publishableKey: "vh_pk_…",
});
await voidhash.page("Pricing", { experiment: "annual-first" });Campaign attribution is recorded either way.
page() sends the same $pageview event the SDK records automatically. If your app calls page()
on every route change, set capturePageViews: false, or every navigation is counted twice.
$pageview is a built-in event that self-hosted installs do not store by default. Turn it on in
the project's event settings, or page views are discarded when they arrive.
Create one client per project on a page. Two clients for the same project each record their own page views, so every page would be counted twice.
Keep pages out
To keep pages out of page views and out of every event's context, for example because their
address carries personal data, return null for them from analytics.sanitizeUrl. It can also
rewrite an address, for example to replace a token in an invitation link. See
Keep private pages out of analytics.
Sessions
Every event carries a session id. A session ends after 30 minutes without events, the same as on
iOS, Android and React Native. All tabs of your site share the session, and reset() starts a new
one.
Campaign attribution
A visit whose URL has any of these parameters is a campaign touch:
| Parameter | Source |
|---|---|
utm_source, utm_medium, utm_campaign, utm_term, utm_content, utm_id | Any campaign |
fbclid | Meta ad clicks |
gclid | Google Ads clicks |
campaign_id, adset_id, ad_id | Ad ids you add to links |
The SDK writes each touch to the visitor's person profile:
- The first touch is kept forever, as the
$initial_…traits. - Every new touch replaces the
$latest_…traits.
| Trait suffix | Value |
|---|---|
utm_*, fbclid, gclid | The URL parameters, unchanged |
ad_network | meta for fbclid or a Facebook, Instagram or Meta utm_source; google for gclid; otherwise the lowercased utm_source |
ad_campaign_id | campaign_id, or utm_id when there is no campaign_id |
ad_adset_id | adset_id |
ad_id | ad_id |
landing_path | The path of the page the visitor landed on, without the query string |
referrer_origin | The origin of the referring page |
touch_at | When the touch happened, in ISO-8601 UTC |
For example, $initial_ad_campaign_id is the campaign of the visitor's first ad click and
$latest_utm_source the source of their most recent campaign visit. The page view of the touch
carries the same values as $utm_source, $ad_campaign_id and so on.
The SDK reads the campaign from the page's address when it initializes and after each client-side
navigation. If your app removes the campaign parameters from the address, for example to tidy the
URL, initialize the SDK first. A page that analytics.sanitizeUrl keeps out records no campaign;
for an address it rewrites, the SDK reads the campaign and the landing path from the rewritten one.
Reloading the page or navigating within your site with the same campaign parameters does not count
as a new touch. Each touch writes every trait, with null for the values it does not have, so the
first-touch and latest-touch traits always describe a single visit and never mix values from two.
The page view of a touch carries only the values it has.
When a visitor signs in, identify() links their anonymous profile, including its campaign traits,
to the user.
Voidhash also keeps each touch's campaign values and time, so Ads reports can credit a signup to the visitor's last touch before it, within your project's attribution window. See Ad measurement.
Meta ads
Add these URL parameters to your ads. In Ads Manager, set them under Tracking > URL parameters at the ad level:
utm_source=facebook&utm_medium=paid_social&utm_campaign={{campaign.name}}&utm_term={{adset.name}}&utm_content={{ad.name}}&campaign_id={{campaign.id}}&adset_id={{adset.id}}&ad_id={{ad.id}}Meta fills in the {{…}} values when the ad is clicked, and adds fbclid itself. The ids give
exact attribution to a campaign, ad set and ad even after you rename them.
If your ads already have URL parameters of their own, you can keep them as long as one of them
carries {{ad.id}}, such as utm_id={{ad.id}}. With your
Meta ad account connected, Voidhash finds the ad set and campaign of that ID
among the synced ads.
The SDK also stores Meta's click and browser ids on the person, as $fbc and $fbp:
$fbccomes from the Meta pixel's_fbccookie when it is present and less than 90 days old. Otherwise the SDK builds it from thefbclidof the visit, in Meta'sfb.1.<timestamp>.<fbclid>format.$fbpcomes from the Meta pixel's_fbpcookie, when your site uses the pixel.
The SDK reads these cookies but never sets any.
Attribute Stripe payments
To credit revenue, renewals, and refunds to the campaign that brought the visitor, pass the visitor's Voidhash ID to Stripe when your server creates the Checkout Session or customer. See Stripe revenue.
Bots and crawlers
Events from well-known crawlers, link-preview fetchers, uptime monitors and headless browsers are discarded when they arrive, so they do not count as page views, sessions or campaign touches.