Skip to content

A/B Testing Infrastructure

Overview

A/B testing infrastructure enables data-driven decision making by running controlled experiments on mobile applications. This documentation covers comprehensive A/B testing implementation across Android, iOS, React Native, and Flutter platforms with statistical analysis and automated decision making.

Firebase A/B Testing

Android Implementation

kotlin
// ABTestManager.kt
import com.google.firebase.analytics.FirebaseAnalytics
import com.google.firebase.remoteconfig.FirebaseRemoteConfig
import com.google.firebase.remoteconfig.FirebaseRemoteConfigSettings
import kotlinx.coroutines.tasks.await
import android.content.Context
import java.util.concurrent.ConcurrentHashMap

class ABTestManager(private val context: Context) {
    private val remoteConfig = FirebaseRemoteConfig.getInstance()
    private val analytics = FirebaseAnalytics.getInstance(context)
    private val activeExperiments = ConcurrentHashMap<String, ExperimentVariant>()
    
    companion object {
        private const val EXPERIMENT_PREFIX = "experiment_"
        private const val CACHE_EXPIRATION = 3600L // 1 hour
    }
    
    init {
        setupRemoteConfig()
    }
    
    private fun setupRemoteConfig() {
        val configSettings = FirebaseRemoteConfigSettings.Builder()
            .setMinimumFetchIntervalInSeconds(CACHE_EXPIRATION)
            .build()
        
        remoteConfig.setConfigSettingsAsync(configSettings)
        setExperimentDefaults()
    }
    
    private fun setExperimentDefaults() {
        val defaults = mapOf(
            "${EXPERIMENT_PREFIX}new_checkout_flow" to "control",
            "${EXPERIMENT_PREFIX}product_recommendation_algorithm" to "collaborative_filtering",
            "${EXPERIMENT_PREFIX}onboarding_flow" to "standard",
            "${EXPERIMENT_PREFIX}pricing_display" to "original",
            "${EXPERIMENT_PREFIX}button_color" to "#2196F3"
        )
        
        remoteConfig.setDefaultsAsync(defaults)
    }
    
    suspend fun initializeExperiments(): Boolean {
        return try {
            remoteConfig.fetchAndActivate().await()
            loadActiveExperiments()
            true
        } catch (e: Exception) {
            false
        }
    }
    
    private fun loadActiveExperiments() {
        val allKeys = remoteConfig.getKeysByPrefix(EXPERIMENT_PREFIX)
        
        for (key in allKeys) {
            val experimentName = key.removePrefix(EXPERIMENT_PREFIX)
            val variantName = remoteConfig.getString(key)
            
            val variant = ExperimentVariant(
                experimentName = experimentName,
                variantName = variantName,
                parameters = getExperimentParameters(experimentName, variantName)
            )
            
            activeExperiments[experimentName] = variant
            
            // Track experiment assignment
            analytics.logEvent("experiment_assigned") {
                param("experiment_name", experimentName)
                param("variant_name", variantName)
            }
        }
    }
    
    fun getExperimentVariant(experimentName: String): ExperimentVariant? {
        return activeExperiments[experimentName]
    }
    
    fun isExperimentActive(experimentName: String): Boolean {
        return activeExperiments.containsKey(experimentName)
    }
    
    fun trackExperimentEvent(experimentName: String, eventName: String, parameters: Map<String, Any> = emptyMap()) {
        val variant = activeExperiments[experimentName] ?: return
        
        val eventParameters = mutableMapOf<String, Any>().apply {
            put("experiment_name", experimentName)
            put("variant_name", variant.variantName)
            putAll(parameters)
        }
        
        analytics.logEvent("experiment_$eventName", eventParameters)
    }
    
    fun trackConversion(experimentName: String, conversionType: String, value: Double = 0.0) {
        trackExperimentEvent(
            experimentName, 
            "conversion",
            mapOf(
                "conversion_type" to conversionType,
                "conversion_value" to value
            )
        )
    }
    
    private fun getExperimentParameters(experimentName: String, variantName: String): Map<String, Any> {
        return when (experimentName) {
            "new_checkout_flow" -> when (variantName) {
                "single_page" -> mapOf("steps" to 1, "layout" to "compact")
                "multi_step" -> mapOf("steps" to 3, "layout" to "detailed")
                else -> mapOf("steps" to 2, "layout" to "standard")
            }
            "button_color" -> mapOf("color" to variantName)
            "pricing_display" -> mapOf("format" to variantName)
            else -> emptyMap()
        }
    }
}

// Data Classes
data class ExperimentVariant(
    val experimentName: String,
    val variantName: String,
    val parameters: Map<String, Any>
)

// Usage in Activity
class CheckoutActivity : AppCompatActivity() {
    private lateinit var abTestManager: ABTestManager
    
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        
        abTestManager = ABTestManager(this)
        
        lifecycleScope.launch {
            abTestManager.initializeExperiments()
            setupUIBasedOnExperiments()
        }
    }
    
    private fun setupUIBasedOnExperiments() {
        val checkoutExperiment = abTestManager.getExperimentVariant("new_checkout_flow")
        
        checkoutExperiment?.let { variant ->
            when (variant.variantName) {
                "single_page" -> setupSinglePageCheckout()
                "multi_step" -> setupMultiStepCheckout()
                else -> setupStandardCheckout()
            }
            
            // Track experiment exposure
            abTestManager.trackExperimentEvent("new_checkout_flow", "exposure")
        }
    }
    
    private fun onPurchaseCompleted(amount: Double) {
        abTestManager.trackConversion("new_checkout_flow", "purchase", amount)
    }
}

iOS Implementation

swift
// ABTestManager.swift
import FirebaseRemoteConfig
import FirebaseAnalytics
import Foundation

class ABTestManager: ObservableObject {
    static let shared = ABTestManager()
    
    private let remoteConfig = RemoteConfig.remoteConfig()
    private let experimentPrefix = "experiment_"
    private let cacheExpiration: TimeInterval = 3600 // 1 hour
    
    @Published var activeExperiments: [String: ExperimentVariant] = [:]
    @Published var isInitialized = false
    
    private init() {
        setupRemoteConfig()
    }
    
    private func setupRemoteConfig() {
        let settings = RemoteConfigSettings()
        settings.minimumFetchInterval = cacheExpiration
        remoteConfig.configSettings = settings
        
        setExperimentDefaults()
    }
    
    private func setExperimentDefaults() {
        let defaults: [String: NSObject] = [
            "\(experimentPrefix)new_checkout_flow": "control" as NSObject,
            "\(experimentPrefix)product_recommendation_algorithm": "collaborative_filtering" as NSObject,
            "\(experimentPrefix)onboarding_flow": "standard" as NSObject,
            "\(experimentPrefix)pricing_display": "original" as NSObject,
            "\(experimentPrefix)button_color": "#2196F3" as NSObject
        ]
        
        remoteConfig.setDefaults(defaults)
    }
    
    func initializeExperiments() async -> Bool {
        do {
            _ = try await remoteConfig.fetch(withExpirationDuration: cacheExpiration)
            let activated = try await remoteConfig.activate()
            
            if activated {
                loadActiveExperiments()
                isInitialized = true
            }
            
            return activated
        } catch {
            print("Error initializing experiments: \(error)")
            return false
        }
    }
    
    private func loadActiveExperiments() {
        let allKeys = remoteConfig.allKeys(from: .remote, namespace: .default)
        let experimentKeys = allKeys.filter { $0.hasPrefix(experimentPrefix) }
        
        var experiments: [String: ExperimentVariant] = [:]
        
        for key in experimentKeys {
            let experimentName = String(key.dropFirst(experimentPrefix.count))
            let variantName = remoteConfig.configValue(forKey: key).stringValue ?? "control"
            
            let variant = ExperimentVariant(
                experimentName: experimentName,
                variantName: variantName,
                parameters: getExperimentParameters(experimentName: experimentName, variantName: variantName)
            )
            
            experiments[experimentName] = variant
            
            // Track experiment assignment
            Analytics.logEvent("experiment_assigned", parameters: [
                "experiment_name": experimentName,
                "variant_name": variantName
            ])
        }
        
        DispatchQueue.main.async {
            self.activeExperiments = experiments
        }
    }
    
    func getExperimentVariant(_ experimentName: String) -> ExperimentVariant? {
        return activeExperiments[experimentName]
    }
    
    func isExperimentActive(_ experimentName: String) -> Bool {
        return activeExperiments[experimentName] != nil
    }
    
    func trackExperimentEvent(_ experimentName: String, eventName: String, parameters: [String: Any] = [:]) {
        guard let variant = activeExperiments[experimentName] else { return }
        
        var eventParameters = parameters
        eventParameters["experiment_name"] = experimentName
        eventParameters["variant_name"] = variant.variantName
        
        Analytics.logEvent("experiment_\(eventName)", parameters: eventParameters)
    }
    
    func trackConversion(_ experimentName: String, conversionType: String, value: Double = 0.0) {
        trackExperimentEvent(
            experimentName,
            eventName: "conversion",
            parameters: [
                "conversion_type": conversionType,
                "conversion_value": value
            ]
        )
    }
    
    private func getExperimentParameters(experimentName: String, variantName: String) -> [String: Any] {
        switch experimentName {
        case "new_checkout_flow":
            switch variantName {
            case "single_page":
                return ["steps": 1, "layout": "compact"]
            case "multi_step":
                return ["steps": 3, "layout": "detailed"]
            default:
                return ["steps": 2, "layout": "standard"]
            }
        case "button_color":
            return ["color": variantName]
        case "pricing_display":
            return ["format": variantName]
        default:
            return [:]
        }
    }
}

// Data Structures
struct ExperimentVariant {
    let experimentName: String
    let variantName: String
    let parameters: [String: Any]
}

// Usage in SwiftUI
struct CheckoutView: View {
    @StateObject private var abTestManager = ABTestManager.shared
    @State private var checkoutVariant: ExperimentVariant?
    
    var body: some View {
        VStack {
            if let variant = checkoutVariant {
                switch variant.variantName {
                case "single_page":
                    SinglePageCheckoutView()
                case "multi_step":
                    MultiStepCheckoutView()
                default:
                    StandardCheckoutView()
                }
            } else {
                StandardCheckoutView()
            }
        }
        .onAppear {
            if abTestManager.isInitialized {
                setupExperiment()
            }
        }
        .onReceive(abTestManager.$isInitialized) { initialized in
            if initialized {
                setupExperiment()
            }
        }
    }
    
    private func setupExperiment() {
        checkoutVariant = abTestManager.getExperimentVariant("new_checkout_flow")
        
        if let variant = checkoutVariant {
            abTestManager.trackExperimentEvent("new_checkout_flow", eventName: "exposure")
        }
    }
    
    private func onPurchaseCompleted(amount: Double) {
        abTestManager.trackConversion("new_checkout_flow", conversionType: "purchase", value: amount)
    }
}

Best Practices

1. Experiment Design

  • Define clear success metrics
  • Ensure adequate sample sizes
  • Use proper randomization
  • Control for confounding variables

2. Statistical Rigor

  • Calculate statistical power
  • Set significance thresholds
  • Account for multiple comparisons
  • Monitor for early stopping

3. Technical Implementation

  • Consistent user assignment
  • Minimize performance impact
  • Handle edge cases gracefully
  • Implement proper logging

4. Business Considerations

  • Align with business goals
  • Consider long-term effects
  • Plan for rollout strategies
  • Document learnings

5. Ethics and Compliance

  • Obtain user consent where required
  • Protect user privacy
  • Avoid harmful experiments
  • Follow data regulations

This comprehensive A/B testing infrastructure provides enterprise-grade experimentation capabilities with proper statistical analysis, automated decision making, and robust tracking across all mobile platforms.

Created by Eren Demir.