eppo-server-sdk 0.3.0 → 3.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +22 -0
- data/.rspec +3 -0
- data/.rubocop.yml +8 -0
- data/Cargo.lock +1963 -0
- data/Cargo.toml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +22 -0
- data/Rakefile +53 -0
- data/Steepfile +27 -0
- data/ext/eppo_client/Cargo.toml +19 -0
- data/ext/eppo_client/build.rs +5 -0
- data/ext/eppo_client/extconf.rb +6 -0
- data/ext/eppo_client/src/client.rs +119 -0
- data/ext/eppo_client/src/lib.rs +26 -0
- data/lib/eppo_client/assignment_logger.rb +7 -3
- data/lib/eppo_client/client.rb +99 -196
- data/lib/eppo_client/config.rb +4 -4
- data/lib/eppo_client/custom_errors.rb +0 -17
- data/lib/eppo_client/validation.rb +2 -2
- data/lib/eppo_client/version.rb +1 -1
- data/lib/eppo_client.rb +7 -45
- data/sig/eppo_server_sdk.rbs +96 -0
- metadata +30 -176
- data/lib/eppo_client/configuration_requestor.rb +0 -108
- data/lib/eppo_client/configuration_store.rb +0 -35
- data/lib/eppo_client/constants.rb +0 -20
- data/lib/eppo_client/http_client.rb +0 -75
- data/lib/eppo_client/lru_cache.rb +0 -28
- data/lib/eppo_client/poller.rb +0 -48
- data/lib/eppo_client/rules.rb +0 -119
- data/lib/eppo_client/shard.rb +0 -30
- data/lib/eppo_client/variation_type.rb +0 -39
data/Cargo.toml
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2024 Eppo
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# Eppo SDK for Ruby
|
2
|
+
|
3
|
+
## Getting Started
|
4
|
+
|
5
|
+
Refer to our [SDK documentation](https://docs.geteppo.com/feature-flags/sdks/ruby) for how to install and use the SDK.
|
6
|
+
|
7
|
+
## Supported Ruby Versions
|
8
|
+
This version of the SDK is compatible with Ruby 3.0.6 and above.
|
9
|
+
|
10
|
+
# Contributing
|
11
|
+
|
12
|
+
## Testing with local version of `eppo_core`
|
13
|
+
|
14
|
+
To run build and tests against a local version of `eppo_core`, you should instruct Cargo to look for it at the local path.
|
15
|
+
|
16
|
+
Add the following to `.cargo/config.toml` file (relative to `ruby-sdk`):
|
17
|
+
```toml
|
18
|
+
[patch.crates-io]
|
19
|
+
eppo_core = { path = '../eppo_core' }
|
20
|
+
```
|
21
|
+
|
22
|
+
Make sure you remove the override before updating `Cargo.lock`. Otherwise, the lock file will be missing `eppo_core` checksum and will be unsuitable for release. (CI will warn you if you do this accidentally.)
|
data/Rakefile
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "bundler/gem_tasks"
|
4
|
+
require "rspec/core/rake_task"
|
5
|
+
require_relative 'lib/eppo_client/version'
|
6
|
+
|
7
|
+
GEM_NAME = 'eppo-server-sdk'
|
8
|
+
GEM_VERSION = EppoClient::VERSION
|
9
|
+
|
10
|
+
RSpec::Core::RakeTask.new(:spec)
|
11
|
+
|
12
|
+
require "rubocop/rake_task"
|
13
|
+
|
14
|
+
RuboCop::RakeTask.new
|
15
|
+
|
16
|
+
require "rb_sys/extensiontask"
|
17
|
+
|
18
|
+
task default: :build
|
19
|
+
|
20
|
+
GEMSPEC = Gem::Specification.load("eppo-server-sdk.gemspec")
|
21
|
+
|
22
|
+
RbSys::ExtensionTask.new("eppo_client", GEMSPEC) do |ext|
|
23
|
+
ext.lib_dir = "lib/eppo_client"
|
24
|
+
end
|
25
|
+
|
26
|
+
task build: :compile do
|
27
|
+
system "gem build #{GEM_NAME}.gemspec"
|
28
|
+
end
|
29
|
+
|
30
|
+
task install: :build do
|
31
|
+
system "gem install #{GEM_NAME}-#{GEM_VERSION}.gem"
|
32
|
+
end
|
33
|
+
|
34
|
+
task devinstall: :build do
|
35
|
+
system "gem install #{GEM_NAME}-#{GEM_VERSION}.gem --dev"
|
36
|
+
end
|
37
|
+
|
38
|
+
task publish: :build do
|
39
|
+
system "gem push #{GEM_NAME}-#{GEM_VERSION}.gem"
|
40
|
+
end
|
41
|
+
|
42
|
+
task :clean do
|
43
|
+
system 'rm *.gem'
|
44
|
+
end
|
45
|
+
|
46
|
+
RSpec::Core::RakeTask.new(:test) do |task|
|
47
|
+
root_dir = Rake.application.original_dir
|
48
|
+
task.pattern = "#{root_dir}/spec/*_spec.rb"
|
49
|
+
task.verbose = false
|
50
|
+
end
|
51
|
+
|
52
|
+
task test: :devinstall
|
53
|
+
task test_refreshed_data: [:devinstall, 'test-data']
|
data/Steepfile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# D = Steep::Diagnostic
|
2
|
+
#
|
3
|
+
target :lib do
|
4
|
+
signature "sig"
|
5
|
+
|
6
|
+
check "lib" # Directory name
|
7
|
+
|
8
|
+
library "singleton"
|
9
|
+
library "logger"
|
10
|
+
|
11
|
+
# configure_code_diagnostics(D::Ruby.default) # `default` diagnostics setting (applies by default)
|
12
|
+
# configure_code_diagnostics(D::Ruby.strict) # `strict` diagnostics setting
|
13
|
+
# configure_code_diagnostics(D::Ruby.lenient) # `lenient` diagnostics setting
|
14
|
+
# configure_code_diagnostics(D::Ruby.silent) # `silent` diagnostics setting
|
15
|
+
# configure_code_diagnostics do |hash| # You can setup everything yourself
|
16
|
+
# hash[D::Ruby::NoMethod] = :information
|
17
|
+
# end
|
18
|
+
end
|
19
|
+
|
20
|
+
target :test do
|
21
|
+
signature "sig", "sig-private"
|
22
|
+
|
23
|
+
check "test"
|
24
|
+
|
25
|
+
library "singleton"
|
26
|
+
library "logger"
|
27
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
[package]
|
2
|
+
name = "eppo_client"
|
3
|
+
version = "3.0.0"
|
4
|
+
edition = "2021"
|
5
|
+
license = "MIT"
|
6
|
+
publish = false
|
7
|
+
rust-version = "1.71.1"
|
8
|
+
|
9
|
+
[lib]
|
10
|
+
crate-type = ["cdylib"]
|
11
|
+
|
12
|
+
[dependencies]
|
13
|
+
env_logger = { version = "0.11.3", features = ["unstable-kv"] }
|
14
|
+
eppo_core = { version = "1.0.0" }
|
15
|
+
log = { version = "0.4.21", features = ["kv_serde"] }
|
16
|
+
magnus = { version = "0.6.2" }
|
17
|
+
serde = { version = "1.0.203", features = ["derive"] }
|
18
|
+
serde_magnus = "0.8.1"
|
19
|
+
rb-sys = "0.9"
|
@@ -0,0 +1,5 @@
|
|
1
|
+
fn main() {
|
2
|
+
// Without this flag, building via `cargo build` fails with undefined references to ruby
|
3
|
+
// library. This is fine as `eppo_client` is going to be loaded as an extension by the host Ruby.
|
4
|
+
println!("cargo:rustc-link-arg=-Wl,-undefined,dynamic_lookup");
|
5
|
+
}
|
@@ -0,0 +1,119 @@
|
|
1
|
+
use std::{cell::RefCell, sync::Arc};
|
2
|
+
|
3
|
+
use eppo_core::{
|
4
|
+
configuration_fetcher::ConfigurationFetcher, configuration_store::ConfigurationStore,
|
5
|
+
poller_thread::PollerThread, ufc::VariationType, Attributes, ContextAttributes,
|
6
|
+
};
|
7
|
+
use magnus::{error::Result, exception, prelude::*, Error, TryConvert, Value};
|
8
|
+
|
9
|
+
#[derive(Debug)]
|
10
|
+
#[magnus::wrap(class = "EppoClient::Core::Config", size, free_immediately)]
|
11
|
+
pub struct Config {
|
12
|
+
api_key: String,
|
13
|
+
base_url: String,
|
14
|
+
}
|
15
|
+
|
16
|
+
impl TryConvert for Config {
|
17
|
+
// `val` is expected to be of type EppoClient::Config.
|
18
|
+
fn try_convert(val: magnus::Value) -> Result<Self> {
|
19
|
+
let api_key = String::try_convert(val.funcall("api_key", ())?)?;
|
20
|
+
let base_url = String::try_convert(val.funcall("base_url", ())?)?;
|
21
|
+
Ok(Config { api_key, base_url })
|
22
|
+
}
|
23
|
+
}
|
24
|
+
|
25
|
+
#[magnus::wrap(class = "EppoClient::Core::Client")]
|
26
|
+
pub struct Client {
|
27
|
+
configuration_store: Arc<ConfigurationStore>,
|
28
|
+
// Magnus only allows sharing aliased references (&T) through the API, so we need to use RefCell
|
29
|
+
// to get interior mutability.
|
30
|
+
//
|
31
|
+
// This should be safe as Ruby only uses a single OS thread, and `Client` lives in the Ruby
|
32
|
+
// world.
|
33
|
+
poller_thread: RefCell<Option<PollerThread>>,
|
34
|
+
}
|
35
|
+
|
36
|
+
impl Client {
|
37
|
+
pub fn new(config: Config) -> Client {
|
38
|
+
let configuration_store = Arc::new(ConfigurationStore::new());
|
39
|
+
|
40
|
+
let poller_thread = PollerThread::start(
|
41
|
+
ConfigurationFetcher::new(
|
42
|
+
eppo_core::configuration_fetcher::ConfigurationFetcherConfig {
|
43
|
+
base_url: config.base_url,
|
44
|
+
api_key: config.api_key,
|
45
|
+
sdk_name: "ruby".to_owned(),
|
46
|
+
sdk_version: env!("CARGO_PKG_VERSION").to_owned(),
|
47
|
+
},
|
48
|
+
),
|
49
|
+
configuration_store.clone(),
|
50
|
+
)
|
51
|
+
.expect("should be able to start poller thread");
|
52
|
+
|
53
|
+
Client {
|
54
|
+
configuration_store,
|
55
|
+
poller_thread: RefCell::new(Some(poller_thread)),
|
56
|
+
}
|
57
|
+
}
|
58
|
+
|
59
|
+
pub fn get_assignment(
|
60
|
+
&self,
|
61
|
+
flag_key: String,
|
62
|
+
subject_key: String,
|
63
|
+
subject_attributes: Value,
|
64
|
+
expected_type: Value,
|
65
|
+
) -> Result<Value> {
|
66
|
+
let expected_type: VariationType = serde_magnus::deserialize(expected_type)?;
|
67
|
+
let subject_attributes: Attributes = serde_magnus::deserialize(subject_attributes)?;
|
68
|
+
|
69
|
+
let config = self.configuration_store.get_configuration();
|
70
|
+
let result = config
|
71
|
+
.get_assignment(
|
72
|
+
&flag_key,
|
73
|
+
&subject_key,
|
74
|
+
&subject_attributes,
|
75
|
+
Some(expected_type),
|
76
|
+
)
|
77
|
+
// TODO: maybe expose possible errors individually.
|
78
|
+
.map_err(|err| Error::new(exception::runtime_error(), err.to_string()))?;
|
79
|
+
|
80
|
+
Ok(serde_magnus::serialize(&result).expect("assignment value should be serializable"))
|
81
|
+
}
|
82
|
+
|
83
|
+
pub fn get_bandit_action(
|
84
|
+
&self,
|
85
|
+
flag_key: String,
|
86
|
+
subject_key: String,
|
87
|
+
subject_attributes: Value,
|
88
|
+
actions: Value,
|
89
|
+
default_variation: String,
|
90
|
+
) -> Result<Value> {
|
91
|
+
let subject_attributes = serde_magnus::deserialize::<_, ContextAttributes>(
|
92
|
+
subject_attributes,
|
93
|
+
)
|
94
|
+
.map_err(|err| {
|
95
|
+
Error::new(
|
96
|
+
exception::runtime_error(),
|
97
|
+
format!("enexpected value for subject_attributes: {err}"),
|
98
|
+
)
|
99
|
+
})?;
|
100
|
+
let actions = serde_magnus::deserialize(actions)?;
|
101
|
+
|
102
|
+
let config = self.configuration_store.get_configuration();
|
103
|
+
let result = config.get_bandit_action(
|
104
|
+
&flag_key,
|
105
|
+
&subject_key,
|
106
|
+
&subject_attributes,
|
107
|
+
&actions,
|
108
|
+
&default_variation,
|
109
|
+
);
|
110
|
+
|
111
|
+
serde_magnus::serialize(&result)
|
112
|
+
}
|
113
|
+
|
114
|
+
pub fn shutdown(&self) {
|
115
|
+
if let Some(t) = self.poller_thread.take() {
|
116
|
+
let _ = t.shutdown();
|
117
|
+
}
|
118
|
+
}
|
119
|
+
}
|
@@ -0,0 +1,26 @@
|
|
1
|
+
mod client;
|
2
|
+
|
3
|
+
use magnus::{function, method, prelude::*, Error, Object, Ruby};
|
4
|
+
|
5
|
+
use crate::client::Client;
|
6
|
+
|
7
|
+
#[magnus::init]
|
8
|
+
fn init(ruby: &Ruby) -> Result<(), Error> {
|
9
|
+
env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("eppo")).init();
|
10
|
+
|
11
|
+
let eppo_client = ruby.define_module("EppoClient")?;
|
12
|
+
let core = eppo_client.define_module("Core")?;
|
13
|
+
|
14
|
+
let core_client = core.define_class("Client", magnus::class::object())?;
|
15
|
+
core_client.define_singleton_method("new", function!(Client::new, 1))?;
|
16
|
+
core_client.define_method("get_assignment", method!(Client::get_assignment, 4))?;
|
17
|
+
core_client.define_method("get_bandit_action", method!(Client::get_bandit_action, 5))?;
|
18
|
+
core_client.define_method("shutdown", method!(Client::shutdown, 0))?;
|
19
|
+
|
20
|
+
core.const_set(
|
21
|
+
"DEFAULT_BASE_URL",
|
22
|
+
eppo_core::configuration_fetcher::DEFAULT_BASE_URL,
|
23
|
+
)?;
|
24
|
+
|
25
|
+
Ok(())
|
26
|
+
}
|
@@ -1,12 +1,16 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
3
|
+
require_relative "custom_errors"
|
4
4
|
|
5
5
|
module EppoClient
|
6
6
|
# The base assignment logger class to override
|
7
7
|
class AssignmentLogger
|
8
|
-
def log_assignment(
|
9
|
-
raise(EppoClient::AssignmentLoggerError,
|
8
|
+
def log_assignment(_assignment_event)
|
9
|
+
raise(EppoClient::AssignmentLoggerError, "log_assignment has not been set up")
|
10
|
+
end
|
11
|
+
|
12
|
+
def log_bandit_action(_assignment_event)
|
13
|
+
raise(EppoClient::AssignmentLoggerError, "log_bandit_action has not been set up")
|
10
14
|
end
|
11
15
|
end
|
12
16
|
end
|
data/lib/eppo_client/client.rb
CHANGED
@@ -1,238 +1,141 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "singleton"
|
4
|
+
require "logger"
|
5
5
|
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative 'rules'
|
9
|
-
require_relative 'shard'
|
10
|
-
require_relative 'validation'
|
11
|
-
require_relative 'variation_type'
|
6
|
+
require_relative "config"
|
7
|
+
require_relative "eppo_client"
|
12
8
|
|
13
9
|
module EppoClient
|
14
10
|
# The main client singleton
|
15
|
-
# rubocop:disable Metrics/ClassLength
|
16
11
|
class Client
|
17
|
-
extend Gem::Deprecate
|
18
12
|
include Singleton
|
19
|
-
attr_accessor :
|
13
|
+
attr_accessor :assignment_logger
|
20
14
|
|
21
|
-
def
|
22
|
-
|
15
|
+
def init(config)
|
16
|
+
config.validate
|
17
|
+
|
18
|
+
if !@core.nil? then
|
19
|
+
STDERR.puts "Eppo Warning: multiple initialization of the client"
|
20
|
+
@core.shutdown
|
21
|
+
end
|
22
|
+
|
23
|
+
@assignment_logger = config.assignment_logger
|
24
|
+
@core = EppoClient::Core::Client.new(config)
|
23
25
|
end
|
24
26
|
|
25
|
-
def
|
26
|
-
|
27
|
-
flag_key,
|
28
|
-
subject_attributes = {},
|
29
|
-
log_level = EppoClient::DEFAULT_LOGGER_LEVEL
|
30
|
-
)
|
31
|
-
logger = Logger.new($stdout)
|
32
|
-
logger.level = log_level
|
33
|
-
assigned_variation = get_assignment_variation(
|
34
|
-
subject_key, flag_key, subject_attributes,
|
35
|
-
EppoClient::VariationType::STRING_TYPE, logger
|
36
|
-
)
|
37
|
-
assigned_variation&.typed_value
|
27
|
+
def shutdown
|
28
|
+
@core.shutdown
|
38
29
|
end
|
39
30
|
|
40
|
-
def
|
41
|
-
subject_key,
|
42
|
-
flag_key,
|
43
|
-
subject_attributes = {},
|
44
|
-
log_level = EppoClient::DEFAULT_LOGGER_LEVEL
|
45
|
-
)
|
46
|
-
logger = Logger.new($stdout)
|
47
|
-
logger.level = log_level
|
48
|
-
assigned_variation = get_assignment_variation(
|
49
|
-
subject_key, flag_key, subject_attributes,
|
50
|
-
EppoClient::VariationType::NUMERIC_TYPE, logger
|
51
|
-
)
|
52
|
-
assigned_variation&.typed_value
|
31
|
+
def get_string_assignment(flag_key, subject_key, subject_attributes, default_value)
|
32
|
+
get_assignment_inner(flag_key, subject_key, subject_attributes, "STRING", default_value)
|
53
33
|
end
|
54
34
|
|
55
|
-
def
|
56
|
-
subject_key,
|
57
|
-
flag_key,
|
58
|
-
subject_attributes = {},
|
59
|
-
log_level = EppoClient::DEFAULT_LOGGER_LEVEL
|
60
|
-
)
|
61
|
-
logger = Logger.new($stdout)
|
62
|
-
logger.level = log_level
|
63
|
-
assigned_variation = get_assignment_variation(
|
64
|
-
subject_key, flag_key, subject_attributes,
|
65
|
-
EppoClient::VariationType::BOOLEAN_TYPE, logger
|
66
|
-
)
|
67
|
-
assigned_variation&.typed_value
|
35
|
+
def get_numeric_assignment(flag_key, subject_key, subject_attributes, default_value)
|
36
|
+
get_assignment_inner(flag_key, subject_key, subject_attributes, "NUMERIC", default_value)
|
68
37
|
end
|
69
38
|
|
70
|
-
def
|
71
|
-
subject_key,
|
72
|
-
flag_key,
|
73
|
-
subject_attributes = {},
|
74
|
-
log_level = EppoClient::DEFAULT_LOGGER_LEVEL
|
75
|
-
)
|
76
|
-
logger = Logger.new($stdout)
|
77
|
-
logger.level = log_level
|
78
|
-
assigned_variation = get_assignment_variation(
|
79
|
-
subject_key, flag_key, subject_attributes,
|
80
|
-
EppoClient::VariationType::JSON_TYPE, logger
|
81
|
-
)
|
82
|
-
assigned_variation&.typed_value
|
39
|
+
def get_integer_assignment(flag_key, subject_key, subject_attributes, default_value)
|
40
|
+
get_assignment_inner(flag_key, subject_key, subject_attributes, "INTEGER", default_value)
|
83
41
|
end
|
84
42
|
|
85
|
-
def
|
86
|
-
subject_key,
|
87
|
-
flag_key,
|
88
|
-
subject_attributes = {},
|
89
|
-
log_level = EppoClient::DEFAULT_LOGGER_LEVEL
|
90
|
-
)
|
91
|
-
logger = Logger.new($stdout)
|
92
|
-
logger.level = log_level
|
93
|
-
assigned_variation = get_assignment_variation(
|
94
|
-
subject_key, flag_key, subject_attributes,
|
95
|
-
EppoClient::VariationType::JSON_TYPE, logger
|
96
|
-
)
|
97
|
-
assigned_variation&.value
|
43
|
+
def get_boolean_assignment(flag_key, subject_key, subject_attributes, default_value)
|
44
|
+
get_assignment_inner(flag_key, subject_key, subject_attributes, "BOOLEAN", default_value)
|
98
45
|
end
|
99
46
|
|
100
|
-
def
|
101
|
-
subject_key,
|
102
|
-
flag_key,
|
103
|
-
subject_attributes = {},
|
104
|
-
log_level = EppoClient::DEFAULT_LOGGER_LEVEL
|
105
|
-
)
|
106
|
-
logger = Logger.new($stdout)
|
107
|
-
logger.level = log_level
|
108
|
-
assigned_variation = get_assignment_variation(subject_key, flag_key,
|
109
|
-
subject_attributes, nil,
|
110
|
-
logger)
|
111
|
-
assigned_variation&.value
|
47
|
+
def get_json_assignment(flag_key, subject_key, subject_attributes, default_value)
|
48
|
+
get_assignment_inner(flag_key, subject_key, subject_attributes, "JSON", default_value)
|
112
49
|
end
|
113
|
-
deprecate :get_assignment, 'the get_<typed>_assignment methods', 2024, 1
|
114
|
-
|
115
|
-
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
116
|
-
def get_assignment_variation(
|
117
|
-
subject_key,
|
118
|
-
flag_key,
|
119
|
-
subject_attributes,
|
120
|
-
expected_variation_type,
|
121
|
-
logger
|
122
|
-
)
|
123
|
-
EppoClient.validate_not_blank('subject_key', subject_key)
|
124
|
-
EppoClient.validate_not_blank('flag_key', flag_key)
|
125
|
-
experiment_config = @config_requestor.get_configuration(flag_key)
|
126
|
-
override = get_subject_variation_override(experiment_config, subject_key)
|
127
|
-
unless override.nil?
|
128
|
-
unless expected_variation_type.nil?
|
129
|
-
variation_is_expected_type =
|
130
|
-
EppoClient::VariationType.expected_type?(
|
131
|
-
override, expected_variation_type
|
132
|
-
)
|
133
|
-
return nil unless variation_is_expected_type
|
134
|
-
end
|
135
|
-
return override
|
136
|
-
end
|
137
50
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
)
|
143
|
-
return nil
|
144
|
-
end
|
51
|
+
def get_bandit_action(flag_key, subject_key, subject_attributes, actions, default_variation)
|
52
|
+
attributes = coerce_context_attributes(subject_attributes)
|
53
|
+
actions = actions.to_h { |action, attributes| [action, coerce_context_attributes(attributes)] }
|
54
|
+
result = @core.get_bandit_action(flag_key, subject_key, attributes, actions, default_variation)
|
145
55
|
|
146
|
-
|
147
|
-
|
148
|
-
logger.debug(
|
149
|
-
'[Eppo SDK] No assigned variation. Subject attributes do not match '\
|
150
|
-
"targeting rules: #{subject_attributes}"
|
151
|
-
)
|
152
|
-
return nil
|
153
|
-
end
|
56
|
+
log_assignment(result[:assignment_event])
|
57
|
+
log_bandit_action(result[:bandit_event])
|
154
58
|
|
155
|
-
|
156
|
-
|
157
|
-
subject_key,
|
158
|
-
flag_key,
|
159
|
-
experiment_config.subject_shards,
|
160
|
-
allocation.percent_exposure
|
161
|
-
)
|
162
|
-
logger.debug(
|
163
|
-
'[Eppo SDK] No assigned variation. Subject is not part of experiment'\
|
164
|
-
' sample population'
|
165
|
-
)
|
166
|
-
return nil
|
167
|
-
end
|
59
|
+
return {:variation => result[:variation], :action=>result[:action]}
|
60
|
+
end
|
168
61
|
|
169
|
-
|
170
|
-
"assignment-#{subject_key}-#{flag_key}",
|
171
|
-
experiment_config.subject_shards
|
172
|
-
)
|
173
|
-
assigned_variation = allocation.variations.find do |var|
|
174
|
-
var.shard_range.shard_in_range?(shard)
|
175
|
-
end
|
62
|
+
private
|
176
63
|
|
177
|
-
|
178
|
-
|
179
|
-
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
return nil unless variation_is_expected_type
|
64
|
+
# rubocop:disable Metrics/MethodLength
|
65
|
+
def get_assignment_inner(flag_key, subject_key, subject_attributes, expected_type, default_value)
|
66
|
+
logger = Logger.new($stdout)
|
67
|
+
begin
|
68
|
+
assignment = @core.get_assignment(flag_key, subject_key, subject_attributes, expected_type)
|
69
|
+
if not assignment then
|
70
|
+
return default_value
|
185
71
|
end
|
72
|
+
|
73
|
+
log_assignment(assignment[:event])
|
74
|
+
|
75
|
+
return assignment[:value][expected_type]
|
76
|
+
rescue StandardError => error
|
77
|
+
logger.debug("[Eppo SDK] Failed to get assignment: #{error}")
|
78
|
+
|
79
|
+
# TODO: non-graceful mode?
|
80
|
+
default_value
|
186
81
|
end
|
82
|
+
end
|
83
|
+
# rubocop:enable Metrics/MethodLength
|
187
84
|
|
188
|
-
|
189
|
-
|
190
|
-
"experiment": "#{flag_key}-#{matched_rule.allocation_key}",
|
191
|
-
"featureFlag": flag_key,
|
192
|
-
"variation": assigned_variation_value_to_log,
|
193
|
-
"subject": subject_key,
|
194
|
-
"timestamp": Time.now.utc.iso8601,
|
195
|
-
"subjectAttributes": subject_attributes
|
196
|
-
}
|
85
|
+
def log_assignment(event)
|
86
|
+
if not event then return end
|
197
87
|
|
88
|
+
# Because rust's AssignmentEvent has a #[flatten] extra_logging
|
89
|
+
# field, serde_magnus serializes it as a normal HashMap with
|
90
|
+
# string keys.
|
91
|
+
#
|
92
|
+
# Convert keys to symbols here, so that logger sees symbol-keyed
|
93
|
+
# events for both flag assignment and bandit actions.
|
94
|
+
event = event.to_h { |key, value| [key.to_sym, value]}
|
95
|
+
|
96
|
+
enrich_event_metadata(event)
|
198
97
|
begin
|
199
|
-
@assignment_logger.log_assignment(
|
200
|
-
rescue EppoClient::AssignmentLoggerError
|
201
|
-
|
202
|
-
rescue StandardError =>
|
203
|
-
logger.
|
98
|
+
@assignment_logger.log_assignment(event)
|
99
|
+
rescue EppoClient::AssignmentLoggerError
|
100
|
+
# Error means log_assignment was not set up. This is okay to ignore.
|
101
|
+
rescue StandardError => error
|
102
|
+
logger = Logger.new($stdout)
|
103
|
+
logger.error("[Eppo SDK] Error logging assignment event: #{error}")
|
204
104
|
end
|
205
|
-
|
206
|
-
assigned_variation
|
207
105
|
end
|
208
|
-
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
209
106
|
|
210
|
-
def
|
211
|
-
|
212
|
-
end
|
107
|
+
def log_bandit_action(event)
|
108
|
+
if not event then return end
|
213
109
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
experiment_config.overrides[subject_hash],
|
223
|
-
experiment_config.typed_overrides[subject_hash],
|
224
|
-
EppoClient::ShardRange.new(0, 1000)
|
225
|
-
)
|
110
|
+
enrich_event_metadata(event)
|
111
|
+
begin
|
112
|
+
@assignment_logger.log_bandit_action(event)
|
113
|
+
rescue EppoClient::AssignmentLoggerError
|
114
|
+
# Error means log_assignment was not set up. This is okay to ignore.
|
115
|
+
rescue StandardError => error
|
116
|
+
logger = Logger.new($stdout)
|
117
|
+
logger.error("[Eppo SDK] Error logging bandit action event: #{error}")
|
226
118
|
end
|
227
119
|
end
|
228
|
-
# rubocop:enable Metrics/MethodLength
|
229
120
|
|
230
|
-
def
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
121
|
+
def enrich_event_metadata(event)
|
122
|
+
event[:metaData]["sdkName"] = "ruby"
|
123
|
+
event[:metaData]["sdkVersion"] = EppoClient::VERSION
|
124
|
+
end
|
125
|
+
|
126
|
+
def coerce_context_attributes(attributes)
|
127
|
+
numeric_attributes = attributes[:numeric_attributes] || attributes["numericAttributes"]
|
128
|
+
categorical_attributes = attributes[:categorical_attributes] || attributes["categoricalAttributes"]
|
129
|
+
if numeric_attributes || categorical_attributes then
|
130
|
+
{
|
131
|
+
numericAttributes: numeric_attributes.to_h do |key, value|
|
132
|
+
value.is_a?(Numeric) ? [key, value] : [nil, nil]
|
133
|
+
end.compact,
|
134
|
+
categoricalAttributes: categorical_attributes.to_h do |key, value|
|
135
|
+
value.nil? ? [nil, nil] : [key, value.to_s]
|
136
|
+
end.compact,
|
137
|
+
}
|
138
|
+
end
|
235
139
|
end
|
236
140
|
end
|
237
|
-
# rubocop:enable Metrics/ClassLength
|
238
141
|
end
|