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 +4 -4
- data/lib/{assignment_logger.rb → eppo_client/assignment_logger.rb} +1 -1
- data/lib/eppo_client/client.rb +238 -0
- data/lib/{config.rb → eppo_client/config.rb} +2 -2
- data/lib/{configuration_requestor.rb → eppo_client/configuration_requestor.rb} +24 -13
- data/lib/{configuration_store.rb → eppo_client/configuration_store.rb} +1 -1
- data/lib/{http_client.rb → eppo_client/http_client.rb} +1 -1
- data/lib/{rules.rb → eppo_client/rules.rb} +4 -3
- data/lib/{shard.rb → eppo_client/shard.rb} +2 -2
- data/lib/eppo_client/validation.rb +14 -0
- data/lib/eppo_client/variation_type.rb +39 -0
- data/lib/eppo_client/version.rb +5 -0
- data/lib/eppo_client.rb +19 -15
- metadata +21 -39
- data/lib/client.rb +0 -102
- data/lib/validation.rb +0 -12
- /data/lib/{constants.rb → eppo_client/constants.rb} +0 -0
- /data/lib/{custom_errors.rb → eppo_client/custom_errors.rb} +0 -0
- /data/lib/{lru_cache.rb → eppo_client/lru_cache.rb} +0 -0
- /data/lib/{poller.rb → eppo_client/poller.rb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d9c570d34608a7d3037d80c5aa161c383e2d7b618eef75383f53d002e772204a
|
4
|
+
data.tar.gz: 6fd21d3000781bada11f2f43b49fce0871fc74b1929387bddf277a1cb3c5db7f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 27fbeb76e52adcce05cffa3a41e92b09790db53e76dafa4ded3d5d7df4d6b4cce37cb7c79056153d6965ec01d579a42a3140b88626c26c0eebe3874cde49dee4
|
7
|
+
data.tar.gz: 3a1f20881a19c385e4c37c141045d8e870d85fc81f650d07a736b4c8f39b8ebba0a24380231da334c40a51c8b49b3431a2096746d3e9d4f2fdaaa8d382e568c4
|
@@ -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,16 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
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,
|
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,
|
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(
|
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['
|
70
|
-
|
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(
|
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(
|
100
|
+
Logger.new($stdout).error(
|
101
|
+
"Error retrieving assignment configurations: #{e}"
|
102
|
+
)
|
92
103
|
end
|
93
104
|
configs
|
94
105
|
end
|
@@ -47,7 +47,7 @@ module EppoClient
|
|
47
47
|
true
|
48
48
|
end
|
49
49
|
|
50
|
-
# rubocop:disable Metrics/AbcSize, Metrics/
|
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/
|
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,
|
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
|
data/lib/eppo_client.rb
CHANGED
@@ -1,15 +1,14 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
-
|
36
|
-
|
37
|
-
http_client = EppoClient::HttpClient.new(config.base_url,
|
38
|
-
|
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(
|
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.
|
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-
|
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:
|
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:
|
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/
|
182
|
-
- lib/
|
183
|
-
- lib/
|
184
|
-
- lib/
|
185
|
-
- lib/
|
186
|
-
- lib/
|
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.
|
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.
|
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
|
File without changes
|