launchdarkly-server-sdk 6.1.1 → 6.4.0
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/README.md +4 -5
- data/lib/ldclient-rb/config.rb +118 -4
- data/lib/ldclient-rb/evaluation_detail.rb +104 -14
- data/lib/ldclient-rb/events.rb +201 -107
- data/lib/ldclient-rb/file_data_source.rb +9 -300
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +116 -62
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +22 -9
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +53 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
- data/lib/ldclient-rb/impl/event_summarizer.rb +63 -0
- data/lib/ldclient-rb/impl/event_types.rb +90 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +177 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +7 -37
- data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
- data/lib/ldclient-rb/impl/util.rb +62 -1
- data/lib/ldclient-rb/integrations/consul.rb +8 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +48 -3
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +42 -2
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
- data/lib/ldclient-rb/integrations/test_data.rb +209 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
- data/lib/ldclient-rb/integrations.rb +2 -51
- data/lib/ldclient-rb/interfaces.rb +152 -2
- data/lib/ldclient-rb/ldclient.rb +131 -33
- data/lib/ldclient-rb/polling.rb +22 -41
- data/lib/ldclient-rb/requestor.rb +3 -3
- data/lib/ldclient-rb/stream.rb +4 -3
- data/lib/ldclient-rb/util.rb +10 -1
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +0 -1
- metadata +35 -132
- data/.circleci/config.yml +0 -40
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
- data/.github/ISSUE_TEMPLATE/config.yml +0 -5
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- data/.github/pull_request_template.md +0 -21
- data/.gitignore +0 -16
- data/.hound.yml +0 -2
- data/.ldrelease/build-docs.sh +0 -18
- data/.ldrelease/circleci/linux/execute.sh +0 -18
- data/.ldrelease/circleci/mac/execute.sh +0 -18
- data/.ldrelease/circleci/template/build.sh +0 -29
- data/.ldrelease/circleci/template/publish.sh +0 -23
- data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
- data/.ldrelease/circleci/template/test.sh +0 -10
- data/.ldrelease/circleci/template/update-version.sh +0 -8
- data/.ldrelease/circleci/windows/execute.ps1 +0 -19
- data/.ldrelease/config.yml +0 -29
- data/.rspec +0 -2
- data/.rubocop.yml +0 -600
- data/.simplecov +0 -4
- data/CHANGELOG.md +0 -351
- data/CODEOWNERS +0 -1
- data/CONTRIBUTING.md +0 -37
- data/Gemfile +0 -3
- data/azure-pipelines.yml +0 -51
- data/docs/Makefile +0 -26
- data/docs/index.md +0 -9
- data/launchdarkly-server-sdk.gemspec +0 -45
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/impl/event_factory.rb +0 -120
- data/spec/config_spec.rb +0 -63
- data/spec/diagnostic_events_spec.rb +0 -163
- data/spec/evaluation_detail_spec.rb +0 -135
- data/spec/event_sender_spec.rb +0 -197
- data/spec/event_summarizer_spec.rb +0 -63
- data/spec/events_spec.rb +0 -607
- data/spec/expiring_cache_spec.rb +0 -76
- data/spec/feature_store_spec_base.rb +0 -213
- data/spec/file_data_source_spec.rb +0 -283
- data/spec/fixtures/feature.json +0 -37
- data/spec/fixtures/feature1.json +0 -36
- data/spec/fixtures/user.json +0 -9
- data/spec/flags_state_spec.rb +0 -81
- data/spec/http_util.rb +0 -132
- data/spec/impl/evaluator_bucketing_spec.rb +0 -111
- data/spec/impl/evaluator_clause_spec.rb +0 -55
- data/spec/impl/evaluator_operators_spec.rb +0 -141
- data/spec/impl/evaluator_rule_spec.rb +0 -96
- data/spec/impl/evaluator_segment_spec.rb +0 -125
- data/spec/impl/evaluator_spec.rb +0 -305
- data/spec/impl/evaluator_spec_base.rb +0 -75
- data/spec/impl/model/serialization_spec.rb +0 -41
- data/spec/in_memory_feature_store_spec.rb +0 -12
- data/spec/integrations/consul_feature_store_spec.rb +0 -40
- data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
- data/spec/integrations/store_wrapper_spec.rb +0 -276
- data/spec/launchdarkly-server-sdk_spec.rb +0 -13
- data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
- data/spec/ldclient_end_to_end_spec.rb +0 -157
- data/spec/ldclient_spec.rb +0 -643
- data/spec/newrelic_spec.rb +0 -5
- data/spec/polling_spec.rb +0 -120
- data/spec/redis_feature_store_spec.rb +0 -121
- data/spec/requestor_spec.rb +0 -196
- data/spec/segment_store_spec_base.rb +0 -95
- data/spec/simple_lru_cache_spec.rb +0 -24
- data/spec/spec_helper.rb +0 -9
- data/spec/store_spec.rb +0 -10
- data/spec/stream_spec.rb +0 -45
- data/spec/user_filter_spec.rb +0 -91
- data/spec/util_spec.rb +0 -17
- data/spec/version_spec.rb +0 -7
@@ -1,9 +1,17 @@
|
|
1
1
|
require "ldclient-rb/evaluation_detail"
|
2
2
|
require "ldclient-rb/impl/evaluator_bucketing"
|
3
|
+
require "ldclient-rb/impl/evaluator_helpers"
|
3
4
|
require "ldclient-rb/impl/evaluator_operators"
|
4
5
|
|
5
6
|
module LaunchDarkly
|
6
7
|
module Impl
|
8
|
+
# Used internally to record that we evaluated a prerequisite flag.
|
9
|
+
PrerequisiteEvalRecord = Struct.new(
|
10
|
+
:prereq_flag, # the prerequisite flag that we evaluated
|
11
|
+
:prereq_of_flag, # the flag that it was a prerequisite of
|
12
|
+
:detail # the EvaluationDetail representing the evaluation result
|
13
|
+
)
|
14
|
+
|
7
15
|
# Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
|
8
16
|
# if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
|
9
17
|
# is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
|
@@ -16,16 +24,28 @@ module LaunchDarkly
|
|
16
24
|
# flag data - or nil if the flag is unknown or deleted
|
17
25
|
# @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
|
18
26
|
# @param logger [Logger] the client's logger
|
19
|
-
def initialize(get_flag, get_segment, logger)
|
27
|
+
def initialize(get_flag, get_segment, get_big_segments_membership, logger)
|
20
28
|
@get_flag = get_flag
|
21
29
|
@get_segment = get_segment
|
30
|
+
@get_big_segments_membership = get_big_segments_membership
|
22
31
|
@logger = logger
|
23
32
|
end
|
24
|
-
|
25
|
-
# Used internally to hold an evaluation result and
|
26
|
-
#
|
27
|
-
#
|
28
|
-
|
33
|
+
|
34
|
+
# Used internally to hold an evaluation result and additional state that may be accumulated during an
|
35
|
+
# evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
|
36
|
+
# trying to use a pure functional approach, and since we're not exposing this object to any application code
|
37
|
+
# or retaining it anywhere, we don't have to be quite as strict about immutability.
|
38
|
+
#
|
39
|
+
# The big_segments_status and big_segments_membership properties are not used by the caller; they are used
|
40
|
+
# during an evaluation to cache the result of any Big Segments query that we've done for this user, because
|
41
|
+
# we don't want to do multiple queries for the same user if multiple Big Segments are referenced in the same
|
42
|
+
# evaluation.
|
43
|
+
EvalResult = Struct.new(
|
44
|
+
:detail, # the EvaluationDetail representing the evaluation result
|
45
|
+
:prereq_evals, # an array of PrerequisiteEvalRecord instances, or nil
|
46
|
+
:big_segments_status,
|
47
|
+
:big_segments_membership
|
48
|
+
)
|
29
49
|
|
30
50
|
# Helper function used internally to construct an EvaluationDetail for an error result.
|
31
51
|
def self.error_result(errorKind, value = nil)
|
@@ -38,43 +58,47 @@ module LaunchDarkly
|
|
38
58
|
#
|
39
59
|
# @param flag [Object] the flag
|
40
60
|
# @param user [Object] the user properties
|
41
|
-
# @param event_factory [EventFactory] called to construct a feature request event when a prerequisite flag is
|
42
|
-
# evaluated; the caller is responsible for constructing the feature event for the top-level evaluation
|
43
61
|
# @return [EvalResult] the evaluation result
|
44
|
-
def evaluate(flag, user
|
62
|
+
def evaluate(flag, user)
|
63
|
+
result = EvalResult.new
|
45
64
|
if user.nil? || user[:key].nil?
|
46
|
-
|
65
|
+
result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED)
|
66
|
+
return result
|
47
67
|
end
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
68
|
+
|
69
|
+
detail = eval_internal(flag, user, result)
|
70
|
+
if !result.big_segments_status.nil?
|
71
|
+
# If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
|
72
|
+
# some point and we will want to include the status in the evaluation reason.
|
73
|
+
detail = EvaluationDetail.new(detail.value, detail.variation_index,
|
74
|
+
detail.reason.with_big_segments_status(result.big_segments_status))
|
55
75
|
end
|
76
|
+
result.detail = detail
|
77
|
+
return result
|
78
|
+
end
|
56
79
|
|
57
|
-
|
58
|
-
|
80
|
+
def self.make_big_segment_ref(segment) # method is visible for testing
|
81
|
+
# The format of Big Segment references is independent of what store implementation is being
|
82
|
+
# used; the store implementation receives only this string and does not know the details of
|
83
|
+
# the data model. The Relay Proxy will use the same format when writing to the store.
|
84
|
+
"#{segment[:key]}.g#{segment[:generation]}"
|
59
85
|
end
|
60
86
|
|
61
87
|
private
|
62
88
|
|
63
|
-
def eval_internal(flag, user,
|
89
|
+
def eval_internal(flag, user, state)
|
64
90
|
if !flag[:on]
|
65
|
-
return
|
91
|
+
return EvaluatorHelpers.off_result(flag)
|
66
92
|
end
|
67
93
|
|
68
|
-
|
69
|
-
if !
|
70
|
-
return get_off_value(flag, prereq_failure_reason)
|
71
|
-
end
|
94
|
+
prereq_failure_result = check_prerequisites(flag, user, state)
|
95
|
+
return prereq_failure_result if !prereq_failure_result.nil?
|
72
96
|
|
73
97
|
# Check user target matches
|
74
98
|
(flag[:targets] || []).each do |target|
|
75
99
|
(target[:values] || []).each do |value|
|
76
100
|
if value == user[:key]
|
77
|
-
return
|
101
|
+
return EvaluatorHelpers.target_match_result(target, flag)
|
78
102
|
end
|
79
103
|
end
|
80
104
|
end
|
@@ -83,22 +107,24 @@ module LaunchDarkly
|
|
83
107
|
rules = flag[:rules] || []
|
84
108
|
rules.each_index do |i|
|
85
109
|
rule = rules[i]
|
86
|
-
if rule_match_user(rule, user)
|
110
|
+
if rule_match_user(rule, user, state)
|
87
111
|
reason = rule[:_reason] # try to use cached reason for this rule
|
88
112
|
reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
|
89
|
-
return get_value_for_variation_or_rollout(flag, rule, user, reason
|
113
|
+
return get_value_for_variation_or_rollout(flag, rule, user, reason,
|
114
|
+
EvaluatorHelpers.rule_precomputed_results(rule))
|
90
115
|
end
|
91
116
|
end
|
92
117
|
|
93
118
|
# Check the fallthrough rule
|
94
119
|
if !flag[:fallthrough].nil?
|
95
|
-
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough
|
120
|
+
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough,
|
121
|
+
EvaluatorHelpers.fallthrough_precomputed_results(flag))
|
96
122
|
end
|
97
123
|
|
98
124
|
return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
|
99
125
|
end
|
100
126
|
|
101
|
-
def check_prerequisites(flag, user,
|
127
|
+
def check_prerequisites(flag, user, state)
|
102
128
|
(flag[:prerequisites] || []).each do |prerequisite|
|
103
129
|
prereq_ok = true
|
104
130
|
prereq_key = prerequisite[:key]
|
@@ -109,44 +135,44 @@ module LaunchDarkly
|
|
109
135
|
prereq_ok = false
|
110
136
|
else
|
111
137
|
begin
|
112
|
-
prereq_res = eval_internal(prereq_flag, user,
|
138
|
+
prereq_res = eval_internal(prereq_flag, user, state)
|
113
139
|
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
114
140
|
# off variation was. But we still need to evaluate it in order to generate an event.
|
115
141
|
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
116
142
|
prereq_ok = false
|
117
143
|
end
|
118
|
-
|
119
|
-
|
144
|
+
prereq_eval = PrerequisiteEvalRecord.new(prereq_flag, flag, prereq_res)
|
145
|
+
state.prereq_evals = [] if state.prereq_evals.nil?
|
146
|
+
state.prereq_evals.push(prereq_eval)
|
120
147
|
rescue => exn
|
121
148
|
Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
122
149
|
prereq_ok = false
|
123
150
|
end
|
124
151
|
end
|
125
152
|
if !prereq_ok
|
126
|
-
|
127
|
-
return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
|
153
|
+
return EvaluatorHelpers.prerequisite_failed_result(prerequisite, flag)
|
128
154
|
end
|
129
155
|
end
|
130
156
|
nil
|
131
157
|
end
|
132
158
|
|
133
|
-
def rule_match_user(rule, user)
|
159
|
+
def rule_match_user(rule, user, state)
|
134
160
|
return false if !rule[:clauses]
|
135
161
|
|
136
162
|
(rule[:clauses] || []).each do |clause|
|
137
|
-
return false if !clause_match_user(clause, user)
|
163
|
+
return false if !clause_match_user(clause, user, state)
|
138
164
|
end
|
139
165
|
|
140
166
|
return true
|
141
167
|
end
|
142
168
|
|
143
|
-
def clause_match_user(clause, user)
|
169
|
+
def clause_match_user(clause, user, state)
|
144
170
|
# In the case of a segment match operator, we check if the user is in any of the segments,
|
145
171
|
# and possibly negate
|
146
172
|
if clause[:op].to_sym == :segmentMatch
|
147
173
|
result = (clause[:values] || []).any? { |v|
|
148
174
|
segment = @get_segment.call(v)
|
149
|
-
!segment.nil? && segment_match_user(segment, user)
|
175
|
+
!segment.nil? && segment_match_user(segment, user, state)
|
150
176
|
}
|
151
177
|
clause[:negate] ? !result : result
|
152
178
|
else
|
@@ -168,11 +194,42 @@ module LaunchDarkly
|
|
168
194
|
clause[:negate] ? !result : result
|
169
195
|
end
|
170
196
|
|
171
|
-
def segment_match_user(segment, user)
|
197
|
+
def segment_match_user(segment, user, state)
|
172
198
|
return false unless user[:key]
|
199
|
+
segment[:unbounded] ? big_segment_match_user(segment, user, state) : simple_segment_match_user(segment, user, true)
|
200
|
+
end
|
173
201
|
|
174
|
-
|
175
|
-
|
202
|
+
def big_segment_match_user(segment, user, state)
|
203
|
+
if !segment[:generation]
|
204
|
+
# Big segment queries can only be done if the generation is known. If it's unset,
|
205
|
+
# that probably means the data store was populated by an older SDK that doesn't know
|
206
|
+
# about the generation property and therefore dropped it from the JSON data. We'll treat
|
207
|
+
# that as a "not configured" condition.
|
208
|
+
state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
|
209
|
+
return false
|
210
|
+
end
|
211
|
+
if !state.big_segments_status
|
212
|
+
result = @get_big_segments_membership.nil? ? nil : @get_big_segments_membership.call(user[:key])
|
213
|
+
if result
|
214
|
+
state.big_segments_membership = result.membership
|
215
|
+
state.big_segments_status = result.status
|
216
|
+
else
|
217
|
+
state.big_segments_membership = nil
|
218
|
+
state.big_segments_status = BigSegmentsStatus::NOT_CONFIGURED
|
219
|
+
end
|
220
|
+
end
|
221
|
+
segment_ref = Evaluator.make_big_segment_ref(segment)
|
222
|
+
membership = state.big_segments_membership
|
223
|
+
included = membership.nil? ? nil : membership[segment_ref]
|
224
|
+
return included if !included.nil?
|
225
|
+
simple_segment_match_user(segment, user, false)
|
226
|
+
end
|
227
|
+
|
228
|
+
def simple_segment_match_user(segment, user, use_includes_and_excludes)
|
229
|
+
if use_includes_and_excludes
|
230
|
+
return true if segment[:included].include?(user[:key])
|
231
|
+
return false if segment[:excluded].include?(user[:key])
|
232
|
+
end
|
176
233
|
|
177
234
|
(segment[:rules] || []).each do |r|
|
178
235
|
return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
|
@@ -190,35 +247,32 @@ module LaunchDarkly
|
|
190
247
|
return true if !rule[:weight]
|
191
248
|
|
192
249
|
# All of the clauses are met. See if the user buckets in
|
193
|
-
bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt)
|
250
|
+
bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
|
194
251
|
weight = rule[:weight].to_f / 100000.0
|
195
252
|
return bucket < weight
|
196
253
|
end
|
197
254
|
|
198
255
|
private
|
199
|
-
|
200
|
-
def
|
201
|
-
|
202
|
-
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
|
203
|
-
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
204
|
-
end
|
205
|
-
EvaluationDetail.new(flag[:variations][index], index, reason)
|
206
|
-
end
|
207
|
-
|
208
|
-
def get_off_value(flag, reason)
|
209
|
-
if flag[:offVariation].nil? # off variation unspecified - return default value
|
210
|
-
return EvaluationDetail.new(nil, nil, reason)
|
211
|
-
end
|
212
|
-
get_variation(flag, flag[:offVariation], reason)
|
213
|
-
end
|
214
|
-
|
215
|
-
def get_value_for_variation_or_rollout(flag, vr, user, reason)
|
216
|
-
index = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
256
|
+
|
257
|
+
def get_value_for_variation_or_rollout(flag, vr, user, reason, precomputed_results)
|
258
|
+
index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
217
259
|
if index.nil?
|
218
260
|
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
219
261
|
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
220
262
|
end
|
221
|
-
|
263
|
+
if precomputed_results
|
264
|
+
return precomputed_results.for_variation(index, in_experiment)
|
265
|
+
else
|
266
|
+
#if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
|
267
|
+
if in_experiment
|
268
|
+
if reason.kind == :FALLTHROUGH
|
269
|
+
reason = EvaluationReason::fallthrough(in_experiment)
|
270
|
+
elsif reason.kind == :RULE_MATCH
|
271
|
+
reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
|
272
|
+
end
|
273
|
+
end
|
274
|
+
return EvaluatorHelpers.evaluation_detail_for_variation(flag, index, reason)
|
275
|
+
end
|
222
276
|
end
|
223
277
|
end
|
224
278
|
end
|
@@ -10,20 +10,26 @@ module LaunchDarkly
|
|
10
10
|
# @param user [Object] the user properties
|
11
11
|
# @return [Number] the variation index, or nil if there is an error
|
12
12
|
def self.variation_index_for_user(flag, rule, user)
|
13
|
+
|
13
14
|
variation = rule[:variation]
|
14
|
-
return variation if !variation.nil? # fixed variation
|
15
|
+
return variation, false if !variation.nil? # fixed variation
|
15
16
|
rollout = rule[:rollout]
|
16
|
-
return nil if rollout.nil?
|
17
|
+
return nil, false if rollout.nil?
|
17
18
|
variations = rollout[:variations]
|
18
19
|
if !variations.nil? && variations.length > 0 # percentage rollout
|
19
|
-
rollout = rule[:rollout]
|
20
20
|
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
|
21
|
-
|
21
|
+
|
22
|
+
seed = rollout[:seed]
|
23
|
+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt], seed) # may not be present
|
22
24
|
sum = 0;
|
23
25
|
variations.each do |variate|
|
26
|
+
if rollout[:kind] == "experiment" && !variate[:untracked]
|
27
|
+
in_experiment = true
|
28
|
+
end
|
29
|
+
|
24
30
|
sum += variate[:weight].to_f / 100000.0
|
25
31
|
if bucket < sum
|
26
|
-
return variate[:variation]
|
32
|
+
return variate[:variation], !!in_experiment
|
27
33
|
end
|
28
34
|
end
|
29
35
|
# The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
|
@@ -31,9 +37,12 @@ module LaunchDarkly
|
|
31
37
|
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
|
32
38
|
# this case (or changing the scaling, which would potentially change the results for *all* users), we
|
33
39
|
# will simply put the user in the last bucket.
|
34
|
-
variations[-1]
|
40
|
+
last_variation = variations[-1]
|
41
|
+
in_experiment = rollout[:kind] == "experiment" && !last_variation[:untracked]
|
42
|
+
|
43
|
+
[last_variation[:variation], in_experiment]
|
35
44
|
else # the rule isn't well-formed
|
36
|
-
nil
|
45
|
+
[nil, false]
|
37
46
|
end
|
38
47
|
end
|
39
48
|
|
@@ -44,7 +53,7 @@ module LaunchDarkly
|
|
44
53
|
# @param bucket_by [String|Symbol] the name of the user attribute to be used for bucketing
|
45
54
|
# @param salt [String] the feature flag's or segment's salt value
|
46
55
|
# @return [Number] the bucket value, from 0 inclusive to 1 exclusive
|
47
|
-
def self.bucket_user(user, key, bucket_by, salt)
|
56
|
+
def self.bucket_user(user, key, bucket_by, salt, seed)
|
48
57
|
return nil unless user[:key]
|
49
58
|
|
50
59
|
id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
|
@@ -56,7 +65,11 @@ module LaunchDarkly
|
|
56
65
|
id_hash += "." + user[:secondary].to_s
|
57
66
|
end
|
58
67
|
|
59
|
-
|
68
|
+
if seed
|
69
|
+
hash_key = "%d.%s" % [seed, id_hash]
|
70
|
+
else
|
71
|
+
hash_key = "%s.%s.%s" % [key, salt, id_hash]
|
72
|
+
end
|
60
73
|
|
61
74
|
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
|
62
75
|
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "ldclient-rb/evaluation_detail"
|
2
|
+
|
3
|
+
# This file contains any pieces of low-level evaluation logic that don't need to be inside the Evaluator
|
4
|
+
# class, because they don't depend on any SDK state outside of their input parameters.
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
module Impl
|
8
|
+
module EvaluatorHelpers
|
9
|
+
def self.off_result(flag, logger = nil)
|
10
|
+
pre = flag[:_preprocessed]
|
11
|
+
pre ? pre.off_result : evaluation_detail_for_off_variation(flag, EvaluationReason::off, logger)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.target_match_result(target, flag, logger = nil)
|
15
|
+
pre = target[:_preprocessed]
|
16
|
+
pre ? pre.match_result : evaluation_detail_for_variation(
|
17
|
+
flag, target[:variation], EvaluationReason::target_match, logger)
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.prerequisite_failed_result(prereq, flag, logger = nil)
|
21
|
+
pre = prereq[:_preprocessed]
|
22
|
+
pre ? pre.failed_result : evaluation_detail_for_off_variation(
|
23
|
+
flag, EvaluationReason::prerequisite_failed(prereq[:key]), logger
|
24
|
+
)
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.fallthrough_precomputed_results(flag)
|
28
|
+
pre = flag[:_preprocessed]
|
29
|
+
pre ? pre.fallthrough_factory : nil
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.rule_precomputed_results(rule)
|
33
|
+
pre = rule[:_preprocessed]
|
34
|
+
pre ? pre.all_match_results : nil
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.evaluation_detail_for_off_variation(flag, reason, logger = nil)
|
38
|
+
index = flag[:offVariation]
|
39
|
+
index.nil? ? EvaluationDetail.new(nil, nil, reason) : evaluation_detail_for_variation(flag, index, reason, logger)
|
40
|
+
end
|
41
|
+
|
42
|
+
def self.evaluation_detail_for_variation(flag, index, reason, logger = nil)
|
43
|
+
vars = flag[:variations] || []
|
44
|
+
if index < 0 || index >= vars.length
|
45
|
+
logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index") unless logger.nil?
|
46
|
+
EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
47
|
+
else
|
48
|
+
EvaluationDetail.new(vars[index], index, reason)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -89,7 +89,7 @@ module LaunchDarkly
|
|
89
89
|
|
90
90
|
private
|
91
91
|
|
92
|
-
BUILTINS = Set[:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
92
|
+
BUILTINS = Set[:key, :secondary, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
|
93
93
|
NUMERIC_VERSION_COMPONENTS_REGEX = Regexp.new("^[0-9.]*")
|
94
94
|
|
95
95
|
private_constant :BUILTINS
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require "ldclient-rb/impl/event_types"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
EventSummary = Struct.new(:start_date, :end_date, :counters)
|
6
|
+
|
7
|
+
EventSummaryFlagInfo = Struct.new(:default, :versions)
|
8
|
+
|
9
|
+
EventSummaryFlagVariationCounter = Struct.new(:value, :count)
|
10
|
+
|
11
|
+
# Manages the state of summarizable information for the EventProcessor, including the
|
12
|
+
# event counters and user deduplication. Note that the methods of this class are
|
13
|
+
# deliberately not thread-safe; the EventProcessor is responsible for enforcing
|
14
|
+
# synchronization across both the summarizer and the event queue.
|
15
|
+
class EventSummarizer
|
16
|
+
class Counter
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
clear
|
21
|
+
end
|
22
|
+
|
23
|
+
# Adds this event to our counters, if it is a type of event we need to count.
|
24
|
+
def summarize_event(event)
|
25
|
+
return if !event.is_a?(LaunchDarkly::Impl::EvalEvent)
|
26
|
+
|
27
|
+
counters_for_flag = @counters[event.key]
|
28
|
+
if counters_for_flag.nil?
|
29
|
+
counters_for_flag = EventSummaryFlagInfo.new(event.default, Hash.new)
|
30
|
+
@counters[event.key] = counters_for_flag
|
31
|
+
end
|
32
|
+
counters_for_flag_version = counters_for_flag.versions[event.version]
|
33
|
+
if counters_for_flag_version.nil?
|
34
|
+
counters_for_flag_version = Hash.new
|
35
|
+
counters_for_flag.versions[event.version] = counters_for_flag_version
|
36
|
+
end
|
37
|
+
variation_counter = counters_for_flag_version[event.variation]
|
38
|
+
if variation_counter.nil?
|
39
|
+
counters_for_flag_version[event.variation] = EventSummaryFlagVariationCounter.new(event.value, 1)
|
40
|
+
else
|
41
|
+
variation_counter.count = variation_counter.count + 1
|
42
|
+
end
|
43
|
+
time = event.timestamp
|
44
|
+
if !time.nil?
|
45
|
+
@start_date = time if @start_date == 0 || time < @start_date
|
46
|
+
@end_date = time if time > @end_date
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Returns a snapshot of the current summarized event data, and resets this state.
|
51
|
+
def snapshot
|
52
|
+
ret = EventSummary.new(@start_date, @end_date, @counters)
|
53
|
+
ret
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear
|
57
|
+
@start_date = 0
|
58
|
+
@end_date = 0
|
59
|
+
@counters = {}
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
module LaunchDarkly
|
2
|
+
module Impl
|
3
|
+
class Event
|
4
|
+
def initialize(timestamp, user)
|
5
|
+
@timestamp = timestamp
|
6
|
+
@user = user
|
7
|
+
end
|
8
|
+
|
9
|
+
attr_reader :timestamp
|
10
|
+
attr_reader :kind
|
11
|
+
attr_reader :user
|
12
|
+
end
|
13
|
+
|
14
|
+
class EvalEvent < Event
|
15
|
+
def initialize(timestamp, user, key, version = nil, variation = nil, value = nil, reason = nil, default = nil,
|
16
|
+
track_events = false, debug_until = nil, prereq_of = nil)
|
17
|
+
super(timestamp, user)
|
18
|
+
@key = key
|
19
|
+
@version = version
|
20
|
+
@variation = variation
|
21
|
+
@value = value
|
22
|
+
@reason = reason
|
23
|
+
@default = default
|
24
|
+
# avoid setting rarely-used attributes if they have no value - this saves a little space per instance
|
25
|
+
@track_events = track_events if track_events
|
26
|
+
@debug_until = debug_until if debug_until
|
27
|
+
@prereq_of = prereq_of if prereq_of
|
28
|
+
end
|
29
|
+
|
30
|
+
attr_reader :key
|
31
|
+
attr_reader :version
|
32
|
+
attr_reader :variation
|
33
|
+
attr_reader :value
|
34
|
+
attr_reader :reason
|
35
|
+
attr_reader :default
|
36
|
+
attr_reader :track_events
|
37
|
+
attr_reader :debug_until
|
38
|
+
attr_reader :prereq_of
|
39
|
+
end
|
40
|
+
|
41
|
+
class IdentifyEvent < Event
|
42
|
+
def initialize(timestamp, user)
|
43
|
+
super(timestamp, user)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class CustomEvent < Event
|
48
|
+
def initialize(timestamp, user, key, data = nil, metric_value = nil)
|
49
|
+
super(timestamp, user)
|
50
|
+
@key = key
|
51
|
+
@data = data if !data.nil?
|
52
|
+
@metric_value = metric_value if !metric_value.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
attr_reader :key
|
56
|
+
attr_reader :data
|
57
|
+
attr_reader :metric_value
|
58
|
+
end
|
59
|
+
|
60
|
+
class AliasEvent < Event
|
61
|
+
def initialize(timestamp, key, context_kind, previous_key, previous_context_kind)
|
62
|
+
super(timestamp, nil)
|
63
|
+
@key = key
|
64
|
+
@context_kind = context_kind
|
65
|
+
@previous_key = previous_key
|
66
|
+
@previous_context_kind = previous_context_kind
|
67
|
+
end
|
68
|
+
|
69
|
+
attr_reader :key
|
70
|
+
attr_reader :context_kind
|
71
|
+
attr_reader :previous_key
|
72
|
+
attr_reader :previous_context_kind
|
73
|
+
end
|
74
|
+
|
75
|
+
class IndexEvent < Event
|
76
|
+
def initialize(timestamp, user)
|
77
|
+
super(timestamp, user)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
class DebugEvent < Event
|
82
|
+
def initialize(eval_event)
|
83
|
+
super(eval_event.timestamp, eval_event.user)
|
84
|
+
@eval_event = eval_event
|
85
|
+
end
|
86
|
+
|
87
|
+
attr_reader :eval_event
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|