All Downloads are FREE. Search and download functionalities are using the official Maven repository.

.experiment-jvm-server.1.6.1.source-code.LocalEvaluationClient.kt Maven / Gradle / Ivy

The newest version!
@file:OptIn(ExperimentalApi::class)

package com.amplitude.experiment

import com.amplitude.Amplitude
import com.amplitude.Options
import com.amplitude.experiment.assignment.AmplitudeAssignmentService
import com.amplitude.experiment.assignment.Assignment
import com.amplitude.experiment.assignment.AssignmentService
import com.amplitude.experiment.assignment.InMemoryAssignmentFilter
import com.amplitude.experiment.cohort.CohortApi
import com.amplitude.experiment.cohort.DynamicCohortApi
import com.amplitude.experiment.cohort.InMemoryCohortStorage
import com.amplitude.experiment.cohort.ProxyCohortMembershipApi
import com.amplitude.experiment.cohort.ProxyCohortStorage
import com.amplitude.experiment.deployment.DeploymentRunner
import com.amplitude.experiment.evaluation.EvaluationEngine
import com.amplitude.experiment.evaluation.EvaluationEngineImpl
import com.amplitude.experiment.evaluation.EvaluationFlag
import com.amplitude.experiment.evaluation.topologicalSort
import com.amplitude.experiment.flag.DynamicFlagConfigApi
import com.amplitude.experiment.flag.FlagConfigStreamApi
import com.amplitude.experiment.flag.InMemoryFlagConfigStorage
import com.amplitude.experiment.util.LocalEvaluationMetricsWrapper
import com.amplitude.experiment.util.Logger
import com.amplitude.experiment.util.USER_GROUP_TYPE
import com.amplitude.experiment.util.filterDefaultVariants
import com.amplitude.experiment.util.getAllCohortIds
import com.amplitude.experiment.util.getGroupedCohortIds
import com.amplitude.experiment.util.toEvaluationContext
import com.amplitude.experiment.util.toVariants
import com.amplitude.experiment.util.wrapMetrics
import okhttp3.HttpUrl
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient

class LocalEvaluationClient internal constructor(
    apiKey: String,
    private val config: LocalEvaluationConfig = LocalEvaluationConfig(),
    private val httpClient: OkHttpClient = OkHttpClient(),
    private val metrics: LocalEvaluationMetrics = LocalEvaluationMetricsWrapper(config.metrics),
    cohortApi: CohortApi? = getCohortDownloadApi(config, httpClient, metrics),
) {
    private val assignmentService: AssignmentService? = createAssignmentService(apiKey)
    private val serverUrl: HttpUrl = getServerUrl(config)
    private val streamServerUrl: HttpUrl = getStreamServerUrl(config)
    private val evaluation: EvaluationEngine = EvaluationEngineImpl()
    private val flagConfigApi = DynamicFlagConfigApi(apiKey, serverUrl, null, httpClient, metrics)
    private val proxyUrl: HttpUrl? = getProxyUrl(config)
    private val flagConfigProxyApi = if (proxyUrl == null) null else DynamicFlagConfigApi(apiKey, proxyUrl, null, httpClient)
    private val flagConfigStreamApi = if (config.streamUpdates) FlagConfigStreamApi(apiKey, streamServerUrl, httpClient, config.streamFlagConnTimeoutMillis) else null
    private val flagConfigStorage = InMemoryFlagConfigStorage()
    private val cohortStorage = if (config.cohortSyncConfig == null) {
        null
    } else if (config.evaluationProxyConfig == null) {
        InMemoryCohortStorage()
    } else {
        ProxyCohortStorage(
            proxyConfig = config.evaluationProxyConfig,
            membershipApi = ProxyCohortMembershipApi(apiKey, config.evaluationProxyConfig.proxyUrl.toHttpUrl(), httpClient),
            metrics = metrics,
        )
    }

    private val deploymentRunner = DeploymentRunner(
        config = config,
        flagConfigApi = flagConfigApi,
        flagConfigProxyApi = flagConfigProxyApi,
        flagConfigStreamApi = flagConfigStreamApi,
        flagConfigStorage = flagConfigStorage,
        cohortApi = cohortApi,
        cohortStorage = cohortStorage,
        metrics = metrics,
    )

    fun start() {
        try {
            deploymentRunner.start()
        } catch (t: Throwable) {
            throw ExperimentException(
                message = "Failed to start local evaluation client.",
                cause = t
            )
        }
    }

    fun stop() {
        deploymentRunner.stop()
    }

    private fun createAssignmentService(deploymentKey: String): AssignmentService? {
        if (config.assignmentConfiguration == null) return null
        return AmplitudeAssignmentService(
            Amplitude.getInstance(deploymentKey).apply {
                init(config.assignmentConfiguration.apiKey)
                setEventUploadThreshold(config.assignmentConfiguration.eventUploadThreshold)
                setEventUploadPeriodMillis(config.assignmentConfiguration.eventUploadPeriodMillis)
                setOptions(Options().setMinIdLength(1))
                setServerUrl(getEventServerUrl(config, config.assignmentConfiguration))
            },
            InMemoryAssignmentFilter(config.assignmentConfiguration.cacheCapacity),
            metrics = metrics,
        )
    }

    @JvmOverloads
    @Deprecated(
        "Use the evaluateV2 method. EvaluateV2 returns variant objects with default values (e.g. null/off) if the user is evaluated, but not assigned a variant.",
        ReplaceWith("evaluateV2(user, flagKeys)")
    )
    fun evaluate(user: ExperimentUser, flagKeys: List = listOf()): Map {
        return evaluateV2(user, flagKeys.toSet()).filterDefaultVariants()
    }

    @JvmOverloads
    fun evaluateV2(user: ExperimentUser, flagKeys: Set = setOf()): Map {
        val flagConfigs = flagConfigStorage.getFlagConfigs()
        val sortedFlagConfigs = topologicalSort(flagConfigs, flagKeys)
        if (sortedFlagConfigs.isEmpty()) {
            return mapOf()
        }
        val enrichedUser = enrichUser(user, sortedFlagConfigs)
        val evaluationResults = wrapMetrics(
            metric = metrics::onEvaluation,
            failure = metrics::onEvaluationFailure,
        ) {
            evaluation.evaluate(enrichedUser.toEvaluationContext(), sortedFlagConfigs)
        }
        val variants = evaluationResults.toVariants()
        assignmentService?.track(Assignment(user, variants))
        return variants
    }

    private fun enrichUser(user: ExperimentUser, flagConfigs: List): ExperimentUser {
        val groupedCohortIds = flagConfigs.getGroupedCohortIds()
        if (cohortStorage == null) {
            if (groupedCohortIds.isNotEmpty()) {
                val flagKeys = flagConfigs.mapNotNull { flag ->
                    val cohortIds = flag.getAllCohortIds()
                    if (cohortIds.isEmpty()) {
                        null
                    } else {
                        flag.key
                    }
                }
                Logger.e("Local evaluation flags $flagKeys target cohorts but cohort targeting is not configured.")
            }
            return user
        }
        return user.copyToBuilder().apply {
            val userCohortsIds = groupedCohortIds[USER_GROUP_TYPE]
            if (!userCohortsIds.isNullOrEmpty() && user.userId != null) {
                cohortIds(cohortStorage.getCohortsForUser(user.userId, userCohortsIds))
            }
            if (user.groups != null) {
                for (group in user.groups) {
                    val groupType = group.key
                    val groupName = group.value.firstOrNull() ?: continue
                    val cohortIds = groupedCohortIds[groupType]
                    if (cohortIds.isNullOrEmpty()) {
                        continue
                    }
                    groupCohortIds(
                        groupType,
                        groupName,
                        cohortStorage.getCohortsForGroup(groupType, groupName, cohortIds)
                    )
                }
            }
        }.build()
    }
}

private fun getCohortDownloadApi(
    config: LocalEvaluationConfig,
    httpClient: OkHttpClient,
    metrics: LocalEvaluationMetrics
): CohortApi? {
    return if (config.cohortSyncConfig != null) {
        DynamicCohortApi(
            apiKey = config.cohortSyncConfig.apiKey,
            secretKey = config.cohortSyncConfig.secretKey,
            maxCohortSize = config.cohortSyncConfig.maxCohortSize,
            serverUrl = getCohortServerUrl(config),
            proxyUrl = getProxyUrl(config),
            httpClient = httpClient,
            metrics = metrics
        )
    } else {
        null
    }
}

private fun getServerUrl(config: LocalEvaluationConfig): HttpUrl {
    return if (config.serverUrl == LocalEvaluationConfig.Defaults.SERVER_URL) {
        when (config.serverZone) {
            ServerZone.US -> US_SERVER_URL.toHttpUrl()
            ServerZone.EU -> EU_SERVER_URL.toHttpUrl()
        }
    } else {
        config.serverUrl.toHttpUrl()
    }
}

private fun getStreamServerUrl(config: LocalEvaluationConfig): HttpUrl {
    return if (config.streamServerUrl == LocalEvaluationConfig.Defaults.STREAM_SERVER_URL) {
        when (config.serverZone) {
            ServerZone.US -> US_STREAM_SERVER_URL.toHttpUrl()
            ServerZone.EU -> EU_STREAM_SERVER_URL.toHttpUrl()
        }
    } else {
        config.streamServerUrl.toHttpUrl()
    }
}

private fun getProxyUrl(config: LocalEvaluationConfig): HttpUrl? {
    return config.evaluationProxyConfig?.proxyUrl?.toHttpUrl()
}

private fun getCohortServerUrl(config: LocalEvaluationConfig): HttpUrl {
    return if (config.cohortSyncConfig?.cohortServerUrl == LocalEvaluationConfig.Defaults.COHORT_SERVER_URL) {
        when (config.serverZone) {
            ServerZone.US -> US_COHORT_SERVER_URL.toHttpUrl()
            ServerZone.EU -> EU_COHORT_SERVER_URL.toHttpUrl()
        }
    } else {
        config.cohortSyncConfig?.cohortServerUrl?.toHttpUrl()
            ?: LocalEvaluationConfig.Defaults.COHORT_SERVER_URL.toHttpUrl()
    }
}

private fun getEventServerUrl(
    config: LocalEvaluationConfig,
    assignmentConfiguration: AssignmentConfiguration
): String {
    return if (assignmentConfiguration.serverUrl == LocalEvaluationConfig.Defaults.EVENT_SERVER_URL) {
        when (config.serverZone) {
            ServerZone.US -> if (assignmentConfiguration.useBatchMode) {
                US_BATCH_SERVER_URL
            } else {
                US_EVENT_SERVER_URL
            }
            ServerZone.EU -> if (assignmentConfiguration.useBatchMode) {
                EU_BATCH_SERVER_URL
            } else {
                EU_EVENT_SERVER_URL
            }
        }
    } else {
        assignmentConfiguration.serverUrl
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy