@trevosdk/react-native
v0.1.1
Published
Trevo React Native SDK — deterministic variant assignment and durable event tracking for iOS and Android
Maintainers
Readme
@trevosdk/react-native
Experiments and event tracking for React Native and Expo, assigning the same variants the web SDK does for the same user.
npm install @trevosdk/react-native @react-native-async-storage/async-storagePure TypeScript — no native module, no linking step, no config plugin.
Quick start
import { createClient } from '@trevosdk/react-native';
export const trevo = createClient({
apiKey: process.env.EXPO_PUBLIC_TREVO_KEY,
appVersion: '1.4.0',
});
await trevo.ready();
const variant = trevo.getVariant('checkout-v2');
trevo.track('add_to_cart', { sku: 'ABC' });Use a publishable key (tsk_live_…). A shipped binary can be unpacked, so
a secret key in an app is a leaked key.
Read this before you plan your first mobile experiment
Three properties of mobile are not choices we made, and every vendor shares them. Planning around them is easier than discovering them mid-experiment.
A mobile cohort ramps over weeks, not days. Experiment code ships inside an app release, so your population is whoever has updated. Web reaches full traffic within a day of activation; mobile climbs as adoption climbs, and both variants coexist across app versions for as long as old builds stay installed.
An experiment can be switched off without a release, but not changed. Config is re-read on every foreground, so pausing or stopping is immediate. Changing what a variant does is code, and code ships through App Store review.
Anonymous users are per-device. A visitor who browses on a laptop and then
opens the app is two participants until they sign in. identify() links them
from that point forward; nothing can link them retroactively.
Durability
Phones lose the network constantly, are killed without warning, and have wrong clocks more often than desktops. The SDK is built around that:
- Events survive a cold start. The queue is written to AsyncStorage and replayed on next launch. Delivery is at-least-once, and every event carries an idempotency key so a replay is recorded once.
- Timestamps survive a wrong clock. Each batch is stamped with
sentAtwhen it leaves the device. Ingestion compares that against its own clock to recover the device's offset and correct every event in the batch, which is what lets a queue drained three days late land on a real timeline. Nothing is dropped for being out of range — out-of-bounds values are clamped and marked. - Assignment survives no network at all. Config is persisted, so a cold start in a lift assigns the same variant it assigned yesterday instead of falling back to control and silently switching once the network returns.
minAppVersion
An experiment whose variant code only exists from a given build onward should not enrol devices that cannot render it:
{ "experimentKey": "checkout-v2", "minAppVersion": { "ios": "1.4.0", "android": "1.4.0" } }Builds below the threshold get control and record no exposure, so they never
appear in the results. This requires you to pass appVersion — without it, a
gated experiment returns control for everyone, because the SDK cannot prove the
build is new enough and guessing wrong means a broken screen.
API
createClient(options)
| Option | Default | Notes |
| --- | --- | --- |
| apiKey | — | Required. tsk_live_… |
| appVersion | — | This build's version. Required for minAppVersion gating |
| platform | Platform.OS | Override for tests |
| bootstrapConfig | — | Assign before storage and network answer |
| pollIntervalMs | 300000 | Foreground config refresh happens regardless |
| flushIntervalMs | 15000 | Also flushes on background |
| maxBatchSize | 50 | Capped at 500 by the server |
| storage | AsyncStorage | Override for tests |
| appState | RN AppState | Override for tests |
| fetch | global | Override for tests |
| onError | console.warn | Background failures surface here |
Methods
getVariant(key, { trackExposure })— deterministic and synchronous. Returnscontroluntilready()resolves.track(event, properties, { insertId })— queues an event.identify(userId)— associates subsequent events and assignment with a user.reset()— clears the user and mints a new anonymous id. Call on sign-out.getAnonymousId()— the device's stable id, once storage has been read.ready()— resolves once stored state is loaded and the first fetch settles.flush()/shutdown().
Runtime constraints
Hermes is not a browser. The SDK avoids sendBeacon, CompressionStream,
AbortSignal.timeout and crypto.randomUUID, none of which exist there;
the build fails if one is reintroduced.
Expo
Works in Expo Go and in development builds with no config plugin — there is no native code to link. AsyncStorage is a required peer, so install it too:
npx expo install @react-native-async-storage/async-storageIt is not optional: without persistence the anonymous id is re-minted on every launch, which re-buckets every user and corrupts any running experiment.
Store compliance
Apple
The package ships ios/PrivacyInfo.xcprivacy. Xcode aggregates third-party
manifests automatically — an app embedding an SDK without one is rejected at
submission. It declares product interaction and a user id, both linked, neither
used for tracking, and no required-reason APIs.
Google Play
Data safety content you can paste into the Play Console:
| Question | Answer | | --- | --- | | Does your app collect or share user data? | Yes | | Data type | App activity → App interactions | | Data type | App info and performance → Crash logs (only if you forward SDK errors) | | Data type | Device or other IDs → No — the id is generated by the SDK, not read from the device | | Collected or shared? | Collected | | Is it processed ephemerally? | No | | Is collection required? | Optional, if you gate initialisation on consent | | Purpose | Analytics; App functionality | | Is data encrypted in transit? | Yes | | Can users request deletion? | Yes — via your Trevo workspace |
The anonymous id is minted on device and is not an advertising or hardware identifier, which is why "Device or other IDs" is No. Declaring it there invites a policy review you do not need.
Correctness
This package runs the shared conformance vectors against its public API in CI, so a change that would bucket a user differently from the browser fails the build. The normative rules are in the bucketing spec.
License
MIT
