# Client Persistent Assignment

Persistent assignment ensures that a user's variant stays consistent while an experiment is running, regardless of changes to allocation or targeting.

## Persistent storage

Persistent storage uses an adapter approach, allowing you to plug in a storage solution of your choice to store assignments. The SDK references stored assignments later to ensure a user stays in the same bucket. You can implement an adapter that uses Local Storage, or one that uses remote storage, to enable persistence across multiple devices.
The user persistent storage interface consists of a `load`/`loadAsync`, `save`, and `delete` API for read/write operations.

{% callout type="info" %}
Statsig supports Persistent Storage on:

* The modern [`Javascript,`](https://docs.statsig.com/client/javascript-sdk) [`React`](https://docs.statsig.com/client/React), and [`React Native`](https://docs.statsig.com/client/ReactNative) SDKs, including on-device evaluation
* [`Android, on-device evaluation`](https://docs.statsig.com/client/androidOnDeviceEvaluationSDK) only
* [`iOS, on-device evaluation`](https://docs.statsig.com/client/swiftOnDeviceEvaluationSDK) only
* Go to [Support in iOS and Android SDKs](#support-in-ios-and-android-sdks) for Android and iOS details
{% /callout %}

### Persistent storage logic

* Providing a storage adapter on Statsig initialization gives the SDK access to read & write on your custom storage.
* Providing user persisted values to `get_experiment` informs the SDK to
  * **save** the evaluation of the current user **on first evaluation**
  * **load** the previously saved evaluation of a persisted user **on subsequent evaluations**
  * **delete** the previously saved evaluation of a persisted user if the experiment is no longer active
* Not providing user persisted values to `get_experiment` **deletes** a previously saved evaluation.

### Example usage

{% tabs %}
{% tab title="Javascript" %}
```typescript
import { StatsigClient } from '@statsig/js-client';
import { UserPersistentOverrideAdapter } from '@statsig/js-user-persisted-storage';

// Custom storage implementation using localStorage
class LocalStorageUserPersistedStorage {
  load(key) {
    return JSON.parse(localStorage.getItem(key) ?? '{}');
  }

  save(key, experiment, data) {
    const values = JSON.parse(localStorage.getItem(key) ?? '{}');
    values[experiment] = JSON.parse(data);
    localStorage.setItem(key, JSON.stringify(values));
  }

  delete(key, experiment) {
    const data = JSON.parse(localStorage.getItem(key) ?? '{}');
    delete data[experiment];
    localStorage.setItem(key, JSON.stringify(data));
  }
}

const storage = new LocalStorageUserPersistedStorage();
const adapter = new UserPersistentOverrideAdapter(storage);
const client = new StatsigClient('client-key', { overrideAdapter: adapter });

await client.initializeAsync({ userID: "123" });

const user = { userID: "123" };
const userPersistedValues = adapter.loadUserPersistedValues(user, 'userID');

const experiment = client.getExperiment('active_experiment', { userPersistedValues });
console.log(experiment.getGroupName()); // 'Control'

// Switch to different user - will maintain same experiment group due to persistence
const newUser = { userID: "456" };
const newExperiment = client.getExperiment('active_experiment', { userPersistedValues });
console.log(newExperiment.getGroupName()); // Still 'Control'
```
{% /tab %}

{% tab title="React" %}
The syntax for React matches vanilla JavaScript. For a full implementation example, refer to the [Persistent Storage Example](https://github.com/statsig-io/js-client-monorepo/tree/main/samples/next-js/src/app/persisted-user-storage-example) in Next.js.
{% /tab %}

{% tab title="Android On-Device Eval" %}
#### Synchronous persistent evaluations

The `UserPersistentStorageInterface` exposes two methods for synchronous persistent storage, which the SDK calls by default when evaluating an experiment.

```
interface UserPersistentStorageInterface {
    suspend fun load(key: String): PersistedValues
    fun save(key: String, experimentName: String, data: String)
    fun delete(key: String, experiment: String)
    ...
}
```

The `key` string is a combination of ID and ID Type: for example, "123:userID" or "abc:stableID". The SDK constructs this key and calls `get` and `set` on it by default.

You can use this interface to persist evaluations synchronously to local storage. If you need an async interface, refer to Asynchronous persistent evaluations.

#### Asynchronous persistent evaluations

The `UserPersistentStorageInterface` exposes two methods for asynchronous persistent evaluations. Because the `getExperiment` call is synchronous, load the value first and pass it in as `userPersistedValues`.

```kotlin
interface UserPersistentStorageInterface {
    fun loadAsync(key: String, callback: IPersistentStorageCallback)
    fun save(key: String, experimentName: String, data: String)
    fun delete(key: String, experiment: String)
    ...
}
interface IPersistentStorageCallback {
    fun onLoaded(values: PersistedValues)
}
```

A top-level method loads the value for a given user and ID type:

```kotlin
// Asynchronous load values
val userPersistedValues = Statsig.client.loadUserPersistedValuesAsync(
  user: StatsigUser,
  idType: string, // userID, stableID, customIDxyz, etc
  callback: IPersistentStorageCallback
);

// Synchronous load values 
val userPersistedvalues = Statsig.client.loadUserPersistedValues(
    user: StatsigUser,
  idType: string, // userID, stableID, customIDxyz, etc
)
```

After you implement `UserPersistentStorageInterface` and set it on `StatsigOptions`, the call site looks like this:

```kotlin
// Asynchronous 
val callback = object: IPersistentStorageCallback {
    @override
    fun onLoaded(values: PersistedValues) {
        Statsig.getExperiment(user, "sample_experiment", GetExperimentOptions(userPersistedValues = values))
    }
}
val userValues = Statsig.client.loadUserPersistedValuesAsync(user, "userID", callback)

// Synchronous
val user = StatsigUser(userID = "user123")
val userValues = Statsig.client.loadUserPersistedValues(user, 'userID');
const experiment = statsig.getExperiment({userID: "123"}, 'the_allocated_experiment', { userPersistedValues: userValues });
```

If you use Java, you can only override the loadAsync function and leave the load function empty.
{% /tab %}

{% tab title="JS On-Device Eval" %}
```typescript
const storage = new CustomStorageAdapter(); // Need to implement
const adapter = new UserPersistentOverrideAdapter(storage);
const client = new StatsigOnDeviceEvalClient(
  'client-key', 
  { overrideAdapter: adapter }
);
await client.initializeAsync();

const userInControl = { userID: "123" }
const userInUnknown = { userID: "unknown" }
const userPersistedValues = adapter.loadUserPersistedValues(user, 'userID');

let experiment = client.getExperiment('active_experiment', user, { userPersistedValues });
console.log(experiment.getGroupName()) // 'Control'

experiment = client.getExperiment('active_experiment', userInUnknown, { userPersistedValues });
console.log(experiment.getGroupName()) // 'Control'
```

For a full implementation example, refer to the [Persistent Storage On-Device Eval Example](https://github.com/statsig-io/js-client-monorepo/tree/main/samples/next-js/src/app/persisted-user-storage-example-on-device) in Next.js.
{% /tab %}

{% tab title="JS (legacy)" %}
```typescript
await statsig.initialize(
  'client-key',
  { userPersistentStorage: new CustomStorageAdapter() } // Need to implement
);

const userInControl = { userID: "123" }
const userInUnknown = { userID: "unknown" }
const userPersistedValues = await statsig.loadUserPersistedValuesAsync(userInControl, 'userID');

let experiment = statsig.getExperiment(userInControl, 'active_experiment', { userPersistedValues });
console.log(experiment.getGroupName()) // 'Control'

experiment = statsig.getExperiment(userInUnknown, 'active_experiment', { userPersistedValues });
console.log(experiment.getGroupName()) // 'Control'
```
{% /tab %}
{% /tabs %}

## Support in iOS and Android SDKs

Android and iOS SDKs offer a simplified version of Persistent Storage called `keepDeviceValues` that relies on on-device storage. This option is less flexible but requires only a single boolean flag. When you enable it, the SDK checks internally for a previously stored value in the `getExperiment`/`getLayer` call. The SDK uses that stored value even if allocation or targeting has changed. When the experiment ends, the SDK stops persisting values.

{% tabs %}
{% tab title="iOS" %}
#### Swift

```swift
// With an Experiment:
let titleExperiment = Statsig.getExperiment("new_user_promo_title", true) // <-- "true" flag sets keep device values
// Use the experiment like normal:
let promoTitle = titleExperiment.getValue(forKey: "title", defaultValue: "Welcome to Statsig!")

// Or a Layer:
let layer = Statsig.getLayer("user_promo_experiments", true) // <-- "true" flag sets keep device values
// Use the layer like normal:
let promoTitle = layer.getValue(forKey: "title", defaultValue: "Welcome to Statsig!")
```

#### Objective C

```objc
// With an Experiment:
DynamicConfig *expConfig = [Statsig getExperimentForName:@"new_user_promo_title" true]; // <-- "true" flag sets keep device values
// Use the experiment like normal:
NSString *promoTitle = [expConfig getStringForKey:@"title" defaultValue:@"Welcome to Statsig! Use discount code WELCOME10OFF for 10% off your first purchase!"];
double discount = [expConfig getDoubleForKey:@"discount" defaultValue:0.1];

double price = msrp * (1 - discount);
```
{% /tab %}

{% tab title="Android" %}
#### Java

```Java
// With an Experiment:
DynamicConfig titleExperiment = Statsig.getExperiment("new_user_promo_title", true); // <-- "true" flag sets keep device values
// Use the experiment like normal:
String promoTitle = titleExperiment.getString("title", "Welcome to Statsig!");

// Or a Layer:
Layer layer = Statsig.getLayer("user_promo_experiments", true) // <-- "true" flag sets keep device values
// Use the layer like normal:
String promoTitle = layer.getString("title", "Welcome to Statsig!");
```

#### Kotlin

```kotlin
// With an Experiment:
val titleExperiment = Statsig.getExperiment("new_user_promo_title", true) // <-- "true" flag sets keep device values
// Use the experiment like normal:
val promoTitle = titleExperiment.getString("title", "Welcome to Statsig!")

// Or a Layer:
val layer = Statsig.getLayer("user_promo_experiments", true) // <-- "true" flag sets keep device values
// Use the layer like normal:
val promoTitle = layer.getString("title", "Welcome to Statsig!")

```
{% /tab %}
{% /tabs %}
