Apps
A/B testing and analytics in mobile apps: screens as page views, app version targeting, offline events, retention, and why app experiments live in the cloud.
SplitHub runs in mobile and desktop apps the same way it runs on a website: one install counts screens and events, and runs experiments with the same verdicts. An app and a website can share a project, so a visitor who signs up on the web and buys in the app is one funnel.
Pick your SDK
| App | SDK | Install |
|---|---|---|
| React Native, Expo | @splithub/react-native |
npx splithub init (detects Expo and React Native) |
| iOS, macOS (Swift) | Swift package SplitHub |
Xcode → Add Package Dependencies |
| Android (Kotlin) | io.splithub:splithub (+ splithub-compose) |
one Gradle line |
Guides: React Native and Expo, iOS (Swift), Android (Kotlin).
The native iOS and Android SDKs are new (version 0.1). They pass the same assignment tests as the web SDK; expect their API to grow.
What stays the same
- Assignment is the same local hash on every platform: no network call on render, and the same visitor id gets the same variant on web, iOS and Android.
- Events are
track(name, props?). Any name works; mark one as a goal and experiments count it. - Verdicts come from the same sequential test, with the same split check, minimum sample and stars. See Statistics.
- The CLI and the MCP server read app experiments and app analytics with the same commands and tools.
Screens are page views
A screen is a page row: the url is the route (/product/42 with Expo Router) or /<screen name> (/Settings), the title is the screen name. So every page report, funnel step (page:/Checkout) and pageview goal ({ type: 'pageview', path: '/Checkout' }) works on screens with no change.
React Native tracks screens for you with <ExpoRouterScreens /> or <NavigationScreens />. Native apps call SplitHub.screen("Settings"), or use .splithubScreen("Settings") in SwiftUI and TrackScreen("Settings") in Compose.
Why app experiments live in the cloud
A website deploys in minutes. An app waits for store review, then for people to update, and old versions stay in use for months. So for apps:
- The variants ship inside the build: the code for each version you want to try.
- Everything else lives in the dashboard: start, pause, finish, weights,
valuepayloads, targeting. Installed apps pick it up on their next launch, with no release. - New
valueexperiments (copy, prices, flags as JSON) can start from the dashboard, as long as the app reads that experiment.
On the very first launch the SDK waits up to 400 ms for the experiments and shows the control meanwhile. After that it keeps a copy on the device, decides at once, and refreshes in the background. A change never flips a variant in the middle of a session.
That is why native apps always use SplitHub Cloud, and why init suggests the cloud for React Native too. A React Native app can still keep splithub.config.ts; then npx splithub push keeps the cloud and the file in agreement, and pushed experiments reach apps whose build has their variants. Redirect experiments don't apply in an app: use value or component.
Target app versions
Every event carries the app's platform and version, so a targeting flow can use them:
| Field | Values | Operators |
|---|---|---|
platform |
ios, android, macos, windows, linux, web |
is, is_not, in, not_in |
app_version |
2.4.1 |
gte (at least), lt (below), and the text operators |
flow: {
start: 'new-app',
nodes: {
'new-app': { type: 'condition', conditions: [{ field: 'app_version', op: 'gte', value: '2.4' }], then: 'split', else: 'out' },
split: { type: 'split', outputs: [{ weight: 50, to: 'a' }, { weight: 50, to: 'b' }] },
a: { type: 'variant', variant: 'control' },
b: { type: 'variant', variant: 'new-checkout' },
out: { type: 'skip' },
},
},
Versions compare part by part, so 2.10.0 is at least 2.9. Gate on the first version that ships the new variant's code: older builds see the control and are not counted. More in Targeting flows.
Offline and sessions
- Events go out in small batches, and right away when the app moves to the background.
- Without a connection they wait on the device (up to 1,000) and go out later with their original time. Today's numbers can still grow a little when phones come back online.
- A new session starts when the app returns after 30 minutes in the background.
- Development builds send nothing, so your own taps never count. Check tracking with a release build.
Reports for apps
Make an app property for each app: npx splithub analytics connect creates one with kind: 'app' in an app project, or pick App when you create a property in the dashboard. It uses screens instead of pages and hides the web-only panels.
- Platform and app version are dimensions and filters everywhere:
analytics <id> --by app_version,results <id> --by platform,--filter platform=ios. - Retention is the report most apps live by: users grouped by the day or week they started, and how many came back after.
npx splithub analytics retention <id>; see Reports.
Allowed domains for apps
An app has no web origin, so its app id stands in for one. Add it to an experiment's or property's allowed list next to your sites:
npx splithub domains set property sh-x1h78w example.com com.acme.shop
App ids match exactly. A list that names only websites refuses apps.