eppo-server-sdk 0.1.0 → 0.2.3

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