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 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() was called. |
$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()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. - Person attributes aren't sent, and
setPersonAttributesStatereturns.skipped. - Apple Ads attribution doesn't run.
- The first skipped call reports the
ANALYTICS_CONSENT_DENIEDdiagnostic. - Purchases, restores, access checks, feature flags and paywalls work as usual.
identifystill links the user and sends the email and name you pass. - Requests carry an
x-analytics-consentheader, so Voidhash records no paywall exposure for them.
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.
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 and, like any attribute, needs analytics consent.
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.