Skip to content

persistent datastore

Ahmed Abbas edited this page Aug 7, 2026 · 4 revisions

Persistent DataStore

By default, the SDK keeps bucketing decisions in memory only. This means each new session (JavaScript) or HTTP request (PHP) recalculates which variation a visitor sees. While the MurmurHash algorithm ensures deterministic bucketing for the same visitor ID, some changes to a running experiment can still shift a returning visitor to a different variation — adding or removing variations, or re-weighting the split between them. (On new experiments, ramping total traffic up or down is safe and needs no DataStore — see Bucketing Algorithm.)

A persistent DataStore solves this by saving bucketing decisions to durable storage. Once a visitor is bucketed, subsequent sessions or requests read the stored assignment instead of recalculating it, keeping the visitor's experience consistent even when the project configuration changes.

Persistence also enables cross-request conversion attribution in PHP. For example, a visitor bucketed on page 1 can have a purchase tracked on page 3 and the SDK will correctly link the conversion to the original variation.

Android and iOS: these SDKs persist sticky bucketing decisions and visitor state automatically in app-private storage (Android SharedPreferences plus on-disk files; iOS Keychain plus Application Support) — there is no DataStore interface to implement. The rest of this guide applies to the JavaScript, PHP, Python, and Ruby SDKs.

The DataStore Interface

The JavaScript, PHP, and Ruby SDKs expect a DataStore with two methods:

Method Signature Description
get get(key): value Retrieve a value by key. When called without a key (JS only), return all stored data.
set set(key, value): void Store a value under the given key.

In the JavaScript SDK, you pass any object implementing these two methods via the dataStore configuration option.

In the PHP SDK, the DataStore defaults to the PSR-16 CacheInterface provided via the cache option. This cache serves a dual purpose: it stores both the fetched project configuration (with a TTL controlled by dataRefreshInterval) and visitor bucketing data. You can optionally pass a separate dataStore to decouple visitor data from config caching.

In the Ruby SDK, pass any object responding to get(key) / set(key, value) via the store: seam on ConvertSdk.create. The default is an in-process MemoryStore; a production-ready ConvertSdk::Stores::RedisStore ships in the box for sharing state across a Puma cluster, a Sidekiq fleet, or Lambda invocations.

In the Python SDK, pass any object satisfying the @runtime_checkable DataStore Protocol via the data_store field on SDKConfig. The Protocol is slightly richer — four duck-typed methods: get(key), set(key, value, ttl=None), has(key), and delete(key). The default is the in-process InMemoryDataStore; no Redis store ships in the box, so for sharing state across a gunicorn/uvicorn fleet, Celery workers, or Lambda invocations you supply your own Redis- or DB-backed implementation of the Protocol.

Implementing a Custom DataStore

# The DataStore Protocol is @runtime_checkable and duck-typed — implement four
# methods (get, set with optional ttl, has, delete). No subclassing required.
from typing import Any, Optional
from convert_sdk import Core, SDKConfig

class CustomDataStore:
    def __init__(self) -> None:
        self._data: dict[str, Any] = {}

    def get(self, key: str) -> Any:
        return self._data.get(key)

    def set(self, key: str, value: Any, ttl: Optional[int] = None) -> None:
        self._data[key] = value

    def has(self, key: str) -> bool:
        return key in self._data

    def delete(self, key: str) -> None:
        self._data.pop(key, None)

core = Core(SDKConfig(sdk_key="your-sdk-key", data_store=CustomDataStore())).initialize()

The JavaScript example above uses an in-memory object for illustration. In production, replace the backing store with Redis, a database, or any persistent layer appropriate for your environment. For Cloudflare Workers, use the KVDataStore from @convertcom/js-sdk-cloudflare.

The PHP example uses Symfony's Redis adapter wrapped in a PSR-16 interface. Any PSR-16 implementation works -- Memcached, filesystem, database, or a custom adapter.

Using Memcached (PHP)

Separating Config Cache from Visitor Store (PHP)

If you need a different storage backend for visitor data than for config caching, pass the dataStore option separately:

When dataStore is provided, it takes precedence over cache for visitor data.

Configuring the DataStore at Initialization

Pass the DataStore when creating the SDK instance:

from convert_sdk import Core, SDKConfig

# No Redis store ships in the box — supply your own object satisfying the
# DataStore Protocol (get/set/has/delete), backed by Redis or your DB, so the
# decision is shared across processes / invocations:
CONVERT = Core(SDKConfig(sdk_key="your-sdk-key", data_store=your_shared_data_store())).initialize()

# Process 1: visitor is bucketed — the decision is persisted via the DataStore
context = CONVERT.create_context("visitor-123", visitor_attributes={"country": "US"})
variation = context.run_experience("homepage-redesign")

# --- later, in a separate process / invocation ---

# Process 2: conversion is attributed to the correct variation
context = CONVERT.create_context("visitor-123")
context.track_conversion("purchase-completed")
# The SDK reads the stored bucketing -> conversion links to the variation

Visitor ID Continuity (PHP)

The SDK identifies visitors by the $visitorId you pass to createContext(). You are responsible for providing the same ID across requests. Common approaches:

  • Session ID -- session_id() (works for web apps with PHP sessions)
  • Cookie -- a persistent cookie with a unique visitor token
  • Authenticated user ID -- for logged-in users

How It Works Internally

Without a DataStore, bucketing decisions live only in memory. With a DataStore, the SDK reads from and writes to it via a DataStoreManager wrapper. On each bucketing call the SDK checks the DataStore first; if a stored decision exists for that visitor and experience, it is returned directly. Otherwise, the SDK calculates a new bucketing decision and writes it to the DataStore for future use.

In PHP, the default in-memory ArrayCache is replaced on each request, so bucketing is recalculated every time. Swapping in a persistent PSR-16 cache (Redis, Memcached, etc.) makes the first request calculate and store the decision, while subsequent requests for the same visitor read it from the cache and skip the computation.

See the SDK configuration options (JS | PHP) for the full list of initialization parameters, and the visitor context guide for details on creating and managing visitor contexts.

Clone this wiki locally