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
Current section
Files
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