Skip to content

Commit 4d666b6

Browse files
prepare 6.2.0 release (#175)
1 parent fc57982 commit 4d666b6

File tree

8 files changed

+389
-44
lines changed

8 files changed

+389
-44
lines changed

lib/ldclient-rb/evaluation_detail.rb

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,9 @@ class EvaluationReason
120120
# or deleted. If {#kind} is not {#RULE_MATCH}, this will be `nil`.
121121
attr_reader :rule_id
122122

123+
# A boolean or nil value representing if the rule or fallthrough has an experiment rollout.
124+
attr_reader :in_experiment
125+
123126
# The key of the prerequisite flag that did not return the desired variation. If {#kind} is not
124127
# {#PREREQUISITE_FAILED}, this will be `nil`.
125128
attr_reader :prerequisite_key
@@ -136,8 +139,12 @@ def self.off
136139

137140
# Returns an instance whose {#kind} is {#FALLTHROUGH}.
138141
# @return [EvaluationReason]
139-
def self.fallthrough
140-
@@fallthrough
142+
def self.fallthrough(in_experiment=false)
143+
if in_experiment
144+
@@fallthrough_with_experiment
145+
else
146+
@@fallthrough
147+
end
141148
end
142149

143150
# Returns an instance whose {#kind} is {#TARGET_MATCH}.
@@ -153,10 +160,16 @@ def self.target_match
153160
# @param rule_id [String] unique string identifier for the matched rule
154161
# @return [EvaluationReason]
155162
# @raise [ArgumentError] if `rule_index` is not a number or `rule_id` is not a string
156-
def self.rule_match(rule_index, rule_id)
163+
def self.rule_match(rule_index, rule_id, in_experiment=false)
157164
raise ArgumentError.new("rule_index must be a number") if !(rule_index.is_a? Numeric)
158165
raise ArgumentError.new("rule_id must be a string") if !rule_id.nil? && !(rule_id.is_a? String) # in test data, ID could be nil
159-
new(:RULE_MATCH, rule_index, rule_id, nil, nil)
166+
167+
if in_experiment
168+
er = new(:RULE_MATCH, rule_index, rule_id, nil, nil, true)
169+
else
170+
er = new(:RULE_MATCH, rule_index, rule_id, nil, nil)
171+
end
172+
er
160173
end
161174

162175
# Returns an instance whose {#kind} is {#PREREQUISITE_FAILED}.
@@ -204,11 +217,17 @@ def to_s
204217
def inspect
205218
case @kind
206219
when :RULE_MATCH
207-
"RULE_MATCH(#{@rule_index},#{@rule_id})"
220+
if @in_experiment
221+
"RULE_MATCH(#{@rule_index},#{@rule_id},#{@in_experiment})"
222+
else
223+
"RULE_MATCH(#{@rule_index},#{@rule_id})"
224+
end
208225
when :PREREQUISITE_FAILED
209226
"PREREQUISITE_FAILED(#{@prerequisite_key})"
210227
when :ERROR
211228
"ERROR(#{@error_kind})"
229+
when :FALLTHROUGH
230+
@in_experiment ? "FALLTHROUGH(#{@in_experiment})" : @kind.to_s
212231
else
213232
@kind.to_s
214233
end
@@ -225,11 +244,21 @@ def as_json(*) # parameter is unused, but may be passed if we're using the json
225244
# as_json and then modify the result.
226245
case @kind
227246
when :RULE_MATCH
228-
{ kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id }
247+
if @in_experiment
248+
{ kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id, inExperiment: @in_experiment }
249+
else
250+
{ kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id }
251+
end
229252
when :PREREQUISITE_FAILED
230253
{ kind: @kind, prerequisiteKey: @prerequisite_key }
231254
when :ERROR
232255
{ kind: @kind, errorKind: @error_kind }
256+
when :FALLTHROUGH
257+
if @in_experiment
258+
{ kind: @kind, inExperiment: @in_experiment }
259+
else
260+
{ kind: @kind }
261+
end
233262
else
234263
{ kind: @kind }
235264
end
@@ -263,14 +292,15 @@ def [](key)
263292

264293
private
265294

266-
def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind)
295+
def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind, in_experiment=nil)
267296
@kind = kind.to_sym
268297
@rule_index = rule_index
269298
@rule_id = rule_id
270299
@rule_id.freeze if !rule_id.nil?
271300
@prerequisite_key = prerequisite_key
272301
@prerequisite_key.freeze if !prerequisite_key.nil?
273302
@error_kind = error_kind
303+
@in_experiment = in_experiment
274304
end
275305

276306
private_class_method :new
@@ -279,6 +309,7 @@ def self.make_error(error_kind)
279309
new(:ERROR, nil, nil, nil, error_kind)
280310
end
281311

312+
@@fallthrough_with_experiment = new(:FALLTHROUGH, nil, nil, nil, nil, true)
282313
@@fallthrough = new(:FALLTHROUGH, nil, nil, nil, nil)
283314
@@off = new(:OFF, nil, nil, nil, nil)
284315
@@target_match = new(:TARGET_MATCH, nil, nil, nil, nil)

lib/ldclient-rb/impl/evaluator.rb

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -190,7 +190,7 @@ def segment_rule_match_user(rule, user, segment_key, salt)
190190
return true if !rule[:weight]
191191

192192
# 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)
193+
bucket = EvaluatorBucketing.bucket_user(user, segment_key, rule[:bucketBy].nil? ? "key" : rule[:bucketBy], salt, nil)
194194
weight = rule[:weight].to_f / 100000.0
195195
return bucket < weight
196196
end
@@ -213,7 +213,13 @@ def get_off_value(flag, reason)
213213
end
214214

215215
def get_value_for_variation_or_rollout(flag, vr, user, reason)
216-
index = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
216+
index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
217+
#if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
218+
if in_experiment && reason.kind == :FALLTHROUGH
219+
reason = EvaluationReason::fallthrough(in_experiment)
220+
elsif in_experiment && reason.kind == :RULE_MATCH
221+
reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
222+
end
217223
if index.nil?
218224
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
219225
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)

lib/ldclient-rb/impl/evaluator_bucketing.rb

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,30 +10,39 @@ module EvaluatorBucketing
1010
# @param user [Object] the user properties
1111
# @return [Number] the variation index, or nil if there is an error
1212
def self.variation_index_for_user(flag, rule, user)
13+
1314
variation = rule[:variation]
14-
return variation if !variation.nil? # fixed variation
15+
return variation, false if !variation.nil? # fixed variation
1516
rollout = rule[:rollout]
16-
return nil if rollout.nil?
17+
return nil, false if rollout.nil?
1718
variations = rollout[:variations]
1819
if !variations.nil? && variations.length > 0 # percentage rollout
19-
rollout = rule[:rollout]
2020
bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
21-
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
21+
22+
seed = rollout[:seed]
23+
bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt], seed) # may not be present
2224
sum = 0;
2325
variations.each do |variate|
26+
if rollout[:kind] == "experiment" && !variate[:untracked]
27+
in_experiment = true
28+
end
29+
2430
sum += variate[:weight].to_f / 100000.0
2531
if bucket < sum
26-
return variate[:variation]
32+
return variate[:variation], !!in_experiment
2733
end
2834
end
2935
# The user's bucket value was greater than or equal to the end of the last bucket. This could happen due
3036
# to a rounding error, or due to the fact that we are scaling to 100000 rather than 99999, or the flag
3137
# data could contain buckets that don't actually add up to 100000. Rather than returning an error in
3238
# this case (or changing the scaling, which would potentially change the results for *all* users), we
3339
# will simply put the user in the last bucket.
34-
variations[-1][:variation]
40+
last_variation = variations[-1]
41+
in_experiment = rollout[:kind] == "experiment" && !last_variation[:untracked]
42+
43+
[last_variation[:variation], in_experiment]
3544
else # the rule isn't well-formed
36-
nil
45+
[nil, false]
3746
end
3847
end
3948

@@ -44,7 +53,7 @@ def self.variation_index_for_user(flag, rule, user)
4453
# @param bucket_by [String|Symbol] the name of the user attribute to be used for bucketing
4554
# @param salt [String] the feature flag's or segment's salt value
4655
# @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)
4857
return nil unless user[:key]
4958

5059
id_hash = bucketable_string_value(EvaluatorOperators.user_value(user, bucket_by))
@@ -56,7 +65,11 @@ def self.bucket_user(user, key, bucket_by, salt)
5665
id_hash += "." + user[:secondary].to_s
5766
end
5867

59-
hash_key = "%s.%s.%s" % [key, salt, id_hash]
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
6073

6174
hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
6275
hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)

lib/ldclient-rb/impl/event_factory.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ def context_to_context_kind(user)
103103

104104
def is_experiment(flag, reason)
105105
return false if !reason
106+
107+
if reason.in_experiment
108+
return true
109+
end
110+
106111
case reason[:kind]
107112
when 'RULE_MATCH'
108113
index = reason[:ruleIndex]
@@ -115,6 +120,7 @@ def is_experiment(flag, reason)
115120
end
116121
false
117122
end
123+
118124
end
119125
end
120126
end

0 commit comments

Comments
 (0)