diff --git a/.github/workflows/create_release.yml b/.github/workflows/create_release.yml new file mode 100644 index 000000000..de7f6c2ce --- /dev/null +++ b/.github/workflows/create_release.yml @@ -0,0 +1,32 @@ +name: Create Release + +on: + workflow_dispatch: + push: + tags: + - 'v*.*.*' + +env: + # GH Octokit (Kong App) + GH_APP_ID: '229901' + GH_APP_INSTALLATION_ID: '36921303' + GH_APP_PRIVATE_KEY: ${{ secrets.KONG_APP_KEY_V2 }} + +jobs: + create-release: + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/heads/releases/') || startsWith(github.ref, 'refs/heads/betas/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: pnpm/action-setup@v4 + with: + version: 7.32.4 + + - name: Install CLI(Tore) Dependencies + run: pnpm install --dir cli + + - name: Create GitHub Release + run: ./tore gh-create-release private-statsig-server-core diff --git a/Cargo.lock b/Cargo.lock index 0d22b7019..23fe93b71 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2373,7 +2373,7 @@ dependencies = [ [[package]] name = "sigstat-grpc" -version = "0.7.4-beta.2508160236" +version = "0.8.0" dependencies = [ "async-trait", "chrono", @@ -2437,7 +2437,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "statsig-node" -version = "0.7.4-beta.2508160236" +version = "0.8.0" dependencies = [ "async-trait", "napi", @@ -2450,7 +2450,7 @@ dependencies = [ [[package]] name = "statsig-pyo3" -version = "0.7.4-beta.2508160236" +version = "0.8.0" dependencies = [ "async-trait", "lazy_static", @@ -2464,7 +2464,7 @@ dependencies = [ [[package]] name = "statsig-rust" -version = "0.7.4-beta.2508160236" +version = "0.8.0" dependencies = [ "ahash", "arc-swap", @@ -2504,7 +2504,7 @@ dependencies = [ [[package]] name = "statsig_elixir" -version = "0.7.4-beta.2508160236" +version = "0.8.0" dependencies = [ "parking_lot", "rustler", @@ -2516,7 +2516,7 @@ dependencies = [ [[package]] name = "statsig_ffi" -version = "0.7.4-beta.2508160236" +version = "0.8.0" dependencies = [ "async-trait", "cbindgen", @@ -2725,9 +2725,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.43.0" +version = "1.43.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" +checksum = "492a604e2fd7f814268a378409e6c92b5525d747d10db9a229723f55a417958c" dependencies = [ "backtrace", "bytes", diff --git a/Cargo.toml b/Cargo.toml index 4a1fa2f54..6ed7c31c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,7 +25,7 @@ exclude = [ [workspace.package] edition = "2021" license = "ISC" -version = "0.7.4-beta.2508160236" +version = "0.8.0" homepage = "https://statsig.com/" authors = ["Statsig", "Daniel Loomb "] readme = "README.md" diff --git a/cli/src/commands/builders/elixir-builder.ts b/cli/src/commands/builders/elixir-builder.ts index 72ebde17d..47cdcc8cf 100644 --- a/cli/src/commands/builders/elixir-builder.ts +++ b/cli/src/commands/builders/elixir-builder.ts @@ -7,12 +7,13 @@ import { Log } from '@/utils/terminal_utils.js'; const NIF_VERSION = "nif-2.15" export function buildElixir(options: BuilderOptions) { options.subProject = 'statsig_elixir'; + let RUSTFLAGS = "" if (options.os == 'windows') { - // options.envSetupForBuild = 'set RUSTFLAGS="-C target-cpu=native" &&'; + // RUSTFLAGS = 'RUSTFLAGS="-C target-cpu=native"'; } else { - options.envSetupForBuild = 'RUSTFLAGS="-C target-feature=-crt-static"'; + RUSTFLAGS = 'RUSTFLAGS="-C target-feature=-crt-static"'; } - let buildcommand = `cargo build --release -p statsig_elixir --target-dir target/${options.target}` + let buildcommand = `${RUSTFLAGS} cargo build --release -p statsig_elixir --target-dir target/${options.target}` execAndLogSync(buildcommand); let binPath = `target/${options.target}/release`; diff --git a/cli/src/commands/publishers/elixir-publisher.ts b/cli/src/commands/publishers/elixir-publisher.ts index 97efb5b62..ccfe9f9f9 100644 --- a/cli/src/commands/publishers/elixir-publisher.ts +++ b/cli/src/commands/publishers/elixir-publisher.ts @@ -1,4 +1,4 @@ -import { getRootedPath } from '@/utils/file_utils.js'; +import { BASE_DIR, getRootedPath } from '@/utils/file_utils.js'; import { GhRelease, createReleaseForVersion, @@ -19,7 +19,7 @@ import { PublisherOptions } from './publisher-options.js'; const ELIXIR_REPRO_NAME = 'statsig-elixir-core'; const EXPECTED_ZIPPED_FILES = 6; const COMPRESSED_DIR = 'artifacts/elixir_compressed_dir'; -const ELIXIR_DIR = 'statsig-elixir'; +const ELIXIR_DIR = `${BASE_DIR}/statsig-elixir`; export async function publishElixir(options: PublisherOptions) { const octokit = await getOctokit(); @@ -30,7 +30,7 @@ export async function publishElixir(options: PublisherOptions) { // step 2. zip path is **/target/**/release/libstatsig_elixir**.so const zippedFilesPath = await compressLibraries(); // step 3. upload - for (const path in zippedFilesPath) { + for (const path of zippedFilesPath) { await uploadRelease(octokit, release, path); } // step 4. run checksum @@ -40,23 +40,26 @@ export async function publishElixir(options: PublisherOptions) { } async function publishToHex() { - Log.stepBegin("Publish package to hex") - execSync(`mix hex.user auth ${process.env.HEX_API_KEY}`, { cwd: ELIXIR_DIR }) - execSync(`mix hex.publish`, { cwd: ELIXIR_DIR }) + Log.stepBegin('Publish package to hex'); + execSync(`HEX_API_KEY=${process.env.HEX_API_KEY} mix hex.publish --yes`, { cwd: ELIXIR_DIR, stdio: 'inherit' }); } async function runCheckSum() { Log.stepBegin('Setup elixir build environment'); - execSync('mix local.hex', { cwd: ELIXIR_DIR }); - execSync('mix local.rebar', { cwd: ELIXIR_DIR }); - execSync('mix deps.get', { cwd: ELIXIR_DIR }); + execSync('mix local.hex', { cwd: ELIXIR_DIR, stdio: 'inherit'}); + execSync('mix local.rebar', { cwd: ELIXIR_DIR, stdio: 'inherit'}); + execSync('mix deps.get', { cwd: ELIXIR_DIR, stdio: 'inherit'}); Log.stepEnd('Setup elixir build environment'); Log.stepBegin('Rerun checksum'); - execSync( - `FORCE_STATSIG_NATIVE_BUILD="true" mix rustler_precompiled.download NativeBindings --all --printls`, - { cwd: ELIXIR_DIR }, - ); + execSync('FORCE_STATSIG_NATIVE_BUILD="true" mix compile', { + cwd: ELIXIR_DIR, + stdio: 'inherit' + }); + execSync(`mix rustler_precompiled.download Statsig.NativeBindings --all --printls`, { + cwd: ELIXIR_DIR, + stdio: 'inherit' + }); Log.stepEnd('Rerun checksum'); } @@ -72,12 +75,11 @@ async function uploadRelease( } Log.stepProgress(`Release upload URL: ${uploadUrl}`); - const { result, error } = await uploadReleaseAsset( octokit, ELIXIR_REPRO_NAME, release.id, - COMPRESSED_DIR, + path, ); } @@ -105,13 +107,13 @@ async function compressLibraries() { const compressedPath = []; Log.stepBegin('Compressing: Create tar gz files'); const matches = await glob( - 'artifacts/**/target/**/release/libstatsig_elixir**.so', + '/tmp/statsig-server-core-publish/**/target/**/release/libstatsig_elixir**.so', { nodir: true, }, ); - if (matches.length != EXPECTED_ZIPPED_FILES) { + if (matches.length < EXPECTED_ZIPPED_FILES) { console.error('Found less binaries'); process.exit(1); @@ -131,7 +133,7 @@ async function compressLibraries() { compressedPath.push(tarName); } Log.stepEnd('Compressing: Create tar gz files'); - if (compressedPath.length != EXPECTED_ZIPPED_FILES) { + if (compressedPath.length <= EXPECTED_ZIPPED_FILES) { console.error('Found less zipped files'); process.exit(1); } diff --git a/examples/rust/mem-bench/Cargo.lock b/examples/rust/mem-bench/Cargo.lock index 369baa1ed..8ed786266 100644 --- a/examples/rust/mem-bench/Cargo.lock +++ b/examples/rust/mem-bench/Cargo.lock @@ -1815,7 +1815,7 @@ checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" [[package]] name = "statsig-rust" -version = "0.7.4-beta.2508160236" +version = "0.8.0" dependencies = [ "ahash", "arc-swap", diff --git a/statsig-dotnet/Directory.Build.props b/statsig-dotnet/Directory.Build.props index dacd9a592..93907726a 100644 --- a/statsig-dotnet/Directory.Build.props +++ b/statsig-dotnet/Directory.Build.props @@ -1,6 +1,6 @@ - 0.7.4-beta.2508160236 + 0.8.0 ISC \ No newline at end of file diff --git a/statsig-dotnet/src/Statsig/Statsig.cs b/statsig-dotnet/src/Statsig/Statsig.cs index 6477bd22e..f4c59df98 100644 --- a/statsig-dotnet/src/Statsig/Statsig.cs +++ b/statsig-dotnet/src/Statsig/Statsig.cs @@ -284,6 +284,30 @@ unsafe public Layer GetLayer(StatsigUser user, string layerName, EvaluationOptio } } + unsafe public Layer GetPrompt(StatsigUser user, string promptName, EvaluationOptions? options = null) + { + int nameLen = Encoding.UTF8.GetByteCount(promptName); + Span nameBytes = nameLen + 1 <= SpecNameStackThreshold ? stackalloc byte[nameLen + 1] : new byte[nameLen + 1]; + int written = Encoding.UTF8.GetBytes(promptName, nameBytes[..nameLen]); + nameBytes[written] = 0; + + string? optionsJson = options != null ? JsonConvert.SerializeObject(options) : null; + byte[]? optBytes = optionsJson != null ? Encoding.UTF8.GetBytes(optionsJson) : null; + + fixed (byte* optionsPtr = optBytes) + fixed (byte* promptNamePtr = nameBytes) + { + var jsonStringPtr = + StatsigFFI.statsig_get_prompt(_statsigRef, user.Reference, promptNamePtr, optionsPtr); + var jsonString = StatsigUtils.ReadStringFromPointer(jsonStringPtr); + if (jsonString == null) + { + return new Layer(string.Empty, _statsigRef, options); + } + return new Layer(jsonString, _statsigRef, options); + } + } + unsafe public void ManuallyLogLayerParameterExposure(StatsigUser user, string layerName, string parameterName) { int layerNameLen = Encoding.UTF8.GetByteCount(layerName); diff --git a/statsig-dotnet/src/Statsig/StatsigFFI.g.cs b/statsig-dotnet/src/Statsig/StatsigFFI.g.cs index 317cf76c1..f387ed3fd 100644 --- a/statsig-dotnet/src/Statsig/StatsigFFI.g.cs +++ b/statsig-dotnet/src/Statsig/StatsigFFI.g.cs @@ -133,6 +133,9 @@ internal static unsafe partial class StatsigFFI [DllImport(__DllName, EntryPoint = "statsig_get_layer", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern byte* statsig_get_layer(ulong statsig_ref, ulong user_ref, byte* layer_name, byte* options_json); + [DllImport(__DllName, EntryPoint = "statsig_get_prompt", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern byte* statsig_get_prompt(ulong statsig_ref, ulong user_ref, byte* prompt_name, byte* options_json); + [DllImport(__DllName, EntryPoint = "statsig_log_layer_param_exposure", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern void statsig_log_layer_param_exposure(ulong statsig_ref, byte* layer_json, byte* param_name); diff --git a/statsig-elixir/lib/statsig.ex b/statsig-elixir/lib/statsig.ex index 3522f7522..0bce1dd9b 100644 --- a/statsig-elixir/lib/statsig.ex +++ b/statsig-elixir/lib/statsig.ex @@ -9,7 +9,7 @@ defmodule Statsig do def init({sdk_key, statsig_options}) do try do - instance = NativeBindings.new(sdk_key, statsig_options) + instance = NativeBindings.new(sdk_key, statsig_options,get_system_info()) {:ok, instance} rescue exception -> {:error, Exception.message(exception)} @@ -68,11 +68,11 @@ defmodule Statsig do end end - def get_config(config_name, statsig_user, options \\nil) do + def get_dynamic_config(config_name, statsig_user, options \\nil) do try do instance = get_statsig_instance() - case NativeBindings.get_config(instance, config_name, statsig_user, options) do + case NativeBindings.get_dynamic_config(instance, config_name, statsig_user, options) do {:error, e} -> {:error, e} config -> {:ok, config} end @@ -116,6 +116,37 @@ defmodule Statsig do end end + def get_prompt(prompt_name, statsig_user, options \\nil) do + try do + instance = get_statsig_instance() + case NativeBindings.get_prompt(instance, prompt_name, statsig_user, options) do + {:error, e} -> {:error, e} + layer -> {:ok, layer} + end + rescue + exception -> {:error, Exception.message(exception)} + catch + :exit, reason -> {:error, {:exit, reason}} + exception -> {:error, Exception.message(exception)} + end + end + + def get_client_init_response_as_string(statsig_user, options \\nil) do + try do + instance = get_statsig_instance() + + case NativeBindings.get_client_init_response_as_string(instance, statsig_user, options) do + {:error, e} -> {:error, e} + response -> {:ok, response} + end + rescue + exception -> {:error, Exception.message(exception)} + catch + :exit, reason -> {:error, {:exit, reason}} + exception -> {:error, Exception.message(exception)} + end + end + @spec log_event(%Statsig.User{}, String.t(), String.t() | number(), %{String.t() => String.t()}) :: any() def log_event(statsig_user, event_name, value, metadata) do @@ -123,7 +154,7 @@ defmodule Statsig do instance = get_statsig_instance() case value do - value when is_binary(value) -> + value when is_binary(value) or is_nil(value) -> NativeBindings.log_event(instance, statsig_user, event_name, value, metadata) value when is_number(value) -> @@ -169,4 +200,26 @@ defmodule Statsig do exception -> {:error, Exception.message(exception)} end end + + def get_system_info do + try do + %{ + "os"=> :os.type() |> elem(0) |> Atom.to_string(), + "arch"=> :erlang.system_info(:system_architecture) |> List.to_string(), + "language_version"=> System.version() + } + rescue + _ -> %{ + "os"=> "unknown", + "arch"=> "unknown", + "language_version"=> "unknown" + } + catch + _, _ -> %{ + "os"=> "unknown", + "arch"=> "unknown", + "language_version"=> "unknown" + } + end + end end diff --git a/statsig-elixir/lib/statsig/dynamic_config.ex b/statsig-elixir/lib/statsig/dynamic_config.ex index cace1c1fc..2c65d6a6d 100644 --- a/statsig-elixir/lib/statsig/dynamic_config.ex +++ b/statsig-elixir/lib/statsig/dynamic_config.ex @@ -3,14 +3,14 @@ defmodule Statsig.DynamicConfig do :name, :value, :rule_id, - :id_type + :id_type, ] @type t :: %__MODULE__{ name: String.t(), value: String.t(), rule_id: String.t(), - id_type: String.t() + id_type: String.t(), } def get_param_value(config, param_name) do diff --git a/statsig-elixir/lib/statsig/native_bindings.ex b/statsig-elixir/lib/statsig/native_bindings.ex index 88920caca..d3b2bae1d 100644 --- a/statsig-elixir/lib/statsig/native_bindings.ex +++ b/statsig-elixir/lib/statsig/native_bindings.ex @@ -4,7 +4,7 @@ defmodule Statsig.NativeBindings do otp_app: :statsig_elixir, crate: "statsig_elixir", version: version, - base_url: "https://github.com/statsig-io/statsig-server-core/releases/download/#{version}/", + base_url: "https://github.com/statsig-io/statsig-elixir-core/releases/download/#{version}/", force_build: System.get_env("FORCE_STATSIG_NATIVE_BUILD") in ["1", "true"], targets: [ "aarch64-apple-darwin", # Add other supported targets if needed @@ -14,13 +14,15 @@ defmodule Statsig.NativeBindings do "x86_64-unknown-linux-musl", "aarch64-unknown-linux-musl", ] - def new(_key, _options), do: :erlang.nif_error(:nif_not_loaded) + def new(_key, _options, _system_metadata), do: :erlang.nif_error(:nif_not_loaded) def initialize(_statsig), do: :erlang.nif_error(:nif_not_loaded) def check_gate(_statsig, _gate_name, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) def get_feature_gate(_statsig, _gate_name, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) - def get_config(_statsig, _config_name, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) + def get_dynamic_config(_statsig, _config_name, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) def get_experiment(_statsig, _experiment_name, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) def get_layer(_statsig, _layer_name, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) + def get_prompt(_statsig, _prompt_name, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) + def get_client_init_response_as_string(_statsig, _statsig_user, _options), do: :erlang.nif_error(:nif_not_loaded) def log_event(_statsig, _statsig_user, _event_name,_value, _metadata), do: :erlang.nif_error(:nif_not_loaded) def log_event_with_number(_statsig, _statsig_user, _event_name,_value, _metadata), do: :erlang.nif_error(:nif_not_loaded) def flush(_statsig), do: :erlang.nif_error(:nif_not_loaded) diff --git a/statsig-elixir/lib/statsig/options.ex b/statsig-elixir/lib/statsig/options.ex index d7f796dde..fbaa6f0b1 100644 --- a/statsig-elixir/lib/statsig/options.ex +++ b/statsig-elixir/lib/statsig/options.ex @@ -41,4 +41,12 @@ end defmodule Statsig.DynamicConfigEvaluationOptions do defstruct disable_exposure_logging: false +end + +defmodule Statsig.ClientInitResponseOptions do + defstruct [ + hash_algorithm: nil, + client_sdk_key: nil, + include_local_overrides: nil + ] end \ No newline at end of file diff --git a/statsig-elixir/lib/statsig/user.ex b/statsig-elixir/lib/statsig/user.ex index 77c98d5a7..9f1f17a80 100644 --- a/statsig-elixir/lib/statsig/user.ex +++ b/statsig-elixir/lib/statsig/user.ex @@ -12,6 +12,19 @@ defmodule Statsig.User do app_version: nil, ] + def new(attrs) when is_map(attrs) do + %__MODULE__{} + |> struct(attrs) + |> validate_presence_of_id() + end + + defp validate_presence_of_id(%__MODULE__{user_id: nil, custom_ids: nil}) do + raise ArgumentError, + "Either `user_id` or `custom_ids` must be set" + end + + defp validate_presence_of_id(struct), do: struct + @type custom_value :: String.t() | number() | boolean() | nil @type custom_attributes :: %{String.t() => custom_value()} diff --git a/statsig-elixir/mix.exs b/statsig-elixir/mix.exs index 3cdf14eb2..581e74c75 100644 --- a/statsig-elixir/mix.exs +++ b/statsig-elixir/mix.exs @@ -4,7 +4,7 @@ defmodule Statsigelixir.MixProject do def project do [ app: :statsig_elixir, - version: "0.7.4-beta.2508160236", + version: "0.8.0", elixir: "~> 1.0", start_permanent: Mix.env() == :prod, description: description(), diff --git a/statsig-elixir/native/statsig_elixir/Cargo.toml b/statsig-elixir/native/statsig_elixir/Cargo.toml index 441e6121b..72a793ce3 100644 --- a/statsig-elixir/native/statsig_elixir/Cargo.toml +++ b/statsig-elixir/native/statsig_elixir/Cargo.toml @@ -14,6 +14,6 @@ rustler = "0.36.1" statsig-rust = { path = "../../../statsig-rust", features = [ "with_zstd", ] } -tokio = { version = "1.39.1", features = ["rt", "rt-multi-thread", "macros"] } +tokio = { version = "1.43.1", features = ["rt", "rt-multi-thread", "macros"] } serde_json = { version = "1.0.125", features = ["float_roundtrip"] } serde = { version = "1.0.204", features = ["derive"] } diff --git a/statsig-elixir/native/statsig_elixir/src/statsig_nfi.rs b/statsig-elixir/native/statsig_elixir/src/statsig_nfi.rs index 2d9ec872c..37822777b 100644 --- a/statsig-elixir/native/statsig_elixir/src/statsig_nfi.rs +++ b/statsig-elixir/native/statsig_elixir/src/statsig_nfi.rs @@ -1,5 +1,7 @@ use rustler::{Env, Error, ResourceArc, Term}; -use statsig_rust::{statsig_types::Layer as LayerActual, Statsig}; +use statsig_rust::{ + statsig_metadata::StatsigMetadata, statsig_types::Layer as LayerActual, Statsig, +}; use std::{ collections::HashMap, sync::{Arc, RwLock}, @@ -8,8 +10,8 @@ use std::{ use crate::{ statsig_options_nfi::StatsigOptions, statsig_types_nfi::{ - AllowedPrimitive, DynamicConfig, DynamicConfigEvaluationOptions, Experiment, - ExperimentEvaluationOptions, FeatureGate, FeatureGateEvaluationOptions, + AllowedPrimitive, ClientInitResponseOptions, DynamicConfig, DynamicConfigEvaluationOptions, + Experiment, ExperimentEvaluationOptions, FeatureGate, FeatureGateEvaluationOptions, LayerEvaluationOptions, }, statsig_user_nfi::StatsigUser, @@ -43,7 +45,9 @@ impl LayerResource { pub fn new( sdk_key: String, options: Option, + system_metadata: HashMap, ) -> Result, Error> { + update_metadata(system_metadata); let statsig = Statsig::new(&sdk_key, options.map(|op| Arc::new(op.into()))); Ok(ResourceArc::new(StatsigResource { statsig_core: RwLock::new(Arc::new(statsig)), @@ -106,7 +110,7 @@ pub fn check_gate( } #[rustler::nif] -pub fn get_config( +pub fn get_dynamic_config( statsig: ResourceArc, config_name: &str, statsig_user: StatsigUser, @@ -163,6 +167,26 @@ pub fn get_layer( } } +#[rustler::nif] +pub fn get_prompt( + statsig: ResourceArc, + prompt_name: &str, + statsig_user: StatsigUser, + options: Option, +) -> Result, Error> { + match statsig.statsig_core.read() { + Ok(read_guard) => { + let layer = read_guard.get_prompt_with_options( + &statsig_user.into(), + prompt_name, + options.map(|o| o.into()).unwrap_or_default(), + ); + Ok(ResourceArc::new(LayerResource::new(layer))) + } + Err(_) => Err(Error::RaiseAtom("Failed to get Statsig")), + } +} + #[rustler::nif] pub fn log_event( statsig: ResourceArc, @@ -202,6 +226,27 @@ pub fn log_event_with_number( } } +#[rustler::nif(schedule = "DirtyCpu")] +pub fn get_client_init_response_as_string( + statsig: ResourceArc, + statsig_user: StatsigUser, + options: Option, +) -> Result { + match statsig.statsig_core.read() { + Ok(read_guard) => { + let response = match options { + Some(o) => read_guard.get_client_init_response_with_options_as_string( + &statsig_user.into(), + &o.into(), + ), + None => read_guard.get_client_init_response_as_string(&statsig_user.into()), + }; + Ok(response) + } + Err(_) => Err(Error::RaiseAtom("Failed to get Statsig")), + } +} + #[rustler::nif(schedule = "DirtyIo")] pub fn flush(statsig: ResourceArc) -> Result<(), Error> { match statsig.statsig_core.read() { @@ -280,10 +325,25 @@ pub fn layer_get_rule_id(layer: ResourceArc) -> Result) -> Result, Error> { + println!("get group name"); match layer.core.read() { Ok(read_guard) => Ok(read_guard.group_name.clone()), Err(_) => Err(Error::RaiseAtom("Failed to get Statsig")), } } +// Util Functions +fn update_metadata(system_metadata: HashMap) { + let unknown = "unknown".to_string(); + let os = system_metadata.get("os").unwrap_or(&unknown); + let arch = system_metadata.get("arch").unwrap_or(&unknown); + let language_version = system_metadata.get("language_version").unwrap_or(&unknown); + StatsigMetadata::update_values( + "statsig-server-core-elixir".to_owned(), + os.to_string(), + arch.to_string(), + language_version.to_string(), + ); +} + rustler::init!("Elixir.Statsig.NativeBindings", load = load); diff --git a/statsig-elixir/native/statsig_elixir/src/statsig_types_nfi.rs b/statsig-elixir/native/statsig_elixir/src/statsig_types_nfi.rs index f93f366ed..18fc1a330 100644 --- a/statsig-elixir/native/statsig_elixir/src/statsig_types_nfi.rs +++ b/statsig-elixir/native/statsig_elixir/src/statsig_types_nfi.rs @@ -11,6 +11,7 @@ use statsig_rust::statsig_types::{ FeatureGate as FeatureGateActual, }; use statsig_rust::DynamicValue; +use statsig_rust::{ClientInitResponseOptions as ClientInitResponseOptionsActual, HashAlgorithm}; #[derive(NifStruct)] #[module = "Statsig.Experiment"] pub struct Experiment { @@ -90,6 +91,8 @@ impl<'a> Decoder<'a> for AllowedPrimitive { Ok(AllowedPrimitive::Str(s)) } else if let Ok(b) = bool::decode(term) { Ok(AllowedPrimitive::Bool(b)) + } else if let Ok(f) = f64::decode(term) { + Ok(AllowedPrimitive::Float(f)) } else { Err(rustler::Error::BadArg) } @@ -182,3 +185,24 @@ impl From for DynamicConfigEvaluationOptionsActu } } } + +#[derive(NifStruct)] +#[module = "Statsig.ClientInitResponseOptions"] +pub struct ClientInitResponseOptions { + pub hash_algorithm: Option, + pub client_sdk_key: Option, + pub include_local_overrides: Option, +} + +impl From for ClientInitResponseOptionsActual { + fn from(option: ClientInitResponseOptions) -> Self { + ClientInitResponseOptionsActual { + hash_algorithm: option + .hash_algorithm + .and_then(|v| HashAlgorithm::from_string(v.as_str())), + client_sdk_key: option.client_sdk_key, + include_local_overrides: option.include_local_overrides, + ..Default::default() + } + } +} diff --git a/statsig-elixir/native/statsig_elixir/src/statsig_user_nfi.rs b/statsig-elixir/native/statsig_elixir/src/statsig_user_nfi.rs index af0f83b03..a2ce9489d 100644 --- a/statsig-elixir/native/statsig_elixir/src/statsig_user_nfi.rs +++ b/statsig-elixir/native/statsig_elixir/src/statsig_user_nfi.rs @@ -6,7 +6,13 @@ use std::collections::HashMap; macro_rules! to_value_with_dynamic { ($map:expr) => {{ $map.into_iter() - .map(|(key, value)| (key, value.into())) + .map(|(key, value)| { + let converted_value = match value { + Some(v) => v.into(), + None => DynamicValue::new(), + }; + (key, converted_value) + }) .collect::>() }}; } @@ -14,11 +20,11 @@ macro_rules! to_value_with_dynamic { #[derive(NifStruct)] #[module = "Statsig.User"] pub struct StatsigUser { - pub user_id: String, + pub user_id: Option, pub email: Option, - pub custom: Option>, + pub custom: Option>>, pub custom_ids: Option>, - pub private_attributes: Option>, + pub private_attributes: Option>>, pub ip: Option, pub user_agent: Option, pub country: Option, @@ -28,16 +34,29 @@ pub struct StatsigUser { impl From for StatsigUserActual { fn from(user: StatsigUser) -> Self { - StatsigUserBuilder::new_with_user_id(user.user_id) - .custom_ids(user.custom_ids) - .app_version(user.app_version) - .email(user.email) - .ip(user.ip) - .user_agent(user.user_agent) - .locale(user.locale) - .country(user.country) - .custom(user.custom.map(|m| to_value_with_dynamic!(m))) - .private_attributes(user.private_attributes.map(|m| to_value_with_dynamic!(m))) - .build() + // We enforce either user id or custom ids being set on elixir side, so making assumption if no user id there must be custom_id + match user.user_id { + Some(id) => StatsigUserBuilder::new_with_user_id(id) + .custom_ids(user.custom_ids) + .app_version(user.app_version) + .email(user.email) + .ip(user.ip) + .user_agent(user.user_agent) + .locale(user.locale) + .country(user.country) + .custom(user.custom.map(|m| to_value_with_dynamic!(m))) + .private_attributes(user.private_attributes.map(|m| to_value_with_dynamic!(m))) + .build(), + None => StatsigUserBuilder::new_with_custom_ids(user.custom_ids.unwrap_or_default()) + .app_version(user.app_version) + .email(user.email) + .ip(user.ip) + .user_agent(user.user_agent) + .locale(user.locale) + .country(user.country) + .custom(user.custom.map(|m| to_value_with_dynamic!(m))) + .private_attributes(user.private_attributes.map(|m| to_value_with_dynamic!(m))) + .build(), + } } } diff --git a/statsig-elixir/test/statsig_test.exs b/statsig-elixir/test/statsig_test.exs index f2280d67a..6ab478a8c 100644 --- a/statsig-elixir/test/statsig_test.exs +++ b/statsig-elixir/test/statsig_test.exs @@ -24,8 +24,8 @@ defmodule StatsigTest do statsig_options = %Options{enable_id_lists: true, output_log_level: "debug"} IO.puts("Initializing with SDK key: #{sdk_key}") - Statsig.start_link(sdk_key, statsig_options) - + initres = Statsig.start_link(sdk_key, statsig_options) + IO.inspect(initres) # Create a test user user = %User{ user_id: "test_user_123", diff --git a/statsig-ffi/Cargo.toml b/statsig-ffi/Cargo.toml index 825cfab2a..807a4b14a 100644 --- a/statsig-ffi/Cargo.toml +++ b/statsig-ffi/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true [dependencies] statsig-rust = { path = "../statsig-rust" } serde_json = "1.0.120" -tokio = "1.39.1" +tokio = "1.43.1" lazy_static = "1.5.0" log = "0.4.22" serde = { version = "1.0.204", features = ["derive"] } diff --git a/statsig-ffi/include/statsig_ffi.h b/statsig-ffi/include/statsig_ffi.h index e9bb2235c..8ef98f0df 100644 --- a/statsig-ffi/include/statsig_ffi.h +++ b/statsig-ffi/include/statsig_ffi.h @@ -190,6 +190,11 @@ char *statsig_get_layer(uint64_t statsig_ref, const char *layer_name, const char *options_json); +char *statsig_get_prompt(uint64_t statsig_ref, + uint64_t user_ref, + const char *prompt_name, + const char *options_json); + void statsig_log_layer_param_exposure(uint64_t statsig_ref, const char *layer_json, const char *param_name); diff --git a/statsig-ffi/src/jni/statsig_jni.rs b/statsig-ffi/src/jni/statsig_jni.rs index ba2549762..857000dc7 100644 --- a/statsig-ffi/src/jni/statsig_jni.rs +++ b/statsig-ffi/src/jni/statsig_jni.rs @@ -447,6 +447,33 @@ pub extern "system" fn Java_com_statsig_StatsigJNI_statsigGetLayer( serialize_json_to_jstring(&mut env, &result) } +#[no_mangle] +pub extern "system" fn Java_com_statsig_StatsigJNI_statsigGetPrompt( + mut env: JNIEnv, + _class: jclass, + statsig_ref: jlong, + user_ref: jlong, + prompt_name: JString, + options: JObject, +) -> jstring { + let statsig = get_instance_or_return_c!(Statsig, &(statsig_ref as u64), std::ptr::null_mut()); + let user = get_instance_or_return_c!(StatsigUser, &(user_ref as u64), std::ptr::null_mut()); + + let prompt_name: String = match env.get_string(&prompt_name) { + Ok(s) => s.into(), + Err(_) => return std::ptr::null_mut(), + }; + + let options = convert_java_get_layer_options_to_rust(&mut env, options); + + let result = match options { + Some(options) => statsig.get_prompt_with_options(user.as_ref(), &prompt_name, options), + None => statsig.get_prompt(user.as_ref(), &prompt_name), + }; + + serialize_json_to_jstring(&mut env, &result) +} + #[no_mangle] pub extern "system" fn Java_com_statsig_StatsigJNI_statsigGetFieldsNeededForLayer( mut env: JNIEnv, diff --git a/statsig-ffi/src/statsig_c.rs b/statsig-ffi/src/statsig_c.rs index c7c2d61a8..61f50ff93 100644 --- a/statsig-ffi/src/statsig_c.rs +++ b/statsig-ffi/src/statsig_c.rs @@ -829,6 +829,32 @@ pub extern "C" fn statsig_get_layer( string_to_c_char(result) } +#[no_mangle] +pub extern "C" fn statsig_get_prompt( + statsig_ref: u64, + user_ref: u64, + prompt_name: *const c_char, + options_json: *const c_char, +) -> *mut c_char { + let statsig = get_instance_or_return_c!(Statsig, &statsig_ref, null_mut()); + let user = get_instance_or_return_c!(StatsigUser, &user_ref, null_mut()); + let prompt_name = unwrap_or_return!(c_char_to_string(prompt_name), null_mut()); + + let layer = match c_char_to_string(options_json) { + Some(opts) => match serde_json::from_str::(&opts) { + Ok(options) => statsig.get_prompt_with_options(&user, &prompt_name, options), + Err(e) => { + log_e!(TAG, "Failed to parse options: {}", e); + return null_mut(); + } + }, + None => statsig.get_prompt(&user, &prompt_name), + }; + + let result = json!(layer).to_string(); + string_to_c_char(result) +} + #[no_mangle] pub extern "C" fn statsig_log_layer_param_exposure( statsig_ref: u64, diff --git a/statsig-go/cmd/post-install/main.go b/statsig-go/cmd/post-install/main.go index 4115d1b78..f30e6643b 100644 --- a/statsig-go/cmd/post-install/main.go +++ b/statsig-go/cmd/post-install/main.go @@ -12,7 +12,7 @@ import ( ) const ( - version = "0.7.4-beta.2508160236" + version = "0.8.0" ) var output_dir string diff --git a/statsig-go/src/statsig.go b/statsig-go/src/statsig.go index 7de18ffaa..7d0df9dc8 100644 --- a/statsig-go/src/statsig.go +++ b/statsig-go/src/statsig.go @@ -201,6 +201,30 @@ func (s *Statsig) GetLayer(user StatsigUser, layerName string, layerOptions *Get } +func (s *Statsig) GetPrompt(user StatsigUser, promptName string, layerOptions *GetLayerOptions) Layer { + var layer Layer + + if layerOptions == nil { + layerOptions = &GetLayerOptions{} + } + + layerJson := C.statsig_get_prompt(C.uint64_t(s.InnerRef), C.uint64_t(user.innerRef), C.CString(promptName), C.CString(utils.ConvertJSONToString(layerOptions))) + + if layerJson != nil { + err := json.Unmarshal([]byte(C.GoString(layerJson)), &layer) + if err != nil { + return Layer{} + } + + layer.setStatsigInstance(s) + layer.setDisableExposureLogging(layerOptions != nil && layerOptions.DisableExposureLogging) + layer.setRawResult(C.GoString(layerJson)) + } + + return layer + +} + func (s *Statsig) ManuallyLogLayerParameterExposure(user StatsigUser, layerName string, paramName string) { C.statsig_manually_log_layer_parameter_exposure(C.uint64_t(s.InnerRef), C.uint64_t(user.innerRef), C.CString(layerName), C.CString(paramName)) } diff --git a/statsig-grpc/Cargo.toml b/statsig-grpc/Cargo.toml index 70dca4d08..ef0cae546 100644 --- a/statsig-grpc/Cargo.toml +++ b/statsig-grpc/Cargo.toml @@ -10,7 +10,7 @@ homepage.workspace = true repository.workspace = true [dependencies] -tokio = { version = "1.39.1", features = ["full"] } +tokio = { version = "1.43.1", features = ["full"] } async-trait = "0.1.81" tonic = { version = "0.12.1", features = ["tls"] } parking_lot = "0.12.1" diff --git a/statsig-java/gradle.properties b/statsig-java/gradle.properties index 589f7af07..dc4548c8a 100644 --- a/statsig-java/gradle.properties +++ b/statsig-java/gradle.properties @@ -1 +1 @@ -version=0.7.4-beta.2508160236 \ No newline at end of file +version=0.8.0 \ No newline at end of file diff --git a/statsig-java/src/main/java/com/statsig/Statsig.java b/statsig-java/src/main/java/com/statsig/Statsig.java index de9e686b6..c2205c28a 100644 --- a/statsig-java/src/main/java/com/statsig/Statsig.java +++ b/statsig-java/src/main/java/com/statsig/Statsig.java @@ -207,6 +207,27 @@ public Layer getLayer(StatsigUser user, String layerName, GetLayerOptions option return layer; } + public Layer getPrompt(StatsigUser user, String promptName) { + String layerJson = StatsigJNI.statsigGetPrompt(ref, user.getRef(), promptName, null); + Layer layer = JacksonUtil.fromJsonWithRawJson(layerJson, Layer.class); + if (layer != null) { + // Set the Statsig reference in the Layer instance + layer.setStatsigInstance(this); + } + return layer; + } + + public Layer getPrompt(StatsigUser user, String promptName, GetLayerOptions options) { + String layerJson = StatsigJNI.statsigGetPrompt(ref, user.getRef(), promptName, options); + Layer layer = JacksonUtil.fromJsonWithRawJson(layerJson, Layer.class); + if (layer != null) { + // Set the Statsig reference in the Layer instance + layer.setStatsigInstance(this); + layer.setDisableExposureLogging(options != null && options.disableExposureLogging); + } + return layer; + } + public void manuallyLogLayerParamExposure(StatsigUser user, String layerName, String param) { StatsigJNI.statsigManuallyLogLayerParamExposure(ref, user.getRef(), layerName, param); } diff --git a/statsig-java/src/main/java/com/statsig/StatsigJNI.java b/statsig-java/src/main/java/com/statsig/StatsigJNI.java index 356db6634..87e731a68 100644 --- a/statsig-java/src/main/java/com/statsig/StatsigJNI.java +++ b/statsig-java/src/main/java/com/statsig/StatsigJNI.java @@ -80,6 +80,9 @@ public static native String statsigGetFieldsNeededForDynamicConfig( public static native String statsigGetLayer( long statsigRef, long userRef, String layerName, GetLayerOptions options); + public static native String statsigGetPrompt( + long statsigRef, long userRef, String promptName, GetLayerOptions options); + public static native void statsigManuallyLogLayerParamExposure( long statsigRef, long userRef, String layerName, String param); diff --git a/statsig-node/npm/aarch64-apple-darwin.package.json b/statsig-node/npm/aarch64-apple-darwin.package.json index d19f2cca5..76010405d 100644 --- a/statsig-node/npm/aarch64-apple-darwin.package.json +++ b/statsig-node/npm/aarch64-apple-darwin.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-darwin-arm64", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "darwin" ], diff --git a/statsig-node/npm/aarch64-unknown-linux-gnu.package.json b/statsig-node/npm/aarch64-unknown-linux-gnu.package.json index fec93babd..f2ba61f9f 100644 --- a/statsig-node/npm/aarch64-unknown-linux-gnu.package.json +++ b/statsig-node/npm/aarch64-unknown-linux-gnu.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-linux-arm64-gnu", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "linux" ], diff --git a/statsig-node/npm/aarch64-unknown-linux-musl.package.json b/statsig-node/npm/aarch64-unknown-linux-musl.package.json index a4f3b512f..af0a3ba4d 100644 --- a/statsig-node/npm/aarch64-unknown-linux-musl.package.json +++ b/statsig-node/npm/aarch64-unknown-linux-musl.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-linux-arm64-musl", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "linux" ], diff --git a/statsig-node/npm/i686-pc-windows-msvc.package.json b/statsig-node/npm/i686-pc-windows-msvc.package.json index 29a6d187a..2dd597eea 100644 --- a/statsig-node/npm/i686-pc-windows-msvc.package.json +++ b/statsig-node/npm/i686-pc-windows-msvc.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-win32-ia32-msvc", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "win32" ], diff --git a/statsig-node/npm/x86_64-apple-darwin.package.json b/statsig-node/npm/x86_64-apple-darwin.package.json index 6c003e7bb..a3033c870 100644 --- a/statsig-node/npm/x86_64-apple-darwin.package.json +++ b/statsig-node/npm/x86_64-apple-darwin.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-darwin-x64", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "darwin" ], diff --git a/statsig-node/npm/x86_64-pc-windows-msvc.package.json b/statsig-node/npm/x86_64-pc-windows-msvc.package.json index 9ae3ef930..cad7edf68 100644 --- a/statsig-node/npm/x86_64-pc-windows-msvc.package.json +++ b/statsig-node/npm/x86_64-pc-windows-msvc.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-win32-x64-msvc", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "win32" ], diff --git a/statsig-node/npm/x86_64-unknown-linux-gnu.package.json b/statsig-node/npm/x86_64-unknown-linux-gnu.package.json index 1a0ce8aa2..4464b906a 100644 --- a/statsig-node/npm/x86_64-unknown-linux-gnu.package.json +++ b/statsig-node/npm/x86_64-unknown-linux-gnu.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-linux-x64-gnu", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "linux" ], diff --git a/statsig-node/npm/x86_64-unknown-linux-musl.package.json b/statsig-node/npm/x86_64-unknown-linux-musl.package.json index cb509e4b8..da5afa8fe 100644 --- a/statsig-node/npm/x86_64-unknown-linux-musl.package.json +++ b/statsig-node/npm/x86_64-unknown-linux-musl.package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core-linux-x64-musl", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "os": [ "linux" ], diff --git a/statsig-node/package.json b/statsig-node/package.json index b2209ddfa..ef63c7ef3 100644 --- a/statsig-node/package.json +++ b/statsig-node/package.json @@ -1,6 +1,6 @@ { "name": "@statsig/statsig-node-core", - "version": "0.7.4-beta.2508160236", + "version": "0.8.0", "main": "index.js", "scripts": { "test": "jest --colors" diff --git a/statsig-node/src/__tests__/NumThreads.test.ts b/statsig-node/src/__tests__/NumThreads.test.ts new file mode 100644 index 000000000..3c0eccc01 --- /dev/null +++ b/statsig-node/src/__tests__/NumThreads.test.ts @@ -0,0 +1,33 @@ +import { execSync } from 'node:child_process'; + +import { Statsig } from '../../build/index.js'; + +function getNumThreads() { + const pid = process.pid; + + try { + const threads = execSync(`ps -o nlwp ${pid}`) + .toString() + .split('\n')[1] + .trim(); + console.log(`Process owns ${threads} threads`); + return threads; + } catch (err) { + console.error('Failed to get thread count', err); + } +} + +test('Has correct number of threads', async () => { + const instances = []; + for (let i = 0; i < 10; i++) { + const statsig = new Statsig('secret-num-threads-test', { + disableNetwork: true, + }); + instances.push(statsig); + } + + await Promise.all(instances.map((statsig) => statsig.initialize())); + + const threads = getNumThreads(); + expect(threads).toBe('16'); +}); diff --git a/statsig-node/src/lib.rs b/statsig-node/src/lib.rs index 7acee8854..da83069f1 100644 --- a/statsig-node/src/lib.rs +++ b/statsig-node/src/lib.rs @@ -13,9 +13,12 @@ pub mod statsig_user_napi; mod statsig_metadata_napi; -use napi::module_init; +use napi::{bindgen_prelude::create_custom_tokio_runtime, module_init}; #[module_init] fn init() { statsig_metadata_napi::update_statsig_metadata(None); + + let rt = statsig_rust::statsig_runtime::create_new_runtime(); + create_custom_tokio_runtime(rt); } diff --git a/statsig-node/src/lib/statsig-generated.d.ts b/statsig-node/src/lib/statsig-generated.d.ts index 8c4a6d3f3..110231e91 100644 --- a/statsig-node/src/lib/statsig-generated.d.ts +++ b/statsig-node/src/lib/statsig-generated.d.ts @@ -64,6 +64,7 @@ export declare class StatsigNapiInternal { getExperimentByGroupName(experimentName: string, groupName: string): Experiment getFieldsNeededForExperiment(experimentName: string): Array getLayer(user: StatsigUser, layerName: string, options?: LayerEvaluationOptions | undefined | null): Layer + getPrompt(user: StatsigUser, promptName: string, options?: LayerEvaluationOptions | undefined | null): Layer getFieldsNeededForLayer(layerName: string): Array identify(user: StatsigUser): void getParameterStore(user: StatsigUser, parameterStoreName: string, options?: ParameterStoreEvaluationOptions | undefined | null): ParameterStore @@ -156,6 +157,7 @@ export interface EvaluationDetails { export interface ExperimentEvaluationOptions { disableExposureLogging?: boolean + userPersistedValues?: Record } export interface FeatureGate { @@ -171,6 +173,7 @@ export interface FeatureGateEvaluationOptions { export interface LayerEvaluationOptions { disableExposureLogging?: boolean + userPersistedValues?: Record } export interface NapiNetworkFuncResult { diff --git a/statsig-node/src/statsig_napi.rs b/statsig-node/src/statsig_napi.rs index a968cfbb0..4b75a914f 100644 --- a/statsig-node/src/statsig_napi.rs +++ b/statsig-node/src/statsig_napi.rs @@ -40,23 +40,25 @@ impl StatsigNapiInternal { sdk_key: String, options: Option, ) -> Self { - log_d!(TAG, "StatsigNapi new"); + within_runtime_if_available(|| { + log_d!(TAG, "StatsigNapi new"); - statsig_metadata_napi::update_statsig_metadata(Some(env)); + statsig_metadata_napi::update_statsig_metadata(Some(env)); - let (inner_opts, obs_client) = options - .map(|opts| opts.safe_convert_to_inner()) - .unwrap_or((None, None)); + let (inner_opts, obs_client) = options + .map(|opts| opts.safe_convert_to_inner()) + .unwrap_or((None, None)); - let network_provider: Arc = - Arc::new(NetworkProviderNapi { network_func }); - NetworkProviderGlobal::set(&network_provider); + let network_provider: Arc = + Arc::new(NetworkProviderNapi { network_func }); + NetworkProviderGlobal::set(&network_provider); - Self { - inner: Arc::new(StatsigActual::new(&sdk_key, inner_opts)), - observability_client: Mutex::new(obs_client), - network_provider: Mutex::new(Some(network_provider)), - } + Self { + inner: Arc::new(StatsigActual::new(&sdk_key, inner_opts)), + observability_client: Mutex::new(obs_client), + network_provider: Mutex::new(Some(network_provider)), + } + }) } #[napi] @@ -252,6 +254,22 @@ impl StatsigNapiInternal { .into() } + #[napi] + pub fn get_prompt( + &self, + user: &StatsigUser, + prompt_name: String, + options: Option, + ) -> Layer { + self.inner + .get_prompt_with_options( + user.as_inner(), + &prompt_name, + options.map(|opts| opts.into()).unwrap_or_default(), + ) + .into() + } + #[napi] pub fn get_fields_needed_for_layer(&self, layer_name: String) -> Vec { self.inner.get_fields_needed_for_layer(layer_name.as_str()) diff --git a/statsig-php/post-install.php b/statsig-php/post-install.php index 2c733b8ec..b773d4f8f 100644 --- a/statsig-php/post-install.php +++ b/statsig-php/post-install.php @@ -1,7 +1,7 @@ __ref); } + public function getPrompt(StatsigUser $user, string $name, ?array $options = null): Layer + { + $ptr = StatsigFFI::get()->statsig_get_prompt( + $this->__ref, + $user->__ref, + $name, + encode_or_null($options) + ); + + $raw_result = StatsigFFI::takeString($ptr); + return new Layer($raw_result, $this->__ref); + } + public function manuallyLogLayerParameterExposure(StatsigUser $user, string $layer_name, string $param_name): void { StatsigFFI::get()->statsig_manually_log_layer_parameter_exposure( diff --git a/statsig-pyo3/py_src/statsig_python_core/statsig_python_core.pyi b/statsig-pyo3/py_src/statsig_python_core/statsig_python_core.pyi index b43222a69..db41eaba1 100644 --- a/statsig-pyo3/py_src/statsig_python_core/statsig_python_core.pyi +++ b/statsig-pyo3/py_src/statsig_python_core/statsig_python_core.pyi @@ -223,6 +223,9 @@ class StatsigBasePy: def get_layer(self, user:StatsigUser, name:builtins.str, options:typing.Optional[LayerEvaluationOptions]=None) -> Layer: ... + def get_prompt(self, user:StatsigUser, name:builtins.str, options:typing.Optional[LayerEvaluationOptions]=None) -> Layer: + ... + def manually_log_layer_parameter_exposure(self, user:StatsigUser, name:builtins.str, param_name:builtins.str) -> None: ... diff --git a/statsig-pyo3/src/statsig_base_py.rs b/statsig-pyo3/src/statsig_base_py.rs index bb47a4afd..c2c5bfa87 100644 --- a/statsig-pyo3/src/statsig_base_py.rs +++ b/statsig-pyo3/src/statsig_base_py.rs @@ -373,6 +373,37 @@ impl StatsigBasePy { } } + #[pyo3(signature = (user, name, options=None))] + pub fn get_prompt( + &self, + user: &StatsigUserPy, + name: &str, + options: Option, + py: Python, + ) -> LayerPy { + let mut options_actual = options + .as_ref() + .map_or(LayerEvaluationOptions::default(), |o| o.into()); + + options_actual.user_persisted_values = options + .and_then(|o| o.user_persisted_values) + .and_then(|v| extract_user_persisted_values(py, name, v)); + + let layer = self + .inner + .get_prompt_with_options(&user.inner, name, options_actual); + + LayerPy { + name: layer.name.clone(), + rule_id: layer.rule_id.clone(), + group_name: layer.group_name.clone(), + allocated_experiment_name: layer.allocated_experiment_name.clone(), + value: map_to_py_dict(py, &layer.__value), + details: layer.details.clone().into(), + inner: layer, + } + } + #[pyo3(signature = (user, name, param_name))] pub fn manually_log_layer_parameter_exposure( &self, diff --git a/statsig-rust/Cargo.toml b/statsig-rust/Cargo.toml index 6e771f0c1..402c0c182 100644 --- a/statsig-rust/Cargo.toml +++ b/statsig-rust/Cargo.toml @@ -38,9 +38,9 @@ serde_json = { version = "1.0.125", features = [ ] } serde_with = "3.4.0" sha2 = "0.10.8" -sigstat-grpc = { path = "../statsig-grpc", version = "0.7.4-beta.2508160236", optional = true } +sigstat-grpc = { path = "../statsig-grpc", version = "0.8.0", optional = true } simple_logger = { version = "5.0.0" } -tokio = { version = "1.39.1", features = ["full"] } +tokio = { version = "1.43.1", features = ["full"] } uaparser = "0.6.4" uuid = { version = "1.10.0", features = ["v4", "fast-rng"] } zstd = { version = "0.13.2", features = ["zdict_builder"] } diff --git a/statsig-rust/src/lib.rs b/statsig-rust/src/lib.rs index 4c8e43f7f..350c95a5c 100644 --- a/statsig-rust/src/lib.rs +++ b/statsig-rust/src/lib.rs @@ -48,6 +48,7 @@ pub mod statsig_core_api_options; pub mod statsig_global; pub mod statsig_metadata; pub mod statsig_options; +pub mod statsig_runtime; pub mod statsig_types; pub mod user; @@ -64,6 +65,5 @@ mod spec_store; mod specs_adapter; mod statsig; mod statsig_err; -mod statsig_runtime; mod statsig_type_factories; mod utils; diff --git a/statsig-rust/src/statsig.rs b/statsig-rust/src/statsig.rs index db310798b..c3fe54af8 100644 --- a/statsig-rust/src/statsig.rs +++ b/statsig-rust/src/statsig.rs @@ -923,7 +923,7 @@ impl Statsig { } } - pub fn get_parameter_store(&self, parameter_store_name: &str) -> ParameterStore { + pub fn get_parameter_store(&self, parameter_store_name: &str) -> ParameterStore<'_> { self.get_parameter_store_with_options( parameter_store_name, ParameterStoreEvaluationOptions::default(), @@ -934,7 +934,7 @@ impl Statsig { &self, parameter_store_name: &str, options: ParameterStoreEvaluationOptions, - ) -> ParameterStore { + ) -> ParameterStore<'_> { self.event_logger .increment_non_exposure_checks(parameter_store_name); @@ -1688,6 +1688,19 @@ impl Statsig { self.get_layer_impl(user_internal, layer_name, options) } + pub fn get_prompt(&self, user: &StatsigUser, prompt_name: &str) -> Layer { + self.get_layer(user, prompt_name) + } + + pub fn get_prompt_with_options( + &self, + user: &StatsigUser, + prompt_name: &str, + options: LayerEvaluationOptions, + ) -> Layer { + self.get_layer_with_options(user, prompt_name, options) + } + pub fn manually_log_layer_parameter_exposure( &self, user: &StatsigUser, diff --git a/statsig-rust/src/statsig_metadata.rs b/statsig-rust/src/statsig_metadata.rs index e6846163a..c6fbbce95 100644 --- a/statsig-rust/src/statsig_metadata.rs +++ b/statsig-rust/src/statsig_metadata.rs @@ -49,7 +49,7 @@ pub struct StatsigMetadataWithLogEventExtras { impl StatsigMetadata { fn new() -> Self { Self { - sdk_version: "0.7.4-beta.2508160236".to_string(), + sdk_version: "0.8.0".to_string(), sdk_type: "statsig-server-core".to_string(), session_id: Uuid::new_v4().to_string(), os: None, diff --git a/statsig-rust/src/statsig_runtime.rs b/statsig-rust/src/statsig_runtime.rs index 4cf9dfa7f..f822acfbd 100644 --- a/statsig-rust/src/statsig_runtime.rs +++ b/statsig-rust/src/statsig_runtime.rs @@ -8,7 +8,7 @@ use std::future::Future; use std::sync::atomic::AtomicBool; use std::sync::Arc; use std::time::Duration; -use tokio::runtime::{Builder, Handle}; +use tokio::runtime::{Builder, Handle, Runtime}; use tokio::sync::Notify; use tokio::task::JoinHandle; @@ -191,6 +191,15 @@ impl StatsigRuntime { } } +pub fn create_new_runtime() -> Runtime { + Builder::new_multi_thread() + .worker_threads(5) + .thread_name("statsig") + .enable_all() + .build() + .expect("Failed to create a tokio Runtime") +} + fn remove_join_handle_with_id( spawned_tasks: Arc>>>, tag: String, @@ -232,14 +241,7 @@ fn create_runtime_if_required() { } None => { log_d!(TAG, "Creating new tokio runtime for StatsigGlobal"); - let rt = Arc::new( - Builder::new_multi_thread() - .worker_threads(5) - .thread_name("statsig") - .enable_all() - .build() - .expect("Failed to find or create a tokio Runtime"), - ); + let rt = Arc::new(create_new_runtime()); lock.replace(rt); }