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.
- Is
initialize()called? In React, the provider calls it. Elsewhere, call it once per page. - Is analytics consent granted? With
analyticsConsent: "pending", nothing is sent until you callsetAnalyticsConsent("granted"). TheANALYTICS_CONSENT_DENIEDdiagnostic says so. WithanalyticsConsentSource: "app", consent starts aspendingon every page until you pass your consent tool's answer. - Is recording paused, or are the events dropped on purpose?
pauseAnalytics()stops recording untilresumeAnalytics();isAnalyticsPaused()tells.analytics.beforeSenddrops every event it returnsnullor anything else but an event for, and every event it throws on (BEFORE_SEND_FAILED). A pageanalytics.sanitizeUrlreturnsnullfor gets no page views. - Is the publishable key right? A rejected key pauses all requests and reports
AUTHENTICATION_FAILEDonce. Copy the key again from Settings → API keys in Studio. - Does a Content Security Policy block the requests? Add
https://api.voidhash.com, or your proxy's or tracking domain's origin, toconnect-src. See Content Security Policy. - Does an ad blocker block them? In the Network tab, look for blocked requests to
/i/v1/batch. Set up a first-party proxy. - 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.
- Is the event turned on? Self-hosted projects store
$pageviewand$sign_outonly 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 setanalytics: { capturePageViews: false }. - Two clients for the same project run on the page, for example one from a module and one from a
VoidhashProviderwithconfig. Create one client and pass it to the provider withclient.
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. Returnnullfor the page fromanalytics.sanitizeUrlinstead; see Keep private pages out of analytics. - Its data is in an event's properties or title.
sanitizeUrlkeeps the page's address and title out, not what you pass totrack()orsetPersonAttributes(), and not the title of a page you keep. Remove those inanalytics.beforeSend. - Another client runs on the page, for example one from a
VoidhashProviderwithconfigand 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
storeBeforeConsentisfalse. 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.
localStorageis per origin, sowww.example.comandapp.example.comhave 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_idmetadata, 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
| Error | Cause and fix |
|---|---|
VoidhashNotInitializedError | A method ran before initialize() was called. Call it first, or use the provider. |
VoidhashDestroyedError | The client was destroyed, for example by a provider that unmounted. Create a new client. |
VoidhashConfigurationError | An option is invalid, such as a blank publishableKey. |
initialize() needs a browser | initialize() ran on a server. Call it from client-side code, such as an effect, or render the provider. |
VoidhashInvalidArgumentError | identify() 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 VoidhashProvider | A 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.
| Code | Kind | Meaning |
|---|---|---|
TRANSPORT_FAILED | transport | A request failed or timed out. Events and identity writes are retried. |
ANALYTICS_UNEXPECTED_STATUS | transport | Voidhash answered with an unexpected status. The batch stays queued. |
MALFORMED_CAPTURE_RESPONSE | transport | Voidhash accepted a batch but its answer could not be read. |
KEEPALIVE_EVENT_TOO_LARGE | transport | An event did not fit the request sent while leaving the page. It is sent on the next visit. |
KEEPALIVE_UNCONFIRMED | transport | A closed page sent events while leaving and could not see the answer. They count as delivered and are not sent again. |
IDENTITY_WRITE_REFUSED | transport | Voidhash refused an identity link for good. It was dropped; the browser keeps its identity. |
IDENTITY_WRITE_DROPPED | eviction | More than 20 users identified while Voidhash could not be reached; the oldest link was dropped. |
ANALYTICS_EVENT_DROPPED | eviction | An event was dropped: the queue was full, or Voidhash refused it for good. |
BEFORE_SEND_FAILED | eviction | analytics.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_FAILED | eviction | analytics.sanitizeUrl threw, so the address was not recorded. The error client event carries what it threw. |
CIRCUIT_OPENED | breaker | Five requests in a row failed, timed out, or got a 408, 500, 502, 503, or 504 answer, so requests to Voidhash are paused. They resume on their own. |
CIRCUIT_OPEN | breaker | A request was skipped while requests are paused. |
AUTHENTICATION_FAILED | auth | The 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_FAILED | cache | A 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_FAILED | cache | Browser storage refused a write, for example when full. The queue stays in memory. |
ANALYTICS_CONSENT_DENIED | consent | Analytics consent is not granted, so analytics are not recorded. Reported once. |
Report auth diagnostics to your monitoring: they mean no data reaches Voidhash.