
project.ProjectProxy.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of evaluation-proxy-core Show documentation
Show all versions of evaluation-proxy-core Show documentation
Core package for Amplitude's evaluation proxy.
package com.amplitude.project
import com.amplitude.Configuration
import com.amplitude.EvaluationProxyResponse
import com.amplitude.assignment.Assignment
import com.amplitude.assignment.AssignmentTracker
import com.amplitude.cohort.CohortApiV1
import com.amplitude.cohort.CohortLoader
import com.amplitude.cohort.CohortStorage
import com.amplitude.cohort.GetCohortResponse
import com.amplitude.cohort.USER_GROUP_TYPE
import com.amplitude.deployment.DeploymentApiV2
import com.amplitude.deployment.DeploymentLoader
import com.amplitude.deployment.DeploymentStorage
import com.amplitude.experiment.evaluation.EvaluationEngineImpl
import com.amplitude.experiment.evaluation.EvaluationVariant
import com.amplitude.experiment.evaluation.topologicalSort
import com.amplitude.util.getGroupedCohortIds
import com.amplitude.util.json
import com.amplitude.util.logger
import com.amplitude.util.toEvaluationContext
import io.ktor.http.HttpStatusCode
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.launch
import kotlinx.serialization.encodeToString
internal class ProjectProxy(
private val project: Project,
configuration: Configuration,
private val assignmentTracker: AssignmentTracker,
private val deploymentStorage: DeploymentStorage,
private val cohortStorage: CohortStorage,
) {
companion object {
val log by logger()
}
private val engine = EvaluationEngineImpl()
private val projectApi = ProjectApiV1(configuration.managementServerUrl, project.managementKey)
private val deploymentApi = DeploymentApiV2(configuration.serverUrl)
private val cohortApi = CohortApiV1(configuration.cohortServerUrl, project.apiKey, project.secretKey)
private val cohortLoader = CohortLoader(configuration.maxCohortSize, cohortApi, cohortStorage)
private val deploymentLoader = DeploymentLoader(deploymentApi, deploymentStorage, cohortLoader)
private val projectRunner =
ProjectRunner(
project,
configuration,
projectApi,
deploymentLoader,
deploymentStorage,
cohortLoader,
cohortStorage,
)
suspend fun start() {
log.info("Starting project. projectId=${project.id}")
projectRunner.start()
}
suspend fun shutdown() {
log.info("Shutting down project. project.id=${project.id}")
projectRunner.stop()
}
suspend fun getFlagConfigs(deploymentKey: String?): EvaluationProxyResponse {
if (deploymentKey.isNullOrEmpty()) {
return EvaluationProxyResponse.error(HttpStatusCode.Unauthorized, "Invalid deployment")
}
val result = deploymentStorage.getAllFlags(deploymentKey).values.toList()
return EvaluationProxyResponse.error(HttpStatusCode.OK, json.encodeToString(result))
}
suspend fun getCohort(
cohortId: String?,
lastModified: Long?,
maxCohortSize: Int?,
): EvaluationProxyResponse {
if (cohortId.isNullOrEmpty()) {
return EvaluationProxyResponse.error(HttpStatusCode.NotFound, "Cohort not found")
}
val cohortDescription =
cohortStorage.getCohortDescription(cohortId)
?: return EvaluationProxyResponse.error(HttpStatusCode.NotFound, "Cohort not found")
if (cohortDescription.size > (maxCohortSize ?: Int.MAX_VALUE)) {
return EvaluationProxyResponse.error(
HttpStatusCode.PayloadTooLarge,
"Cohort $cohortId sized ${cohortDescription.size} is greater than max cohort size $maxCohortSize",
)
}
if (cohortDescription.lastModified == lastModified) {
return EvaluationProxyResponse.error(HttpStatusCode.NoContent, "Cohort not modified")
}
val cohort =
cohortStorage.getCohort(cohortId)
?: return EvaluationProxyResponse.error(HttpStatusCode.NotFound, "Cohort members not found")
return EvaluationProxyResponse.json(HttpStatusCode.OK, GetCohortResponse.fromCohort(cohort))
}
suspend fun getCohortMemberships(
deploymentKey: String?,
groupType: String?,
groupName: String?,
): EvaluationProxyResponse {
if (deploymentKey.isNullOrEmpty()) {
return EvaluationProxyResponse.error(HttpStatusCode.Unauthorized, "Invalid deployment")
}
if (groupType.isNullOrEmpty()) {
return EvaluationProxyResponse.error(HttpStatusCode.BadRequest, "Invalid group type")
}
if (groupName.isNullOrEmpty()) {
return EvaluationProxyResponse.error(HttpStatusCode.BadRequest, "Invalid group name")
}
val cohortIds = deploymentStorage.getAllFlags(deploymentKey).values.getGroupedCohortIds()[groupType]
if (cohortIds.isNullOrEmpty()) {
return EvaluationProxyResponse.json(HttpStatusCode.OK, emptySet())
}
val result = cohortStorage.getCohortMemberships(groupType, groupName, cohortIds)
return EvaluationProxyResponse.json(HttpStatusCode.OK, result)
}
suspend fun evaluate(
deploymentKey: String?,
user: Map?,
flagKeys: Set? = null,
): EvaluationProxyResponse {
if (deploymentKey.isNullOrEmpty()) {
return EvaluationProxyResponse.error(HttpStatusCode.Unauthorized, "Invalid deployment")
}
val result = evaluateInternal(deploymentKey, user, flagKeys)
return EvaluationProxyResponse(HttpStatusCode.OK, json.encodeToString(result))
}
suspend fun evaluateV1(
deploymentKey: String?,
user: Map?,
flagKeys: Set? = null,
): EvaluationProxyResponse {
if (deploymentKey.isNullOrEmpty()) {
return EvaluationProxyResponse(HttpStatusCode.Unauthorized, "Invalid deployment")
}
val result =
evaluateInternal(deploymentKey, user, flagKeys).filter { entry ->
val default = entry.value.metadata?.get("default") as? Boolean ?: false
val deployed = entry.value.metadata?.get("deployed") as? Boolean ?: true
(!default && deployed)
}
return EvaluationProxyResponse(HttpStatusCode.OK, json.encodeToString(result))
}
private suspend fun evaluateInternal(
deploymentKey: String,
user: Map?,
flagKeys: Set? = null,
): Map {
// Get flag configs for the deployment from storage and topo sort.
val storageFlags = deploymentStorage.getAllFlags(deploymentKey)
if (storageFlags.isEmpty()) {
return mapOf()
}
val flags = topologicalSort(storageFlags, flagKeys ?: setOf())
if (flags.isEmpty()) {
return mapOf()
}
val groupedCohortIds = flags.getGroupedCohortIds()
// Enrich user with cohort IDs and build the evaluation context
val userId = user?.get("user_id") as? String
val enrichedUser = user?.toMutableMap() ?: mutableMapOf()
val userCohortIds = groupedCohortIds[USER_GROUP_TYPE]
if (userId != null && userCohortIds != null) {
enrichedUser["cohort_ids"] = cohortStorage.getCohortMemberships(USER_GROUP_TYPE, userId, userCohortIds)
}
val groups = enrichedUser["groups"] as? Map<*, *>
if (!groups.isNullOrEmpty()) {
val groupCohortIds = mutableMapOf>>()
for (entry in groups.entries) {
val groupType = entry.key as? String
val groupName = (entry.value as? Collection<*>)?.firstOrNull() as? String
val groupTypeCohortIds = groupedCohortIds[groupType]
if (groupType != null && groupName != null && groupTypeCohortIds != null) {
val cohortIds = cohortStorage.getCohortMemberships(groupType, groupName, groupTypeCohortIds)
if (groupCohortIds.isNotEmpty()) {
groupCohortIds.putIfAbsent(groupType, mutableMapOf(groupName to cohortIds))
}
}
}
if (groupCohortIds.isNotEmpty()) {
enrichedUser["group_cohort_ids"] = groupCohortIds
}
}
val evaluationContext = enrichedUser.toEvaluationContext()
// Evaluate results
log.debug("evaluate - context={}", evaluationContext)
val result = engine.evaluate(evaluationContext, flags)
if (enrichedUser.isNotEmpty()) {
coroutineScope {
launch {
assignmentTracker.track(Assignment(evaluationContext, result))
}
}
}
return result
}
// Internal
internal suspend fun getDeployments(): Set {
return deploymentStorage.getDeployments().keys
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy