launchdarkly-server-sdk 6.3.1 → 6.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +1 -2
- data/lib/ldclient-rb/config.rb +37 -0
- data/lib/ldclient-rb/events.rb +201 -107
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/evaluator.rb +42 -46
- 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 +1 -1
- 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/util.rb +58 -0
- data/lib/ldclient-rb/integrations/consul.rb +1 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +1 -1
- data/lib/ldclient-rb/integrations/redis.rb +1 -1
- data/lib/ldclient-rb/ldclient.rb +106 -28
- data/lib/ldclient-rb/requestor.rb +2 -2
- data/lib/ldclient-rb/stream.rb +4 -3
- data/lib/ldclient-rb/util.rb +9 -0
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +0 -1
- metadata +11 -9
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
@@ -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
|
@@ -22,7 +30,7 @@ module LaunchDarkly
|
|
22
30
|
@get_big_segments_membership = get_big_segments_membership
|
23
31
|
@logger = logger
|
24
32
|
end
|
25
|
-
|
33
|
+
|
26
34
|
# Used internally to hold an evaluation result and additional state that may be accumulated during an
|
27
35
|
# evaluation. It's simpler and a bit more efficient to represent these as mutable properties rather than
|
28
36
|
# trying to use a pure functional approach, and since we're not exposing this object to any application code
|
@@ -34,7 +42,7 @@ module LaunchDarkly
|
|
34
42
|
# evaluation.
|
35
43
|
EvalResult = Struct.new(
|
36
44
|
:detail, # the EvaluationDetail representing the evaluation result
|
37
|
-
:
|
45
|
+
:prereq_evals, # an array of PrerequisiteEvalRecord instances, or nil
|
38
46
|
:big_segments_status,
|
39
47
|
:big_segments_membership
|
40
48
|
)
|
@@ -50,17 +58,15 @@ module LaunchDarkly
|
|
50
58
|
#
|
51
59
|
# @param flag [Object] the flag
|
52
60
|
# @param user [Object] the user properties
|
53
|
-
# @param event_factory [EventFactory] called to construct a feature request event when a prerequisite flag is
|
54
|
-
# evaluated; the caller is responsible for constructing the feature event for the top-level evaluation
|
55
61
|
# @return [EvalResult] the evaluation result
|
56
|
-
def evaluate(flag, user
|
62
|
+
def evaluate(flag, user)
|
57
63
|
result = EvalResult.new
|
58
64
|
if user.nil? || user[:key].nil?
|
59
65
|
result.detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED)
|
60
66
|
return result
|
61
67
|
end
|
62
68
|
|
63
|
-
detail = eval_internal(flag, user, result
|
69
|
+
detail = eval_internal(flag, user, result)
|
64
70
|
if !result.big_segments_status.nil?
|
65
71
|
# If big_segments_status is non-nil at the end of the evaluation, it means a query was done at
|
66
72
|
# some point and we will want to include the status in the evaluation reason.
|
@@ -80,21 +86,19 @@ module LaunchDarkly
|
|
80
86
|
|
81
87
|
private
|
82
88
|
|
83
|
-
def eval_internal(flag, user, state
|
89
|
+
def eval_internal(flag, user, state)
|
84
90
|
if !flag[:on]
|
85
|
-
return
|
91
|
+
return EvaluatorHelpers.off_result(flag)
|
86
92
|
end
|
87
93
|
|
88
|
-
|
89
|
-
if !
|
90
|
-
return get_off_value(flag, prereq_failure_reason)
|
91
|
-
end
|
94
|
+
prereq_failure_result = check_prerequisites(flag, user, state)
|
95
|
+
return prereq_failure_result if !prereq_failure_result.nil?
|
92
96
|
|
93
97
|
# Check user target matches
|
94
98
|
(flag[:targets] || []).each do |target|
|
95
99
|
(target[:values] || []).each do |value|
|
96
100
|
if value == user[:key]
|
97
|
-
return
|
101
|
+
return EvaluatorHelpers.target_match_result(target, flag)
|
98
102
|
end
|
99
103
|
end
|
100
104
|
end
|
@@ -106,19 +110,21 @@ module LaunchDarkly
|
|
106
110
|
if rule_match_user(rule, user, state)
|
107
111
|
reason = rule[:_reason] # try to use cached reason for this rule
|
108
112
|
reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
|
109
|
-
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))
|
110
115
|
end
|
111
116
|
end
|
112
117
|
|
113
118
|
# Check the fallthrough rule
|
114
119
|
if !flag[:fallthrough].nil?
|
115
|
-
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))
|
116
122
|
end
|
117
123
|
|
118
124
|
return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
|
119
125
|
end
|
120
126
|
|
121
|
-
def check_prerequisites(flag, user, state
|
127
|
+
def check_prerequisites(flag, user, state)
|
122
128
|
(flag[:prerequisites] || []).each do |prerequisite|
|
123
129
|
prereq_ok = true
|
124
130
|
prereq_key = prerequisite[:key]
|
@@ -129,23 +135,22 @@ module LaunchDarkly
|
|
129
135
|
prereq_ok = false
|
130
136
|
else
|
131
137
|
begin
|
132
|
-
prereq_res = eval_internal(prereq_flag, user, state
|
138
|
+
prereq_res = eval_internal(prereq_flag, user, state)
|
133
139
|
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
134
140
|
# off variation was. But we still need to evaluate it in order to generate an event.
|
135
141
|
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
136
142
|
prereq_ok = false
|
137
143
|
end
|
138
|
-
|
139
|
-
state.
|
140
|
-
state.
|
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)
|
141
147
|
rescue => exn
|
142
148
|
Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
143
149
|
prereq_ok = false
|
144
150
|
end
|
145
151
|
end
|
146
152
|
if !prereq_ok
|
147
|
-
|
148
|
-
return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
|
153
|
+
return EvaluatorHelpers.prerequisite_failed_result(prerequisite, flag)
|
149
154
|
end
|
150
155
|
end
|
151
156
|
nil
|
@@ -248,35 +253,26 @@ module LaunchDarkly
|
|
248
253
|
end
|
249
254
|
|
250
255
|
private
|
251
|
-
|
252
|
-
def
|
253
|
-
if index < 0 || index >= flag[:variations].length
|
254
|
-
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": invalid variation index")
|
255
|
-
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
256
|
-
end
|
257
|
-
EvaluationDetail.new(flag[:variations][index], index, reason)
|
258
|
-
end
|
259
|
-
|
260
|
-
def get_off_value(flag, reason)
|
261
|
-
if flag[:offVariation].nil? # off variation unspecified - return default value
|
262
|
-
return EvaluationDetail.new(nil, nil, reason)
|
263
|
-
end
|
264
|
-
get_variation(flag, flag[:offVariation], reason)
|
265
|
-
end
|
266
|
-
|
267
|
-
def get_value_for_variation_or_rollout(flag, vr, user, reason)
|
256
|
+
|
257
|
+
def get_value_for_variation_or_rollout(flag, vr, user, reason, precomputed_results)
|
268
258
|
index, in_experiment = EvaluatorBucketing.variation_index_for_user(flag, vr, user)
|
269
|
-
#if in experiment is true, set reason to a different reason instance/singleton with in_experiment set
|
270
|
-
if in_experiment && reason.kind == :FALLTHROUGH
|
271
|
-
reason = EvaluationReason::fallthrough(in_experiment)
|
272
|
-
elsif in_experiment && reason.kind == :RULE_MATCH
|
273
|
-
reason = EvaluationReason::rule_match(reason.rule_index, reason.rule_id, in_experiment)
|
274
|
-
end
|
275
259
|
if index.nil?
|
276
260
|
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
277
261
|
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
278
262
|
end
|
279
|
-
|
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
|
280
276
|
end
|
281
277
|
end
|
282
278
|
end
|
@@ -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
|
@@ -35,7 +35,7 @@ module LaunchDarkly
|
|
35
35
|
@client = Aws::DynamoDB::Client.new(opts[:dynamodb_opts] || {})
|
36
36
|
end
|
37
37
|
|
38
|
-
@logger.info("
|
38
|
+
@logger.info("#{description}: using DynamoDB table \"#{table_name}\"")
|
39
39
|
end
|
40
40
|
|
41
41
|
def stop
|
@@ -0,0 +1,177 @@
|
|
1
|
+
require "ldclient-rb/impl/evaluator_helpers"
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
module Impl
|
5
|
+
module DataModelPreprocessing
|
6
|
+
#
|
7
|
+
# Container for a precomputed result that includes a specific variation index and value, an
|
8
|
+
# evaluation reason, and optionally an alternate evaluation reason that corresponds to the
|
9
|
+
# "in experiment" state.
|
10
|
+
#
|
11
|
+
class EvalResultsForSingleVariation
|
12
|
+
def initialize(value, variation_index, regular_reason, in_experiment_reason = nil)
|
13
|
+
@regular_result = EvaluationDetail.new(value, variation_index, regular_reason)
|
14
|
+
@in_experiment_result = in_experiment_reason ?
|
15
|
+
EvaluationDetail.new(value, variation_index, in_experiment_reason) :
|
16
|
+
@regular_result
|
17
|
+
end
|
18
|
+
|
19
|
+
# @param in_experiment [Boolean] indicates whether we want the result to include
|
20
|
+
# "inExperiment: true" in the reason or not
|
21
|
+
# @return [EvaluationDetail]
|
22
|
+
def get_result(in_experiment = false)
|
23
|
+
in_experiment ? @in_experiment_result : @regular_result
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
#
|
28
|
+
# Container for a set of precomputed results, one for each possible flag variation.
|
29
|
+
#
|
30
|
+
class EvalResultFactoryMultiVariations
|
31
|
+
def initialize(variation_factories)
|
32
|
+
@factories = variation_factories
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param index [Integer] the variation index
|
36
|
+
# @param in_experiment [Boolean] indicates whether we want the result to include
|
37
|
+
# "inExperiment: true" in the reason or not
|
38
|
+
def for_variation(index, in_experiment)
|
39
|
+
if index < 0 || index >= @factories.length
|
40
|
+
EvaluationDetail.new(nil, nil, EvaluationReason.error(EvaluationReason::ERROR_MALFORMED_FLAG))
|
41
|
+
else
|
42
|
+
@factories[index].get_result(in_experiment)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Base class for all of the preprocessed data classes we embed in our data model. Using this class
|
48
|
+
# ensures that none of its properties will be included in JSON representations. It also overrides
|
49
|
+
# == to say that it is always equal with another instance of the same class; equality tests on
|
50
|
+
# this class are only ever done in test code, and we want the contents of these classes to be
|
51
|
+
# ignored in test code unless we are looking at specific attributes.
|
52
|
+
class PreprocessedDataBase
|
53
|
+
def as_json(*)
|
54
|
+
nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def to_json(*a)
|
58
|
+
"null"
|
59
|
+
end
|
60
|
+
|
61
|
+
def ==(other)
|
62
|
+
other.class == self.class
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
class FlagPreprocessed < PreprocessedDataBase
|
67
|
+
def initialize(off_result, fallthrough_factory)
|
68
|
+
@off_result = off_result
|
69
|
+
@fallthrough_factory = fallthrough_factory
|
70
|
+
end
|
71
|
+
|
72
|
+
# @return [EvalResultsForSingleVariation]
|
73
|
+
attr_reader :off_result
|
74
|
+
# @return [EvalResultFactoryMultiVariations]
|
75
|
+
attr_reader :fallthrough_factory
|
76
|
+
end
|
77
|
+
|
78
|
+
class PrerequisitePreprocessed < PreprocessedDataBase
|
79
|
+
def initialize(failed_result)
|
80
|
+
@failed_result = failed_result
|
81
|
+
end
|
82
|
+
|
83
|
+
# @return [EvalResultsForSingleVariation]
|
84
|
+
attr_reader :failed_result
|
85
|
+
end
|
86
|
+
|
87
|
+
class TargetPreprocessed < PreprocessedDataBase
|
88
|
+
def initialize(match_result)
|
89
|
+
@match_result = match_result
|
90
|
+
end
|
91
|
+
|
92
|
+
# @return [EvalResultsForSingleVariation]
|
93
|
+
attr_reader :match_result
|
94
|
+
end
|
95
|
+
|
96
|
+
class FlagRulePreprocessed < PreprocessedDataBase
|
97
|
+
def initialize(all_match_results)
|
98
|
+
@all_match_results = all_match_results
|
99
|
+
end
|
100
|
+
|
101
|
+
# @return [EvalResultsForSingleVariation]
|
102
|
+
attr_reader :all_match_results
|
103
|
+
end
|
104
|
+
|
105
|
+
class Preprocessor
|
106
|
+
def initialize(logger = nil)
|
107
|
+
@logger = logger
|
108
|
+
end
|
109
|
+
|
110
|
+
def preprocess_item!(kind, item)
|
111
|
+
if kind.eql? FEATURES
|
112
|
+
preprocess_flag!(item)
|
113
|
+
elsif kind.eql? SEGMENTS
|
114
|
+
preprocess_segment!(item)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def preprocess_all_items!(kind, items_map)
|
119
|
+
return items_map if !items_map
|
120
|
+
items_map.each do |key, item|
|
121
|
+
preprocess_item!(kind, item)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def preprocess_flag!(flag)
|
126
|
+
flag[:_preprocessed] = FlagPreprocessed.new(
|
127
|
+
EvaluatorHelpers.off_result(flag),
|
128
|
+
precompute_multi_variation_results(flag, EvaluationReason::fallthrough(false), EvaluationReason::fallthrough(true))
|
129
|
+
)
|
130
|
+
(flag[:prerequisites] || []).each do |prereq|
|
131
|
+
preprocess_prerequisite!(prereq, flag)
|
132
|
+
end
|
133
|
+
(flag[:targets] || []).each do |target|
|
134
|
+
preprocess_target!(target, flag)
|
135
|
+
end
|
136
|
+
rules = flag[:rules]
|
137
|
+
(rules || []).each_index do |index|
|
138
|
+
preprocess_flag_rule!(rules[index], index, flag)
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
def preprocess_segment!(segment)
|
143
|
+
# nothing to do for segments currently
|
144
|
+
end
|
145
|
+
|
146
|
+
private def preprocess_prerequisite!(prereq, flag)
|
147
|
+
prereq[:_preprocessed] = PrerequisitePreprocessed.new(
|
148
|
+
EvaluatorHelpers.prerequisite_failed_result(prereq, flag, @logger)
|
149
|
+
)
|
150
|
+
end
|
151
|
+
|
152
|
+
private def preprocess_target!(target, flag)
|
153
|
+
target[:_preprocessed] = TargetPreprocessed.new(
|
154
|
+
EvaluatorHelpers.target_match_result(target, flag, @logger)
|
155
|
+
)
|
156
|
+
end
|
157
|
+
|
158
|
+
private def preprocess_flag_rule!(rule, index, flag)
|
159
|
+
match_reason = EvaluationReason::rule_match(index, rule[:id])
|
160
|
+
match_reason_in_experiment = EvaluationReason::rule_match(index, rule[:id], true)
|
161
|
+
rule[:_preprocessed] = FlagRulePreprocessed.new(
|
162
|
+
precompute_multi_variation_results(flag, match_reason, match_reason_in_experiment)
|
163
|
+
)
|
164
|
+
end
|
165
|
+
|
166
|
+
private def precompute_multi_variation_results(flag, regular_reason, in_experiment_reason)
|
167
|
+
factories = []
|
168
|
+
vars = flag[:variations] || []
|
169
|
+
vars.each_index do |index|
|
170
|
+
factories << EvalResultsForSingleVariation.new(vars[index], index, regular_reason, in_experiment_reason)
|
171
|
+
end
|
172
|
+
EvalResultFactoryMultiVariations.new(factories)
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
@@ -1,13 +1,14 @@
|
|
1
|
+
require "ldclient-rb/impl/model/preprocessed_data"
|
1
2
|
|
2
3
|
module LaunchDarkly
|
3
4
|
module Impl
|
4
5
|
module Model
|
5
6
|
# Abstraction of deserializing a feature flag or segment that was read from a data store or
|
6
7
|
# received from LaunchDarkly.
|
7
|
-
def self.deserialize(kind, json)
|
8
|
+
def self.deserialize(kind, json, logger = nil)
|
8
9
|
return nil if json.nil?
|
9
10
|
item = JSON.parse(json, symbolize_names: true)
|
10
|
-
|
11
|
+
DataModelPreprocessing::Preprocessor.new(logger).preprocess_item!(kind, item)
|
11
12
|
item
|
12
13
|
end
|
13
14
|
|
@@ -18,45 +19,14 @@ module LaunchDarkly
|
|
18
19
|
end
|
19
20
|
|
20
21
|
# Translates a { flags: ..., segments: ... } object received from LaunchDarkly to the data store format.
|
21
|
-
def self.make_all_store_data(received_data)
|
22
|
+
def self.make_all_store_data(received_data, logger = nil)
|
23
|
+
preprocessor = DataModelPreprocessing::Preprocessor.new(logger)
|
22
24
|
flags = received_data[:flags]
|
23
|
-
|
25
|
+
preprocessor.preprocess_all_items!(FEATURES, flags)
|
24
26
|
segments = received_data[:segments]
|
25
|
-
|
27
|
+
preprocessor.preprocess_all_items!(SEGMENTS, segments)
|
26
28
|
{ FEATURES => flags, SEGMENTS => segments }
|
27
29
|
end
|
28
|
-
|
29
|
-
# Called after we have deserialized a model item from JSON (because we received it from LaunchDarkly,
|
30
|
-
# or read it from a persistent data store). This allows us to precompute some derived attributes that
|
31
|
-
# will never change during the lifetime of that item.
|
32
|
-
def self.postprocess_item_after_deserializing!(kind, item)
|
33
|
-
return if !item
|
34
|
-
# Currently we are special-casing this for FEATURES; eventually it will be handled by delegating
|
35
|
-
# to the "kind" object or the item class.
|
36
|
-
if kind.eql? FEATURES
|
37
|
-
# For feature flags, we precompute all possible parameterized EvaluationReason instances.
|
38
|
-
prereqs = item[:prerequisites]
|
39
|
-
if !prereqs.nil?
|
40
|
-
prereqs.each do |prereq|
|
41
|
-
prereq[:_reason] = EvaluationReason::prerequisite_failed(prereq[:key])
|
42
|
-
end
|
43
|
-
end
|
44
|
-
rules = item[:rules]
|
45
|
-
if !rules.nil?
|
46
|
-
rules.each_index do |i|
|
47
|
-
rule = rules[i]
|
48
|
-
rule[:_reason] = EvaluationReason::rule_match(i, rule[:id])
|
49
|
-
end
|
50
|
-
end
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def self.postprocess_items_after_deserializing!(kind, items_map)
|
55
|
-
return items_map if !items_map
|
56
|
-
items_map.each do |key, item|
|
57
|
-
postprocess_item_after_deserializing!(kind, item)
|
58
|
-
end
|
59
|
-
end
|
60
30
|
end
|
61
31
|
end
|
62
32
|
end
|