eppo-server-sdk 0.2.5 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
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