eppo-server-sdk 0.1.0 → 0.2.2

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: 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