Server Persistent Assignment
Persistent assignment allows you to ensure that a user's variant stays consistent while an experiment is running, regardless of changes to allocation or targeting.
Persistent Storage Adapter
The persistent storage adapter allows you to plug in your own storage solution that Statsig SDK uses to persist user assignments.
The storage interface consists of just a load
and save
API for read/write operations.
info
Currently only supported in Go
, Ruby
, Legacy Node
, Node Core
, Kotlin
, .Net
, Python Core
Persistent Storage Logic
- Providing a storage adapter on Statsig initialization will give the SDK access to read & write on your custom storage
- Providing user persisted values to
get_experiment
will inform the SDK to- save the evaluation of the current user on first evaluation
- Will only save when experiment / layer is active
- load the previously saved evaluation of a persisted user on subsequent evaluations
- save the evaluation of the current user on first evaluation
- CAVEAT Persisted Value will be deleted when:
- When you provided call
getExperiment
withuser_persisted_values=None
- or When experiment is not active
- When you provided call
Persistent Assignment Options (Limited SDK Support)
- Enforce Targeting:
boolean
, default:false
- Whether or not to enforce targeting rules before assigning persisted values
- Node JS
- Kotlin
val options = GetExperimentOptions(
...
persistentAssignmentOptions = PersistentAssignmentOptions(
enforceTargeting = true,
)
)
const options: GetExperimentOptions = {
...
persistentAssignmentOptions: {
enforceTargeting: true,
}
}
Example usage
- Python (Python Core)
- Node Core
- Node JS
- Kotlin
- Ruby
- Go
- .Net
Statsig.initialize(
'secret-key',
StatsigOptions.new(
user_persistent_storage: DummyPersistentStorageAdapter.new
)
)
persisted_user = StatsigUser.new({ 'userID' => 'test-123' })
exp = Statsig.get_experiment( # User gets saved to persisted storage
persisted_user,
'active_experiment',
Statsig::GetExperimentOptions.new(
user_persisted_values: Statsig.get_user_persisted_values(persisted_user, 'userID')
)
)
puts exp.group_name # 'Control'
exp = Statsig.get_experiment( # User evaluates using values from persisted storage
StatsigUser.new({'userID' => 'unknown'}),
'active_experiment',
Statsig::GetExperimentOptions.new(
user_persisted_values: Statsig.get_user_persisted_values(persisted_user, 'userID')
)
)
puts exp.group_name # 'Control'
from statsig_python_core import Statsig, StatsigUser, StatsigOptions, ExperimentEvaluationOptions, PersistentStorage
options = StatsigOptions(persistent_storage = MyPersistentStorage())
statsig = Statsig.initialize(options).wait
user = StatsigUser("a-user")
exp = statsig.get_experiment(StatsigUser("a-user"), ExperimentEvaluationOptions(user_persisted_values= PersistentStorage.get_user_persisted_value(user, "user_id")))
print(f"{exp.group_name}") # control
import { PersistentStorage, StickyValues, UserPersistedValues } from '@statsig/statsig-node-core';
class MyPersistentStorage implements PersistentStorage {
private storage = new Map<string, UserPersistedValues>();
load(key: string): UserPersistedValues | null {
return this.storage.get(key) || null;
}
save(key: string, config_name: string, data: StickyValues): void {
const existing = this.storage.get(key) || {};
existing[config_name] = data;
this.storage.set(key, existing);
}
delete(key: string, config_name: string): void {
const existing = this.storage.get(key);
if (existing) {
delete existing[config_name];
this.storage.set(key, existing);
}
}
}
runBlocking {
Statsig.initialize(
"secret-key",
StatsigOptions(userPersistentStorage = MyPersistentStorageAdapter())
)
}
val persistedUser = StatsigUser("test-123")
var exp = Statsig.getExperimentSync(
persistedUser,
"active_experiment",
GetExperimentOptions(
userPersistedValues = Statsig.getUserPersistedValues(persistedUser, "userID"),
),
)
println(exp.groupName) // "Control"
exp = Statsig.getExperimentSync(
StatsigUser("unknown"),
"active_experiment",
GetExperimentOptions(
userPersistedValues = Statsig.getUserPersistedValues(persistedUser, "userID"),
),
)
println(exp.groupName) // "Control"
await Statsig.initialize(
"secret-key",
{
userPersistentStorage: new MyPersistentStorageAdapter()
}
)
const persistedUser: StatsigUser = { userID: "123" }
let exp = Statsig.getExperimentSync(
persistedUser,
"active_experiment",
{
userPersistedValues: Statsig.getUserPersistedValues(user, "userID")
},
)
console.log(exp.getGroupName()) // "Control"
exp = Statsig.getExperimentSync(
{ userID: "unknown" },
"active_experiment",
{
userPersistedValues: Statsig.getUserPersistedValues(user, "userID")
},
)
console.log(exp.getGroupName()) // "Control"
InitializeWithOptions(
"secret-key",
&Options{
UserPersistentStorage: persistentStorage,
}
)
persistedUser := User{UserID: "123"}
exp := GetExperimentWithOptions(
persistedUser,
"active_experiment",
&GetExperimentOptions{
PersistedValues: GetUserPersistedValues(persistedUser, "userID")
}
)
fmt.Println(exp.GroupName) // "Control"
exp = GetExperimentWithOptions(
User{UserID: "unknown"},
"active_experiment",
&GetExperimentOptions{
PersistedValues: GetUserPersistedValues(persistedUser, "userID")
}
)
fmt.Println(exp.GroupName) // "Control"
var options = new StatsigServerOptions();
options.UserPersistentStorage = new MyPersistentStorageAdapter()
await StatsigServer.Initialize("server-secret-key", options);
var persistedUser = new StatsigUser { UserID = "123" };
var values = await StatsigServer.GetUserPersistedValues(persistedUser, "userID");
var getExpOptions = new StatsigGetExperimentOptions(values);
var exp = StatsigServer.GetExperimentSync(persistedUser, "active_experiment", getExpOptions);
Console.WriteLine(exp.GroupName); // "Control"
var newValues = await StatsigServer.GetUserPersistedValues(persistedUser, "userID");
var newGetExpOptions = StatsigGetExperimentOptions(newValues);
var newExp = StatsigServer.GetExperimentSync(new StatsigUser {UserID = "unknown"}, "active_experiment", newGetExpOptions);
Console.WriteLine(newExp.GroupName); // "Control"