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
Kotlin SDK
Kotlin 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.capture(
    "upgrade_button_clicked",
    mapOf("source" to "settings", "plan" to "monthly"),
)

Use clear, stable event names. Names that begin with $ are reserved for Voidhash's own events. The SDK trims the name. An event with a blank name is not captured; the SDK reports it through the ANALYTICS_EVENT_INVALID diagnostic instead.

Events you capture before initialize() are kept and sent after it. Each one keeps the time it was captured.

Automatic events

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

  • $app_installed: the first launch of this install of the app. A reinstall, or a new device restored from a backup, is a new install.
  • $app_updated: the first launch after the app's version or build changed.
  • $app_opened: the first time one of the app's activities comes to the foreground in a process. Android also starts your app's process in the background, for example for a push message, a scheduled job or a broadcast. Such a start records no $app_opened until the user brings up an activity.
  • $app_backgrounded: the app's last visible activity stopped.
  • $app_became_active: an activity came back to the foreground after $app_backgrounded, in the same process.
  • $app_crashed: the app crashed or stopped responding, see Crashes.
  • $sign_out
  • $screen, see Track screens

$app_installed and $app_updated are always the first events of a launch. Configure Voidhash in Application.onCreate so that the SDK sees your first activity start.

When the process starts in the background, initialize() does not connect to Google Play or refresh the project, customer and flag data. The first activity start does both.

Crashes

$app_crashed counts crashes from the records Android keeps on its own, which the SDK reads when one of your activities first comes to the foreground after a launch. The SDK installs no signal or uncaught exception handler, so it does not interfere with your crash reporter, and the read happens 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.

  • crash_kind is crash for a Java or Kotlin crash, native_crash for a crash in native code, and anr when Android stopped the app because it did not respond.
  • $app_version is the version running when the event is reported. Android does not record which version crashed, so a crash followed by an update counts towards the new version.
  • The event's time is the time of the crash. Voidhash dates an event no more than 24 hours back, so a crash reported later than that counts as 24 hours old.

Android records these on Android 11 (API level 30) and later. Earlier versions report no crashes.

Report opened notifications

When an end user opens a notification you sent through Voidhash, tell the SDK from your own code. It records $push_opened with the notification's push_notification_send_id, which push open rates are built from. The SDK does not extend your messaging service or replace your activities, so this call is the only way it hears about an opened notification.

When your app is in the background, Android shows the notification itself. Tapping it starts your launcher activity with the notification's data as the intent's extras:

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // A restored activity gets its old intent again.
        if (savedInstanceState == null) voidhash.trackPushOpened(intent.extras)
    }

    override fun onNewIntent(intent: Intent) {
        super.onNewIntent(intent)
        voidhash.trackPushOpened(intent.extras)
    }
}

When your app shows a notification itself, keep the message's data with it, for example in its PendingIntent, and report that data when the end user opens the notification:

voidhash.trackPushOpened(remoteMessage.data)

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.

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:

val sessionId = voidhash.sessionId

Flush before a boundary

Events flush automatically in batches. A batch is sent when it reaches 20 events or every 5 seconds while the app is in the foreground, whichever comes first, and once more when the app goes to the background. Failed requests are retried with exponential backoff (each retry waits longer than the last), and the SDK honors the server's Retry-After header for up to 30 seconds. If Voidhash refuses a batch because one of its events is invalid, the SDK still delivers the other events and drops only the invalid one. Force delivery when the app is about to cross an important boundary, such as a screen the customer may not return from:

voidhash.flush()

This sends everything that is due: attribute and sign-in updates first, then purchase reports, then analytics events. If the SDK stopped contacting Voidhash after repeated failures, flush() tries again at once. It returns once each queue was tried, or after 2 seconds while delivery carries on in the background; one call sends at most ten batches of events, and the rest follow every 5 seconds.

Update customer traits

Trait updates travel through the same pipeline as analytics events. See Set customer attributes for how to set them.

Analytics consent

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

voidhash.setAnalyticsConsent(AnalyticsConsent.DENIED) // GRANTED, DENIED or PENDING
val consent = voidhash.analyticsConsent

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 setPersonAttributesWithStatus returns WriteStatus.SKIPPED. The exception is taking back ad data sharing.
  • The SDK reads no install referrer. 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 AnalyticsConsent.GRANTED, so existing apps keep collecting analytics. Set it to AnalyticsConsent.PENDING when your app asks for consent first. setAnalyticsConsent is not a suspending function and applies before it returns. 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:

voidhash.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:

voidhash.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(AnalyticsConsent.DENIED). Agreeing again drops a revocation still waiting to be sent.

Credit installs to ad campaigns

On a new install, the SDK reads the Google Play install referrer, and records the campaign it names on the person as their first and latest ad touch: the utm_* parameters, click IDs such as gclid and fbclid, campaign_id, adset_id and ad_id, the ad network they point to, and the time of the click. The touch stays with an anonymous person and carries over when you later call identify. A referrer carries campaign values only. The SDK never takes an identity from it.

Google Play's default for an organic install (utm_source=google-play&utm_medium=organic), and a referrer with no campaign values, record nothing. A link from your website can carry the campaign into the install with playStoreUrl(). Such a link also carries when the visitor arrived from that campaign, and the touch keeps the earlier of that time and Google Play's click time, so an old ad click does not look like a new one.

The SDK does this on its own, once per install, in the background and once an activity has started, 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 Google Play or one of Meta's apps cannot answer yet, a later launch asks again, up to 10 launches. Nothing is thrown into your app. The INSTALL_ATTRIBUTION_DEFERRED diagnostic reports that the SDK will ask again, and INSTALL_ATTRIBUTION_ABANDONED that it will not, for example on a device without Google Play.
  • Existing installs. An install that was already running an earlier version of the SDK is not read, because its referrer would describe a click from long ago. A reinstall counts as a new install, even where Android restored the app's data from a backup.

To turn it off, pass installAttribution = false in VoidhashOptions.

Meta app-install ads

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, and says whether the ad was clicked or only viewed. It records the touch with the same consent and timing as above:

val voidhash = Voidhash.configure(
    context = this,
    publishableKey = "vh_pk_...",
    options = VoidhashOptions(metaAppId = "1234567890"),
)

The SDK's manifest declares the <queries> entries these three apps need, which your merged manifest picks up. Meta encrypts the ad details, both here and when they arrive through Google Play, 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.

Next steps

  • Identify customers before and after sign-in
PreviousEvaluate feature flagsNextTrack screens

On this page

Capture an eventAutomatic eventsCrashesReport opened notificationsSessionsFlush before a boundaryUpdate customer traitsAnalytics consentShare purchases with ad platformsCredit installs to ad campaignsMeta app-install adsNext steps