eppo-server-sdk 0.1.0 → 0.2.2

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: 5fd8f3093395da766b70f3e7cba84b6fc712f58c05092b3594ab53cb0d0f827d
4
+ data.tar.gz: f773c3f04448a9a63c2e495c07099d952df7579b87472f9cf281e22d69698127
5
5
  SHA512:
6
- metadata.gz: debd90881651fa4d3f85d6af2ce71673c59378b3238f7443a2b851f16a762f667198e61f8494d28957da2ecb28dc896c5a99564a54e6fb5ec14ddde0c1a4c06a
7
- data.tar.gz: 2559d1d149a37e6dd8f6779511e72532a7a4046feda48e9657939de1db34ba30d2c5d52b5df6d413279f67ff4d9cfcd03a5b905bc723e0d13c236be5295e3a66
6
+ metadata.gz: bdf91e318a7a4ef6e8eeb90650b94f6ce712ca58b35fdf99e08e60a71c79ef91c769e0798e709c1b59da8c060bffcb765ff8e9afc06d58330ceab2155d71d26a
7
+ data.tar.gz: 22f8876c55dd41194a25bd9b31174f3cb84e8a760507afe72f5eb8a9f9859e8d32d4938b1a6c82147fafd011bcfd1df1f848bea818bd9d927bbd2051db657dd5
data/lib/client.rb CHANGED
@@ -8,10 +8,13 @@ require 'custom_errors'
8
8
  require 'rules'
9
9
  require 'shard'
10
10
  require 'validation'
11
+ require 'variation_type'
11
12
 
12
13
  module EppoClient
13
14
  # The main client singleton
15
+ # rubocop:disable Metrics/ClassLength
14
16
  class Client
17
+ extend Gem::Deprecate
15
18
  include Singleton
16
19
  attr_accessor :config_requestor, :assignment_logger, :poller
17
20
 
@@ -19,24 +22,123 @@ module EppoClient
19
22
  Client.instance
20
23
  end
21
24
 
22
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
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
+
23
100
  def get_assignment(
24
101
  subject_key,
25
- flag_or_experiment_key,
102
+ flag_key,
26
103
  subject_attributes = {},
27
104
  log_level = EppoClient::DEFAULT_LOGGER_LEVEL
28
105
  )
29
106
  logger = Logger.new($stdout)
30
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
+ )
31
123
  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)
124
+ EppoClient.validate_not_blank('flag_key', flag_key)
125
+ experiment_config = @config_requestor.get_configuration(flag_key)
34
126
  override = get_subject_variation_override(experiment_config, subject_key)
35
- return override unless override.nil?
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
36
137
 
37
138
  if experiment_config.nil? || experiment_config.enabled == false
38
139
  logger.debug(
39
- "[Eppo SDK] No assigned variation. No active experiment or flag for key: #{flag_or_experiment_key}"
140
+ '[Eppo SDK] No assigned variation. No active experiment or flag for '\
141
+ "key: #{flag_key}"
40
142
  )
41
143
  return nil
42
144
  end
@@ -44,7 +146,8 @@ module EppoClient
44
146
  matched_rule = EppoClient.find_matching_rule(subject_attributes, experiment_config.rules)
45
147
  if matched_rule.nil?
46
148
  logger.debug(
47
- "[Eppo SDK] No assigned variation. Subject attributes do not match targeting rules: #{subject_attributes}"
149
+ '[Eppo SDK] No assigned variation. Subject attributes do not match '\
150
+ "targeting rules: #{subject_attributes}"
48
151
  )
49
152
  return nil
50
153
  end
@@ -52,22 +155,41 @@ module EppoClient
52
155
  allocation = experiment_config.allocations[matched_rule.allocation_key]
53
156
  unless in_experiment_sample?(
54
157
  subject_key,
55
- flag_or_experiment_key,
158
+ flag_key,
56
159
  experiment_config.subject_shards,
57
160
  allocation.percent_exposure
58
161
  )
59
162
  logger.debug(
60
- '[Eppo SDK] No assigned variation. Subject is not part of experiment sample population'
163
+ '[Eppo SDK] No assigned variation. Subject is not part of experiment'\
164
+ ' sample population'
61
165
  )
62
166
  return nil
63
167
  end
64
168
 
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
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
67
187
 
68
188
  assignment_event = {
69
- "experiment": flag_or_experiment_key,
70
- "variation": assigned_variation,
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,
71
193
  "subject": subject_key,
72
194
  "timestamp": Time.now.utc.iso8601,
73
195
  "subjectAttributes": subject_attributes
@@ -76,7 +198,7 @@ module EppoClient
76
198
  begin
77
199
  @assignment_logger.log_assignment(assignment_event)
78
200
  rescue EppoClient::AssignmentLoggerError => e
79
- # This error means that log_assignment was not set up. This is okay to ignore.
201
+ # Error means log_assignment was not set up. This is okay to ignore.
80
202
  rescue StandardError => e
81
203
  logger.error("[Eppo SDK] Error logging assignment event: #{e}")
82
204
  end
@@ -89,14 +211,28 @@ module EppoClient
89
211
  @poller.stop
90
212
  end
91
213
 
214
+ # rubocop:disable Metrics/MethodLength
92
215
  def get_subject_variation_override(experiment_config, subject)
93
216
  subject_hash = Digest::MD5.hexdigest(subject.to_s)
94
- experiment_config&.overrides && experiment_config.overrides[subject_hash]
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
95
227
  end
228
+ # rubocop:enable Metrics/MethodLength
96
229
 
97
- def in_experiment_sample?(subject, experiment_key, subject_shards, percent_exposure)
98
- shard = EppoClient.get_shard("exposure-#{subject}-#{experiment_key}", subject_shards)
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)
99
234
  shard <= percent_exposure * subject_shards
100
235
  end
101
236
  end
237
+ # rubocop:enable Metrics/ClassLength
102
238
  end
@@ -6,11 +6,12 @@ require 'constants'
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
data/lib/eppo_client.rb CHANGED
@@ -29,21 +29,28 @@ module EppoClient
29
29
  end
30
30
  # rubocop:enable Metrics/MethodLength
31
31
 
32
- # rubocop:disable Metrics/MethodLength
32
+ # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
33
33
  def init(config)
34
34
  config.validate
35
- sdk_version = ParseGemspec::Specification.load('eppo-server-sdk.gemspec').to_hash_object[:version]
35
+ sdk_version = ParseGemspec::Specification.load(
36
+ 'eppo-server-sdk.gemspec'
37
+ ).to_hash_object[:version]
36
38
  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)
39
+ http_client = EppoClient::HttpClient.new(config.base_url,
40
+ sdk_params.formatted)
41
+ config_store = EppoClient::ConfigurationStore.new(
42
+ EppoClient::MAX_CACHE_ENTRIES
43
+ )
39
44
  config_store.lock.with_write_lock do
40
45
  EppoClient.initialize_client(
41
- EppoClient::ExperimentConfigurationRequestor.new(http_client, config_store),
46
+ EppoClient::ExperimentConfigurationRequestor.new(
47
+ http_client, config_store
48
+ ),
42
49
  config.assignment_logger
43
50
  )
44
51
  end
45
52
  end
46
- # rubocop:enable Metrics/MethodLength
53
+ # rubocop:enable Metrics/MethodLength, Metrics/AbcSize
47
54
 
48
55
  module_function :init, :initialize_client
49
56
  end
data/lib/rules.rb CHANGED
@@ -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
data/lib/shard.rb CHANGED
@@ -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
data/lib/validation.rb CHANGED
@@ -4,9 +4,11 @@ require 'custom_errors'
4
4
 
5
5
  # The helper module to validate keys
6
6
  module EppoClient
7
+ module_function
8
+
7
9
  def validate_not_blank(field_name, field_value)
8
- (field_value.nil? || field_value == '') && raise(EppoClient::InvalidValueError, "#{field_name} cannot be blank")
10
+ (field_value.nil? || field_value == '') && raise(
11
+ EppoClient::InvalidValueError, "#{field_name} cannot be blank"
12
+ )
9
13
  end
10
-
11
- module_function :validate_not_blank
12
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
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.2
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-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: concurrent-ruby
@@ -136,14 +136,14 @@ dependencies:
136
136
  requirements:
137
137
  - - "~>"
138
138
  - !ruby/object:Gem::Version
139
- version: '1.41'
139
+ version: 0.82.0
140
140
  type: :development
141
141
  prerelease: false
142
142
  version_requirements: !ruby/object:Gem::Requirement
143
143
  requirements:
144
144
  - - "~>"
145
145
  - !ruby/object:Gem::Version
146
- version: '1.41'
146
+ version: 0.82.0
147
147
  - !ruby/object:Gem::Dependency
148
148
  name: webmock
149
149
  requirement: !ruby/object:Gem::Requirement
@@ -184,6 +184,7 @@ files:
184
184
  - lib/rules.rb
185
185
  - lib/shard.rb
186
186
  - lib/validation.rb
187
+ - lib/variation_type.rb
187
188
  homepage: https://github.com/Eppo-exp/ruby-sdk
188
189
  licenses:
189
190
  - MIT
@@ -201,14 +202,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
201
202
  requirements:
202
203
  - - ">="
203
204
  - !ruby/object:Gem::Version
204
- version: 3.1.2
205
+ version: 3.0.6
205
206
  required_rubygems_version: !ruby/object:Gem::Requirement
206
207
  requirements:
207
208
  - - ">="
208
209
  - !ruby/object:Gem::Version
209
210
  version: '0'
210
211
  requirements: []
211
- rubygems_version: 3.3.26
212
+ rubygems_version: 3.4.6
212
213
  signing_key:
213
214
  specification_version: 4
214
215
  summary: Eppo SDK for Ruby