ldclient-rb 5.1.0 → 5.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +5 -9
- data/lib/ldclient-rb/evaluation.rb +123 -77
- data/lib/ldclient-rb/events.rb +1 -0
- data/lib/ldclient-rb/flags_state.rb +2 -1
- data/lib/ldclient-rb/ldclient.rb +110 -54
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_spec.rb +267 -95
- data/spec/ldclient_spec.rb +98 -17
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: aed3c4885f97dd1ad3f8dad1e9ea37ae53931fd6
|
4
|
+
data.tar.gz: a33110bc3288b9529aebc076e2fa5c6c405ea41d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0e8fe67e4835f5a961a5bd65b3ef383f85910253c361202b75f18dff585dbb8e210b1bc388297cd771111409bf4f0620f8aeb9197d8db17da3a644976c87ff1d
|
7
|
+
data.tar.gz: 85ad89319527662c86f18c6d472476b887f0382715577bf6fcf105c32dce3f24b51ea888a0022daff017cb9ffe4f1a3601edaa24f4d85d073de35284e478ac5b
|
data/CHANGELOG.md
CHANGED
@@ -2,18 +2,14 @@
|
|
2
2
|
|
3
3
|
All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
-
## [5.
|
5
|
+
## [5.2.0] - 2018-08-29
|
6
6
|
### Added:
|
7
|
-
- The new `LDClient` method `
|
8
|
-
- The `all_flags_state()` method also allows you to select only client-side-enabled flags to pass to the front end, by using the option `client_side_only: true`.
|
9
|
-
|
10
|
-
### Changed:
|
11
|
-
- Unexpected exceptions are now logged at `ERROR` level, and exception stacktraces at `DEBUG` level. Previously, both were being logged at `WARN` level.
|
12
|
-
|
13
|
-
### Deprecated:
|
14
|
-
- `LDClient.all_flags()`
|
7
|
+
- The new `LDClient` method `variation_detail` allows you to evaluate a feature flag (using the same parameters as you would for `variation`) and receive more information about how the value was calculated. This information is returned in an `EvaluationDetail` object, which contains both the result value and a "reason" object which will tell you, for instance, if the user was individually targeted for the flag or was matched by one of the flag's rules, or if the flag returned the default value due to an error.
|
15
8
|
|
9
|
+
### Fixed:
|
10
|
+
- Evaluating a prerequisite feature flag did not produce an analytics event if the prerequisite flag was off.
|
16
11
|
|
12
|
+
|
17
13
|
## [5.1.0] - 2018-08-27
|
18
14
|
### Added:
|
19
15
|
- The new `LDClient` method `all_flags_state()` should be used instead of `all_flags()` if you are passing flag data to the front end for use with the JavaScript SDK. It preserves some flag metadata that the front end requires in order to send analytics events correctly. Versions 2.5.0 and above of the JavaScript SDK are able to use this metadata, but the output of `all_flags_state()` will still work with older versions.
|
@@ -2,6 +2,37 @@ require "date"
|
|
2
2
|
require "semantic"
|
3
3
|
|
4
4
|
module LaunchDarkly
|
5
|
+
# An object returned by `LDClient.variation_detail`, combining the result of a flag evaluation with
|
6
|
+
# an explanation of how it was calculated.
|
7
|
+
class EvaluationDetail
|
8
|
+
def initialize(value, variation_index, reason)
|
9
|
+
@value = value
|
10
|
+
@variation_index = variation_index
|
11
|
+
@reason = reason
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [Object] The result of the flag evaluation. This will be either one of the flag's
|
15
|
+
# variations or the default value that was passed to the `variation` method.
|
16
|
+
attr_reader :value
|
17
|
+
|
18
|
+
# @return [int|nil] The index of the returned value within the flag's list of variations, e.g.
|
19
|
+
# 0 for the first variation - or `nil` if the default value was returned.
|
20
|
+
attr_reader :variation_index
|
21
|
+
|
22
|
+
# @return [Hash] An object describing the main factor that influenced the flag evaluation value.
|
23
|
+
attr_reader :reason
|
24
|
+
|
25
|
+
# @return [boolean] True if the flag evaluated to the default value rather than to one of its
|
26
|
+
# variations.
|
27
|
+
def default_value?
|
28
|
+
variation_index.nil?
|
29
|
+
end
|
30
|
+
|
31
|
+
def ==(other)
|
32
|
+
@value == other.value && @variation_index == other.variation_index && @reason == other.reason
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
5
36
|
module Evaluation
|
6
37
|
BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
7
38
|
|
@@ -107,113 +138,103 @@ module LaunchDarkly
|
|
107
138
|
end
|
108
139
|
}
|
109
140
|
|
110
|
-
|
141
|
+
# Used internally to hold an evaluation result and the events that were generated from prerequisites.
|
142
|
+
EvalResult = Struct.new(:detail, :events)
|
143
|
+
|
144
|
+
def error_result(errorKind, value = nil)
|
145
|
+
EvaluationDetail.new(value, nil, { kind: 'ERROR', errorKind: errorKind })
|
111
146
|
end
|
112
147
|
|
113
|
-
# Evaluates a feature flag
|
114
|
-
#
|
115
|
-
# Will return nil, but not raise an exception, indicating that the rules (including fallthrough) did not match
|
116
|
-
# In that case, the caller should return the default value.
|
148
|
+
# Evaluates a feature flag and returns an EvalResult. The result.value will be nil if the flag returns
|
149
|
+
# the default value. Error conditions produce a result with an error reason, not an exception.
|
117
150
|
def evaluate(flag, user, store, logger)
|
118
|
-
if flag.nil?
|
119
|
-
raise EvaluationError, "Flag does not exist"
|
120
|
-
end
|
121
|
-
|
122
151
|
if user.nil? || user[:key].nil?
|
123
|
-
|
152
|
+
return EvalResult.new(error_result('USER_NOT_SPECIFIED'), [])
|
124
153
|
end
|
125
154
|
|
126
155
|
events = []
|
156
|
+
detail = eval_internal(flag, user, store, events, logger)
|
157
|
+
return EvalResult.new(detail, events)
|
158
|
+
end
|
159
|
+
|
160
|
+
def eval_internal(flag, user, store, events, logger)
|
161
|
+
if !flag[:on]
|
162
|
+
return get_off_value(flag, { kind: 'OFF' }, logger)
|
163
|
+
end
|
127
164
|
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
165
|
+
prereq_failure_reason = check_prerequisites(flag, user, store, events, logger)
|
166
|
+
if !prereq_failure_reason.nil?
|
167
|
+
return get_off_value(flag, prereq_failure_reason, logger)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Check user target matches
|
171
|
+
(flag[:targets] || []).each do |target|
|
172
|
+
(target[:values] || []).each do |value|
|
173
|
+
if value == user[:key]
|
174
|
+
return get_variation(flag, target[:variation], { kind: 'TARGET_MATCH' }, logger)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Check custom rules
|
180
|
+
rules = flag[:rules] || []
|
181
|
+
rules.each_index do |i|
|
182
|
+
rule = rules[i]
|
183
|
+
if rule_match_user(rule, user, store)
|
184
|
+
return get_value_for_variation_or_rollout(flag, rule, user,
|
185
|
+
{ kind: 'RULE_MATCH', ruleIndex: i, ruleId: rule[:id] }, logger)
|
133
186
|
end
|
134
187
|
end
|
135
188
|
|
136
|
-
|
137
|
-
if !
|
138
|
-
|
139
|
-
|
189
|
+
# Check the fallthrough rule
|
190
|
+
if !flag[:fallthrough].nil?
|
191
|
+
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user,
|
192
|
+
{ kind: 'FALLTHROUGH' }, logger)
|
140
193
|
end
|
141
194
|
|
142
|
-
|
195
|
+
return EvaluationDetail.new(nil, nil, { kind: 'FALLTHROUGH' })
|
143
196
|
end
|
144
197
|
|
145
|
-
def
|
146
|
-
failed_prereq = false
|
147
|
-
# Evaluate prerequisites, if any
|
198
|
+
def check_prerequisites(flag, user, store, events, logger)
|
148
199
|
(flag[:prerequisites] || []).each do |prerequisite|
|
149
|
-
|
200
|
+
prereq_ok = true
|
201
|
+
prereq_key = prerequisite[:key]
|
202
|
+
prereq_flag = store.get(FEATURES, prereq_key)
|
150
203
|
|
151
|
-
if prereq_flag.nil?
|
152
|
-
|
204
|
+
if prereq_flag.nil?
|
205
|
+
logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
|
206
|
+
prereq_ok = false
|
153
207
|
else
|
154
208
|
begin
|
155
209
|
prereq_res = eval_internal(prereq_flag, user, store, events, logger)
|
210
|
+
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
211
|
+
# off variation was. But we still need to evaluate it in order to generate an event.
|
212
|
+
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
213
|
+
prereq_ok = false
|
214
|
+
end
|
156
215
|
event = {
|
157
216
|
kind: "feature",
|
158
|
-
key:
|
159
|
-
variation: prereq_res.
|
160
|
-
value: prereq_res.
|
217
|
+
key: prereq_key,
|
218
|
+
variation: prereq_res.variation_index,
|
219
|
+
value: prereq_res.value,
|
161
220
|
version: prereq_flag[:version],
|
162
221
|
prereqOf: flag[:key],
|
163
222
|
trackEvents: prereq_flag[:trackEvents],
|
164
223
|
debugEventsUntilDate: prereq_flag[:debugEventsUntilDate]
|
165
224
|
}
|
166
225
|
events.push(event)
|
167
|
-
if prereq_res.nil? || prereq_res[:variation] != prerequisite[:variation]
|
168
|
-
failed_prereq = true
|
169
|
-
end
|
170
226
|
rescue => exn
|
171
|
-
logger
|
172
|
-
|
227
|
+
Util.log_exception(logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"{flag[:key]}\"", exn)
|
228
|
+
prereq_ok = false
|
173
229
|
end
|
174
230
|
end
|
175
|
-
|
176
|
-
|
177
|
-
if failed_prereq
|
178
|
-
return nil
|
179
|
-
end
|
180
|
-
# The prerequisites were satisfied.
|
181
|
-
# Now walk through the evaluation steps and get the correct
|
182
|
-
# variation index
|
183
|
-
eval_rules(flag, user, store)
|
184
|
-
end
|
185
|
-
|
186
|
-
def eval_rules(flag, user, store)
|
187
|
-
# Check user target matches
|
188
|
-
(flag[:targets] || []).each do |target|
|
189
|
-
(target[:values] || []).each do |value|
|
190
|
-
if value == user[:key]
|
191
|
-
return { variation: target[:variation], value: get_variation(flag, target[:variation]) }
|
192
|
-
end
|
231
|
+
if !prereq_ok
|
232
|
+
return { kind: 'PREREQUISITE_FAILED', prerequisiteKey: prereq_key }
|
193
233
|
end
|
194
234
|
end
|
195
|
-
|
196
|
-
# Check custom rules
|
197
|
-
(flag[:rules] || []).each do |rule|
|
198
|
-
return variation_for_user(rule, user, flag) if rule_match_user(rule, user, store)
|
199
|
-
end
|
200
|
-
|
201
|
-
# Check the fallthrough rule
|
202
|
-
if !flag[:fallthrough].nil?
|
203
|
-
return variation_for_user(flag[:fallthrough], user, flag)
|
204
|
-
end
|
205
|
-
|
206
|
-
# Not even the fallthrough matched-- return the off variation or default
|
207
235
|
nil
|
208
236
|
end
|
209
237
|
|
210
|
-
def get_variation(flag, index)
|
211
|
-
if index >= flag[:variations].length
|
212
|
-
raise EvaluationError, "Invalid variation index"
|
213
|
-
end
|
214
|
-
flag[:variations][index]
|
215
|
-
end
|
216
|
-
|
217
238
|
def rule_match_user(rule, user, store)
|
218
239
|
return false if !rule[:clauses]
|
219
240
|
|
@@ -242,9 +263,8 @@ module LaunchDarkly
|
|
242
263
|
return false if val.nil?
|
243
264
|
|
244
265
|
op = OPERATORS[clause[:op].to_sym]
|
245
|
-
|
246
266
|
if op.nil?
|
247
|
-
|
267
|
+
return false
|
248
268
|
end
|
249
269
|
|
250
270
|
if val.is_a? Enumerable
|
@@ -257,9 +277,9 @@ module LaunchDarkly
|
|
257
277
|
maybe_negate(clause, match_any(op, val, clause[:values]))
|
258
278
|
end
|
259
279
|
|
260
|
-
def
|
280
|
+
def variation_index_for_user(flag, rule, user)
|
261
281
|
if !rule[:variation].nil? # fixed variation
|
262
|
-
return
|
282
|
+
return rule[:variation]
|
263
283
|
elsif !rule[:rollout].nil? # percentage rollout
|
264
284
|
rollout = rule[:rollout]
|
265
285
|
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
@@ -268,12 +288,12 @@ module LaunchDarkly
|
|
268
288
|
rollout[:variations].each do |variate|
|
269
289
|
sum += variate[:weight].to_f / 100000.0
|
270
290
|
if bucket < sum
|
271
|
-
return
|
291
|
+
return variate[:variation]
|
272
292
|
end
|
273
293
|
end
|
274
294
|
nil
|
275
295
|
else # the rule isn't well-formed
|
276
|
-
|
296
|
+
nil
|
277
297
|
end
|
278
298
|
end
|
279
299
|
|
@@ -350,5 +370,31 @@ module LaunchDarkly
|
|
350
370
|
end
|
351
371
|
return false
|
352
372
|
end
|
373
|
+
|
374
|
+
private
|
375
|
+
|
376
|
+
def get_variation(flag, index, reason, logger)
|
377
|
+
if index < 0 || index >= flag[:variations].length
|
378
|
+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
|
379
|
+
return error_result('MALFORMED_FLAG')
|
380
|
+
end
|
381
|
+
EvaluationDetail.new(flag[:variations][index], index, reason)
|
382
|
+
end
|
383
|
+
|
384
|
+
def get_off_value(flag, reason, logger)
|
385
|
+
if flag[:offVariation].nil? # off variation unspecified - return default value
|
386
|
+
return EvaluationDetail.new(nil, nil, reason)
|
387
|
+
end
|
388
|
+
get_variation(flag, flag[:offVariation], reason, logger)
|
389
|
+
end
|
390
|
+
|
391
|
+
def get_value_for_variation_or_rollout(flag, vr, user, reason, logger)
|
392
|
+
index = variation_index_for_user(flag, vr, user)
|
393
|
+
if index.nil?
|
394
|
+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
395
|
+
return error_result('MALFORMED_FLAG')
|
396
|
+
end
|
397
|
+
return get_variation(flag, index, reason, logger)
|
398
|
+
end
|
353
399
|
end
|
354
400
|
end
|
data/lib/ldclient-rb/events.rb
CHANGED
@@ -15,12 +15,13 @@ module LaunchDarkly
|
|
15
15
|
end
|
16
16
|
|
17
17
|
# Used internally to build the state map.
|
18
|
-
def add_flag(flag, value, variation)
|
18
|
+
def add_flag(flag, value, variation, reason = nil)
|
19
19
|
key = flag[:key]
|
20
20
|
@flag_values[key] = value
|
21
21
|
meta = { version: flag[:version], trackEvents: flag[:trackEvents] }
|
22
22
|
meta[:variation] = variation if !variation.nil?
|
23
23
|
meta[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
|
24
|
+
meta[:reason] = reason if !reason.nil?
|
24
25
|
@flag_metadata[key] = meta
|
25
26
|
end
|
26
27
|
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -115,57 +115,54 @@ module LaunchDarkly
|
|
115
115
|
# @param key [String] the unique feature key for the feature flag, as shown
|
116
116
|
# on the LaunchDarkly dashboard
|
117
117
|
# @param user [Hash] a hash containing parameters for the end user requesting the flag
|
118
|
-
# @param default
|
118
|
+
# @param default the default value of the flag
|
119
119
|
#
|
120
120
|
# @return the variation to show the user, or the
|
121
121
|
# default value if there's an an error
|
122
122
|
def variation(key, user, default)
|
123
|
-
|
124
|
-
|
125
|
-
if !initialized?
|
126
|
-
if @store.initialized?
|
127
|
-
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
128
|
-
else
|
129
|
-
@config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
|
130
|
-
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
|
131
|
-
return default
|
132
|
-
end
|
133
|
-
end
|
134
|
-
|
135
|
-
sanitize_user(user) if !user.nil?
|
136
|
-
feature = @store.get(FEATURES, key)
|
137
|
-
|
138
|
-
if feature.nil?
|
139
|
-
@config.logger.info { "[LDClient] Unknown feature flag #{key}. Returning default value" }
|
140
|
-
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
|
141
|
-
return default
|
142
|
-
end
|
143
|
-
|
144
|
-
unless user
|
145
|
-
@config.logger.error { "[LDClient] Must specify user" }
|
146
|
-
@event_processor.add_event(make_feature_event(feature, user, nil, default, default))
|
147
|
-
return default
|
148
|
-
end
|
123
|
+
evaluate_internal(key, user, default, false).value
|
124
|
+
end
|
149
125
|
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
126
|
+
#
|
127
|
+
# Determines the variation of a feature flag for a user, like `variation`, but also
|
128
|
+
# provides additional information about how this value was calculated.
|
129
|
+
#
|
130
|
+
# The return value of `variation_detail` is an `EvaluationDetail` object, which has
|
131
|
+
# three properties:
|
132
|
+
#
|
133
|
+
# `value`: the value that was calculated for this user (same as the return value
|
134
|
+
# of `variation`)
|
135
|
+
#
|
136
|
+
# `variation_index`: the positional index of this value in the flag, e.g. 0 for the
|
137
|
+
# first variation - or `nil` if the default value was returned
|
138
|
+
#
|
139
|
+
# `reason`: a hash describing the main reason why this value was selected. Its `:kind`
|
140
|
+
# property will be one of the following:
|
141
|
+
#
|
142
|
+
# * `'OFF'`: the flag was off and therefore returned its configured off value
|
143
|
+
# * `'FALLTHROUGH'`: the flag was on but the user did not match any targets or rules
|
144
|
+
# * `'TARGET_MATCH'`: the user key was specifically targeted for this flag
|
145
|
+
# * `'RULE_MATCH'`: the user matched one of the flag's rules; the `:ruleIndex` and
|
146
|
+
# `:ruleId` properties indicate the positional index and unique identifier of the rule
|
147
|
+
# * `'PREREQUISITE_FAILED`': the flag was considered off because it had at least one
|
148
|
+
# prerequisite flag that either was off or did not return the desired variation; the
|
149
|
+
# `:prerequisiteKey` property indicates the key of the prerequisite that failed
|
150
|
+
# * `'ERROR'`: the flag could not be evaluated, e.g. because it does not exist or due
|
151
|
+
# to an unexpected error, and therefore returned the default value; the `:errorKind`
|
152
|
+
# property describes the nature of the error, such as `'FLAG_NOT_FOUND'`
|
153
|
+
#
|
154
|
+
# The `reason` will also be included in analytics events, if you are capturing
|
155
|
+
# detailed event data for this flag.
|
156
|
+
#
|
157
|
+
# @param key [String] the unique feature key for the feature flag, as shown
|
158
|
+
# on the LaunchDarkly dashboard
|
159
|
+
# @param user [Hash] a hash containing parameters for the end user requesting the flag
|
160
|
+
# @param default the default value of the flag
|
161
|
+
#
|
162
|
+
# @return an `EvaluationDetail` object describing the result
|
163
|
+
#
|
164
|
+
def variation_detail(key, user, default)
|
165
|
+
evaluate_internal(key, user, default, true)
|
169
166
|
end
|
170
167
|
|
171
168
|
#
|
@@ -213,6 +210,8 @@ module LaunchDarkly
|
|
213
210
|
# @param options={} [Hash] Optional parameters to control how the state is generated
|
214
211
|
# @option options [Boolean] :client_side_only (false) True if only flags marked for use with the
|
215
212
|
# client-side SDK should be included in the state. By default, all flags are included.
|
213
|
+
# @option options [Boolean] :with_reasons (false) True if evaluation reasons should be included
|
214
|
+
# in the state (see `variation_detail`). By default, they are not included.
|
216
215
|
# @return [FeatureFlagsState] a FeatureFlagsState object which can be serialized to JSON
|
217
216
|
#
|
218
217
|
def all_flags_state(user, options={})
|
@@ -234,16 +233,17 @@ module LaunchDarkly
|
|
234
233
|
|
235
234
|
state = FeatureFlagsState.new(true)
|
236
235
|
client_only = options[:client_side_only] || false
|
236
|
+
with_reasons = options[:with_reasons] || false
|
237
237
|
features.each do |k, f|
|
238
238
|
if client_only && !f[:clientSide]
|
239
239
|
next
|
240
240
|
end
|
241
241
|
begin
|
242
242
|
result = evaluate(f, user, @store, @config.logger)
|
243
|
-
state.add_flag(f, result
|
243
|
+
state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil)
|
244
244
|
rescue => exn
|
245
245
|
Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
|
246
|
-
state.add_flag(f, nil, nil)
|
246
|
+
state.add_flag(f, nil, nil, with_reasons ? { kind: 'ERROR', errorKind: 'EXCEPTION' } : nil)
|
247
247
|
end
|
248
248
|
end
|
249
249
|
|
@@ -261,27 +261,83 @@ module LaunchDarkly
|
|
261
261
|
@store.stop
|
262
262
|
end
|
263
263
|
|
264
|
+
private
|
265
|
+
|
266
|
+
# @return [EvaluationDetail]
|
267
|
+
def evaluate_internal(key, user, default, include_reasons_in_events)
|
268
|
+
if @config.offline?
|
269
|
+
return error_result('CLIENT_NOT_READY', default)
|
270
|
+
end
|
271
|
+
|
272
|
+
if !initialized?
|
273
|
+
if @store.initialized?
|
274
|
+
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
275
|
+
else
|
276
|
+
@config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
|
277
|
+
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
|
278
|
+
return error_result('CLIENT_NOT_READY', default)
|
279
|
+
end
|
280
|
+
end
|
281
|
+
|
282
|
+
sanitize_user(user) if !user.nil?
|
283
|
+
feature = @store.get(FEATURES, key)
|
284
|
+
|
285
|
+
if feature.nil?
|
286
|
+
@config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
|
287
|
+
detail = error_result('FLAG_NOT_FOUND', default)
|
288
|
+
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user,
|
289
|
+
reason: include_reasons_in_events ? detail.reason : nil)
|
290
|
+
return detail
|
291
|
+
end
|
292
|
+
|
293
|
+
unless user
|
294
|
+
@config.logger.error { "[LDClient] Must specify user" }
|
295
|
+
detail = error_result('USER_NOT_SPECIFIED', default)
|
296
|
+
@event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
|
297
|
+
return detail
|
298
|
+
end
|
299
|
+
|
300
|
+
begin
|
301
|
+
res = evaluate(feature, user, @store, @config.logger)
|
302
|
+
if !res.events.nil?
|
303
|
+
res.events.each do |event|
|
304
|
+
@event_processor.add_event(event)
|
305
|
+
end
|
306
|
+
end
|
307
|
+
detail = res.detail
|
308
|
+
if detail.default_value?
|
309
|
+
detail = EvaluationDetail.new(default, nil, detail.reason)
|
310
|
+
end
|
311
|
+
@event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
|
312
|
+
return detail
|
313
|
+
rescue => exn
|
314
|
+
Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
|
315
|
+
detail = error_result('EXCEPTION', default)
|
316
|
+
@event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
|
317
|
+
return detail
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
264
321
|
def sanitize_user(user)
|
265
322
|
if user[:key]
|
266
323
|
user[:key] = user[:key].to_s
|
267
324
|
end
|
268
325
|
end
|
269
326
|
|
270
|
-
def make_feature_event(flag, user,
|
327
|
+
def make_feature_event(flag, user, detail, default, with_reasons)
|
271
328
|
{
|
272
329
|
kind: "feature",
|
273
330
|
key: flag[:key],
|
274
331
|
user: user,
|
275
|
-
variation:
|
276
|
-
value: value,
|
332
|
+
variation: detail.variation_index,
|
333
|
+
value: detail.value,
|
277
334
|
default: default,
|
278
335
|
version: flag[:version],
|
279
336
|
trackEvents: flag[:trackEvents],
|
280
|
-
debugEventsUntilDate: flag[:debugEventsUntilDate]
|
337
|
+
debugEventsUntilDate: flag[:debugEventsUntilDate],
|
338
|
+
reason: with_reasons ? detail.reason : nil
|
281
339
|
}
|
282
340
|
end
|
283
|
-
|
284
|
-
private :evaluate, :sanitize_user, :make_feature_event
|
285
341
|
end
|
286
342
|
|
287
343
|
#
|