Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: evaluation v2 #6

Merged
merged 49 commits into from
Jul 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
57de44a
initial implementation
bgiori Jun 20, 2023
e756cab
add serialization utils
bgiori Jun 21, 2023
1e4de13
remove old serialization util
bgiori Jun 21, 2023
729b7ab
remove kotlinx serialization from api
bgiori Jun 21, 2023
626725b
make operator public
bgiori Jun 21, 2023
b44db1a
fix comparable fallback logic
bgiori Jun 21, 2023
d4edd7f
set & glob operator support, typed selectable
bgiori Jun 21, 2023
6f56f61
implement custom semver; add regex match; remove glob match
bgiori Jun 22, 2023
1041511
fix: lint formatting
bgiori Jun 22, 2023
2a018eb
remove js;serialization; fix build; update interop
bgiori Jun 23, 2023
07d98d7
add payload to variant
bgiori Jun 23, 2023
3fbf445
change interop version
bgiori Jun 23, 2023
2a69b76
sign publication
bgiori Jun 23, 2023
252b5dc
add integration tests; fix bugs
bgiori Jul 6, 2023
d2dc5df
2.0.0-alpha.2
bgiori Jul 6, 2023
db07ea6
minor test change
bgiori Jul 6, 2023
1a902a9
add topo sort, tests
bgiori Jul 12, 2023
2317d72
minor change to topological sort
bgiori Jul 13, 2023
7ee3d36
release: 2.0.0-alpha.3
bgiori Jul 13, 2023
27532ae
add set does not contain any operator
bgiori Jul 15, 2023
94c4376
release: 2.0.0-alpha.4
bgiori Jul 15, 2023
0eabe30
fix: tests
bgiori Jul 26, 2023
a7b26f1
fix matches is with case insensitive booleans; fix tests
bgiori Jul 26, 2023
2b53e84
release: 2.0.0-alpha.5
bgiori Jul 26, 2023
667fbee
variant payload as any instead of object
bgiori Jul 29, 2023
90077c1
release: 2.0.0-alpha.7
bgiori Jul 29, 2023
cf38277
fix set contains operators
bgiori Aug 14, 2023
11a9de0
release 2.0.0-alpha.8
bgiori Aug 14, 2023
244ce32
dont use permyriad range for distribution
bgiori Aug 18, 2023
1b6a983
release: 2.0.0-alpha.9
bgiori Aug 18, 2023
7ea67f3
update tests
bgiori Aug 18, 2023
8238814
fix comarable matching logic
bgiori Aug 19, 2023
1b161f9
release: 2.0.0-alpha.10
bgiori Aug 19, 2023
6b54097
dont add one to the distribution range
bgiori Aug 22, 2023
009c831
release: 2.0.0-beta.1
bgiori Aug 22, 2023
3685923
udpate defaultVariant to variant
bgiori Aug 25, 2023
c63e45b
release 2.0.0-beta.2
bgiori Aug 25, 2023
cc30f88
fix integration tests
bgiori Aug 25, 2023
efc50bd
simplify murmurhash, fix test
bgiori Sep 20, 2023
bbcfed5
update kotlin
bgiori Oct 16, 2023
ecdc406
WIP add model and clean up
bgiori Feb 17, 2024
73e8368
add max to allocation range
bgiori Mar 15, 2024
f50cd2d
fix: optimize cpu; precompile semver regex; containsBooleans
bgiori Jul 23, 2024
27b4d39
chore: update release action
bgiori Jul 23, 2024
2c18a19
fix: integration tests on mac
bgiori Jul 23, 2024
7aabc6f
fix: lint, test
bgiori Jul 23, 2024
1167fc5
chore: add source sets
bgiori Jul 23, 2024
6e2c621
chore: attempt to fix build
bgiori Jul 23, 2024
e826d37
chore: only run jvm tests in action
bgiori Jul 23, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 0 additions & 40 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,6 @@ on:
options:
- '-'
- evaluation-core
- evaluation-serialization
- evaluation-js
- evaluation-interop
- all
version:
Expand Down Expand Up @@ -95,44 +93,6 @@ jobs:
./gradlew evaluation-core:publishKotlinMultiplatformPublicationToSonatypeRepository
./gradlew evaluation-core:publishJvmPublicationToSonatypeRepository

- name: Set Version (evaluation-serialization)
if: ${{ github.event.inputs.dryRun == 'false' && (github.event.inputs.releaseModule == 'evaluation-serialization' || github.event.inputs.releaseModule == 'all') }}
uses: jacobtomlinson/gha-find-replace@v2
with:
find: 'version = ".*"'
replace: 'version = "${{ github.event.inputs.version }}"'
include: 'evaluation-serialization/build.gradle.kts'
regex: true

- name: Release evaluation-serialization ${{ github.event.inputs.version }}
if: ${{ github.event.inputs.dryRun == 'false' && (github.event.inputs.releaseModule == 'evaluation-serialization' || github.event.inputs.releaseModule == 'all') }}
env:
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
SONATYPE_STAGING_PROFILE_ID: ${{ secrets.SONATYPE_STAGING_PROFILE_ID }}
SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }}
SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }}
SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
run: |
./gradlew evaluation-serialization:publishKotlinMultiplatformPublicationToSonatypeRepository
./gradlew evaluation-serialization:publishJvmPublicationToSonatypeRepository

- name: Set Version (evaluation-js)
if: ${{ github.event.inputs.dryRun == 'false' && (github.event.inputs.releaseModule == 'evaluation-js' || github.event.inputs.releaseModule == 'all') }}
uses: jacobtomlinson/gha-find-replace@v2
with:
find: 'version = ".*"'
replace: 'version = "${{ github.event.inputs.version }}"'
include: 'evaluation-js/build.gradle.kts'
regex: true

- name: Release evaluation-js ${{ github.event.inputs.version }}
if: ${{ github.event.inputs.dryRun == 'false' && (github.event.inputs.releaseModule == 'evaluation-js' || github.event.inputs.releaseModule == 'all') }}
env:
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
run: |
./gradlew evaluation-js:publishJsNpmPublicationToNpmjs

- name: Set Version (evaluation-interop)
if: ${{ github.event.inputs.dryRun == 'false' && (github.event.inputs.releaseModule == 'evaluation-interop' || github.event.inputs.releaseModule == 'all') }}
uses: jacobtomlinson/gha-find-replace@v2
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,4 @@ jobs:
key: ${{ runner.os }}-gradle-caches-${{ hashFiles('**/*.gradle', '**/*.gradle.kts') }}

- name: Test
run: ./gradlew allTests --info
run: ./gradlew jvmTest --info
2 changes: 1 addition & 1 deletion buildSrc/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
plugins {
kotlin("jvm") version "1.8.10"
kotlin("jvm") version "1.9.10"
}

repositories {
Expand Down
10 changes: 6 additions & 4 deletions buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
object Versions {
const val serializationPlugin = "1.8.0"
const val serializationRuntime = "1.4.1"
const val npmPublishPlugin = "2.0.2"
const val kotlinLint = "10.2.1"
const val serializationPlugin = "1.9.0"
const val serializationRuntime = "1.6.0"
const val kotlinLint = "11.5.1"

// Testing
const val ktorVersion = "2.3.3"
}
191 changes: 191 additions & 0 deletions docs/model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Evaluation Model

* Version: `2.0.0`
* Created: 2024-02-02
* Last Modified: -
* Author: Brian Giori (@bgiori)

This document defines the evaluation model.

The model is defined in **Kotlin** syntax, and each data class is serialized as
JSON using `camelCase` as defined by the class variables.

## Flag

A flag defines targeting, bucketing, and variants for a feature.

```kotlin
data class EvaluationFlag(
// The flag key. Must be unique within a project.
val key: String,

// The flag's variants. The result of a flag evaluation is zero or one
// variant.
val variants: Map<String, EvaluationVariant>,

// The targeting segments. targets and buckets users into a variant.
val segments: List<EvaluationSegment>,

// The flag's dependencies, used to order the flags prior to evaluation.
val dependencies: Set<String>? = null,

// An object of metadata for this flag. Contains information useful
// outside evaluation. The bucketing segment's metadata is merged with
// the flag metadata and returned within the evaluation result.
val metadata: Map<String, Any?>? = null
)
```

## Variant

A variant is the result of a flag's evaluation.

```kotlin
data class EvaluationVariant(
// The key must be unique for a flag's variant. I.e. no two variants on one
// flag can have the same key.
val key: String,
// The variant value is used primarily in the application build feature
// logic.
val value: Any? = null,
// The payload may contain additional data for use in the application.
val payload: Any? = null,
// Metadata is aggregated from the flag, segment and variant upon assignment
// and may be used for tracking and debugging purposes, among others.
val metadata: Map<String, Any?>? = null,
)
```

## Segment

A segment targets and buckets users into a variant.

The `conditions` define if the user should be bucketed. If the user should be bucketed, the `bucket` determines which variant the user is assigned. If the conditions or bucket is `null` or the bucket does not assign a variant, then the default `variant` is assigned. If the user is not bucketed, and the `variant` is `null` then the user falls through to the next segment.

```kotlin
data class EvaluationSegment(
// How to bucket the user given a matching condition. If the bucket is null,
// assign the default variant.
val bucket: EvaluationBucket? = null,

// The targeting conditions. On match, bucket the user. The outer list
// is operated with "OR" and the inner list is operated with "AND". If the
// conditions are null, assign the default variant.
val conditions: List<List<EvaluationCondition>>? = null,

// The default variant if the conditions match but either no bucket is set,
// or the bucket does not produce a variant.
val variant: String? = null,

// An object of metadata for this segment. For example, contains the
// segment name and may contain the experiment key associated with this
// segment. The bucketing segment's metadata is passed back in the
// evaluation result after being merged with the along with the vairant and
// flag metadata.
val metadata: Map<String, Any?>? = null
)
```

## Condition

A condition represents a function which returns a boolean value.

The `selector` is used to select a value from the target which is compared to the `values`. The specific behavior of the function depends on the `op`.

```kotlin
data class EvaluationCondition(
// How to select the property from the evaluation state. Each entry in the
// selector will access a key from the target. The resulting value is used
// in the operator function.
val selector: List<String>,

// The operator. Defines the function to use with the selection and values.
val op: String,

// The values to compare to.
val values: Set<String>
)
```

## Bucket

The bucket defines which variant, if any, the user should be assigned.

The `allocations` determine which variant, if any, the user is assigned to. If assigned, the `selector` is used to access the value from the target. The selected value from the target is appended to the `salt` before being hashed.

```kotlin
data class EvaluationBucket(
// How to select the property value from the target.
val selector: List<String>,

// A random string used to salt the bucketing value prior to hashing.
val salt: String,

// Determines which variant, if any, should be returned based on the
// result of the hash functions applied on these allocations.
val allocations: List<EvaluationAllocation>,
)
```

## Allocation

An allocation defines a `max`, `range`, and the `distribution` of variants within that range.

```kotlin
data class EvaluationAllocation(
// The max for the allocation range. This number is used to modulo the hash
// to compare with the range.
val max: Int = 100,

// The distribution range [0, max). That is the possibles values are [0, max-1].
// E.g. with max 100, [0, 49] is 50% allocation
val range: List<Int>,

// The distribution of variants if allocated.
val distributions: List<EvaluationDistribution>,
)
```

## Distribution

A distribution defines a `range`, and the `variant` to assign if the range matches.

```kotlin
data class EvaluationDistribution(
// The key of the variant to deliver if this range matches.
val variant: String,

// The distribution range [start, end), where the max value is 42949672.
// E.g. [0, 42949673] = [0%, 100%]
val range: List<Int>,
)
```

## Operator

An operation is represented as a `String` in a condition.

```kotlin
object EvaluationOperator {
const val IS = "is"
const val IS_NOT = "is not"
const val CONTAINS = "contains"
const val DOES_NOT_CONTAIN = "does not contain"
const val LESS_THAN = "less"
const val LESS_THAN_EQUALS = "less or equal"
const val GREATER_THAN = "greater"
const val GREATER_THAN_EQUALS = "greater or equal"
const val VERSION_LESS_THAN = "version less"
const val VERSION_LESS_THAN_EQUALS = "version less or equal"
const val VERSION_GREATER_THAN = "version greater"
const val VERSION_GREATER_THAN_EQUALS = "version greater or equal"
const val SET_IS = "set is"
const val SET_IS_NOT = "set is not"
const val SET_CONTAINS = "set contains"
const val SET_DOES_NOT_CONTAIN = "set does not contain"
const val SET_CONTAINS_ANY = "set contains any"
const val SET_DOES_NOT_CONTAIN_ANY = "set does not contain any"
const val REGEX_MATCH = "regex match"
const val REGEX_DOES_NOT_MATCH = "regex does not match"
}
```
16 changes: 8 additions & 8 deletions evaluation-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import Versions.ktorVersion

plugins {
kotlin("multiplatform")
kotlin("plugin.serialization") version Versions.serializationPlugin
Expand All @@ -6,7 +8,7 @@ plugins {
id("org.jlleitschuh.gradle.ktlint") version Versions.kotlinLint
}

version = "1.1.1"
version = "2.0.0"

kotlin {

Expand All @@ -26,19 +28,18 @@ kotlin {
}
}

js(IR) {
nodejs()
}

sourceSets {
val commonMain by getting {
dependencies {
implementation("io.github.z4kn4fein:semver:1.4.2")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-core:${Versions.serializationRuntime}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${Versions.serializationRuntime}")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
}
}
}
Expand Down Expand Up @@ -88,10 +89,9 @@ publishing {

signing {
val publishing = extensions.findByType<PublishingExtension>()
val signingKeyId = System.getenv("SIGNING_KEY_ID")
val signingKey = System.getenv("SIGNING_KEY")
val signingPassword = System.getenv("SIGNING_PASSWORD")
useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword)
useInMemoryPgpKeys(signingKey, signingPassword)
sign(publishing?.publications)
}

Expand Down
50 changes: 0 additions & 50 deletions evaluation-core/src/commonMain/kotlin/Allocation.kt

This file was deleted.

17 changes: 17 additions & 0 deletions evaluation-core/src/commonMain/kotlin/EvaluationAllocation.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.amplitude.experiment.evaluation

import kotlinx.serialization.Serializable

@Serializable
data class EvaluationAllocation(
// The max for the allocation range. This number is used to modulo the hash
// to compare with the range.
val max: Int = 100,

// The distribution range [0, max). That is the possibles values are [0, max-1].
// E.g. with max 100, [0, 49] is 50% allocation
val range: List<Int>,

// The distribution of variants if allocated.
val distributions: List<EvaluationDistribution>
)
Loading
Loading