eppo-server-sdk 0.3.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|