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
Swift SDK
Swift 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:

await voidhash.capture("upgrade_button_clicked", properties: [
    "source": .string("settings"),
    "plan": .string("monthly"),
])

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: the first launch of this install of the app.
  • $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. iOS also launches your app in the background, for example for a silent push, a background fetch or a location update. Such a launch records no $app_opened until the user brings the app up.
  • $app_backgrounded: the app moved to the background.
  • $app_became_active: the app came back to the foreground after $app_backgrounded, in the same process.
  • $app_crashed: the app crashed, see Crashes.
  • $sign_out
  • $screen, see Track screens

To record the app events and $sign_out yourself, set automaticLifecycleEvents to false in the options. The SDK then stops capturing them, but it still notices the app coming and going: it saves queued events when the app leaves and resumes delivery when it returns.

Crashes

$app_crashed counts crashes from the reports iOS keeps on its own, which it hands to the SDK 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 reports off your app's launch path.

The event carries crash_kind, which is crash, and $app_version, the version that crashed. 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 iOS gives for the report, not the time it arrived. Voidhash dates an event no more than 24 hours back, so a report that arrives later than that counts as 24 hours old.

iOS decides when it hands over a report and may not hand over every one. The SDK reads these reports on iOS only, so the event is not recorded on macOS or visionOS. A simulator does not produce them on its own.

Report opened notifications

When an end user opens a notification you sent through Voidhash, tell the SDK from your own notification handling. It records $push_opened with the notification's push_notification_send_id, which push open rates are built from:

func userNotificationCenter(
    _ center: UNUserNotificationCenter,
    didReceive response: UNNotificationResponse
) async {
    voidhash.trackPushOpened(userInfo: response.notification.request.content.userInfo)
}

The SDK does not take over your app's notification delegate, so this call is the only way it hears about an opened notification. Pass it 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:

let sessionId = await voidhash.sessionId()

Flush before a boundary

Events flush automatically in batches. While the app is in the foreground, 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). When the server asks the SDK to slow down, the SDK waits as long as it asks, up to 30 seconds. Force delivery when the app is about to cross an important boundary, such as a screen the customer may not return from:

await voidhash.flush()

This sends everything that is queued right now: sign-ins and customer traits first, then reported purchases, then events. Items waiting for their next retry go right away too, unless the server asked the SDK to wait or refused a reported purchase for good (see Make and restore purchases). After an outage or a rejected publishable key, flush() also checks whether the backend accepts requests again, at most once a minute.

flush() waits for Voidhash for at most 2 seconds, then returns while delivery continues in the background. One flush sends up to 10 batches of events, so a large backlog after an outage goes out over a few flushes. When the app moves to the background, the SDK sends what is queued once more while iOS still lets it run.

Undelivered events

Events wait on the device until Voidhash accepts them, across app restarts and outages. The device keeps up to 1,000 undelivered events. Beyond that the oldest are dropped, which the SDK reports as the ANALYTICS_EVENT_DROPPED diagnostic and a warning with the number dropped, at most once a minute. When Voidhash refuses an event as invalid, only that event is dropped; the events sent with it are still delivered.

Each event records the app version and build, OS version, locale, and time zone at the moment it was captured. An event sent after an app update still counts towards the release it happened in.

Update customer traits

Trait updates are saved on the device and sent separately from analytics events, so they are never dropped to make room for 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:

await voidhash.setAnalyticsConsent(.denied) // .granted, .denied or .pending
let 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 setPersonAttributesState returns .skipped. The exception is taking back ad data sharing.
  • The SDK does not ask Apple about the install. See Credit installs to Apple Ads.
  • 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. 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:

try await 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:

try await 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(.denied). Agreeing again drops a revocation still waiting to be sent.

Credit installs to Apple Ads

On a new install, the SDK asks Apple whether an Apple Ads campaign brought the end user, and records the answer on the person as their first and latest ad touch: the campaign, ad group and ad, whether the ad was tapped or only viewed, and when. The touch stays with an anonymous person and carries over when you later call identify. The SDK uses Apple's AdServices framework. It sends no advertising identifier and needs no App Tracking Transparency prompt.

The SDK does this on its own, 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. An end user who declines is never looked up, and withdrawing consent stops a lookup in progress.
  • Failures. When Apple cannot answer yet, or the device is offline, 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.
  • Existing installs. An install that was already running an earlier version of the SDK is not looked up, because the answer would describe a tap from long ago.
  • App extensions. An extension never looks anything up. It is not the install an ad brought.

To turn it off, set installAttribution to false in the options:

var options = VoidhashOptions()
options.installAttribution = false

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
PreviousEvaluate feature flagsNextTrack screens

On this page

Capture an eventAutomatic eventsCrashesReport opened notificationsSessionsFlush before a boundaryUndelivered eventsUpdate customer traitsAnalytics consentShare purchases with ad platformsCredit installs to Apple AdsNext steps