VoidhashDocs
DocumentationAPI reference
Dashboard
Get started
Introduction
Quickstart
Setup
Setup overview
Create a project
Environments
Products and perks
Paywall locations
CLI
Paywall components
Components
Develop a component
Editor panels
Component definition
Publishing
Installing and capabilities
SDKs
Swift
Kotlin
React Native
Web
Store setup
Connect the App Store
Connect Google Play
Connect Stripe
Record payments from other sources
API libraries
Node.js
PHP
Rust
Go
Resources
MCP server
Ad measurement
Meta ads
Migrate in observer mode
Delete a person's data
Web SDK
Web quickstart
Script tag
Identify users
Track events
Privacy and consent
Page views and attribution
Sell on the web
Stripe revenue
Web-to-app handoff
First-party proxy
Tracking domain
Feature flags
Troubleshooting

Troubleshooting

Diagnose missing events, duplicate page views, lost visitors, and SDK errors.

Start with diagnostics: they tell you what the SDK is doing in the background, where it never throws. Log them while you integrate:

const voidhash = createVoidhashClient({
  onDiagnostic: (diagnostic) => console.debug("[voidhash]", diagnostic),
  publishableKey: "vh_pk_…",
});

No events arrive

Check these in order.

  1. Is initialize() called? In React, the provider calls it. Elsewhere, call it once per page.
  2. Is analytics consent granted? With analyticsConsent: "pending", nothing is sent until you call setAnalyticsConsent("granted"). The ANALYTICS_CONSENT_DENIED diagnostic says so. With analyticsConsentSource: "app", consent starts as pending on every page until you pass your consent tool's answer.
  3. Is recording paused, or are the events dropped on purpose? pauseAnalytics() stops recording until resumeAnalytics(); isAnalyticsPaused() tells. analytics.beforeSend drops every event it returns null or anything else but an event for, and every event it throws on (BEFORE_SEND_FAILED). A page analytics.sanitizeUrl returns null for gets no page views.
  4. Is the publishable key right? A rejected key pauses all requests and reports AUTHENTICATION_FAILED once. Copy the key again from Settings → API keys in Studio.
  5. Does a Content Security Policy block the requests? Add https://api.voidhash.com, or your proxy's or tracking domain's origin, to connect-src. See Content Security Policy.
  6. Does an ad blocker block them? In the Network tab, look for blocked requests to /i/v1/batch. Set up a first-party proxy.
  7. Are you testing with a headless browser? Events from browsers that identify themselves as headless or as test tools, such as headless Chrome (including Playwright's and Puppeteer's default) or Cypress, are discarded as bot traffic. Test in a regular browser window.
  8. Is the event turned on? Self-hosted projects store $pageview and $sign_out only after they are turned on under Settings → Events.

Events are sent every 5 seconds, so allow a few seconds before they appear in Activity → Analytics events. Call flushAnalytics() to send them at once; it resolves with { flushed, pending, lastError }.

Staging events show up as production

An event's $environment is the environment of the publishable key that sent it. If your staging website's page views appear under Production in Studio, the site uses the production vh_pk_… key: switch it to the test vh_pk_test_… key from Settings → API keys. Its events then appear under Test only. See Analytics events.

Page views are counted twice

  • Your app calls page() on every route change while automatic page views are on. Remove the calls, or set analytics: { capturePageViews: false }.
  • Two clients for the same project run on the page, for example one from a module and one from a VoidhashProvider with config. Create one client and pass it to the provider with client.

React Strict Mode does not double page views: the provider keeps one client through its remount.

A private page shows up in analytics

  • The page is not kept out by its address. A pause applies from the moment you call pauseAnalytics(): the SDK records a page view just after the navigation, so a pause from a component that renders the new page comes too late, and one lifted before the visitor leaves the page does not keep it out. Return null for the page from analytics.sanitizeUrl instead; see Keep private pages out of analytics.
  • Its data is in an event's properties or title. sanitizeUrl keeps the page's address and title out, not what you pass to track() or setPersonAttributes(), and not the title of a page you keep. Remove those in analytics.beforeSend.
  • Another client runs on the page, for example one from a VoidhashProvider with config and one of your own, and only one has the option. Create one client and pass it to the provider.

Returning visitors show up as new visitors

  • The browser cleared storage. The anonymous ID lives in localStorage. Private windows, "clear on exit" settings, and blocked storage start a new visitor every time. Safari and other browsers with tracking prevention may also delete storage a script wrote after about seven days without a visit. Identify signed-in users, so each visit is linked to them again.
  • The publishable key changed. The SDK keeps its saved state per project key, so another key starts new visitors.
  • Consent is not granted yet and storeBeforeConsent is false. Until the visitor grants analytics consent, the SDK stores nothing in the browser, so every page load is a new anonymous visitor. That is intended; the visitor is kept from the page on which they grant consent.
  • Another subdomain. localStorage is per origin, so www.example.com and app.example.com have different anonymous visitors until the visitor is identified on both.

A purchase is not credited to a campaign

  • The Checkout Session or customer carries no voidhash_distinct_id metadata, for example because the purchase came through a Payment Link. See Stripe revenue.
  • The ID was read before identify() while the visitor was signed in, so the purchase landed on the anonymous visitor. It moves to the user when that visitor is identified.
  • The visitor arrived without campaign parameters, or declined analytics consent, so they have no touch.
  • The visitor's last campaign visit came more than your project's attribution window before they signed up, or only after it. See Ad measurement.
  • The purchase was made in Stripe test mode. Test purchases appear under Test in Studio and are read only with test keys, so check the site uses the matching publishable key. See Environments.
  • The ad's URL does not carry the campaign IDs. See Meta ads.

Errors

ErrorCause and fix
VoidhashNotInitializedErrorA method ran before initialize() was called. Call it first, or use the provider.
VoidhashDestroyedErrorThe client was destroyed, for example by a provider that unmounted. Create a new client.
VoidhashConfigurationErrorAn option is invalid, such as a blank publishableKey.
initialize() needs a browserinitialize() ran on a server. Call it from client-side code, such as an effect, or render the provider.
VoidhashInvalidArgumentErroridentify() got a blank ID or one that starts with vh_anon_ or vh:anon:, or track() a blank event name. Nothing was changed.
useVoidhash must be used within a VoidhashProviderA hook rendered outside the provider.

Diagnostics

Each diagnostic has a stable code, a kind for routing, the SDK operation, whether the SDK will retry on its own, the httpStatus when there was a response, and a readable message.

CodeKindMeaning
TRANSPORT_FAILEDtransportA request failed or timed out. Events and identity writes are retried.
ANALYTICS_UNEXPECTED_STATUStransportVoidhash answered with an unexpected status. The batch stays queued.
MALFORMED_CAPTURE_RESPONSEtransportVoidhash accepted a batch but its answer could not be read.
KEEPALIVE_EVENT_TOO_LARGEtransportAn event did not fit the request sent while leaving the page. It is sent on the next visit.
KEEPALIVE_UNCONFIRMEDtransportA closed page sent events while leaving and could not see the answer. They count as delivered and are not sent again.
IDENTITY_WRITE_REFUSEDtransportVoidhash refused an identity link for good. It was dropped; the browser keeps its identity.
IDENTITY_WRITE_DROPPEDevictionMore than 20 users identified while Voidhash could not be reached; the oldest link was dropped.
ANALYTICS_EVENT_DROPPEDevictionAn event was dropped: the queue was full, or Voidhash refused it for good. While a full queue keeps dropping, it is reported once a minute with the count.
BEFORE_SEND_FAILEDevictionanalytics.beforeSend threw or returned something else than an event or null, so the event was dropped. The error client event carries what it threw.
SANITIZE_URL_FAILEDevictionanalytics.sanitizeUrl threw, so the address was not recorded. The error client event carries what it threw.
CIRCUIT_OPENEDbreakerFive requests in a row failed, timed out, or got a 408 or 5xx answer, so requests to Voidhash are paused. They resume on their own.
CIRCUIT_OPENbreakerA request was skipped while requests are paused.
AUTHENTICATION_FAILEDauthThe publishable key was rejected. Requests pause until initialize(), identify(), signOut(), or reset(); otherwise a single request tries the key again once a minute.
CACHE_READ_FAILEDcacheA value the SDK had stored in the browser could not be read, for example because it was changed by hand. It was discarded.
CACHE_WRITE_FAILEDcacheBrowser storage refused a write, for example when full. Reported once per entry until a write succeeds. The queue gives its stored copy back and stays in memory.
ANALYTICS_QUEUE_IN_MEMORYcacheThe queue reached the share of browser storage it may take during an outage. Newer events wait in memory until it shrinks.
ANALYTICS_CONSENT_DENIEDconsentAnalytics consent is not granted, so analytics are not recorded. Reported once.

Report auth diagnostics to your monitoring: they mean no data reaches Voidhash.

PreviousFeature flagsNextConnect the App Store

On this page

No events arriveStaging events show up as productionPage views are counted twiceA private page shows up in analyticsReturning visitors show up as new visitorsA purchase is not credited to a campaignErrorsDiagnostics