eppo-server-sdk 0.3.0 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
metadata CHANGED
@@ -1,191 +1,45 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eppo-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eppo
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-01 00:00:00.000000000 Z
12
- dependencies:
13
- - !ruby/object:Gem::Dependency
14
- name: concurrent-ruby
15
- requirement: !ruby/object:Gem::Requirement
16
- requirements:
17
- - - "~>"
18
- - !ruby/object:Gem::Version
19
- version: '1.1'
20
- - - ">="
21
- - !ruby/object:Gem::Version
22
- version: 1.1.9
23
- type: :runtime
24
- prerelease: false
25
- version_requirements: !ruby/object:Gem::Requirement
26
- requirements:
27
- - - "~>"
28
- - !ruby/object:Gem::Version
29
- version: '1.1'
30
- - - ">="
31
- - !ruby/object:Gem::Version
32
- version: 1.1.9
33
- - !ruby/object:Gem::Dependency
34
- name: faraday
35
- requirement: !ruby/object:Gem::Requirement
36
- requirements:
37
- - - "~>"
38
- - !ruby/object:Gem::Version
39
- version: '2.7'
40
- - - ">="
41
- - !ruby/object:Gem::Version
42
- version: 2.7.1
43
- type: :runtime
44
- prerelease: false
45
- version_requirements: !ruby/object:Gem::Requirement
46
- requirements:
47
- - - "~>"
48
- - !ruby/object:Gem::Version
49
- version: '2.7'
50
- - - ">="
51
- - !ruby/object:Gem::Version
52
- version: 2.7.1
53
- - !ruby/object:Gem::Dependency
54
- name: faraday-retry
55
- requirement: !ruby/object:Gem::Requirement
56
- requirements:
57
- - - "~>"
58
- - !ruby/object:Gem::Version
59
- version: '2.0'
60
- - - ">="
61
- - !ruby/object:Gem::Version
62
- version: 2.0.0
63
- type: :runtime
64
- prerelease: false
65
- version_requirements: !ruby/object:Gem::Requirement
66
- requirements:
67
- - - "~>"
68
- - !ruby/object:Gem::Version
69
- version: '2.0'
70
- - - ">="
71
- - !ruby/object:Gem::Version
72
- version: 2.0.0
73
- - !ruby/object:Gem::Dependency
74
- name: semver2
75
- requirement: !ruby/object:Gem::Requirement
76
- requirements:
77
- - - "~>"
78
- - !ruby/object:Gem::Version
79
- version: '3.4'
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: 3.4.2
83
- type: :runtime
84
- prerelease: false
85
- version_requirements: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '3.4'
90
- - - ">="
91
- - !ruby/object:Gem::Version
92
- version: 3.4.2
93
- - !ruby/object:Gem::Dependency
94
- name: rake
95
- requirement: !ruby/object:Gem::Requirement
96
- requirements:
97
- - - "~>"
98
- - !ruby/object:Gem::Version
99
- version: '13.0'
100
- - - ">="
101
- - !ruby/object:Gem::Version
102
- version: 13.0.6
103
- type: :development
104
- prerelease: false
105
- version_requirements: !ruby/object:Gem::Requirement
106
- requirements:
107
- - - "~>"
108
- - !ruby/object:Gem::Version
109
- version: '13.0'
110
- - - ">="
111
- - !ruby/object:Gem::Version
112
- version: 13.0.6
113
- - !ruby/object:Gem::Dependency
114
- name: rspec
115
- requirement: !ruby/object:Gem::Requirement
116
- requirements:
117
- - - "~>"
118
- - !ruby/object:Gem::Version
119
- version: '3.12'
120
- - - ">="
121
- - !ruby/object:Gem::Version
122
- version: 3.12.0
123
- type: :development
124
- prerelease: false
125
- version_requirements: !ruby/object:Gem::Requirement
126
- requirements:
127
- - - "~>"
128
- - !ruby/object:Gem::Version
129
- version: '3.12'
130
- - - ">="
131
- - !ruby/object:Gem::Version
132
- version: 3.12.0
133
- - !ruby/object:Gem::Dependency
134
- name: rubocop
135
- requirement: !ruby/object:Gem::Requirement
136
- requirements:
137
- - - "~>"
138
- - !ruby/object:Gem::Version
139
- version: 0.82.0
140
- type: :development
141
- prerelease: false
142
- version_requirements: !ruby/object:Gem::Requirement
143
- requirements:
144
- - - "~>"
145
- - !ruby/object:Gem::Version
146
- version: 0.82.0
147
- - !ruby/object:Gem::Dependency
148
- name: webmock
149
- requirement: !ruby/object:Gem::Requirement
150
- requirements:
151
- - - "~>"
152
- - !ruby/object:Gem::Version
153
- version: '3.18'
154
- - - ">="
155
- - !ruby/object:Gem::Version
156
- version: 3.18.1
157
- type: :development
158
- prerelease: false
159
- version_requirements: !ruby/object:Gem::Requirement
160
- requirements:
161
- - - "~>"
162
- - !ruby/object:Gem::Version
163
- version: '3.18'
164
- - - ">="
165
- - !ruby/object:Gem::Version
166
- version: 3.18.1
11
+ date: 2024-08-16 00:00:00.000000000 Z
12
+ dependencies: []
167
13
  description:
168
- email: eppo-team@geteppo.com
14
+ email:
15
+ - eppo-team@geteppo.com
169
16
  executables: []
170
- extensions: []
17
+ extensions:
18
+ - ext/eppo_client/Cargo.toml
171
19
  extra_rdoc_files: []
172
20
  files:
21
+ - ".gitignore"
22
+ - ".rspec"
23
+ - ".rubocop.yml"
24
+ - Cargo.lock
25
+ - Cargo.toml
26
+ - LICENSE.txt
27
+ - README.md
28
+ - Rakefile
29
+ - Steepfile
30
+ - ext/eppo_client/Cargo.toml
31
+ - ext/eppo_client/build.rs
32
+ - ext/eppo_client/extconf.rb
33
+ - ext/eppo_client/src/client.rs
34
+ - ext/eppo_client/src/lib.rs
173
35
  - lib/eppo_client.rb
174
36
  - lib/eppo_client/assignment_logger.rb
175
37
  - lib/eppo_client/client.rb
176
38
  - lib/eppo_client/config.rb
177
- - lib/eppo_client/configuration_requestor.rb
178
- - lib/eppo_client/configuration_store.rb
179
- - lib/eppo_client/constants.rb
180
39
  - lib/eppo_client/custom_errors.rb
181
- - lib/eppo_client/http_client.rb
182
- - lib/eppo_client/lru_cache.rb
183
- - lib/eppo_client/poller.rb
184
- - lib/eppo_client/rules.rb
185
- - lib/eppo_client/shard.rb
186
40
  - lib/eppo_client/validation.rb
187
- - lib/eppo_client/variation_type.rb
188
41
  - lib/eppo_client/version.rb
42
+ - sig/eppo_server_sdk.rbs
189
43
  homepage: https://github.com/Eppo-exp/ruby-sdk
190
44
  licenses:
191
45
  - MIT
@@ -203,14 +57,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
203
57
  requirements:
204
58
  - - ">="
205
59
  - !ruby/object:Gem::Version
206
- version: 3.0.6
60
+ version: 3.0.0
207
61
  required_rubygems_version: !ruby/object:Gem::Requirement
208
62
  requirements:
209
63
  - - ">="
210
64
  - !ruby/object:Gem::Version
211
- version: '0'
65
+ version: 3.3.11
212
66
  requirements: []
213
- rubygems_version: 3.4.6
67
+ rubygems_version: 3.5.11
214
68
  signing_key:
215
69
  specification_version: 4
216
70
  summary: Eppo SDK for Ruby
@@ -1,108 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative 'custom_errors'
4
- require_relative 'constants'
5
-
6
- module EppoClient
7
- # A class for the variation object
8
- class VariationDto
9
- attr_reader :name, :value, :typed_value, :shard_range
10
-
11
- def initialize(name, value, typed_value, shard_range)
12
- @name = name
13
- @value = value
14
- @typed_value = typed_value
15
- @shard_range = shard_range
16
- end
17
- end
18
-
19
- # A class for the allocation object
20
- class AllocationDto
21
- attr_reader :percent_exposure, :variations
22
-
23
- def initialize(percent_exposure, variations)
24
- @percent_exposure = percent_exposure
25
- @variations = variations
26
- end
27
- end
28
-
29
- # A class for the experiment configuration object
30
- class ExperimentConfigurationDto
31
- attr_reader :subject_shards, :enabled, :name, :overrides,
32
- :typed_overrides, :rules, :allocations
33
-
34
- def initialize(exp_config)
35
- @subject_shards = exp_config['subjectShards']
36
- @enabled = exp_config['enabled']
37
- @name = exp_config['name'] || nil
38
- @overrides = exp_config['overrides'] || {}
39
- @typed_overrides = exp_config['typedOverrides'] || {}
40
- @rules = exp_config['rules'] || []
41
- @allocations = exp_config['allocations']
42
- end
43
- end
44
-
45
- # A class for getting exp configs from the local cache or API
46
- class ExperimentConfigurationRequestor
47
- attr_reader :config_store
48
-
49
- def initialize(http_client, config_store)
50
- @http_client = http_client
51
- @config_store = config_store
52
- end
53
-
54
- def get_configuration(experiment_key)
55
- @http_client.is_unauthorized && raise(EppoClient::UnauthorizedError,
56
- 'please check your API key')
57
- @config_store.retrieve_configuration(experiment_key)
58
- end
59
-
60
- # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
61
- def fetch_and_store_configurations
62
- configs = {}
63
- begin
64
- exp_configs = @http_client.get(EppoClient::RAC_ENDPOINT).fetch(
65
- 'flags', {}
66
- )
67
- # rubocop: disable Metrics/BlockLength
68
- exp_configs.each do |exp_key, exp_config|
69
- exp_config['allocations'].each do |k, v|
70
- exp_config['allocations'][k] = EppoClient::AllocationDto.new(
71
- v['percentExposure'],
72
- v['variations'].map do |var|
73
- EppoClient::VariationDto.new(
74
- var['name'], var['value'], var['typedValue'],
75
- EppoClient::ShardRange.new(var['shardRange']['start'],
76
- var['shardRange']['end'])
77
- )
78
- end
79
- )
80
- end
81
- exp_config['rules'] = exp_config['rules'].map do |rule|
82
- EppoClient::Rule.new(
83
- conditions: rule['conditions'].map do |condition|
84
- EppoClient::Condition.new(
85
- value: condition['value'],
86
- operator: condition['operator'],
87
- attribute: condition['attribute']
88
- )
89
- end,
90
- allocation_key: rule['allocationKey']
91
- )
92
- end
93
- configs[exp_key] = EppoClient::ExperimentConfigurationDto.new(
94
- exp_config
95
- )
96
- end
97
- # rubocop: enable Metrics/BlockLength
98
- @config_store.assign_configurations(configs)
99
- rescue EppoClient::HttpRequestError => e
100
- Logger.new($stdout).error(
101
- "Error retrieving assignment configurations: #{e}"
102
- )
103
- end
104
- configs
105
- end
106
- # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
107
- end
108
- end
@@ -1,35 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'concurrent/atomic/read_write_lock'
4
-
5
- require_relative 'lru_cache'
6
-
7
- module EppoClient
8
- # A thread safe store for the configurations to ensure that retrievals pull from a single source of truth
9
- class ConfigurationStore
10
- attr_reader :lock, :cache
11
-
12
- def initialize(max_size)
13
- @cache = EppoClient::LRUCache.new(max_size)
14
- @lock = Concurrent::ReadWriteLock.new
15
- end
16
-
17
- def retrieve_configuration(key)
18
- @lock.with_read_lock { @cache[key] }
19
- end
20
-
21
- def assign_configurations(configs)
22
- @lock.with_write_lock do
23
- # Create a temporary new cache and populate it.
24
- new_cache = EppoClient::LRUCache.new(@cache.size)
25
- configs.each do |key, config|
26
- new_cache[key] = config
27
- end
28
-
29
- # Replace the old cache with the new one.
30
- # Performs an atomic swap.
31
- @cache = new_cache
32
- end
33
- end
34
- end
35
- end
@@ -1,20 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'logger'
4
-
5
- module EppoClient
6
- # default level for logging
7
- DEFAULT_LOGGER_LEVEL = Logger::INFO
8
-
9
- # configuration cache constants
10
- MAX_CACHE_ENTRIES = 1000 # arbitrary; the caching library requires a max limit
11
-
12
- # poller constants
13
- SECOND_MILLIS = 1000
14
- MINUTE_MILLIS = 60 * SECOND_MILLIS
15
- POLL_JITTER_MILLIS = 30 * SECOND_MILLIS
16
- POLL_INTERVAL_MILLIS = 5 * MINUTE_MILLIS
17
-
18
- # the configs endpoint
19
- RAC_ENDPOINT = 'randomized_assignment/v3/config'
20
- end
@@ -1,75 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'faraday'
4
- require 'faraday/retry'
5
-
6
- require_relative 'custom_errors'
7
-
8
- REQUEST_TIMEOUT_SECONDS = 2
9
- # This applies only to failed DNS lookups and connection timeouts,
10
- # never to requests where data has made it to the server.
11
- MAX_RETRIES = 3
12
-
13
- module EppoClient
14
- # The SDK params object
15
- class SdkParams
16
- attr_reader :api_key, :sdk_name, :sdk_version
17
-
18
- def initialize(api_key, sdk_name, sdk_version)
19
- @api_key = api_key
20
- @sdk_name = sdk_name
21
- @sdk_version = sdk_version
22
- end
23
-
24
- # attributes are camelCase because that's what the backend endpoint expects
25
- def formatted
26
- {
27
- 'apiKey' => api_key,
28
- 'sdkName' => sdk_name,
29
- 'sdkVersion' => sdk_version
30
- }
31
- end
32
-
33
- # Hide instance variables (specifically api_key) from logs
34
- def inspect
35
- "#<EppoClient::SdkParams:#{object_id}>"
36
- end
37
- end
38
-
39
- # The http request client with retry/timeout behavior
40
- class HttpClient
41
- attr_reader :is_unauthorized
42
-
43
- @retry_options = {
44
- max: MAX_RETRIES,
45
- interval: 0.05,
46
- interval_randomness: 0.5,
47
- backoff_factor: 2,
48
- exceptions: ['Timeout::Error']
49
- }
50
-
51
- def initialize(base_url, sdk_params)
52
- @base_url = base_url
53
- @sdk_params = sdk_params
54
- @is_unauthorized = false
55
- end
56
-
57
- def get(resource)
58
- conn = Faraday::Connection.new(@base_url, params: @sdk_params) do |f|
59
- f.request :retry, @retry_options
60
- end
61
- conn.options.timeout = REQUEST_TIMEOUT_SECONDS
62
- response = conn.get(resource)
63
- @is_unauthorized = response.status == 401
64
- raise get_http_error(response.status, resource) if response.status != 200
65
-
66
- JSON.parse(response.body)
67
- end
68
-
69
- private
70
-
71
- def get_http_error(status_code, resource)
72
- EppoClient::HttpRequestError.new("HTTP #{status_code} error while requesting resource #{resource}", status_code)
73
- end
74
- end
75
- end
@@ -1,28 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module EppoClient
4
- # The LRU cache relies on the fact that Ruby's Hash class maintains insertion order. So deleting
5
- # and re-inserting a key-value pair on access moves the key to the last position. When an
6
- # entry is added and the cache is full, the first entry is removed.
7
- class LRUCache
8
- attr_reader :cache, :size
9
-
10
- # Creates a new LRUCache that can hold +size+ entries.
11
- def initialize(size)
12
- @size = size
13
- @cache = {}
14
- end
15
-
16
- # Returns the stored value for +key+ or +nil+ if no value was stored under the key.
17
- def [](key)
18
- (val = @cache.delete(key)).nil? ? nil : @cache[key] = val
19
- end
20
-
21
- # Stores the +value+ under the +key+.
22
- def []=(key, value)
23
- @cache.delete(key)
24
- @cache[key] = value
25
- @cache.shift if @cache.length > @size
26
- end
27
- end
28
- end
@@ -1,48 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'concurrent/atom'
4
-
5
- # The poller
6
- module EppoClient
7
- # The poller class invokes a callback and waits on repeat on a separate thread
8
- class Poller
9
- def initialize(interval_millis, jitter_millis, callback)
10
- @jitter_millis = jitter_millis
11
- @interval = interval_millis
12
- @stopped = Concurrent::Atom.new(false)
13
- @callback = callback
14
- @thread = nil
15
- end
16
-
17
- def start
18
- @stopped.reset(false)
19
- @thread = Thread.new { poll }
20
- end
21
-
22
- def stop
23
- @stopped.reset(true)
24
- Thread.kill(@thread)
25
- end
26
-
27
- def stopped?
28
- @stopped.value
29
- end
30
-
31
- def poll
32
- until stopped?
33
- begin
34
- @callback.call
35
- rescue StandardError => e
36
- Logger.new($stdout).error("Unexpected error running poll task: #{e}")
37
- break
38
- end
39
- _wait_for_interval
40
- end
41
- end
42
-
43
- def _wait_for_interval
44
- interval_with_jitter = @interval - rand(@jitter_millis)
45
- sleep interval_with_jitter / 1000
46
- end
47
- end
48
- end
@@ -1,119 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'semver'
4
-
5
- # The helper module for rules
6
- module EppoClient
7
- module OperatorType
8
- MATCHES = 'MATCHES'
9
- GTE = 'GTE'
10
- GT = 'GT'
11
- LTE = 'LTE'
12
- LT = 'LT'
13
- ONE_OF = 'ONE_OF'
14
- NOT_ONE_OF = 'NOT_ONE_OF'
15
- end
16
-
17
- # A class for the Condition object
18
- class Condition
19
- attr_accessor :operator, :attribute, :value
20
-
21
- def initialize(operator:, attribute:, value:)
22
- @operator = operator
23
- @attribute = attribute
24
- @value = value
25
- end
26
- end
27
-
28
- # A class for the Rule object
29
- class Rule
30
- attr_accessor :allocation_key, :conditions
31
-
32
- def initialize(allocation_key:, conditions:)
33
- @allocation_key = allocation_key
34
- @conditions = conditions
35
- end
36
- end
37
-
38
- def find_matching_rule(subject_attributes, rules)
39
- rules.each do |rule|
40
- return rule if matches_rule(subject_attributes, rule)
41
- end
42
- nil
43
- end
44
-
45
- def matches_rule(subject_attributes, rule)
46
- rule.conditions.each do |condition|
47
- return false unless evaluate_condition(subject_attributes, condition)
48
- end
49
- true
50
- end
51
-
52
- # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
53
- def evaluate_condition(subject_attributes, condition)
54
- subject_value = subject_attributes[condition.attribute]
55
- return false if subject_value.nil?
56
-
57
- case condition.operator
58
- when OperatorType::MATCHES
59
- !!(Regexp.new(condition.value) =~ subject_value.to_s)
60
- when OperatorType::ONE_OF
61
- condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
62
- when OperatorType::NOT_ONE_OF
63
- !condition.value.map(&:downcase).include?(subject_value.to_s.downcase)
64
- else
65
- # Numeric operator: value could be numeric or semver.
66
- if subject_value.is_a?(Numeric)
67
- evaluate_numeric_condition(subject_value, condition)
68
- elsif valid_semver?(subject_value)
69
- compare_semver(subject_value, condition.value, condition.operator)
70
- end
71
- end
72
- end
73
- # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
74
-
75
- # rubocop:disable Metrics/MethodLength
76
- def evaluate_numeric_condition(subject_value, condition)
77
- case condition.operator
78
- when OperatorType::GT
79
- subject_value > condition.value
80
- when OperatorType::GTE
81
- subject_value >= condition.value
82
- when OperatorType::LT
83
- subject_value < condition.value
84
- when OperatorType::LTE
85
- subject_value <= condition.value
86
- else
87
- false
88
- end
89
- end
90
- # rubocop:enable Metrics/MethodLength
91
-
92
- # rubocop:disable Metrics/MethodLength
93
- def compare_semver(attribute_value, condition_value, operator)
94
- unless valid_semver?(attribute_value) && valid_semver?(condition_value)
95
- return false
96
- end
97
-
98
- case operator
99
- when OperatorType::GT
100
- SemVer.parse(attribute_value) > SemVer.parse(condition_value)
101
- when OperatorType::GTE
102
- SemVer.parse(attribute_value) >= SemVer.parse(condition_value)
103
- when OperatorType::LT
104
- SemVer.parse(attribute_value) < SemVer.parse(condition_value)
105
- when OperatorType::LTE
106
- SemVer.parse(attribute_value) <= SemVer.parse(condition_value)
107
- else
108
- false
109
- end
110
- end
111
- # rubocop:enable Metrics/MethodLength
112
-
113
- def valid_semver?(string)
114
- !SemVer.parse(string).nil?
115
- end
116
-
117
- module_function :find_matching_rule, :matches_rule, :evaluate_condition,
118
- :evaluate_numeric_condition, :valid_semver?, :compare_semver
119
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'digest'
4
-
5
- # The helper module for shard logic
6
- module EppoClient
7
- # A class for checking if a shard is in a range
8
- class ShardRange
9
- attr_reader :start, :end
10
-
11
- def initialize(range_start, range_end)
12
- @start = range_start
13
- @end = range_end
14
- end
15
-
16
- def shard_in_range?(shard)
17
- shard >= @start && shard < @end
18
- end
19
- end
20
-
21
- module_function
22
-
23
- def get_shard(input, subject_shards)
24
- hash_output = Digest::MD5.hexdigest(input)
25
- # get the first 4 bytes of the md5 hex string and parse it using base 16
26
- # (8 hex characters represent 4 bytes, e.g. 0xffffffff represents the max 4-byte integer)
27
- int_from_hash = hash_output[0...8].to_i(16)
28
- int_from_hash % subject_shards
29
- end
30
- end