Abrova A/B Testing
A/B experiments, feature flags, event tracking, offline sync, mutual exclusion, and holdout groups. SDKs for Android, iOS, Flutter, React Native, Node.js, and Next.js.
Run controlled experiments, ship features behind flags, and measure conversion impact with statistical significance. The Abrova A/B Testing platform includes experiments, feature flags, event tracking, offline sync, mutual exclusion groups, holdout groups, and webhooks.
Using an AI assistant? See AI assistants for how to give it these docs.
2,000 participants are free every month. The price above that is set at launch and will be paid from your prepaid wallet. See Pricing.
Quick Start#
Follow these steps to integrate Abrova A/B Testing into your app:
Add the Abrova Center registry, then install the SDK (version 0.2.3 or later)
Your server mints a user token for the signed-in user
Configure with your API key and a tokenProvider
Fetch variants, then log views and conversions
Platform SDKs#
Select your platform to view detailed installation and usage instructions.
Native Kotlin SDK for Android apps
Native Swift SDK for iOS apps
Cross-platform Dart SDK for Flutter apps
Cross-platform SDK for React Native apps
Client-side SDK for Next.js / React web apps
Server-side SDK for Node.js backends
User tokens#
Your API key identifies your project, not your user. It ships inside every app and web bundle, so anyone can read it. To know which user a request is for, your server issues a short-lived user token and the SDK sends it with every request.
| Credential | Lives in | Proves |
|---|---|---|
API key (ab_live_…) | Your app, public | Which project |
Server secret (ab_srv_…) | Your server only | That your backend is calling |
| User token (1 hour by default) | Your app, per user | Which user |
A user token is required for everything that acts for a user: getting a variant, tracking events and conversions, and evaluating feature flags. Loading the experiment and flag lists works with the API key alone, so your app can do that before anyone signs in.
One token for every Abrova product
Mint the token on your server from the Abrova auth endpoint:
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": "2026-09-27T10:00:00.000Z" }expiresIn is in seconds: 3600 (1 hour) by default, 86400 (24 hours) maximum. The same token works for Abrova A/B Testing, Chat, Sync and Push — give it to every SDK's tokenProvider instead of fetching one per product. Using Node.js? createUserToken(userId) in the Node.js SDK does this call for you.
The SDKs call tokenProvider when they need a token, again shortly before it expires, and once more if the service reports it expired.
The token decides the user
The service takes the user from the signed token, not from the app, and the SDKs follow it: once an SDK has a token, it acts as the user that token was minted for. If you also call setUserId, pass the same ID your server puts in the token. On React Native and Next.js you may skip setUserId altogether.
Before your user signs in
You can initialize a client SDK before anyone is signed in. Have tokenProvider return null while there is no user (on iOS, complete with .failure). From version 0.2.2 the SDK then waits for a user token instead of failing:
getVariantreturns the default you pass. Flags return your default or, on Android, iOS and Flutter, the answer from the cached flag list.- Nothing is sent for a user, and no error is reported.
- Events you track are kept on the device, up to 1,000.
isAwaitingUserToken is true while the SDK waits, and onAwaitingUserToken is called each time it starts waiting. When a token arrives, the SDK carries on by itself: it fetches the variants your app asked for and sends the kept events.
Tell the SDK when the user signs in or out. The calls have the same names in every client SDK:
| When | With a tokenProvider | Handing tokens over yourself |
|---|---|---|
| After sign-in | refreshUserToken() | setUserToken(token) |
| After sign-out | clearUserId() | setUserToken(null) (nil on iOS) |
Events are credited to the user in the token. Events recorded before sign-in are only sent if they were recorded for the same user ID that signs in (you called setUserId with that ID before tracking). Events recorded for an anonymous user are not credited to anyone and are dropped at sign-in.
Signing out and switching users
A sign-out, a setUserId call with a different ID, or a token for another user clears the previous user's variants, attributes, token and pending events before anything is done for the next user. Events of the previous user that are ready to send go out under that user's own token, never under another user's.
Signed-out visitors need a token too if you want them in an experiment. Either have your server mint one for an anonymous ID (for example a device or session ID you pass to setUserId), or run experiments only after sign-in. Without a token, the service records no assignments or events for that user.
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.
The server secret stays on your server. Never put it in an app, a browser bundle, or a NEXT_PUBLIC_ variable. Server-side code (the Node.js SDK) uses the server secret directly instead of a user token.
All Features#
Abrova A/B Testing provides a complete experimentation platform with these features.
Run controlled experiments to optimize user experience
Queue events when offline, sync automatically when back online
Toggle features on/off without deploying code
Prevent users from being in conflicting experiments
Measure cumulative impact of your experimentation program
Track conversions, views, and custom events
Get notified when experiments start, pause, complete, or are archived
One token per user, shared with Abrova Chat, Sync and Push
A/B Testing#
Run controlled experiments to test different variations of your app and measure which performs better.
How it works
- Create an experiment in the dashboard with multiple variants (e.g., control, variant-a, variant-b)
- Set traffic allocation — decide what percentage of users should be in the experiment
- Get variant in your app — the service decides which variant the user should see, and the SDK returns it and remembers it
- Track events — log views, clicks, and conversions
- Analyze results — see which variant performs better with statistical significance
Pseudocode Example
The basic experiment loop, in any client SDK (see each platform page for exact syntax):
// 1. Initialize with your API key and a token provider
AbrovaAbTesting.init({
apiKey: "YOUR_API_KEY",
tokenProvider: () => fetchAbrovaTokenFromYourServer(),
})
// 2. Identify the signed-in user (on logout, call clearUserId())
AbrovaAbTesting.setUserId("user-123")
// 3. Get variant for the current user
const variant = await AbrovaAbTesting.getVariant(
"checkout-button-color",
"control" // default until the service has assigned a variant
)
// 4. Apply variant in your UI
// Android and iOS return the variant key ("variant-a"); Flutter, React Native,
// Next.js and Node.js return the variant name from the Console ("Variant A")
if (variant === "variant-a") {
button.color = "green"
button.text = "Buy Now!"
} else if (variant === "variant-b") {
button.color = "blue"
button.text = "Add to Cart"
} else {
button.color = "gray" // control
button.text = "Purchase"
}
// 5. Track when user views the experiment
AbrovaAbTesting.trackView("checkout-button-color")
// 6. Track conversion when user completes action
AbrovaAbTesting.trackConversion(
"checkout-button-color",
300000 // optional: revenue value
)Variants are decided by the service only. When the service can't be asked (no network, or no user token yet), the SDKs return the assignment they cached earlier, or your default. No SDK works a variant out on the device.
On Android and iOS, the synchronous getVariant never blocks: it returns the stored variant, or your default while the assignment loads. Use getVariantAsync (Android) or the completion form (iOS) the first time a screen needs a variant, so the user doesn't briefly see the default.
- Always track views to accurately measure conversion rates.
- Run experiments for at least 2 weeks to account for weekly patterns.
- Aim for statistical significance (95%+) before making decisions.
- Only test one thing at a time for clear results.
Offline Support#
Abrova A/B Testing SDKs support offline mode, so your app keeps tracking events without a network connection.
- Events are stored on the device while offline
- Automatic sync when network connectivity is restored
- Events persist across app restarts
- Each event carries a unique
clientEventId, saved with the event - Each event carries the time the app recorded it, so events sent together keep their order
- At most 1,000 events are kept on the device
- The SDKs send events in batches, and wait before retrying when the service is busy or your project has reached its usage limit
- The sync endpoint accepts up to 500 events per request
List cache (default 300 seconds) and send interval (default 30 seconds) are set in the Console; the SDKs keep up to 1,000 events and send 100 at a time.
Send interval and list cache
From version 0.2.3, every SDK follows two of your project's settings in the Abrova console (A/B Testing › Settings › Defaults):
- Send interval — how often queued events are sent. The SDKs follow it unless you set your own
flushIntervalin code; then your value wins. Each platform page says how to set it. - List cache — how long an SDK reuses 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, without blocking the call.
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 SDKs keep both values within safe limits. Apps on SDK versions older than 0.2.3 keep their built-in behaviour.
Benefits
- No lost events due to network issues
- Works in subway, airplane, rural areas
- Reduces battery usage with batched syncs
- Survives app crashes and restarts
Flutter
import 'package:abrova_ab_testing/abrova_ab_testing.dart';
// Offline support is always on
await AbrovaAbTesting.init(AbrovaAbTestingConfig(
apiKey: 'YOUR_API_KEY',
tokenProvider: fetchAbrovaToken, // returns a token from your server, or null while signed out
));
await AbrovaAbTesting.instance.setUserId('user-123');
// Events are queued automatically when offline
await AbrovaAbTesting.instance.trackConversion('checkout-flow', value: 1000000);
// Send queued events now (optional - automatic sync runs on the send interval)
await AbrovaAbTesting.instance.flush();Android
import ir.abrova.abtesting.AbrovaAbTesting
import ir.abrova.abtesting.AbrovaAbTestingConfig
import ir.abrova.abtesting.AbrovaTokenProvider
// Offline support is enabled by default
AbrovaAbTesting.init(
context = applicationContext,
config = AbrovaAbTestingConfig(
apiKey = "YOUR_API_KEY",
tokenProvider = AbrovaTokenProvider { myBackend.fetchAbrovaTokenBlocking() }
)
)
// Events are queued automatically when offline
AbrovaAbTesting.trackConversion("checkout-flow", 1000000)
// Send queued events now (optional - automatic sync runs periodically)
AbrovaAbTesting.flush()REST API
For custom SDK implementations, use the sync endpoint to batch upload events. Send the user's token in x-user-token; the events are recorded for the user the token names.
curl -X POST 'https://abtest.abrova.ir/public/sync' \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'x-user-token: USER_TOKEN' \
-d '{
"events": [
{
"experimentId": "checkout-flow",
"variantId": "",
"userId": "user-123",
"eventType": "conversion",
"eventName": "purchase",
"eventValue": 1000000,
"metadata": { "product": "premium" },
"timestamp": 1790505000000,
"clientEventId": "client-event-abc123"
}
],
"deviceId": "device-xyz",
"sdkVersion": "custom-1.0.0"
}'
# Response:
{
"success": true,
"synced": 1,
"failed": 0
}experimentId accepts the experiment key. Leave variantId empty to use the variant the user is assigned to.
timestamp is optional: the time your app recorded the event, in epoch milliseconds (an ISO 8601 string is also accepted). It keeps the events of one request in the order they happened. An event without it is still recorded.
Use the /public/init endpoint to fetch experiments and configuration in a single call. It returns the project's running experiments and the list cache and send interval set in the Console. It needs only the API key.
Feature Flags#
Toggle features on or off without deploying new code. Perfect for gradual rollouts, kill switches, and beta features.
Use Cases
- Gradual Rollouts — Release to 5% users, then 25%, then 100%
- Kill Switches — Instantly disable problematic features
- Beta Features — Enable for specific user segments
- Seasonal Features — Toggle holiday themes on/off
Flag Properties
- Key — Unique identifier (e.g.,
new-checkout-flow) - Enabled — Whether the flag is on or off
- Rollout % — Percentage of users who see the feature
- Targeting — Rules based on user attributes
Pseudocode Example
// Check if a feature is enabled for the signed-in user
const isEnabled = await AbrovaAbTesting.isFeatureEnabled("new-checkout-flow")
if (isEnabled) {
showNewCheckoutFlow()
} else {
showLegacyCheckout()
}
// Get a feature flag value (for multivariate flags)
const value = await AbrovaAbTesting.getFeatureValue(
"button-color",
"blue" // default value
)
button.color = value
// Feature flags with user targeting
// In dashboard, set rules like:
// - Enable for users where plan = "premium"
// - Enable for users where country in ["US", "CA"]
// - Enable for 10% of all other users- Open Abrova Flags in the sidebar and go to Feature Flags (the A/B Testing Feature Flags page links there)
- Click Create Flag
- Enter a key (e.g.,
new-checkout-flow) and name - Set rollout percentage (0-100%)
- Check Enable immediately to turn it on
Mutual Exclusion Groups#
Prevent users from being enrolled in multiple conflicting experiments at the same time.
If you're running two experiments that both affect the checkout flow, a user being in both could skew your results. Mutual exclusion ensures clean, uncontaminated experiment data.
Example Scenario
You have two checkout experiments:
- Experiment A: Button color test
- Experiment B: Checkout flow redesign
If a user is in both, you can't tell which change caused the conversion.
How It Works
- Create an exclusion group
- Add experiments from the same project to the group
- Set total traffic allocation for the group
- Each user is assigned to ONE experiment in the group
Example
// Exclusion Group: "Checkout Experiments"
// Traffic Allocation: 100% (all users eligible)
// Experiments in group:
// - checkout-button-color
// - checkout-flow-redesign
// A user is placed in only one of them - never both.
// In your app, just call getVariant normally:
const variant = await AbrovaAbTesting.getVariant("checkout-button-color")- Go to Exclusion Groups in the sidebar
- Click Create Group
- Name your group (e.g., "Checkout Experiments")
- Set traffic allocation (% of users eligible for this group)
- Select which experiments belong to this group
Holdout Groups#
Measure the cumulative impact of your entire experimentation program by excluding a percentage of users from ALL experiments.
A holdout group is a percentage of users (typically 5-10%) who never see any experiments. They always get the control experience. By comparing the holdout group to users who participate in experiments, you can measure the overall value of your experimentation program.
Always see the control experience. Never enrolled in any experiment.
Participate in experiments as normal. May be in control or variant groups.
Settings in the Console shows how many users are in the holdout and how many are in experiments.
In your app
Nothing changes in your code. For a holdout user, getVariant() returns the control variant for every experiment, so they see the default experience:
const variant = await AbrovaAbTesting.getVariant("checkout-experiment")
applyVariant(variant) // the control variant for holdout usersTracking calls are always safe to make: you don't need to check whether the user is in an experiment before you track. The service decides what counts, and events of a user who is kept out of an experiment never enter that experiment's results.
- Go to Settings in the sidebar
- Find Holdout Group Configuration
- Toggle Enable Holdout Group
- Set percentage (recommended: 5-10%)
- Click Save Holdout Settings
Webhooks#
Get notified in real time when an experiment changes state. Each webhook belongs to one project and receives only that project's events.
Events
experiment.started— Experiment goes liveexperiment.paused— Experiment pausedexperiment.completed— Experiment endsexperiment.archived— Experiment archived
Use Cases
- Post to your team chat when experiments start or end
- Trigger CI/CD pipelines when an experiment completes
- Update external systems with experiment status
Webhook Payload Example
POST https://your-server.com/webhooks/abrova-ab-testing
Content-Type: application/json
X-AbrovaAbTesting-Event: experiment.completed
X-AbrovaAbTesting-Webhook-Id: 7c1f…
X-AbrovaAbTesting-Timestamp: 2026-09-27T10:30:00.000Z
X-AbrovaAbTesting-Signature: 5d41402abc4b2a76b9719d911017c592…
{
"event": "experiment.completed",
"timestamp": "2026-09-27T10:30:00.000Z",
"webhookId": "7c1f…",
"experimentId": "…",
"experimentKey": "checkout-button-color",
"experimentName": "Checkout Button Color",
"status": "completed",
"endDate": "2026-09-27T10:30:00.000Z"
}Verifying the signature
When you set a secret, X-AbrovaAbTesting-Signature is the hex HMAC-SHA256 of the raw request body, keyed with your secret:
import { createHmac, timingSafeEqual } from 'crypto';
function isValid(rawBody: string, signature: string, secret: string) {
const expected = createHmac('sha256', secret).update(rawBody).digest('hex');
return expected.length === signature.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}Webhook URLs must be public http(s) addresses. Failed deliveries are retried.
- Pick your project in the sidebar and go to Webhooks
- Click Create Webhook
- Enter your endpoint URL
- Select which events to receive
- (Optional) Add a secret for signature verification
- Click Test to verify it works
REST API#
Use the REST API directly for platforms without an SDK. Calls that act for a user need the user's token in x-user-token (see User tokens above); from your own server you can send x-server-secret instead.
Get Variant Assignment
curl -X POST 'https://abtest.abrova.ir/public/assign' \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'x-user-token: USER_TOKEN' \
-d '{
"experimentKey": "checkout-button-color",
"userId": "user-123",
"userAttributes": {
"plan": "premium",
"country": "US"
}
}'
# Response:
{
"data": {
"experimentId": "…",
"experimentKey": "checkout-button-color",
"variantId": "…",
"variantKey": "variant-a",
"variantName": "Variant A",
"config": { "buttonColor": "#22C55E", "buttonText": "Buy Now" },
"isControl": false,
"isNew": true
}
}Track Events
Send events with /public/sync (see Offline Support above). It accepts experiment keys and up to 500 events per request.
Evaluate a Feature Flag
curl -X POST 'https://abtest.abrova.ir/public/flag-evaluation' \
-H 'Content-Type: application/json' \
-H 'x-api-key: YOUR_API_KEY' \
-H 'x-user-token: USER_TOKEN' \
-d '{
"flagKey": "new-checkout-flow",
"userId": "user-123",
"userAttributes": {
"plan": "premium"
}
}'
# Response:
{
"flagKey": "new-checkout-flow",
"enabled": true,
"value": "variant-b"
}Errors
A request that acts for a user without a valid token is refused with 401 and a code:
token_required— no user token was senttoken_expired— the token expired; fetch a new one and retrytoken_invalid— the token is not valid for this project
Where to next?#
- Platform SDK docs — Android, iOS, Flutter, React Native, Next.js, Node.js — for install + code examples
- Feature Flags SDK — standalone feature flags package if you don't need experiments
All Abrova A/B Testing SDKs share the same dashboard, the same API key, and the same user token. Pick the SDK for your platform and you're up and running in minutes.