ابرووا
شروع رایگان
☰ فهرست مستندات · iOS
به‌زودی. Abrova A/B Testing به‌زودی فعال می‌شود. این صفحه توضیح می‌دهد چطور کار خواهد کرد؛ هنوز قابل استفاده نیست.
این صفحه هنوز ترجمه نشده — نسخه‌ی انگلیسی نمایش داده می‌شود. English

A/B Testing iOS SDK

Native Swift SDK for A/B experiments, feature flags, and event tracking on iOS. Signed user tokens, SwiftUI-friendly, delegate callbacks, 17 built-in event types, offline-first.

iOS 0.2.3iOS 13+Swift 5.7+به‌روزرسانی: ۵ آبان ۱۴۰۵
AbrovaAbTesting iOS SDK v0.2.3.

Native Swift SDK for A/B testing and feature flags on iOS. SwiftUI-friendly with delegate callbacks, 17 built-in event types, and an offline-first event queue. Anything done on behalf of a user needs a signed user token; you can initialize the SDK before sign-in and it waits for the token. Installs with Swift Package Manager.

Requirements.
  • iOS 13.0 or higher.
  • Swift 5.7 or higher.
  • Xcode 14.0 or higher.
  • A backend endpoint that returns an Abrova user token for the signed-in user (see step 3, User tokens).

Installation#

Install the SDK with Swift Package Manager.

In Xcode: File → Add Package Dependencies, enter the URL below and choose version 0.2.3:

text
https://github.com/Abrova-co/abrova-ab-testing-ios-sdk.git

Or add to your Package.swift:

swift
dependencies: [
    .package(url: "https://github.com/Abrova-co/abrova-ab-testing-ios-sdk.git", from: "0.2.3")
]

Then import the module:

swift
import AbrovaAbTesting

Initialization#

Initialize AbrovaAbTesting once, in your AppDelegate or App struct. Pass a tokenProvider so the SDK can get a user token from your backend.

swift
import AbrovaAbTesting

let config = AbrovaAbTestingConfig(
    apiKey: "YOUR_API_KEY",
    debug: true,           // Log to the console (development only)
    // flushInterval: 10.0, // Your own send interval in seconds. Leave it out to follow the console
    maxQueueSize: 100,     // Max events before auto-flush (default: 100)
    tokenProvider: { completion in
        // Ask YOUR backend for an Abrova user token (see step 3).
        // Complete with .failure while no one is signed in.
        MyBackend.fetchAbrovaToken { result in completion(result) }
    }
)

AbrovaAbTesting.shared.initialize(config: config, delegate: self)

No SDK method stops your app when you call it before initialize, before the first load has finished or before a user is signed in: getters return the default you pass and the track… methods do nothing. If the first load fails (no network), the SDK works from its cache and loads again when the network is back.

Configuration

OptionTypeRequiredDescription
apiKeyString-Your project API key (required)
tokenProviderAbrovaTokenProvider?nilReturns a user token minted by your backend, or fails while no one is signed in. Required for assigning variants, tracking and server flag evaluation
userTokenString?nilA token you already hold. Prefer tokenProvider: a fixed token stops working when it expires. Hand over a newer one with setUserToken(_:)
debugBoolfalseLog to the console. Logs never include your API key, tokens or request bodies
flushIntervalTimeInterval?nilHow often queued events are sent, in seconds. Leave it out (or pass nil) to follow the Send interval set in the Abrova console (see below)
maxQueueSizeInt100Queued events that trigger a flush

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 flushInterval out or pass nil. A value you 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 is not reported to the delegate's didReceiveError.

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.

User tokens#

The API key ships inside your app, so it can't prove which user a request is for. A user token can. Your backend mints it with your project's server secret, the SDK sends it with each request, and Abrova takes the user from the token.

A token is required to get a variant from the server, track events and conversions, sync queued events, and evaluate flags on the server. Loading the experiment and flag lists needs only the API key, so your app can fetch them before anyone signs in.

Mint the token on your backend

Add one endpoint, protected by your own login, that calls Abrova and returns the token to the signed-in user:

bash
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 service takes the user from the token, so if you call setUserId(_:) in the app, pass the same userId.

Never ship the server secret.

The server secret can mint a token for any user. Keep it on your server, out of your app binary 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. If your app uses more than one of them, mint one token and give it to every SDK's tokenProvider instead of fetching one per product.

Give the token to the SDK

tokenProvider is an AbrovaTokenProvider: a closure that receives a completion handler. Call completion with .success(token) or .failure(error), from any thread. The SDK caches the token, asks for a new one shortly before it expires, and asks once more if Abrova reports it expired; requests made at the same time share one call. A provider that never calls completion counts as having no token after 15 seconds.

swift
let tokenProvider: AbrovaTokenProvider = { completion in
    // No one signed in: complete with .failure. The SDK then waits for a token.
    guard let session = SessionStore.shared.current else {
        completion(.failure(AuthError.notSignedIn))
        return
    }
    MyBackend.fetchAbrovaToken(for: session) { result in
        completion(result) // Result<String, Error>
    }
}

If you would rather hand tokens over yourself, leave tokenProvider out and call AbrovaAbTesting.shared.setUserToken(token) after sign-in and whenever you refresh the token. A token passed as userToken in the config works the same way: it isn't refreshed, so hand over a newer one before it expires.

Before your user signs in

You can call initialize before anyone is signed in. Without a token the SDK waits instead of failing:

  • AbrovaAbTesting.shared.isAwaitingUserToken is true, and onAwaitingUserToken is called once, on the main thread, each time the SDK starts waiting.
  • Both forms of getVariant return the default you pass.
  • Nothing is sent for a user and no error is reported. Events you track are kept on the device (at most 1,000).

The SDK carries on by itself as soon as it has a token: it fetches the variants your app asked for (the delegate's didAssignVariant is called for each) and sends the kept events.

swift
AbrovaAbTesting.shared.onAwaitingUserToken = { /* e.g. show the default experience */ }

// After sign-in
AbrovaAbTesting.shared.refreshUserToken()      // with a tokenProvider: asks it again now
AbrovaAbTesting.shared.setUserToken(token)     // without one: hand the token over

// After sign-out
AbrovaAbTesting.shared.clearUserId()           // or setUserToken(nil)

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, so call setUserId(_:) with the id your backend puts in the token before you track. Events recorded for an anonymous user are not credited to anyone and are dropped at sign-in.

Keep it on the device.

A token is valid until it expires, so store it with the user's session (Keychain) and reuse it across app launches. Your tokenProvider then only calls your backend when the stored token is close to expiry, not on every start.

User Identification#

The user token decides who the user is: once the SDK has a token, it acts as the user that token was minted for. Call setUserId(_:) with that same ID when a user signs in, and clearUserId() when they sign out. Until there is a user, the SDK uses a random ID it generates on first launch.

swift
// After login — use the same ID your backend mints the token for
AbrovaAbTesting.shared.setUserId("user-123")
AbrovaAbTesting.shared.refreshUserToken()   // or setUserToken(token) if you hand tokens over yourself

// After logout — forget the user and wait for the next token
AbrovaAbTesting.shared.clearUserId()

// Get current user ID
let userId = AbrovaAbTesting.shared.getUserId()

// Set attributes for targeting rules
AbrovaAbTesting.shared.setUserAttributes([
    "plan": "premium",
    "country": "US",
    "age": 28,
    "platform": "ios"
])

A different user ID (setUserId(_:)), a token for another user, or a sign-out (clearUserId(), setUserToken(nil)) is a user switch. The SDK clears the previous user's variants, attributes, token and pending events before it does anything for the next user. Events of the previous user that are ready to send go out once more under that user's own token, never under another user's. Set the user attributes again after a switch.

Delegate Callbacks#

Implement AbrovaAbTestingDelegate to listen for SDK events. Every method is optional. The SDK holds delegates weakly, so keep a strong reference to yours; add more with addDelegate(_:) and remove them with removeDelegate(_:).

swift
extension AppDelegate: AbrovaAbTestingDelegate {
    func abrovaAbTestingDidInitialize() {
        print("SDK initialized")
    }

    func abrovaAbTesting(didAssignVariant variantKey: String,
                         forExperiment experimentKey: String,
                         config: [String: Any]?) {
        print("Assigned: \(experimentKey) -> \(variantKey)")
    }

    func abrovaAbTesting(didRefreshExperiments experiments: [Experiment]) {
        print("Refreshed \(experiments.count) experiments")
    }

    func abrovaAbTesting(didRefreshFeatureFlags flags: [FeatureFlag]) {
        print("Refreshed \(flags.count) flags")
    }

    func abrovaAbTesting(didReceiveError error: AbrovaAbTestingError) {
        print("Error: \(error.localizedDescription)")
    }
}

A/B Testing#

Get Variant

The variant always comes from the service (this needs a user token) and is then kept on the device; the SDK never picks one itself.

swift
// Asynchronous (recommended) — waits for the service's answer;
// completion runs on the main queue
AbrovaAbTesting.shared.getVariant(experimentKey: "checkout-redesign") { variant in
    switch variant {
    case "variant-a":
        self.showNewCheckout()
    default:
        self.showOriginalCheckout()
    }
}

// Synchronous — never blocks, safe on the main thread
let variant = AbrovaAbTesting.shared.getVariant(
    experimentKey: "checkout-redesign",
    defaultVariant: "control"  // returned until the service has answered
)

The synchronous getVariant returns the variant the service assigned earlier. The first time you ask for an experiment it returns defaultVariant at once and fetches the assignment in the background; the delegate's didAssignVariant tells you when it has arrived. Use the completion form the first time a screen needs the variant, otherwise the user may briefly see the default before their real variant.

You get the default variant back when the experiment isn't running, the user doesn't match its targeting rules, there is no user token yet, or the service can't be reached and nothing is stored. A user the service keeps out of an experiment (targeting, traffic allocation) gets the control variant. Tracking calls are always safe to make: you don't need to check whether the user is in the experiment before you track. The SDK sends these events like any other and the service decides what counts; they never enter the experiment's results.

Get Variant Config

Available once the user has a variant for the experiment.

swift
let config = AbrovaAbTesting.shared.getVariantConfig(experimentKey: "checkout-redesign")
let buttonColor = config?["button_color"] as? String ?? "#7C3AED"
let layout = config?["layout"] as? String ?? "default"

List & Refresh Experiments

Needs only the API key.

swift
// Get all experiments
let experiments = AbrovaAbTesting.shared.getExperiments()
for exp in experiments {
    print("\(exp.key) [\(exp.status.rawValue)] - \(exp.variants.count) variants")
}

// Refresh from server
AbrovaAbTesting.shared.refreshExperiments {
    print("Experiments refreshed")
}

Feature Flags#

Check Feature Flag (Local)

Evaluated on the device against the cached flag list.

swift
let darkMode = AbrovaAbTesting.shared.isFeatureEnabled("dark-mode")
if darkMode {
    enableDarkMode()
}

Check Feature Flag (Server Evaluation)

Needs a user token. Without one, or if the server can't be reached, the SDK falls back to local evaluation. The completion runs on the main queue.

swift
AbrovaAbTesting.shared.isFeatureEnabled("dark-mode", defaultValue: false) { enabled in
    if enabled {
        self.enableDarkMode()
    }
}

Get Feature Value

Returns Any?, so cast it to the type you expect.

swift
let maxUpload = AbrovaAbTesting.shared.getFeatureValue("max-upload-size", defaultValue: 10) as? Int ?? 10
let buttonColor = AbrovaAbTesting.shared.getFeatureValue("button-color", defaultValue: "#7C3AED") as? String ?? "#7C3AED"

List & Refresh Flags

swift
// Get all flags
let flags = AbrovaAbTesting.shared.getFeatureFlags()
for flag in flags {
    print("\(flag.key): enabled=\(flag.enabled), rollout=\(flag.rolloutPercentage)%")
}

// Refresh from server
AbrovaAbTesting.shared.refreshFeatureFlags {
    print("Flags refreshed")
}

Event Tracking#

Track user interactions with 17 built-in event types. Events are credited to the variant the user was assigned, so get a variant for an experiment before tracking events for it. Events tracked while the assignment is still loading (or while the SDK waits for a user token) are kept and sent once the variant is known; events for an experiment you never asked a variant for are dropped.

Queued events are saved to disk and sent when the device is back online. The queue holds at most 1,000 events; when it is full, the oldest are dropped first. Events are sent 100 at a time. When the service can't be reached, asks the SDK to slow down, or your project has reached its usage limit, the SDK waits before it tries again. Each event carries the time your app recorded it, so events sent together keep their order.

Core Events

swift
AbrovaAbTesting.shared.trackView(experimentKey: "experiment-key")
AbrovaAbTesting.shared.trackClick(experimentKey: "experiment-key")
AbrovaAbTesting.shared.trackConversion(experimentKey: "experiment-key", value: 1000000)

// Custom event
AbrovaAbTesting.shared.trackCustomEvent(
    experimentKey: "experiment-key",
    eventName: "button_tap",
    properties: ["element": "cta_button", "duration_ms": 1500]
)

Conversion Events

swift
AbrovaAbTesting.shared.trackAddToCart(experimentKey: "experiment-key", value: 300000, productId: "sku-123", properties: ["quantity": 2])
AbrovaAbTesting.shared.trackRemoveFromCart(experimentKey: "experiment-key", value: 300000, productId: "sku-123")
AbrovaAbTesting.shared.trackBeginCheckout(experimentKey: "experiment-key", value: 600000)
AbrovaAbTesting.shared.trackPurchase(experimentKey: "experiment-key", value: 890000, transactionId: "txn-456", properties: ["currency": "Toman"])

Engagement Events

swift
AbrovaAbTesting.shared.trackScroll(experimentKey: "experiment-key", depth: 75.0)
AbrovaAbTesting.shared.trackFormSubmit(experimentKey: "experiment-key", formName: "signup")
AbrovaAbTesting.shared.trackSearch(experimentKey: "experiment-key", query: "shoes")
AbrovaAbTesting.shared.trackShare(experimentKey: "experiment-key", method: "twitter")

Media Events

swift
AbrovaAbTesting.shared.trackVideoStart(experimentKey: "experiment-key", videoId: "vid-001")
AbrovaAbTesting.shared.trackVideoComplete(experimentKey: "experiment-key", videoId: "vid-001")

Auth Events

swift
AbrovaAbTesting.shared.trackSignUp(experimentKey: "experiment-key", method: "apple")
AbrovaAbTesting.shared.trackLogin(experimentKey: "experiment-key", method: "biometric")
AbrovaAbTesting.shared.trackLogout(experimentKey: "experiment-key")

Generic Event

swift
AbrovaAbTesting.shared.track(
    experimentKey: "experiment-key",
    eventType: .custom,
    eventName: "page_load_time",
    eventValue: 2.3,
    properties: ["page": "/checkout", "cached": false]
)

EventType Enum#

OptionTypeRequiredDescription
.view--User viewed the experiment
.click--User clicked/tapped
.conversion--Conversion event
.custom--Custom event with name
.scroll--Scroll depth tracking
.formSubmit--Form submission
.search--Search query
.share--Social share
.addToCart--Add item to cart
.removeFromCart--Remove item from cart
.beginCheckout--Begin checkout flow
.purchase--Completed purchase
.videoStart--Video playback started
.videoComplete--Video playback completed
.signUp--User registration
.login--User login
.logout--User logout

API Reference#

OptionTypeRequiredDescription
initialize(config:delegate:)--Initialize the SDK
setUserId(_:)--Set user ID; call on login
clearUserId()--Sign-out: forget the user and wait for a token
setUserToken(_:)--Hand over a user token (nil after sign-out)
refreshUserToken()--Ask the tokenProvider again now (after sign-in or sign-out)
isAwaitingUserToken--true while the SDK waits for a user token
onAwaitingUserToken--Called once each time the SDK starts waiting
getUserId()--Get current user ID
setUserAttributes(_:)--Set targeting attributes
getVariant(experimentKey:defaultVariant:)--Get assigned variant (never blocks; default until the service has answered)
getVariant(experimentKey:defaultVariant:completion:)--Get assigned variant (waits for the service)
getVariantConfig(experimentKey:)--Get variant configuration
isFeatureEnabled(_:defaultValue:)--Check feature flag (local)
isFeatureEnabled(_:defaultValue:completion:)--Check flag (server eval)
getFeatureValue(_:defaultValue:)--Get feature flag value
getFeatureFlags()--Get all feature flags
refreshFeatureFlags(completion:)--Refresh flags from server
getExperiments()--Get all experiments
refreshExperiments(completion:)--Refresh experiments from server
track(experimentKey:eventType:eventName:eventValue:properties:)--Track any event type
addDelegate(_:) / removeDelegate(_:)--Add or remove a delegate
flush()--Force sync pending events
reset()--Clear all state, cache and token

Complete Example#

Full SwiftUI integration with user tokens, experiments, feature flags, and event tracking:

swift
import SwiftUI
import AbrovaAbTesting

@main
struct MyApp: App {
    init() {
        AbrovaAbTesting.shared.initialize(config: AbrovaAbTestingConfig(
            apiKey: "YOUR_API_KEY",
            tokenProvider: { completion in
                MyBackend.fetchAbrovaToken { result in completion(result) }
            }
        ))
    }

    var body: some Scene {
        WindowGroup { ContentView() }
    }
}

// In your login flow, once your backend knows the user
func onLoggedIn(_ user: User) {
    AbrovaAbTesting.shared.setUserId(user.id)   // the ID your backend puts in the token
    AbrovaAbTesting.shared.refreshUserToken()   // asks tokenProvider again now
    AbrovaAbTesting.shared.setUserAttributes(["plan": user.plan])
}

// In your logout flow
func onLoggedOut() {
    AbrovaAbTesting.shared.clearUserId()
}

struct ContentView: View {
    @State private var variant = "control"
    @State private var darkMode = false

    var body: some View {
        VStack(spacing: 20) {
            // Feature flag: dark mode
            Toggle("Dark Mode", isOn: $darkMode)
                .padding()

            // A/B test: checkout button
            Button(action: {
                AbrovaAbTesting.shared.trackClick(experimentKey: "checkout-button")
                checkout()
            }) {
                Text(variant == "variant-a" ? "Buy Now" : "Checkout")
                    .foregroundColor(.white)
                    .padding()
                    .frame(maxWidth: .infinity)
                    .background(variant == "variant-a" ? Color.green : Color.blue)
                    .cornerRadius(12)
            }
            .padding(.horizontal)
        }
        .onAppear {
            // Check feature flag
            darkMode = AbrovaAbTesting.shared.isFeatureEnabled("dark-mode")

            // Get variant, then track the view
            AbrovaAbTesting.shared.getVariant(experimentKey: "checkout-button") { v in
                self.variant = v
                AbrovaAbTesting.shared.trackView(experimentKey: "checkout-button")
            }
        }
    }

    func checkout() {
        AbrovaAbTesting.shared.trackConversion(experimentKey: "checkout-button", value: 500000)
        AbrovaAbTesting.shared.trackPurchase(experimentKey: "checkout-button", value: 500000, transactionId: "txn-123")
        AbrovaAbTesting.shared.flush()
    }
}

Lifecycle#

swift
// Refresh experiments from server
AbrovaAbTesting.shared.refreshExperiments()

// Refresh feature flags from server
AbrovaAbTesting.shared.refreshFeatureFlags()

// Flush pending events
AbrovaAbTesting.shared.flush()

// Clear cached experiments, flags, assignments and the token.
// Mainly for tests — call initialize(config:) again before using the SDK.
AbrovaAbTesting.shared.reset()

To switch users, use setUserId(_:) on login and clearUserId() on logout rather than reset().

این صفحه مفید بود؟
گزارش مشکل