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
React Native SDK
React Native quickstart
Display a paywall
Make and restore purchases
Check access
Identify customers
Evaluate feature flags
Capture analytics
Track screens
Development purchase mode
Errors

Capture analytics

Track product events and customer traits with the Voidhash SDK.

The SDK captures product events and sends them to Voidhash for you. Every event is attributed to the current person and delivered in background batches. You do not need a separate analytics package.

Capture an event

Call capture with an event name and any properties you want to attach:

voidhash.client.capture("upgrade_button_clicked", {
  source: "settings",
  plan: "monthly",
});

capture() is synchronous. It queues the event and returns immediately. This is true even while the provider is still initializing, so you can capture events from your first render.

Property values are copied when you call capture(), so changing the object afterwards does not change the event. Values JSON cannot hold are converted: a bigint is sent as a decimal string, a Date as an ISO string and NaN or Infinity as null. Functions, undefined and circular references are left out. An event with more than 10,000 property values is dropped and reported through onDiagnostic as ANALYTICS_EVENT_DROPPED, so pass the fields you need rather than whole framework objects such as press events.

Each event also records the app version, OS version and locale at the moment you capture it. An event that waits offline across an app update still reports the version it happened in.

Use clear, stable event names. Names that begin with $ are reserved for Voidhash's own events.

Automatic events

The SDK records common lifecycle events for you, so you do not need to capture them yourself:

  • $app_installed
  • $app_updated
  • $app_opened
  • $app_backgrounded
  • $app_became_active
  • $app_crashed, see Crashes
  • $sign_out
  • $screen, see Track screens

$app_opened marks a launch the customer sees. When the system starts your app in the background, for example for a silent push or a background fetch, the SDK records nothing for the launch itself and records $app_opened (with $app_installed or $app_updated for a new release) when the customer first opens the app, instead of $app_became_active.

To record the app events and $sign_out yourself, set automaticLifecycleEvents to false in the client options. The SDK then stops capturing them.

Crashes

$app_crashed counts crashes from the records iOS and Android keep on their own, which the SDK reads after the app launches again. The SDK installs no signal or exception handler, so it does not interfere with your crash reporter, and it reads the records off your app's launch path.

The event carries crash_kind and $app_version. It holds no stack trace, thread data or file paths: use your crash reporter to find out why the app crashed. Its time is the time the system gives for the crash. Voidhash dates an event no more than 24 hours back, so a crash reported later than that counts as 24 hours old.

  • On iOS, crash_kind is crash and $app_version is the version that crashed. iOS decides when it hands over a report and may not hand over every one.
  • On Android, crash_kind is crash, native_crash for a crash in native code, or anr when Android stopped the app because it did not respond. $app_version is the version running when the event is reported, as Android does not record which version crashed. Android records these on Android 11 (API level 30) and later; earlier versions report no crashes.

A JavaScript error that ends the app counts when the system records it as a crash.

Report opened notifications

When an end user opens a notification you sent through Voidhash, tell the SDK from the handler of the notification library you use. It records $push_opened with the notification's push_notification_send_id, which push open rates are built from.

With expo-notifications:

const subscription = Notifications.addNotificationResponseReceivedListener((response) => {
  voidhash.client.trackPushOpened(response.notification.request.content.data);
});

With @react-native-firebase/messaging, for a notification that opened the app from the background and for one that launched it:

messaging().onNotificationOpenedApp((message) => {
  voidhash.client.trackPushOpened(message.data);
});

const initial = await messaging().getInitialNotification();
voidhash.client.trackPushOpened(initial?.data);

The SDK has no notification handling of its own, so this call is the only way it hears about an opened notification. Pass every notification you handle: Voidhash adds a small custom key, voidhash_send_id, to the data of each notification it sends, and the SDK ignores any notification without it. End users see no difference, though your app does see the key among the notification's data. Your own data keeps its keys and values.

Reporting the same notification twice in a row counts once, so a handler that reads the last opened notification every time a screen mounts does not inflate the count.

Sessions

Every event carries a session id, so screen 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, including time spent in the background. The next event then opens a new session. Signing out also starts a new session, and the session survives app restarts within the same 30 minutes. Read the current id when you need to correlate your own data with it:

const sessionId = voidhash.client.getSessionId();

Flush before a boundary

Events flush automatically in batches. A batch is sent when it reaches 20 events or every 5 seconds, whichever comes first, and failed requests are retried with exponential backoff (each retry waits longer than the last). Force delivery when the app is about to cross an important boundary, such as a screen the customer may not return from:

await voidhash.client.flush();

You do not need to flush around identity changes. The identity methods flush at the correct time on their own, so events captured before identify() or signOut() stay attached to the previous identity.

Update customer traits

Trait updates are not analytics events. They go to the customer's profile through a queue of their own that is never trimmed to make room for events. See Set customer attributes for how to set them.

Event capture API

Your backend, and any client that is not a mobile app, can send single events or batches through the Event Capture API. Authenticate those requests with the project's publishable token in the request body. Never send a secret key to the ingestion endpoint.

Analytics consent

When an end user declines usage analytics, or has not decided yet, tell the SDK:

await voidhash.client.setAnalyticsConsent("denied"); // "granted", "denied" or "pending"
const consent = voidhash.client.getAnalyticsConsent();

denied and pending behave the same. While consent is either of them:

  • The SDK captures no custom events, screens, app lifecycle events, $sign_out, or paywall impressions and interactions. Nothing is queued, stored on the device, or sent, and no analytics session is kept. $app_installed and $app_updated are recorded once consent is granted. A crash or an opened notification from this time is never reported.
  • Person attributes are neither sent nor queued, and setPersonAttributesSync() answers with the skipped status. The exception is taking back ad data sharing.
  • The SDK reads no install attribution. See Credit installs to ad campaigns.
  • The first analytics call the SDK skips is reported once as the ANALYTICS_CONSENT_DENIED diagnostic.
  • Purchases, restores, entitlements, feature flags, and paywalls work as usual. identify still links the customer and sends the email and name you pass, because purchases are attributed to them.
  • Requests to Voidhash report the missing consent in the x-analytics-consent header, so Voidhash records no paywall exposure for them and discards any analytics that still arrive.

Withdrawing consent also discards the events still waiting on the device, including a batch that is being sent, and ends the analytics session. Granting it collects from that moment on; nothing skipped in the meantime is sent later.

The analyticsConsent option sets the consent at launch and defaults to "granted", so existing apps keep collecting analytics. Set it to "pending" when your app asks for consent first. setAnalyticsConsent() returns an error result with FAILED_TO_SET_ANALYTICS_CONSENT when the choice applies but could not be stored on the device. The choice you set at runtime is stored on the device and takes precedence over the option on later launches. Signing out and resetting keep it, so ask again only when your own consent flow requires it.

Revenue reporting comes from purchases and is unaffected. Metrics built from analytics events, such as paywall impressions and conversion rates, count only end users who granted consent.

Share purchases with ad platforms

If you send purchases to a Conversions API, such as Meta's, Voidhash sends a person's purchases only when they agreed to it. Sharing with ad platforms is separate from analytics consent, which does not cover it. Once the end user agrees, record it:

await voidhash.client.setAdDataSharing(true);

This sets the person trait $ad_data_sharing. Like any person attribute, it does nothing while analytics consent is not granted. To take it back:

await voidhash.client.setAdDataSharing(false);

Analytics consent neither blocks nor discards that. The SDK stores it on the device, including across app restarts, and sends it in the background until Voidhash has written it. If Voidhash has no such person yet, it asks again for up to 48 hours, then gives up, as there is nobody to stop sharing for. It records no analytics. Setting $ad_data_sharing to false as a person attribute does the same. Declining analytics does not take this back for you, so when someone rejects everything, call both setAdDataSharing(false) and setAnalyticsConsent("denied"). Agreeing again drops a revocation still waiting to be sent.

Credit installs to ad campaigns

On a new install, the SDK works out which ad brought the end user, and records it on the person as their first and latest ad touch. The touch stays with an anonymous person and carries over when you later call identify. The native iOS and Android SDKs do this, so your JavaScript has nothing to call.

  • iOS asks Apple whether an Apple Ads campaign brought the install: the campaign, ad group and ad, whether the ad was tapped or only viewed, and when. It sends no advertising identifier and needs no App Tracking Transparency prompt.
  • Android reads the Google Play install referrer: the utm_* parameters, click IDs such as gclid and fbclid, campaign_id, adset_id and ad_id. A link from your website can carry the campaign with playStoreUrl(). A referrer carries campaign values only, and the SDK never takes an identity from it. Google Play's default for an organic install records nothing.

It runs once per install, in the background, so it never holds up your app's launch.

  • Consent. It runs only while analytics consent is granted. When consent arrives later, in the same launch or a later one, it runs then. For an end user who declines, nothing is read, and withdrawing consent stops a read in progress.
  • Failures. When the store cannot answer yet, a later launch asks again, up to 10 launches. Nothing surfaces as an error. The INSTALL_ATTRIBUTION_DEFERRED diagnostic reports that the SDK will ask again, and INSTALL_ATTRIBUTION_ABANDONED that it will not.
  • Existing installs. An install that was already running an earlier version of the SDK is not read, because the answer would describe a click from long ago.

To turn it off, pass installAttribution: false to createVoidhashClient.

On Android, set metaAppId to your Meta (Facebook) app ID, and the SDK also asks the Facebook, Instagram and Facebook Lite apps on the device for the ad behind the install. Their answer is preferred over Google Play's. Meta encrypts the ad details, so Voidhash needs your Meta install referrer decryption key to read them. Add it in Settings, Ad platforms, Meta. Without it, the install still counts towards Meta, without its campaign. See Meta ads. iOS ignores this option.

const voidhash = createVoidhashClient("vh_pk_...", {
  metaAppId: "1234567890",
});

Two kinds of install cannot be credited to a person on iOS:

  • Links from your website. The App Store has no referrer, so a link to it cannot carry a campaign. To connect a web visit to the app, have the end user sign in on both. See Web-to-app handoff.
  • Meta and TikTok app-install ads. Without App Tracking Transparency these platforms have no path to an individual install, so their app-install numbers stay the ones the platform reports. See Ad measurement.

Next steps

  • Identify customers before and after sign-in
  • Review the Event Capture endpoints
PreviousEvaluate feature flagsNextTrack screens

On this page

Capture an eventAutomatic eventsCrashesReport opened notificationsSessionsFlush before a boundaryUpdate customer traitsEvent capture APIAnalytics consentShare purchases with ad platformsCredit installs to ad campaignsNext steps