eppo-server-sdk 0.1.0 → 0.2.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0d2f2b75ea023c388c50b7db7ad56e017b217c21811310116938f35409153520
4
- data.tar.gz: 1fb5b4873b50f1197fcec8ea91ab071d8a5a75c7790e8cd956f011c621a88425
3
+ metadata.gz: d9c570d34608a7d3037d80c5aa161c383e2d7b618eef75383f53d002e772204a
4
+ data.tar.gz: 6fd21d3000781bada11f2f43b49fce0871fc74b1929387bddf277a1cb3c5db7f
5
5
  SHA512:
6
- metadata.gz: debd90881651fa4d3f85d6af2ce71673c59378b3238f7443a2b851f16a762f667198e61f8494d28957da2ecb28dc896c5a99564a54e6fb5ec14ddde0c1a4c06a
7
- data.tar.gz: 2559d1d149a37e6dd8f6779511e72532a7a4046feda48e9657939de1db34ba30d2c5d52b5df6d413279f67ff4d9cfcd03a5b905bc723e0d13c236be5295e3a66
6
+ metadata.gz: 27fbeb76e52adcce05cffa3a41e92b09790db53e76dafa4ded3d5d7df4d6b4cce37cb7c79056153d6965ec01d579a42a3140b88626c26c0eebe3874cde49dee4
7
+ data.tar.gz: 3a1f20881a19c385e4c37c141045d8e870d85fc81f650d07a736b4c8f39b8ebba0a24380231da334c40a51c8b49b3431a2096746d3e9d4f2fdaaa8d382e568c4
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'custom_errors'
3
+ require_relative 'custom_errors'
4
4
 
5
5
  module EppoClient
6
6
  # The base assignment logger class to override
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'singleton'
4
+ require 'time'
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'
12
+
13
+ module EppoClient
14
+ # The main client singleton
15
+ # rubocop:disable Metrics/ClassLength
16
+ class Client
17
+ extend Gem::Deprecate
18
+ include Singleton
19
+ attr_accessor :config_requestor, :assignment_logger, :poller
20
+
21
+ def instance
22
+ Client.instance
23
+ end
24
+
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
38
+ end
39
+
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
53
+ end
54
+
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
68
+ end
69
+
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
83
+ end
84
+
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
98
+ end
99
+
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
112
+ 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
+
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
145
+
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
154
+
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
168
+
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
176
+
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
185
+ end
186
+ end
187
+
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
+ }
197
+
198
+ 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}")
204
+ end
205
+
206
+ assigned_variation
207
+ end
208
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
209
+
210
+ def shutdown
211
+ @poller.stop
212
+ end
213
+
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
+ )
226
+ end
227
+ end
228
+ # rubocop:enable Metrics/MethodLength
229
+
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
235
+ end
236
+ end
237
+ # rubocop:enable Metrics/ClassLength
238
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'validation'
4
- require 'assignment_logger'
3
+ require_relative 'validation'
4
+ require_relative 'assignment_logger'
5
5
 
6
6
  module EppoClient
7
7
  # The class for configuring the Eppo client singleton
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'custom_errors'
4
- require 'constants'
3
+ require_relative 'custom_errors'
4
+ require_relative 'constants'
5
5
 
6
6
  module EppoClient
7
7
  # A class for the variation object
8
8
  class VariationDto
9
- attr_reader :name, :value, :shard_range
9
+ attr_reader :name, :value, :typed_value, :shard_range
10
10
 
11
- def initialize(name, value, shard_range)
11
+ def initialize(name, value, typed_value, shard_range)
12
12
  @name = name
13
13
  @value = value
14
+ @typed_value = typed_value
14
15
  @shard_range = shard_range
15
16
  end
16
17
  end
@@ -27,13 +28,15 @@ module EppoClient
27
28
 
28
29
  # A class for the experiment configuration object
29
30
  class ExperimentConfigurationDto
30
- attr_reader :subject_shards, :enabled, :name, :overrides, :rules, :allocations
31
+ attr_reader :subject_shards, :enabled, :name, :overrides,
32
+ :typed_overrides, :rules, :allocations
31
33
 
32
34
  def initialize(exp_config)
33
35
  @subject_shards = exp_config['subjectShards']
34
36
  @enabled = exp_config['enabled']
35
37
  @name = exp_config['name'] || nil
36
38
  @overrides = exp_config['overrides'] || {}
39
+ @typed_overrides = exp_config['typedOverrides'] || {}
37
40
  @rules = exp_config['rules'] || []
38
41
  @allocations = exp_config['allocations']
39
42
  end
@@ -41,7 +44,6 @@ module EppoClient
41
44
 
42
45
  # A class for getting exp configs from the local cache or API
43
46
  class ExperimentConfigurationRequestor
44
-
45
47
  attr_reader :config_store
46
48
 
47
49
  def initialize(http_client, config_store)
@@ -50,7 +52,8 @@ module EppoClient
50
52
  end
51
53
 
52
54
  def get_configuration(experiment_key)
53
- @http_client.is_unauthorized && raise(EppoClient::UnauthorizedError, 'please check your API key')
55
+ @http_client.is_unauthorized && raise(EppoClient::UnauthorizedError,
56
+ 'please check your API key')
54
57
  @config_store.retrieve_configuration(experiment_key)
55
58
  end
56
59
 
@@ -58,16 +61,19 @@ module EppoClient
58
61
  def fetch_and_store_configurations
59
62
  configs = {}
60
63
  begin
61
- exp_configs = @http_client.get(EppoClient::RAC_ENDPOINT).fetch('flags', {})
64
+ exp_configs = @http_client.get(EppoClient::RAC_ENDPOINT).fetch(
65
+ 'flags', {}
66
+ )
67
+ # rubocop: disable Metrics/BlockLength
62
68
  exp_configs.each do |exp_key, exp_config|
63
69
  exp_config['allocations'].each do |k, v|
64
70
  exp_config['allocations'][k] = EppoClient::AllocationDto.new(
65
71
  v['percentExposure'],
66
72
  v['variations'].map do |var|
67
73
  EppoClient::VariationDto.new(
68
- var['name'],
69
- var['value'],
70
- EppoClient::ShardRange.new(var['shardRange']['start'], var['shardRange']['end'])
74
+ var['name'], var['value'], var['typedValue'],
75
+ EppoClient::ShardRange.new(var['shardRange']['start'],
76
+ var['shardRange']['end'])
71
77
  )
72
78
  end
73
79
  )
@@ -84,11 +90,16 @@ module EppoClient
84
90
  allocation_key: rule['allocationKey']
85
91
  )
86
92
  end
87
- configs[exp_key] = EppoClient::ExperimentConfigurationDto.new(exp_config)
93
+ configs[exp_key] = EppoClient::ExperimentConfigurationDto.new(
94
+ exp_config
95
+ )
88
96
  end
97
+ # rubocop: enable Metrics/BlockLength
89
98
  @config_store.assign_configurations(configs)
90
99
  rescue EppoClient::HttpRequestError => e
91
- Logger.new($stdout).error("Error retrieving assignment configurations: #{e}")
100
+ Logger.new($stdout).error(
101
+ "Error retrieving assignment configurations: #{e}"
102
+ )
92
103
  end
93
104
  configs
94
105
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require 'concurrent/atomic/read_write_lock'
4
4
 
5
- require 'lru_cache'
5
+ require_relative 'lru_cache'
6
6
 
7
7
  module EppoClient
8
8
  # A thread safe store for the configurations to ensure that retrievals pull from a single source of truth
@@ -3,7 +3,7 @@
3
3
  require 'faraday'
4
4
  require 'faraday/retry'
5
5
 
6
- require 'custom_errors'
6
+ require_relative 'custom_errors'
7
7
 
8
8
  REQUEST_TIMEOUT_SECONDS = 2
9
9
  # This applies only to failed DNS lookups and connection timeouts,
@@ -47,7 +47,7 @@ module EppoClient
47
47
  true
48
48
  end
49
49
 
50
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
50
+ # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
51
51
  def evaluate_condition(subject_attributes, condition)
52
52
  subject_value = subject_attributes[condition.attribute]
53
53
  return false if subject_value.nil?
@@ -63,7 +63,7 @@ module EppoClient
63
63
  subject_value.is_a?(Numeric) && evaluate_numeric_condition(subject_value, condition)
64
64
  end
65
65
  end
66
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
66
+ # rubocop:enable Metrics/AbcSize, Metrics/MethodLength
67
67
 
68
68
  # rubocop:disable Metrics/MethodLength
69
69
  def evaluate_numeric_condition(subject_value, condition)
@@ -82,5 +82,6 @@ module EppoClient
82
82
  end
83
83
  # rubocop:enable Metrics/MethodLength
84
84
 
85
- module_function :find_matching_rule, :matches_rule, :evaluate_condition, :evaluate_numeric_condition
85
+ module_function :find_matching_rule, :matches_rule, :evaluate_condition,
86
+ :evaluate_numeric_condition
86
87
  end
@@ -18,6 +18,8 @@ module EppoClient
18
18
  end
19
19
  end
20
20
 
21
+ module_function
22
+
21
23
  def get_shard(input, subject_shards)
22
24
  hash_output = Digest::MD5.hexdigest(input)
23
25
  # get the first 4 bytes of the md5 hex string and parse it using base 16
@@ -25,6 +27,4 @@ module EppoClient
25
27
  int_from_hash = hash_output[0...8].to_i(16)
26
28
  int_from_hash % subject_shards
27
29
  end
28
-
29
- module_function :get_shard
30
30
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'custom_errors'
4
+
5
+ # The helper module to validate keys
6
+ module EppoClient
7
+ module_function
8
+
9
+ def validate_not_blank(field_name, field_value)
10
+ (field_value.nil? || field_value == '') && raise(
11
+ EppoClient::InvalidValueError, "#{field_name} cannot be blank"
12
+ )
13
+ end
14
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module EppoClient
6
+ # The class for configuring the Eppo client singleton
7
+ module VariationType
8
+ STRING_TYPE = 'string'
9
+ NUMERIC_TYPE = 'numeric'
10
+ BOOLEAN_TYPE = 'boolean'
11
+ JSON_TYPE = 'json'
12
+
13
+ module_function
14
+
15
+ # rubocop:disable Metrics/MethodLength, Metrics/CyclomaticComplexity
16
+ def expected_type?(assigned_variation, expected_variation_type)
17
+ case expected_variation_type
18
+ when STRING_TYPE
19
+ assigned_variation.typed_value.is_a?(String)
20
+ when NUMERIC_TYPE
21
+ assigned_variation.typed_value.is_a?(Numeric)
22
+ when BOOLEAN_TYPE
23
+ assigned_variation.typed_value.is_a?(TrueClass) ||
24
+ assigned_variation.typed_value.is_a?(FalseClass)
25
+ when JSON_TYPE
26
+ begin
27
+ parsed_json = JSON.parse(assigned_variation.value)
28
+ JSON.dump(assigned_variation.typed_value)
29
+ parsed_json == assigned_variation.typed_value
30
+ rescue JSON::JSONError
31
+ false
32
+ end
33
+ else
34
+ false
35
+ end
36
+ end
37
+ # rubocop:enable Metrics/MethodLength, Metrics/CyclomaticComplexity
38
+ end
39
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EppoClient
4
+ VERSION = '0.2.3'
5
+ end
data/lib/eppo_client.rb CHANGED
@@ -1,15 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'parse_gemspec'
4
-
5
- require 'assignment_logger'
6
- require 'http_client'
7
- require 'poller'
8
- require 'config'
9
- require 'client'
10
- require 'constants'
11
- require 'configuration_requestor'
12
- require 'configuration_store'
3
+ require_relative 'eppo_client/assignment_logger'
4
+ require_relative 'eppo_client/http_client'
5
+ require_relative 'eppo_client/poller'
6
+ require_relative 'eppo_client/config'
7
+ require_relative 'eppo_client/client'
8
+ require_relative 'eppo_client/constants'
9
+ require_relative 'eppo_client/configuration_requestor'
10
+ require_relative 'eppo_client/configuration_store'
11
+ require_relative 'eppo_client/version'
13
12
 
14
13
  # This module scopes all the client SDK's classes and functions
15
14
  module EppoClient
@@ -32,13 +31,18 @@ module EppoClient
32
31
  # rubocop:disable Metrics/MethodLength
33
32
  def init(config)
34
33
  config.validate
35
- sdk_version = ParseGemspec::Specification.load('eppo-server-sdk.gemspec').to_hash_object[:version]
36
- sdk_params = EppoClient::SdkParams.new(config.api_key, 'ruby', sdk_version)
37
- http_client = EppoClient::HttpClient.new(config.base_url, sdk_params.formatted)
38
- config_store = EppoClient::ConfigurationStore.new(EppoClient::MAX_CACHE_ENTRIES)
34
+ sdk_params = EppoClient::SdkParams.new(config.api_key, 'ruby',
35
+ EppoClient::VERSION)
36
+ http_client = EppoClient::HttpClient.new(config.base_url,
37
+ sdk_params.formatted)
38
+ config_store = EppoClient::ConfigurationStore.new(
39
+ EppoClient::MAX_CACHE_ENTRIES
40
+ )
39
41
  config_store.lock.with_write_lock do
40
42
  EppoClient.initialize_client(
41
- EppoClient::ExperimentConfigurationRequestor.new(http_client, config_store),
43
+ EppoClient::ExperimentConfigurationRequestor.new(
44
+ http_client, config_store
45
+ ),
42
46
  config.assignment_logger
43
47
  )
44
48
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: eppo-server-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Eppo
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-04-26 00:00:00.000000000 Z
11
+ date: 2023-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -70,26 +70,6 @@ dependencies:
70
70
  - - ">="
71
71
  - !ruby/object:Gem::Version
72
72
  version: 2.0.0
73
- - !ruby/object:Gem::Dependency
74
- name: parse_gemspec
75
- requirement: !ruby/object:Gem::Requirement
76
- requirements:
77
- - - "~>"
78
- - !ruby/object:Gem::Version
79
- version: '1.0'
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: 1.0.0
83
- type: :runtime
84
- prerelease: false
85
- version_requirements: !ruby/object:Gem::Requirement
86
- requirements:
87
- - - "~>"
88
- - !ruby/object:Gem::Version
89
- version: '1.0'
90
- - - ">="
91
- - !ruby/object:Gem::Version
92
- version: 1.0.0
93
73
  - !ruby/object:Gem::Dependency
94
74
  name: rake
95
75
  requirement: !ruby/object:Gem::Requirement
@@ -136,14 +116,14 @@ dependencies:
136
116
  requirements:
137
117
  - - "~>"
138
118
  - !ruby/object:Gem::Version
139
- version: '1.41'
119
+ version: 0.82.0
140
120
  type: :development
141
121
  prerelease: false
142
122
  version_requirements: !ruby/object:Gem::Requirement
143
123
  requirements:
144
124
  - - "~>"
145
125
  - !ruby/object:Gem::Version
146
- version: '1.41'
126
+ version: 0.82.0
147
127
  - !ruby/object:Gem::Dependency
148
128
  name: webmock
149
129
  requirement: !ruby/object:Gem::Requirement
@@ -170,20 +150,22 @@ executables: []
170
150
  extensions: []
171
151
  extra_rdoc_files: []
172
152
  files:
173
- - lib/assignment_logger.rb
174
- - lib/client.rb
175
- - lib/config.rb
176
- - lib/configuration_requestor.rb
177
- - lib/configuration_store.rb
178
- - lib/constants.rb
179
- - lib/custom_errors.rb
180
153
  - lib/eppo_client.rb
181
- - lib/http_client.rb
182
- - lib/lru_cache.rb
183
- - lib/poller.rb
184
- - lib/rules.rb
185
- - lib/shard.rb
186
- - lib/validation.rb
154
+ - lib/eppo_client/assignment_logger.rb
155
+ - lib/eppo_client/client.rb
156
+ - lib/eppo_client/config.rb
157
+ - lib/eppo_client/configuration_requestor.rb
158
+ - lib/eppo_client/configuration_store.rb
159
+ - lib/eppo_client/constants.rb
160
+ - lib/eppo_client/custom_errors.rb
161
+ - lib/eppo_client/http_client.rb
162
+ - lib/eppo_client/lru_cache.rb
163
+ - lib/eppo_client/poller.rb
164
+ - lib/eppo_client/rules.rb
165
+ - lib/eppo_client/shard.rb
166
+ - lib/eppo_client/validation.rb
167
+ - lib/eppo_client/variation_type.rb
168
+ - lib/eppo_client/version.rb
187
169
  homepage: https://github.com/Eppo-exp/ruby-sdk
188
170
  licenses:
189
171
  - MIT
@@ -201,14 +183,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
201
183
  requirements:
202
184
  - - ">="
203
185
  - !ruby/object:Gem::Version
204
- version: 3.1.2
186
+ version: 3.0.6
205
187
  required_rubygems_version: !ruby/object:Gem::Requirement
206
188
  requirements:
207
189
  - - ">="
208
190
  - !ruby/object:Gem::Version
209
191
  version: '0'
210
192
  requirements: []
211
- rubygems_version: 3.3.26
193
+ rubygems_version: 3.4.6
212
194
  signing_key:
213
195
  specification_version: 4
214
196
  summary: Eppo SDK for Ruby
data/lib/client.rb DELETED
@@ -1,102 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'singleton'
4
- require 'time'
5
-
6
- require 'constants'
7
- require 'custom_errors'
8
- require 'rules'
9
- require 'shard'
10
- require 'validation'
11
-
12
- module EppoClient
13
- # The main client singleton
14
- class Client
15
- include Singleton
16
- attr_accessor :config_requestor, :assignment_logger, :poller
17
-
18
- def instance
19
- Client.instance
20
- end
21
-
22
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
23
- def get_assignment(
24
- subject_key,
25
- flag_or_experiment_key,
26
- subject_attributes = {},
27
- log_level = EppoClient::DEFAULT_LOGGER_LEVEL
28
- )
29
- logger = Logger.new($stdout)
30
- logger.level = log_level
31
- EppoClient.validate_not_blank('subject_key', subject_key)
32
- EppoClient.validate_not_blank('flag_or_experiment_key', flag_or_experiment_key)
33
- experiment_config = @config_requestor.get_configuration(flag_or_experiment_key)
34
- override = get_subject_variation_override(experiment_config, subject_key)
35
- return override unless override.nil?
36
-
37
- if experiment_config.nil? || experiment_config.enabled == false
38
- logger.debug(
39
- "[Eppo SDK] No assigned variation. No active experiment or flag for key: #{flag_or_experiment_key}"
40
- )
41
- return nil
42
- end
43
-
44
- matched_rule = EppoClient.find_matching_rule(subject_attributes, experiment_config.rules)
45
- if matched_rule.nil?
46
- logger.debug(
47
- "[Eppo SDK] No assigned variation. Subject attributes do not match targeting rules: #{subject_attributes}"
48
- )
49
- return nil
50
- end
51
-
52
- allocation = experiment_config.allocations[matched_rule.allocation_key]
53
- unless in_experiment_sample?(
54
- subject_key,
55
- flag_or_experiment_key,
56
- experiment_config.subject_shards,
57
- allocation.percent_exposure
58
- )
59
- logger.debug(
60
- '[Eppo SDK] No assigned variation. Subject is not part of experiment sample population'
61
- )
62
- return nil
63
- end
64
-
65
- shard = EppoClient.get_shard("assignment-#{subject_key}-#{flag_or_experiment_key}", experiment_config.subject_shards)
66
- assigned_variation = allocation.variations.find { |var| var.shard_range.shard_in_range?(shard) }.value
67
-
68
- assignment_event = {
69
- "experiment": flag_or_experiment_key,
70
- "variation": assigned_variation,
71
- "subject": subject_key,
72
- "timestamp": Time.now.utc.iso8601,
73
- "subjectAttributes": subject_attributes
74
- }
75
-
76
- begin
77
- @assignment_logger.log_assignment(assignment_event)
78
- rescue EppoClient::AssignmentLoggerError => e
79
- # This error means that log_assignment was not set up. This is okay to ignore.
80
- rescue StandardError => e
81
- logger.error("[Eppo SDK] Error logging assignment event: #{e}")
82
- end
83
-
84
- assigned_variation
85
- end
86
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
87
-
88
- def shutdown
89
- @poller.stop
90
- end
91
-
92
- def get_subject_variation_override(experiment_config, subject)
93
- subject_hash = Digest::MD5.hexdigest(subject.to_s)
94
- experiment_config&.overrides && experiment_config.overrides[subject_hash]
95
- end
96
-
97
- def in_experiment_sample?(subject, experiment_key, subject_shards, percent_exposure)
98
- shard = EppoClient.get_shard("exposure-#{subject}-#{experiment_key}", subject_shards)
99
- shard <= percent_exposure * subject_shards
100
- end
101
- end
102
- end
data/lib/validation.rb DELETED
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'custom_errors'
4
-
5
- # The helper module to validate keys
6
- module EppoClient
7
- def validate_not_blank(field_name, field_value)
8
- (field_value.nil? || field_value == '') && raise(EppoClient::InvalidValueError, "#{field_name} cannot be blank")
9
- end
10
-
11
- module_function :validate_not_blank
12
- end
File without changes
File without changes
File without changes