A/B Testing React Native SDK
Cross-platform TypeScript SDK for A/B experiments, feature flags, and event tracking on React Native. AsyncStorage offline cache, event listeners, 17 built-in event types.
Cross-platform TypeScript SDK for A/B testing and feature flags on React Native. AsyncStorage-backed offline cache, event listeners for SDK lifecycle, 17 built-in event types. Installs from Abrova Center.
Signed user tokens via tokenProvider are required for variants, tracking and flag evaluation. New in 0.2.2: you can initialize before sign-in, and the SDK waits for the user token instead of failing. See step 3, User tokens.
- React Native 0.60.0 or higher.
- React 16.8.0 or higher. Android SDK 21+ / iOS 12.0+.
Installation#
Install the package
# 1. .npmrc in your project root: @abrova packages come from Abrova Center @abrova:registry=https://center.abrova.ir/api/packages/abrova/npm/
# 2. Install npm install @abrova/ab-testing-react-native@^0.2.3
Or with Yarn:
yarn add @abrova/ab-testing-react-native@^0.2.3
Install peer dependency
The SDK is pure TypeScript and uses AsyncStorage for offline persistence:
npm install @react-native-async-storage/async-storage
iOS setup
cd ios && pod install && cd ..
No additional setup is required for Android.
Initialization#
Initialize AbrovaAbTesting early in your app, typically in App.tsx or index.js. Pass a tokenProvider that fetches an Abrova user token from your backend (see User tokens below).
import AbrovaAbTesting from '@abrova/ab-testing-react-native';
// Calls YOUR backend, which mints the token with your server secret.
// Returns null while no one is signed in.
async function fetchAbrovaToken(): Promise<string | null> {
if (!yourSessionToken) return null;
const res = await fetch('https://your-api.example.com/abrova-token', {
method: 'POST',
headers: { Authorization: `Bearer ${yourSessionToken}` }, // your app's own login session
});
return (await res.json()).token;
}
// Initialize the SDK
await AbrovaAbTesting.init({
apiKey: 'YOUR_API_KEY',
tokenProvider: fetchAbrovaToken, // required for variants, events and flags
debug: __DEV__, // Enable debug logging in development
// flushInterval: 10000, // Your own send interval in ms. Leave it out to follow the console
maxQueueSize: 100, // Maximum events to queue
});
// Set the signed-in user (optional with a user token: the token says who the user is)
await AbrovaAbTesting.setUserId('user-123');
await AbrovaAbTesting.setUserAttributes({
plan: 'premium',
country: 'US',
});getVariant, the flag getters and the track… methods never reject when you call them before init() or without a signed-in user: getters resolve with the default you pass, and tracking before init() does nothing.
Full App.tsx Example
import React, { useEffect, useState } from 'react';
import { View, ActivityIndicator } from 'react-native';
import AbrovaAbTesting from '@abrova/ab-testing-react-native';
// fetchAbrovaToken() calls your backend — see above.
function App() {
const [isReady, setIsReady] = useState(false);
useEffect(() => {
const initSDK = async () => {
try {
await AbrovaAbTesting.init({
apiKey: 'YOUR_API_KEY',
tokenProvider: fetchAbrovaToken,
debug: __DEV__,
});
await AbrovaAbTesting.setUserId('user-123');
} catch (error) {
console.error('AbrovaAbTesting init error:', error);
} finally {
setIsReady(true);
}
};
initSDK();
// Listen for events
const unsubError = AbrovaAbTesting.on('error', (event) => {
console.error('AbrovaAbTesting error:', event.data);
});
return () => {
unsubError();
};
}, []);
if (!isReady) {
return (
<View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
<ActivityIndicator size="large" />
</View>
);
}
return <MainNavigator />;
}
export default App;Configuration
| Option | Type | Required | Description |
|---|---|---|---|
| apiKey | string | required | Your project API key (`ab_live_…`). |
| tokenProvider | () => string | null | Promise<string | null> | - | Returns an Abrova user token from your backend, or `null` while no one is signed in. |
| userToken | string | - | A token you already hold. Prefer `tokenProvider`: a fixed token stops working when it expires. Hand over a newer one with `setUserToken`. |
| debug | boolean | false | Print SDK logs. |
| flushInterval | number | - | How often queued events are sent, in milliseconds. Leave it out to follow the Send interval set in the Abrova console (see below). |
| maxQueueSize | number | 1000 | Events kept while they cannot be sent. Default and maximum: 1000. |
Send interval and list cache
Two values come from your project's settings in the Abrova console (A/B Testing › Settings › Defaults):
- Send interval — how often queued events are sent. The SDK follows it when you leave
flushIntervalout. AflushIntervalyou pass is your own and is always used. - List cache — how long the SDK uses the experiment list before it asks the service again. There is no SDK option for it. Once the list is older than that, the SDK fetches it again in the background the next time your code reads a variant or tracks an event. The call doesn't wait for it, and a failed refresh does not emit an
errorevent.
A change you make in the console (a new experiment, changed weights or traffic) reaches running apps within the list cache time, without a restart. A user who already has a variant keeps it. A changed send interval is picked up at that same refresh. The SDK keeps the console's values within safe limits, and sends every 30 seconds until it knows the console's value. Apps on SDK versions older than 0.2.3 keep their built-in behaviour.
From 0.2.3, a flushInterval set in code always wins.
User tokens#
Anything the SDK does for a user needs a signed Abrova user token: getting a variant from the server, tracking events and conversions, syncing queued events, and evaluating feature flags. Loading the experiment and flag lists works with the API key alone, so your app can fetch them before sign-in.
Mint the token on your server
Add one endpoint, protected by your own login, that returns a token for the signed-in user. It calls Abrova with your project's server secret:
curl -X POST https://auth.abrova.ir/users/token \
-H "x-api-key: $ABROVA_API_KEY" \
-H "x-server-secret: $ABROVA_SERVER_SECRET" \
-H "Content-Type: application/json" \
-d '{"userId": "user-123", "expiresIn": 3600}'
# → { "token": "eyJhbGciOi…", "userId": "user-123", "expiresIn": 3600, "expiresAt": "…" }expiresIn is in seconds: 3600 (1 hour) by default, 86400 (24 hours) maximum.
The server secret can mint a token for any user. Keep it on your server, out of your JavaScript bundle and source code.
One token for A/B Testing, Chat and Sync
This is the same Abrova user token that Abrova Chat and Abrova Sync use. Give that same token to every SDK's tokenProvider instead of fetching one per product.
A token is valid until it expires, so store it with the user's session (Keychain, Keystore or secure storage) and reuse it across app launches. Your app then only calls your backend when the token is close to expiry — not on every start.
How the SDK uses it
The SDK caches the token, calls `tokenProvider` again shortly before it expires, and asks once more if the service reports it expired.
No `tokenProvider`? Call `setUserToken(token)` after sign-in and whenever you refresh the token, and `setUserToken(null)` at sign-out. A token passed as `userToken` is not refreshed.
The service takes the user from the token, so the SDK does too. You may skip `setUserId` and let the token say who the user is.
If you do call `setUserId`, pass the same user ID the token was minted for.
Before your user signs in
You can start the SDK before anyone is signed in: return null from tokenProvider and call refreshUserToken() when the user signs in or out. If you hand tokens over yourself, call setUserToken(token) on sign-in and setUserToken(null) on sign-out.
Until it has a token, the SDK waits: getVariant and the flag getters return your defaults at once, nothing is sent, and nothing is reported as an error. When the token arrives, the SDK continues by itself.
AbrovaAbTesting.isAwaitingUserToken; // true while it waits
AbrovaAbTesting.onAwaitingUserToken(() => {}); // called each time it starts waiting; returns an unsubscribe function
AbrovaAbTesting.on('userTokenReady', () => {}); // a token arrived: read variants again
// After sign-in
await AbrovaAbTesting.refreshUserToken(); // with a tokenProvider: asks it again now
await AbrovaAbTesting.setUserToken(token); // without one: hand the token over
// After sign-out
await AbrovaAbTesting.clearUserId(); // or setUserToken(null)Because getters return defaults while the SDK waits, read your variants again when userTokenReady fires:
useEffect(() => {
const load = async () => setVariant(await AbrovaAbTesting.getVariant('checkout-style', 'control'));
load();
return AbrovaAbTesting.on('userTokenReady', load);
}, []);Events tracked while the SDK waits are kept (up to 1,000, the oldest dropped first). After sign-in they are sent only if they were recorded for that same user, that is, setUserId was called with their ID before the token arrived. Events recorded while no user was set belong to nobody, are not credited to anyone, and are dropped when a user signs in.
Getting Experiment Variants#
Get the variant assigned to the current user for an experiment. The variant always comes from the service (this needs a user token) and is cached for the user; the SDK never works one out on the device.
The React Native SDK returns the variant's name as it appears in Abrova Console (for example Control or Variant B), not its key (control, variant-b).
import AbrovaAbTesting from '@abrova/ab-testing-react-native';
// Get variant (with optional default)
const variant = await AbrovaAbTesting.getVariant(
'checkout-button-color',
'control' // returned while there is no user token, or when the service
// cannot be reached and nothing is cached
);
// Use the variant
switch (variant) {
case 'Variant B':
showNewCheckout();
break;
default:
showOriginalCheckout();
}Getting Variant Configuration
// Get variant config (set in the Abrova dashboard)
const config = await AbrovaAbTesting.getVariantConfig('checkout-button-color');
const buttonText = config?.buttonText ?? 'Buy Now';
const buttonColor = config?.buttonColor ?? '#6B7280';Event Tracking#
Track user interactions to measure experiment results. The SDK supports 17 built-in event types. Events are queued locally and sent in batches with the current user's token. The queue holds at most 1,000 events (the oldest are dropped first), and the SDK waits before retrying when the service is busy or your project has reached its usage limit. Each event carries the time your app recorded it, so events sent together keep their order.
Core Events
// Track when user sees the experiment
await AbrovaAbTesting.trackView('experiment-key');
// Track click
await AbrovaAbTesting.trackClick('experiment-key');
// Track conversion with value
await AbrovaAbTesting.trackConversion('experiment-key', 300000);
// Track custom event
await AbrovaAbTesting.trackCustomEvent('experiment-key', 'button_hover', {
duration_ms: 1500,
element: 'cta_button',
});Conversion Events
await AbrovaAbTesting.trackAddToCart('experiment-key', 300000, 'sku-123', { quantity: 2 });
await AbrovaAbTesting.trackRemoveFromCart('experiment-key', 300000, 'sku-123');
await AbrovaAbTesting.trackBeginCheckout('experiment-key', 1500000, { coupon: 'SAVE10' });
await AbrovaAbTesting.trackPurchase('experiment-key', 1250000, 'txn-789', { currency: 'Toman' });Engagement Events
await AbrovaAbTesting.trackScroll('experiment-key', 75.0, { page: 'product' });
await AbrovaAbTesting.trackFormSubmit('experiment-key', 'signup_form');
await AbrovaAbTesting.trackSearch('experiment-key', 'search query', { results: 42 });
await AbrovaAbTesting.trackShare('experiment-key', 'twitter');Media Events
await AbrovaAbTesting.trackVideoStart('experiment-key', 'video-001');
await AbrovaAbTesting.trackVideoComplete('experiment-key', 'video-001');Auth Events
await AbrovaAbTesting.trackSignUp('experiment-key', 'google');
await AbrovaAbTesting.trackLogin('experiment-key', 'email');
await AbrovaAbTesting.trackLogout('experiment-key');Generic Event
import { EventType } from '@abrova/ab-testing-react-native';
await AbrovaAbTesting.trackEvent(
'experiment-key',
EventType.CUSTOM,
'page_load_time',
2.3,
{ page: '/checkout', cached: false }
);Feature Flags#
Toggle features remotely without deploying new code. isFeatureEnabled and getFeatureValue are evaluated on the server for the signed-in user, so they need a user token too; while there is no token, or if the request fails, they return the default you pass.
// Check if feature is enabled
const isDarkMode = await AbrovaAbTesting.isFeatureEnabled('dark-mode');
if (isDarkMode) {
enableDarkTheme();
}
// Get feature value (string, number, JSON, etc.)
const maxUpload = await AbrovaAbTesting.getFeatureValue('max-upload-size', 10);
// Get all feature flags
const flags = await AbrovaAbTesting.getFeatureFlags();
flags.forEach((flag) => {
console.log(`${flag.key}: enabled=${flag.enabled}, rollout=${flag.rolloutPercentage}%`);
});
// Refresh flags from server
await AbrovaAbTesting.refreshFeatureFlags();User Identification#
With a user token, the token says who the user is, so setUserId is optional. If you call it, do so on login with the ID the token is minted for, and call clearUserId() on logout.
When a user signs out (clearUserId(), setUserToken(null)) or another signs in (setUserId with their ID, or a token for another user), the previous user's variants, attributes, token and unsent events are dropped, on the device too, before anything is sent for the next one. Events still waiting go out under the previous user's own token, never under another user's.
// Set user ID (e.g., after login)
await AbrovaAbTesting.setUserId('user-123');
// Sign the user out: drop their data, token and unsent events
await AbrovaAbTesting.clearUserId();
// Get current user ID
const userId = await AbrovaAbTesting.getUserId();
// Set user attributes for targeting
await AbrovaAbTesting.setUserAttributes({
plan: 'premium',
country: 'US',
age: 25,
platform: 'react-native',
});Event Listeners#
Subscribe to SDK lifecycle events.
// Listen for experiment assignments
const unsub1 = AbrovaAbTesting.on('experimentAssigned', (event) => {
console.log('Assigned:', event.data);
});
// Listen for experiments refresh
const unsub2 = AbrovaAbTesting.on('experimentsRefreshed', (event) => {
console.log('Experiments refreshed:', event.data);
});
// Listen for feature flag updates
const unsub3 = AbrovaAbTesting.on('featureFlagsRefreshed', (event) => {
console.log('Feature flags refreshed');
});
// Listen for sync events
const unsub4 = AbrovaAbTesting.on('syncCompleted', (event) => {
console.log('Sync:', event.data);
});
// Listen for errors
const unsub5 = AbrovaAbTesting.on('error', (event) => {
console.error('Error:', event.data);
});
// The SDK started waiting for a user token / a token arrived
const unsub6 = AbrovaAbTesting.on('awaitingUserToken', () => {
console.log('Waiting for sign-in');
});
const unsub7 = AbrovaAbTesting.on('userTokenReady', () => {
console.log('User token ready: read variants again');
});
// Cleanup in useEffect
useEffect(() => {
return () => {
unsub1();
unsub2();
unsub3();
unsub4();
unsub5();
unsub6();
unsub7();
};
}, []);Lifecycle#
// Refresh experiments from server await AbrovaAbTesting.refreshExperiments(); // Get all experiments const experiments = await AbrovaAbTesting.getExperiments(); // Flush pending events immediately // (the SDK also sends queued events when the app goes to the background) await AbrovaAbTesting.flush(); // Wipe all SDK state, including unsent events. You must call init() again afterwards. // To sign a user out, call clearUserId() instead. await AbrovaAbTesting.reset();
EventType Enum#
| Option | Type | Required | Description |
|---|---|---|---|
| EventType.VIEW | - | - | trackView() |
| EventType.CLICK | - | - | trackClick() |
| EventType.CONVERSION | - | - | trackConversion() |
| EventType.CUSTOM | - | - | trackCustomEvent() |
| EventType.SCROLL | - | - | trackScroll() |
| EventType.FORM_SUBMIT | - | - | trackFormSubmit() |
| EventType.SEARCH | - | - | trackSearch() |
| EventType.SHARE | - | - | trackShare() |
| EventType.ADD_TO_CART | - | - | trackAddToCart() |
| EventType.REMOVE_FROM_CART | - | - | trackRemoveFromCart() |
| EventType.BEGIN_CHECKOUT | - | - | trackBeginCheckout() |
| EventType.PURCHASE | - | - | trackPurchase() |
| EventType.VIDEO_START | - | - | trackVideoStart() |
| EventType.VIDEO_COMPLETE | - | - | trackVideoComplete() |
| EventType.SIGN_UP | - | - | trackSignUp() |
| EventType.LOGIN | - | - | trackLogin() |
| EventType.LOGOUT | - | - | trackLogout() |
API Reference#
| Option | Type | Required | Description |
|---|---|---|---|
| init(config) | - | - | Initialize the SDK (pass `tokenProvider`) |
| setUserId(id) | - | - | Set the signed-in user. Optional with a user token |
| clearUserId() | - | - | Sign the user out: drop their data, token and unsent events |
| isAwaitingUserToken | - | - | `true` while the SDK waits for a user token |
| onAwaitingUserToken(callback) | - | - | Called each time the SDK starts waiting; returns an unsubscribe function |
| setUserToken(token) | - | - | Hand over a user token; `null` when the user signs out |
| refreshUserToken() | - | - | Ask `tokenProvider` again (call on sign-in and sign-out) |
| getUserId() | - | - | Get current user ID |
| setUserAttributes(attrs) | - | - | Set targeting attributes |
| getVariant(key, default?) | - | - | Get assigned variant |
| getVariantConfig(key) | - | - | Get variant configuration |
| isFeatureEnabled(key, default?) | - | - | Check if feature flag is on |
| getFeatureValue(key, default?) | - | - | Get feature flag value |
| getFeatureFlags() | - | - | Get all feature flags |
| refreshFeatureFlags() | - | - | Refresh flags from server |
| getExperiments() | - | - | Get all experiments |
| refreshExperiments() | - | - | Refresh experiments from server |
| trackEvent(key, type, ...) | - | - | Track generic event |
| flush() | - | - | Force sync pending events |
| reset() | - | - | Clear all state, cache and unsent events |
| on(event, callback) | - | - | Subscribe to SDK events |
| off(event, callback) | - | - | Unsubscribe from events |
Complete Example#
A complete example showing A/B testing in a React Native checkout screen:
import React, { useEffect, useState } from 'react';
import {
View, Text, TouchableOpacity, StyleSheet,
ActivityIndicator,
} from 'react-native';
import AbrovaAbTesting from '@abrova/ab-testing-react-native';
// fetchAbrovaToken() calls your backend — see Initialization.
function CheckoutScreen() {
const [variant, setVariant] = useState<string | null>(null);
const [loading, setLoading] = useState(true);
const orderTotal = 1000000;
useEffect(() => {
const init = async () => {
await AbrovaAbTesting.init({
apiKey: 'YOUR_API_KEY',
tokenProvider: fetchAbrovaToken,
debug: __DEV__,
});
await AbrovaAbTesting.setUserId('user-123');
const v = await AbrovaAbTesting.getVariant('checkout-style', 'control');
setVariant(v);
setLoading(false);
await AbrovaAbTesting.trackView('checkout-style');
};
init();
}, []);
const handleCheckout = async () => {
await AbrovaAbTesting.trackClick('checkout-style');
await AbrovaAbTesting.trackConversion('checkout-style', orderTotal);
console.log('Checkout completed!');
};
if (loading) {
return (
<View style={styles.center}>
<ActivityIndicator size="large" />
</View>
);
}
const buttonColor = variant === 'Variant B' ? '#22C55E' : '#6B7280';
return (
<View style={styles.container}>
<Text style={styles.title}>Order Summary</Text>
<Text style={styles.total}>Total: ${orderTotal.toFixed(2)}</Text>
<TouchableOpacity
style={[styles.button, { backgroundColor: buttonColor }]}
onPress={handleCheckout}
>
<Text style={styles.buttonText}>Complete Purchase</Text>
</TouchableOpacity>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, padding: 20, backgroundColor: '#fff' },
center: { flex: 1, justifyContent: 'center', alignItems: 'center' },
title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
total: { fontSize: 18, color: '#6B7280', marginBottom: 32 },
button: { padding: 16, borderRadius: 8, alignItems: 'center' },
buttonText: { color: '#fff', fontSize: 18, fontWeight: '600' },
});
export default CheckoutScreen;