Capture analytics
Capture events, report opened notifications, and manage analytics consent on iOS.
The SDK records events for the current person and delivers them to Voidhash in background batches.
You don't need a separate analytics package. The event model and built-in $ events are explained
in Events.
Capture an event
await voidhash.capture("upgrade_button_clicked", properties: [
"source": .string("settings"),
"plan": .string("monthly"),
])Use clear, stable names. Names that start with $ are reserved for Voidhash's own events. A blank
name is ignored and reported as the ANALYTICS_EVENT_INVALID diagnostic. A number that isn't
finite is sent as null.
Automatic events
| Event | When |
|---|---|
$app_installed | The first launch of this install. |
$app_updated | The first launch after the app's version or build changed. |
$app_opened | The first time the app comes to the foreground in a process. A background launch, such as a silent push, records none until the user opens the app. |
$app_backgrounded | The app moved to the background. |
$app_became_active | The app returned to the foreground in the same process. |
$app_crashed | The app crashed. See Crashes. |
$sign_out | signOut() or reset() signed a signed-in user out. |
$screen | A screen appeared. See Track screens. |
To record the app events and $sign_out yourself, set options.automaticLifecycleEvents = false.
The SDK still saves its queue when the app leaves and resumes delivery when it returns.
Crashes
$app_crashed comes from the crash reports iOS hands the SDK on the next launch. The SDK installs
no signal or exception handler, so it doesn't interfere with your crash reporter. The event carries
crash_kind (crash) and $app_version, the version that crashed, but no stack trace. iOS decides
which reports it hands over, so counts can be incomplete. Reports aren't read on macOS or visionOS,
and the Simulator doesn't produce them.
Report opened notifications
When a user opens a notification sent through Voidhash, pass it to the SDK from your notification
delegate. It records $push_opened, which push open rates are built from:
func userNotificationCenter(
_ center: UNUserNotificationCenter,
didReceive response: UNNotificationResponse
) async {
Voidhash.shared?.trackPushOpened(userInfo: response.notification.request.content.userInfo)
}The SDK doesn't replace your notification delegate, so this call is how it learns about opens. Pass
every notification you handle: the SDK ignores those without the voidhash_send_id key Voidhash
adds to the ones it sends. Reporting the same notification twice in a row counts once. Registering
device tokens isn't part of the SDK; see Register devices for push.
Sessions
Every event carries a session ID. A session ends after 30 minutes without an event, including time in the background, and survives app restarts within that time. Signing out starts a new session.
let sessionId = await voidhash.sessionId()Without analytics consent there is no session, and sessionId() returns an empty string.
Delivery
While the app is in the foreground, the SDK sends a batch when 20 events are queued or every 5 seconds. Failed requests are retried with growing delays, at most 30 seconds apart when Voidhash asks the SDK to slow down. When the app moves to the background, the SDK sends what is queued once more.
The device keeps up to 1,000 undelivered events across restarts. Beyond that the oldest are dropped
and the SDK reports ANALYTICS_EVENT_DROPPED, at most once a minute. When Voidhash refuses one
event as invalid, only that event is dropped. Each event keeps the app version, OS version, locale
and time zone from when it was captured.
Before an important boundary, such as a screen the user may not return from, send the queue now:
await voidhash.flush()flush() sends identify calls and attributes first, then reported purchases, then up to 10 batches
of events. It waits up to 2 seconds and returns a FlushStatus with flushed and pending counts;
delivery continues in the background.
Analytics consent
When a user declines usage analytics or hasn't decided yet, tell the SDK:
await voidhash.setAnalyticsConsent(.denied) // .granted, .denied or .pending
let consent = voidhash.analyticsConsent.denied and .pending behave the same:
- No custom events, screens, lifecycle events,
$sign_outor paywall events are queued, stored or sent, and no session is kept.$app_installedand$app_updatedare recorded once consent is granted; crashes and opened notifications from this time are never reported. - Apple Ads attribution doesn't run.
- The first skipped event reports the
ANALYTICS_CONSENT_DENIEDdiagnostic. - Purchases, restores, access checks, feature flags and paywalls work as usual.
- What your app says about the user is still sent:
identifywith its email and name, person attributes, and groups with their properties. - Requests carry an
x-analytics-consentheader, so Voidhash records no paywall exposure for them and keeps device details, such as the platform and locale, off the person.
Analytics consent covers what the SDK observes, not what your app says about a person. Feature
flags and cohorts can target a person's attributes and groups, so those must not depend on whether
the user agreed to tracking. To send nothing before consent, don't call identify,
setPersonAttributes, group or setGroups until then, or configure the client with
enabled = false.
Withdrawing consent discards the events still on the device. Granting it collects from then on; nothing skipped is sent later.
options.analyticsConsent sets the consent at launch and defaults to .granted. Set it to
.pending when your app asks first. A choice you set at runtime is stored and takes precedence on
later launches, and survives sign-out. Revenue reporting is unaffected; metrics built from events,
such as paywall conversion, count only users who granted consent.
Keep events until the user answers
By default, what happens while consent is .pending is dropped, so the first screens of a user who
then agrees are missing. With analyticsWhilePending = .buffer, the SDK holds those events in
memory and records them when the user agrees:
options.analyticsConsent = .pending
options.analyticsWhilePending = .buffer
// When the user answers your consent dialog:
await voidhash.setAnalyticsConsent(.granted) // or .denied- Custom events, screens, lifecycle events and paywall events are held, each with the time it
happened.
$sign_outis not. Person attributes and group descriptions are not held either: they are sent at once. - Held events stay in memory only. Nothing is written to disk or sent before the answer, and quitting the app before the answer loses them.
- At most 100 events are held; after that the oldest is dropped and reported as
ANALYTICS_EVENT_DROPPED. .grantedrecords$app_installedor$app_updated, if still unreported, and then the held events, in order and with their original times, for the identity, groups and session in effect at that moment..denieddiscards them..deniednever holds anything, and.dropremains the default.
Let sign-in grant consent
If signing in to your app accepts terms that cover analytics, set grantConsentOnIdentify instead
of showing a dialog to signed-in users. identify then grants a consent that is still .pending:
options.analyticsConsent = .pending
options.analyticsWhilePending = .buffer
options.grantConsentOnIdentify = true- The grant happens before the link, so events held with
.bufferare recorded for the anonymous ID thatidentifythen links to your user. .deniedis never changed: a user who declined stays declined after signing in.- The grant lasts as long as the sign-in, also across launches.
signOut()andreset()first send what is queued, waiting at most 2 seconds, then return consent to.pending. - Any
setAnalyticsConsentcall makes the choice the user's own.signOut()andreset()then keep it.
Share purchases with ad platforms
When you send purchases to Meta, Voidhash sends a person's purchases only after they agreed to it. That agreement is separate from analytics consent. Record it with:
try await voidhash.setAdDataSharing(true)This sets the person attribute $ad_data_sharing. Unlike other attributes, agreeing needs analytics
consent; without it, the agreement is left out.
Taking it back with setAdDataSharing(false) works with or without consent: the SDK stores it and
keeps sending it, for up to 48 hours if Voidhash has no person yet. Agreeing again drops a
revocation that is still queued; if that revocation is already on its way to Voidhash,
setAdDataSharing(true) returns deferred and the agreement is sent right after it. Declining
analytics doesn't revoke it, so when someone rejects everything, call both
setAdDataSharing(false) and setAnalyticsConsent(.denied).
Credit installs to Apple Ads
On a new install, the SDK asks Apple's AdServices framework whether an Apple Ads campaign brought the user, and records the campaign, ad group and ad on the person as their first and latest ad touch. It sends no advertising identifier and needs no App Tracking Transparency prompt. It runs once per install in the background, only with analytics consent.
When Apple can't answer yet or the device is offline, a later launch asks again, up to 10 launches.
The SDK reports INSTALL_ATTRIBUTION_DEFERRED when it will ask again and
INSTALL_ATTRIBUTION_ABANDONED when it gives up. Installs that ran an earlier SDK version, and app
extensions, aren't looked up. Turn it off with options.installAttribution = false.
iOS can't credit two kinds of install to a person: links from your website, because the App Store has no referrer (use web-to-app handoff), and Meta or TikTok app-install ads, which have no path to an individual install without tracking. See Ad attribution in your app.
Next steps
Something wrong or missing on this page? Tell us by email or on Discord.