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_openeduntil 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_kindiscrashfor a Java or Kotlin crash,native_crashfor a crash in native code, andanrwhen Android stopped the app because it did not respond.$app_versionis 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.sessionIdFlush 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.analyticsConsentdenied 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_installedand$app_updatedare 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
setPersonAttributesWithStatusreturnsWriteStatus.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_DENIEDdiagnostic. - Purchases, restores, entitlements, feature flags, and paywalls work as usual.
identifystill 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-consentheader, 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_DEFERREDdiagnostic reports that the SDK will ask again, andINSTALL_ATTRIBUTION_ABANDONEDthat 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.