Packages

statsig_elixir

0.22.1-beta.2608150221
0.22.1-beta.2608220222 0.22.1-beta.2608210228 0.22.1-beta.2608200223 0.22.1-beta.2608150221 0.22.0 0.21.1-rc.2608122242 0.21.0 0.20.3-rc.2608051828 0.20.3-beta.2608120251 0.20.3-beta.2608070259 0.20.3-beta.2608060457 0.20.3-beta.2608050456 0.20.3-beta.2607310301 0.20.3-beta.2607300300 0.20.3-beta.2607290300 0.20.3-beta.2607250300 0.20.3-beta.2607240300 0.20.2 0.20.2-rc.2607201629 0.20.2-beta.2607230301 0.20.2-beta.2607220301 0.20.2-beta.2607180300 0.20.2-beta.2607170300 0.20.1 0.20.1-rc.2607140134 0.20.1-rc.2607132133 0.20.1-beta.2607150300 0.20.0 0.19.9-rc.2607090505 0.19.9-rc.2607010924 0.19.9-beta.2607110300 0.19.9-beta.2607100302 0.19.9-beta.2607090302 0.19.9-beta.2607080301 0.19.9-beta.2607020303 0.19.9-beta.2607010304 0.19.9-beta.2606260304 0.19.8 0.19.7-rc.2606232103 0.19.7-beta.2606240303 0.19.7-beta.2606190306 0.19.7-beta.2606180304 0.19.6 0.19.6-rc.2606161624 0.19.6-rc.2606151903 0.19.6-beta.2606170305 0.19.6-beta.2606100304 0.19.5 0.19.5-rc.2606050349 0.19.5-beta.2606060303 0.19.4 0.19.4-rc.2605212136 0.19.3 0.19.3-beta.2604211739 0.19.2 0.19.2-rc.2604140138 0.19.2-beta.2604150312 0.19.1 0.19.1-rc.2604121707 0.19.1-beta.2604130314 0.19.1-beta.2604110309 0.19.1-beta.2604100313 0.19.0 0.18.2-rc.2604080018 0.18.2-beta.2604080312 0.18.1 0.18.1-rc.2604070138 0.18.1-beta.2604072116 0.18.0 0.17.3-rc.2603272156 0.17.3-beta.2603290312 0.17.3-beta.2603280308 0.17.3-beta.2603250310 0.17.2 0.17.2-rc.2603240138 0.17.2-beta.2603241852 0.17.1 0.17.1-rc.2603201903 0.17.1-rc.2603192323 0.17.1-beta.2603210301 0.17.1-beta.2603200305 0.17.1-beta.2603181954 0.17.0 0.16.6-rc.2603180529 0.16.6-rc.2603170138 0.16.6-beta.2603181807 0.16.5 0.16.5-rc.2603131810 0.16.5-beta.2603130304 0.16.4 0.16.4-rc.2603092140 0.16.4-beta.2603110302 0.16.4-beta.2603060304 0.16.4-beta.2603051742 0.16.4-beta.2603040814 0.16.4-beta.2603040303 0.16.3 0.16.3-rc.2602271954 0.16.3-rc.2602270324 0.16.3-rc.2602240138 0.16.3-beta.2603040747 0.16.3-beta.2603021916 0.16.3-beta.2602280254 0.16.3-beta.2602262056 0.16.3-beta.2602250307 0.16.3-beta.2602240529 0.16.2 0.16.2-rc.2602202152 0.16.2-rc.2602200042 0.16.2-beta.2602210300 0.16.2-beta.2602200305 0.16.1 0.16.1-rc.2602180406 0.16.1-beta.2602190307 0.16.0 0.15.2-rc.2602172254 0.15.2-rc.2602140111 0.15.2-rc.2602130016 0.15.2-rc.2602100139 0.15.2-rc.2602030138 0.15.2-beta.2602180309 0.15.2-beta.2602150309 0.15.2-beta.2602140304 0.15.2-beta.2602130310 0.15.2-beta.2602122245 0.15.2-beta.2602122035 0.15.2-beta.2602120040 0.15.2-beta.2602110310 0.15.2-beta.2602050305 0.15.2-beta.2602040304 0.15.2-beta.2602031515 0.15.1 0.15.1-rc.2601270136 0.15.1-beta.2601310133 0.15.1-beta.2601301825 0.15.1-beta.2601300156 0.15.1-beta.2601292313 0.15.1-beta.2601280249 0.15.0 0.14.2-rc.2601230113 0.14.2-rc.2601200136 0.14.2-rc.2601130135 0.14.2-rc.2601062017 0.14.2-rc.2512230135 0.14.2-beta.2601262335 0.14.2-beta.2601240243 0.14.2-beta.2601230247 0.14.2-beta.2601220251 0.14.2-beta.2601210246 0.14.2-beta.2601170240 0.14.2-beta.2601160247 0.14.2-beta.2601140250 0.14.2-beta.2601130116 0.14.2-beta.2601100241 0.14.2-beta.2601070246 0.14.2-beta.2512310244 0.14.2-beta.2512240241 0.14.1 0.14.1-rc.2512190311 0.14.1-rc.2512162207 0.14.1-rc.2512160223 0.14.1-beta.2512200239 0.14.1-beta.2512190242 0.14.1-beta.2512170241 0.14.0 0.13.1-beta.2512140246 0.13.1-beta.2512130201 0.13.1-beta.2512120242 0.13.1-beta.2512110242 0.13.1-beta.2512110007 0.13.1-beta.2512102250 0.13.1-beta.2512102139 0.13.1-beta.2512100241 0.13.1-beta.2512051931 0.13.1-beta.2512050240 0.12.1 0.12.1-rc.2511121816 0.12.1-rc.2511110014 0.12.1-beta.2511110238 0.12.0 0.11.2-rc.2511040014 0.11.2-beta.2511050238 0.11.2-beta.2511032240 0.11.2-beta.2511032212 0.11.2-beta.2511030239 0.11.2-beta.2510310237 0.11.1 0.11.1-rc.2510292219 0.11.1-rc.2510280013 0.11.1-beta.2510300237 0.11.1-beta.2510290239 0.11.0 0.10.3-rc.2510210409 0.10.3-rc.2510210014 0.10.3-rc.6 0.10.3-beta.2510250234 0.10.3-beta.2510240234 0.10.3-beta.2510230235 0.10.3-beta.2510220237 0.10.2 0.10.2-rc.2510141845 0.10.2-beta.2510180231 0.10.2-beta.2510172041 0.10.2-beta.2510170235 0.10.2-beta.2510160235 0.10.2-beta.2510152259 0.10.2-beta.2510152125 0.10.2-beta.2510151849 0.10.2-beta.2510150236 0.10.2-beta.2510142155 0.10.2-beta.2510142023 0.10.1 0.10.0 0.10.0-rc.2510090432 0.10.0-rc.2510070136 0.10.0-beta.2510120235 0.10.0-beta.2510102126 0.10.0-beta.2510100234 0.10.0-beta.2510090234 0.10.0-beta.2510072144 0.9.6-beta.2510020232 0.9.5-rc.2510030411 0.9.5-rc.2510020138 0.9.5-beta.2510010238 0.9.4-rc.2509300113 0.9.4-beta.2509252256 0.9.3 0.9.3-beta.2509250234 0.9.3-beta.2509240232 0.9.3-beta.2509231844 0.9.2-rc.2509221315 0.9.2-beta.2509190233 0.9.2-beta.2509180233 0.9.2-beta.2509180231 0.9.2-beta.2509170231 0.9.1 0.9.1-rc.2509190115 0.9.1-rc.2509181925 0.9.1-rc.2509171909 0.9.0-rc.1 0.8.9-beta.2509161804 0.8.9-beta.2509160231 0.8.8-beta.2509130225 0.8.8-beta.2509112152 0.8.8-beta.2509112049 0.8.8-beta.2509110233 0.8.7 0.8.7-rc.2509102057 0.8.7-rc.2509102017 0.8.7-rc.2509100003 0.8.7-rc.2509092342 0.8.7-beta.2509092208 0.8.6 0.8.6-rc.2509082013 0.8.6-beta.2509040231 0.8.6-beta.2509020235 0.8.5 0.8.5-beta.2508300230 0.8.4 0.8.3 0.8.3-beta.2508280233 0.8.2 0.8.2-rc.2508282224 0.8.1 0.8.1-rc.2508271913 0.8.1-rc.2508262341 0.8.1-rc.2508251314 0.8.0 0.7.4-rc.2508222145 0.7.4-rc.2508220107 0.7.4-rc.2508220104 0.7.4-rc.2508220101 0.7.4-rc.2508220057 0.7.4-rc.2508220049 0.7.4-rc.2508220046 0.7.4-rc.2508220041 0.7.4-rc.2508220033 0.7.4-rc.2508220032 0.7.4-rc.2508220027 0.7.4-rc.2508220022 0.7.4-rc.2508220017 0.7.4-rc.2508220010 0.7.4-rc.2508220006 0.7.4-rc.2508220000 0.7.4-rc.2508212357 0.7.4-rc.2508212349 0.7.4-rc.2508202203 0.7.4-rc.2508202034 0.7.4-rc.2508191829 0.7.4-beta.2508220235 0.7.4-beta.2508210235 0.7.4-beta.2508200235 0.7.4-beta.2508190236 0.7.4-beta.2508160236 0.7.4-beta.2508150240 0.0.6-beta.7 0.0.6-beta.6 0.0.2 retired 0.0.1 retired

A performant elixir SDK for Statsig feature gates and experiments using Rustler

Current section

Files

Jump to
statsig_elixir lib statsig persistent_storage.ex
Raw

lib/statsig/persistent_storage.ex

defmodule Statsig.PersistentStorage.Reference do
@moduledoc false
@enforce_keys [:pid]
defstruct [:pid]
@type t :: %__MODULE__{pid: pid()}
end
defmodule Statsig.PersistentStorage do
@moduledoc """
Behaviour and helper APIs for receiving persistent-assignment (sticky value)
updates from the Statsig SDK.
Configuring `%Statsig.Options{persistent_storage: reference}` enables
persistent assignment: `user_persisted_values` supplied via
`Statsig.ExperimentEvaluationOptions` / `Statsig.LayerEvaluationOptions` are
honored, and the SDK notifies this process when sticky values should be
saved or deleted.
Reads are NOT bridged through the SDK evaluation path: callers load values
from their own store and pass them per call via the evaluation options.
`get_values_for_user/3` is the caller-side helper for that read — it derives
the storage key from the user and calls the implementation's `c:load/2`.
`c:handle_save/4` receives the sticky values as a decoded map, matching the
shape the SDK expects back in `user_persisted_values`.
Storage keys have the format `"<unit id>:<id type>"` — see `storage_key/2`.
Important: once persistent storage is configured, evaluating an experiment
or layer with `user_persisted_values: nil` signals that the caller has no
persisted values, and the SDK issues a `handle_delete` for that config.
Always pass the loaded values (or an empty map for a user with nothing
stored) when sticky assignment should stay active.
Use `start_link/3` to launch the bridge process and pass the returned
`%Statsig.PersistentStorage.Reference{}` into `Statsig.Options`.
"""
alias Statsig.PersistentStorage.Reference
alias Statsig.PersistentStorage.Server
alias Statsig.User
@typedoc "Opaque state returned from user callbacks."
@type state :: term()
@typedoc "Sticky values for one config, as a decoded map."
@type sticky_values :: %{optional(String.t()) => term()}
@typedoc "Map of config name to sticky values for one storage key."
@type user_persisted_values :: %{optional(String.t()) => sticky_values()}
@callback init(init_arg :: term()) :: {:ok, state()} | {:error, term()}
@callback load(key :: String.t(), state()) ::
{:ok, user_persisted_values() | nil, state()} | {:error, term()}
@callback handle_save(
key :: String.t(),
config_name :: String.t(),
sticky_values :: sticky_values(),
state()
) :: {:ok, state()} | {:error, term()}
@callback handle_delete(key :: String.t(), config_name :: String.t(), state()) ::
{:ok, state()} | {:error, term()}
@optional_callbacks handle_delete: 3
@doc """
Starts a bridge process for the provided implementation module.
Returns `{:ok, %Statsig.PersistentStorage.Reference{}}` which can be
assigned to `%Statsig.Options{persistent_storage: reference}`.
"""
@spec start_link(module(), term(), Keyword.t()) :: {:ok, Reference.t()} | {:error, term()}
def start_link(module, init_arg \\ nil, opts \\ []) do
case Server.start_link(module, init_arg, opts) do
{:ok, pid} -> {:ok, %Reference{pid: pid}}
other -> other
end
end
@doc """
Loads the persisted values for a user, deriving the storage key via
`storage_key/2` and calling the implementation's `c:load/2`.
This is a synchronous call intended for the caller side of an evaluation:
fetch the values here, then pass them as `user_persisted_values` in the
experiment/layer evaluation options.
"""
@spec get_values_for_user(Reference.t(), User.t(), String.t(), timeout()) ::
{:ok, user_persisted_values() | nil} | {:error, term()}
def get_values_for_user(
%Reference{pid: pid},
%User{} = user,
id_type \\ "userID",
timeout \\ 5_000
) do
GenServer.call(pid, {:load, storage_key(user, id_type)}, timeout)
end
@doc """
Derives the storage key for a user and ID type, in the format
`"<unit id>:<id type>"` used by the SDK when issuing save/delete
notifications.
For `"userID"` (any casing, or `"user_id"`) the unit ID is the user's
`user_id`; for any other ID type it is looked up in `custom_ids`. A missing
unit ID yields an empty string, mirroring the other SDK bindings.
"""
@spec storage_key(User.t(), String.t()) :: String.t()
def storage_key(%User{} = user, id_type) when is_binary(id_type) do
if String.downcase(id_type) in ["user_id", "userid"] do
"#{user.user_id}:userID"
else
custom_id = Map.get(user.custom_ids || %{}, id_type) || ""
"#{custom_id}:#{id_type}"
end
end
@doc """
Stops the bridge process for the given reference.
"""
@spec stop(Reference.t(), term(), non_neg_integer()) :: :ok
def stop(%Reference{pid: pid}, reason \\ :normal, timeout \\ 5_000) do
GenServer.stop(pid, reason, timeout)
end
end
defmodule Statsig.PersistentStorage.Server do
@moduledoc false
use GenServer
require Logger
@request_tag :statsig_persistent_storage_request
@spec start_link(module(), term(), Keyword.t()) :: GenServer.on_start()
def start_link(module, init_arg, opts) do
GenServer.start_link(__MODULE__, %{module: module, init_arg: init_arg}, opts)
end
@impl true
def init(%{module: module, init_arg: init_arg}) do
case safe_apply(module, :init, [init_arg]) do
{:ok, module_state} ->
{:ok, %{module: module, module_state: module_state}}
{:error, reason} ->
{:stop, reason}
other ->
{:stop, {:bad_return, {module, :init, other}}}
end
end
@impl true
def handle_call({:load, key}, _from, state) do
case safe_apply(state.module, :load, [key, state.module_state]) do
{:ok, values, new_module_state} ->
{:reply, {:ok, values}, %{state | module_state: new_module_state}}
{:error, reason} ->
{:reply, {:error, reason}, state}
other ->
{:reply, {:error, {:bad_return, {state.module, :load, other}}}, state}
end
end
@impl true
def handle_info({@request_tag, :save, key, config_name, sticky_values}, state) do
{:noreply, dispatch(state, :handle_save, [key, config_name, sticky_values])}
end
@impl true
def handle_info({@request_tag, :delete, key, config_name}, state) do
{:noreply, dispatch(state, :handle_delete, [key, config_name])}
end
@impl true
def handle_info(message, state) do
Logger.debug(
"Statsig.PersistentStorage.Server received unexpected message: #{inspect(message)}"
)
{:noreply, state}
end
# Save/delete notifications are fire-and-forget from the native side: the
# SDK captures this process's pid once at initialization, so a callback that
# raises must never crash the bridge — a supervised restart would leave the
# SDK messaging a dead pid and silently disable persistence.
defp dispatch(state, callback, args) do
arity = length(args) + 1
if function_exported?(state.module, callback, arity) do
case safe_apply(state.module, callback, args ++ [state.module_state]) do
{:ok, new_module_state} ->
%{state | module_state: new_module_state}
{:error, reason} ->
Logger.warning(
"Statsig.PersistentStorage #{callback} failed for #{inspect(state.module)}: #{inspect(reason)}"
)
state
other ->
Logger.warning(
"Statsig.PersistentStorage #{callback} returned unexpected value: #{inspect(other)}"
)
state
end
else
state
end
end
defp safe_apply(module, fun, args) do
try do
apply(module, fun, args)
rescue
exception -> {:error, Exception.message(exception)}
catch
kind, reason -> {:error, {kind, reason}}
end
end
end