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.
data/Cargo.toml ADDED
@@ -0,0 +1,7 @@
1
+ # This Cargo.toml is here to let externals tools (IDEs, etc.) know that this is
2
+ # a Rust project. Your extensions dependencies should be added to the Cargo.toml
3
+ # in the ext/ directory.
4
+
5
+ [workspace]
6
+ members = ["./ext/eppo_client"]
7
+ resolver = "2"
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,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "mkmf"
4
+ require "rb_sys/mkmf"
5
+
6
+ create_rust_makefile("eppo_client")
@@ -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 'custom_errors'
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(_assignment)
9
- raise(EppoClient::AssignmentLoggerError, 'log_assignment has not been set up')
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
@@ -1,238 +1,141 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
- require 'time'
3
+ require "singleton"
4
+ require "logger"
5
5
 
6
- require_relative 'constants'
7
- require_relative 'custom_errors'
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 :config_requestor, :assignment_logger, :poller
13
+ attr_accessor :assignment_logger
20
14
 
21
- def instance
22
- Client.instance
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 get_string_assignment(
26
- subject_key,
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 get_numeric_assignment(
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 get_boolean_assignment(
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 get_parsed_json_assignment(
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 get_json_string_assignment(
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 get_assignment(
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
- if experiment_config.nil? || experiment_config.enabled == false
139
- logger.debug(
140
- '[Eppo SDK] No assigned variation. No active experiment or flag for '\
141
- "key: #{flag_key}"
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
- matched_rule = EppoClient.find_matching_rule(subject_attributes, experiment_config.rules)
147
- if matched_rule.nil?
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
- allocation = experiment_config.allocations[matched_rule.allocation_key]
156
- unless in_experiment_sample?(
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
- shard = EppoClient.get_shard(
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
- assigned_variation_value_to_log = nil
178
- unless assigned_variation.nil?
179
- assigned_variation_value_to_log = assigned_variation.value
180
- unless expected_variation_type.nil?
181
- variation_is_expected_type = EppoClient::VariationType.expected_type?(
182
- assigned_variation, expected_variation_type
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
- assignment_event = {
189
- "allocation": matched_rule.allocation_key,
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(assignment_event)
200
- rescue EppoClient::AssignmentLoggerError => e
201
- # Error means log_assignment was not set up. This is okay to ignore.
202
- rescue StandardError => e
203
- logger.error("[Eppo SDK] Error logging assignment event: #{e}")
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 shutdown
211
- @poller.stop
212
- end
107
+ def log_bandit_action(event)
108
+ if not event then return end
213
109
 
214
- # rubocop:disable Metrics/MethodLength
215
- def get_subject_variation_override(experiment_config, subject)
216
- subject_hash = Digest::MD5.hexdigest(subject.to_s)
217
- if experiment_config&.overrides &&
218
- experiment_config.overrides[subject_hash] &&
219
- experiment_config.typed_overrides[subject_hash]
220
- EppoClient::VariationDto.new(
221
- 'override',
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 in_experiment_sample?(subject, experiment_key, subject_shards,
231
- percent_exposure)
232
- shard = EppoClient.get_shard("exposure-#{subject}-#{experiment_key}",
233
- subject_shards)
234
- shard <= percent_exposure * subject_shards
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