launchdarkly-server-sdk 5.8.2 → 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +28 -122
- data/.ldrelease/circleci/linux/execute.sh +18 -0
- data/.ldrelease/circleci/mac/execute.sh +18 -0
- data/.ldrelease/circleci/template/build.sh +29 -0
- data/.ldrelease/circleci/template/publish.sh +23 -0
- data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
- data/.ldrelease/circleci/template/test.sh +10 -0
- data/.ldrelease/circleci/template/update-version.sh +8 -0
- data/.ldrelease/circleci/windows/execute.ps1 +19 -0
- data/.ldrelease/config.yml +7 -3
- data/CHANGELOG.md +9 -0
- data/CONTRIBUTING.md +1 -1
- data/Gemfile.lock +69 -42
- data/README.md +2 -2
- data/azure-pipelines.yml +1 -1
- data/launchdarkly-server-sdk.gemspec +16 -16
- data/lib/ldclient-rb.rb +0 -1
- data/lib/ldclient-rb/config.rb +15 -3
- data/lib/ldclient-rb/evaluation_detail.rb +293 -0
- data/lib/ldclient-rb/events.rb +1 -4
- data/lib/ldclient-rb/file_data_source.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +225 -0
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
- data/lib/ldclient-rb/impl/event_sender.rb +56 -40
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +5 -9
- data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
- data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
- data/lib/ldclient-rb/ldclient.rb +14 -9
- data/lib/ldclient-rb/polling.rb +1 -4
- data/lib/ldclient-rb/requestor.rb +25 -15
- data/lib/ldclient-rb/stream.rb +9 -6
- data/lib/ldclient-rb/util.rb +12 -8
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_detail_spec.rb +135 -0
- data/spec/event_sender_spec.rb +20 -2
- data/spec/http_util.rb +11 -1
- data/spec/impl/evaluator_bucketing_spec.rb +111 -0
- data/spec/impl/evaluator_clause_spec.rb +55 -0
- data/spec/impl/evaluator_operators_spec.rb +141 -0
- data/spec/impl/evaluator_rule_spec.rb +96 -0
- data/spec/impl/evaluator_segment_spec.rb +125 -0
- data/spec/impl/evaluator_spec.rb +305 -0
- data/spec/impl/evaluator_spec_base.rb +75 -0
- data/spec/impl/model/serialization_spec.rb +41 -0
- data/spec/launchdarkly-server-sdk_spec.rb +1 -1
- data/spec/ldclient_end_to_end_spec.rb +34 -0
- data/spec/ldclient_spec.rb +10 -8
- data/spec/polling_spec.rb +2 -2
- data/spec/redis_feature_store_spec.rb +2 -2
- data/spec/requestor_spec.rb +11 -11
- metadata +89 -46
- data/lib/ldclient-rb/evaluation.rb +0 -462
- data/spec/evaluation_spec.rb +0 -789
data/README.md
CHANGED
@@ -17,7 +17,7 @@ LaunchDarkly overview
|
|
17
17
|
Supported Ruby versions
|
18
18
|
-----------------------
|
19
19
|
|
20
|
-
This version of the LaunchDarkly SDK has a minimum Ruby version of 2.
|
20
|
+
This version of the LaunchDarkly SDK has a minimum Ruby version of 2.5.0, or 9.2.0 for JRuby.
|
21
21
|
|
22
22
|
Getting started
|
23
23
|
-----------
|
@@ -55,4 +55,4 @@ About LaunchDarkly
|
|
55
55
|
* [docs.launchdarkly.com](https://docs.launchdarkly.com/ "LaunchDarkly Documentation") for our documentation and SDK reference guides
|
56
56
|
* [apidocs.launchdarkly.com](https://apidocs.launchdarkly.com/ "LaunchDarkly API Documentation") for our API documentation
|
57
57
|
* [blog.launchdarkly.com](https://blog.launchdarkly.com/ "LaunchDarkly Blog Documentation") for the latest product updates
|
58
|
-
* [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies
|
58
|
+
* [Feature Flagging Guide](https://github.com/launchdarkly/featureflags/ "Feature Flagging Guide") for best practices and strategies
|
data/azure-pipelines.yml
CHANGED
@@ -45,7 +45,7 @@ jobs:
|
|
45
45
|
workingDirectory: $(System.DefaultWorkingDirectory)
|
46
46
|
script: |
|
47
47
|
ruby -v
|
48
|
-
gem install bundler
|
48
|
+
gem install bundler
|
49
49
|
bundle install
|
50
50
|
mkdir rspec
|
51
51
|
bundle exec rspec --format progress --format RspecJunitFormatter -o ./rspec/rspec.xml spec
|
@@ -19,27 +19,27 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
20
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
21
21
|
spec.require_paths = ["lib"]
|
22
|
+
spec.required_ruby_version = ">= 2.5.0"
|
22
23
|
|
23
|
-
spec.add_development_dependency "aws-sdk-dynamodb", "~> 1.
|
24
|
-
spec.add_development_dependency "bundler", "~> 1
|
25
|
-
spec.add_development_dependency "rspec", "~> 3.
|
26
|
-
spec.add_development_dependency "diplomat", "
|
27
|
-
spec.add_development_dependency "redis", "~>
|
28
|
-
spec.add_development_dependency "connection_pool", "
|
29
|
-
spec.add_development_dependency "rspec_junit_formatter", "~> 0.
|
30
|
-
spec.add_development_dependency "timecop", "~> 0.9
|
31
|
-
spec.add_development_dependency "listen", "~> 3.
|
32
|
-
|
33
|
-
#
|
34
|
-
|
35
|
-
spec.add_development_dependency "ffi", "<= 1.12" # >1.12 doesnt support ruby 2.2
|
36
|
-
spec.add_development_dependency "faraday", "~> 0.17" # >=0.18 doesnt support ruby 2.2
|
24
|
+
spec.add_development_dependency "aws-sdk-dynamodb", "~> 1.57"
|
25
|
+
spec.add_development_dependency "bundler", "~> 2.1"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.10"
|
27
|
+
spec.add_development_dependency "diplomat", "~> 2.4.2"
|
28
|
+
spec.add_development_dependency "redis", "~> 4.2"
|
29
|
+
spec.add_development_dependency "connection_pool", "~> 2.2.3"
|
30
|
+
spec.add_development_dependency "rspec_junit_formatter", "~> 0.4"
|
31
|
+
spec.add_development_dependency "timecop", "~> 0.9"
|
32
|
+
spec.add_development_dependency "listen", "~> 3.3" # see file_data_source.rb
|
33
|
+
spec.add_development_dependency "webrick", "~> 1.7"
|
34
|
+
# required by dynamodb
|
35
|
+
spec.add_development_dependency "oga", "~> 2.2"
|
37
36
|
|
38
37
|
spec.add_runtime_dependency "semantic", "~> 1.6"
|
39
|
-
spec.add_runtime_dependency "concurrent-ruby", "~> 1.
|
40
|
-
spec.add_runtime_dependency "ld-eventsource", "
|
38
|
+
spec.add_runtime_dependency "concurrent-ruby", "~> 1.1"
|
39
|
+
spec.add_runtime_dependency "ld-eventsource", "~> 2.0"
|
41
40
|
|
42
41
|
# lock json to 2.3.x as ruby libraries often remove
|
43
42
|
# support for older ruby versions in minor releases
|
44
43
|
spec.add_runtime_dependency "json", "~> 2.3.1"
|
44
|
+
spec.add_runtime_dependency "http", "~> 4.4.1"
|
45
45
|
end
|
data/lib/ldclient-rb.rb
CHANGED
data/lib/ldclient-rb/config.rb
CHANGED
@@ -15,7 +15,7 @@ module LaunchDarkly
|
|
15
15
|
#
|
16
16
|
# @param opts [Hash] the configuration options
|
17
17
|
# @option opts [Logger] :logger See {#logger}.
|
18
|
-
# @option opts [String] :base_uri ("https://
|
18
|
+
# @option opts [String] :base_uri ("https://sdk.launchdarkly.com") See {#base_uri}.
|
19
19
|
# @option opts [String] :stream_uri ("https://stream.launchdarkly.com") See {#stream_uri}.
|
20
20
|
# @option opts [String] :events_uri ("https://events.launchdarkly.com") See {#events_uri}.
|
21
21
|
# @option opts [Integer] :capacity (10000) See {#capacity}.
|
@@ -41,6 +41,7 @@ module LaunchDarkly
|
|
41
41
|
# @option opts [Float] :diagnostic_recording_interval (900) See {#diagnostic_recording_interval}.
|
42
42
|
# @option opts [String] :wrapper_name See {#wrapper_name}.
|
43
43
|
# @option opts [String] :wrapper_version See {#wrapper_version}.
|
44
|
+
# @option opts [#open] :socket_factory See {#socket_factory}.
|
44
45
|
#
|
45
46
|
def initialize(opts = {})
|
46
47
|
@base_uri = (opts[:base_uri] || Config.default_base_uri).chomp("/")
|
@@ -71,6 +72,7 @@ module LaunchDarkly
|
|
71
72
|
opts[:diagnostic_recording_interval] : Config.default_diagnostic_recording_interval
|
72
73
|
@wrapper_name = opts[:wrapper_name]
|
73
74
|
@wrapper_version = opts[:wrapper_version]
|
75
|
+
@socket_factory = opts[:socket_factory]
|
74
76
|
end
|
75
77
|
|
76
78
|
#
|
@@ -305,6 +307,16 @@ module LaunchDarkly
|
|
305
307
|
#
|
306
308
|
attr_reader :wrapper_version
|
307
309
|
|
310
|
+
#
|
311
|
+
# The factory used to construct sockets for HTTP operations. The factory must
|
312
|
+
# provide the method `open(uri, timeout)`. The `open` method must return a
|
313
|
+
# connected stream that implements the `IO` class, such as a `TCPSocket`.
|
314
|
+
#
|
315
|
+
# Defaults to nil.
|
316
|
+
# @return [#open]
|
317
|
+
#
|
318
|
+
attr_reader :socket_factory
|
319
|
+
|
308
320
|
#
|
309
321
|
# The default LaunchDarkly client configuration. This configuration sets
|
310
322
|
# reasonable defaults for most users.
|
@@ -324,10 +336,10 @@ module LaunchDarkly
|
|
324
336
|
|
325
337
|
#
|
326
338
|
# The default value for {#base_uri}.
|
327
|
-
# @return [String] "https://
|
339
|
+
# @return [String] "https://sdk.launchdarkly.com"
|
328
340
|
#
|
329
341
|
def self.default_base_uri
|
330
|
-
"https://
|
342
|
+
"https://sdk.launchdarkly.com"
|
331
343
|
end
|
332
344
|
|
333
345
|
#
|
@@ -0,0 +1,293 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
# An object returned by {LDClient#variation_detail}, combining the result of a flag evaluation with
|
4
|
+
# an explanation of how it was calculated.
|
5
|
+
class EvaluationDetail
|
6
|
+
# Creates a new instance.
|
7
|
+
#
|
8
|
+
# @param value the result value of the flag evaluation; may be of any type
|
9
|
+
# @param variation_index [int|nil] the index of the value within the flag's list of variations, or
|
10
|
+
# `nil` if the application default value was returned
|
11
|
+
# @param reason [EvaluationReason] an object describing the main factor that influenced the result
|
12
|
+
# @raise [ArgumentError] if `variation_index` or `reason` is not of the correct type
|
13
|
+
def initialize(value, variation_index, reason)
|
14
|
+
raise ArgumentError.new("variation_index must be a number") if !variation_index.nil? && !(variation_index.is_a? Numeric)
|
15
|
+
raise ArgumentError.new("reason must be an EvaluationReason") if !(reason.is_a? EvaluationReason)
|
16
|
+
@value = value
|
17
|
+
@variation_index = variation_index
|
18
|
+
@reason = reason
|
19
|
+
end
|
20
|
+
|
21
|
+
#
|
22
|
+
# The result of the flag evaluation. This will be either one of the flag's variations, or the
|
23
|
+
# default value that was passed to {LDClient#variation_detail}. It is the same as the return
|
24
|
+
# value of {LDClient#variation}.
|
25
|
+
#
|
26
|
+
# @return [Object]
|
27
|
+
#
|
28
|
+
attr_reader :value
|
29
|
+
|
30
|
+
#
|
31
|
+
# The index of the returned value within the flag's list of variations. The first variation is
|
32
|
+
# 0, the second is 1, etc. This is `nil` if the default value was returned.
|
33
|
+
#
|
34
|
+
# @return [int|nil]
|
35
|
+
#
|
36
|
+
attr_reader :variation_index
|
37
|
+
|
38
|
+
#
|
39
|
+
# An object describing the main factor that influenced the flag evaluation value.
|
40
|
+
#
|
41
|
+
# @return [EvaluationReason]
|
42
|
+
#
|
43
|
+
attr_reader :reason
|
44
|
+
|
45
|
+
#
|
46
|
+
# Tests whether the flag evaluation returned a default value. This is the same as checking
|
47
|
+
# whether {#variation_index} is nil.
|
48
|
+
#
|
49
|
+
# @return [Boolean]
|
50
|
+
#
|
51
|
+
def default_value?
|
52
|
+
variation_index.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def ==(other)
|
56
|
+
@value == other.value && @variation_index == other.variation_index && @reason == other.reason
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# Describes the reason that a flag evaluation produced a particular value. This is returned by
|
61
|
+
# methods such as {LDClient#variation_detail} as the `reason` property of an {EvaluationDetail}.
|
62
|
+
#
|
63
|
+
# The `kind` property is always defined, but other properties will have non-nil values only for
|
64
|
+
# certain values of `kind`. All properties are immutable.
|
65
|
+
#
|
66
|
+
# There is a standard JSON representation of evaluation reasons when they appear in analytics events.
|
67
|
+
# Use `as_json` or `to_json` to convert to this representation.
|
68
|
+
#
|
69
|
+
# Use factory methods such as {EvaluationReason#off} to obtain instances of this class.
|
70
|
+
class EvaluationReason
|
71
|
+
# Value for {#kind} indicating that the flag was off and therefore returned its configured off value.
|
72
|
+
OFF = :OFF
|
73
|
+
|
74
|
+
# Value for {#kind} indicating that the flag was on but the user did not match any targets or rules.
|
75
|
+
FALLTHROUGH = :FALLTHROUGH
|
76
|
+
|
77
|
+
# Value for {#kind} indicating that the user key was specifically targeted for this flag.
|
78
|
+
TARGET_MATCH = :TARGET_MATCH
|
79
|
+
|
80
|
+
# Value for {#kind} indicating that the user matched one of the flag's rules.
|
81
|
+
RULE_MATCH = :RULE_MATCH
|
82
|
+
|
83
|
+
# Value for {#kind} indicating that the flag was considered off because it had at least one
|
84
|
+
# prerequisite flag that either was off or did not return the desired variation.
|
85
|
+
PREREQUISITE_FAILED = :PREREQUISITE_FAILED
|
86
|
+
|
87
|
+
# Value for {#kind} indicating that the flag could not be evaluated, e.g. because it does not exist
|
88
|
+
# or due to an unexpected error. In this case the result value will be the application default value
|
89
|
+
# that the caller passed to the client. Check {#error_kind} for more details on the problem.
|
90
|
+
ERROR = :ERROR
|
91
|
+
|
92
|
+
# Value for {#error_kind} indicating that the caller tried to evaluate a flag before the client had
|
93
|
+
# successfully initialized.
|
94
|
+
ERROR_CLIENT_NOT_READY = :CLIENT_NOT_READY
|
95
|
+
|
96
|
+
# Value for {#error_kind} indicating that the caller provided a flag key that did not match any known flag.
|
97
|
+
ERROR_FLAG_NOT_FOUND = :FLAG_NOT_FOUND
|
98
|
+
|
99
|
+
# Value for {#error_kind} indicating that there was an internal inconsistency in the flag data, e.g.
|
100
|
+
# a rule specified a nonexistent variation. An error message will always be logged in this case.
|
101
|
+
ERROR_MALFORMED_FLAG = :MALFORMED_FLAG
|
102
|
+
|
103
|
+
# Value for {#error_kind} indicating that the caller passed `nil` for the user parameter, or the
|
104
|
+
# user lacked a key.
|
105
|
+
ERROR_USER_NOT_SPECIFIED = :USER_NOT_SPECIFIED
|
106
|
+
|
107
|
+
# Value for {#error_kind} indicating that an unexpected exception stopped flag evaluation. An error
|
108
|
+
# message will always be logged in this case.
|
109
|
+
ERROR_EXCEPTION = :EXCEPTION
|
110
|
+
|
111
|
+
# Indicates the general category of the reason. Will always be one of the class constants such
|
112
|
+
# as {#OFF}.
|
113
|
+
attr_reader :kind
|
114
|
+
|
115
|
+
# The index of the rule that was matched (0 for the first rule in the feature flag). If
|
116
|
+
# {#kind} is not {#RULE_MATCH}, this will be `nil`.
|
117
|
+
attr_reader :rule_index
|
118
|
+
|
119
|
+
# A unique string identifier for the matched rule, which will not change if other rules are added
|
120
|
+
# or deleted. If {#kind} is not {#RULE_MATCH}, this will be `nil`.
|
121
|
+
attr_reader :rule_id
|
122
|
+
|
123
|
+
# The key of the prerequisite flag that did not return the desired variation. If {#kind} is not
|
124
|
+
# {#PREREQUISITE_FAILED}, this will be `nil`.
|
125
|
+
attr_reader :prerequisite_key
|
126
|
+
|
127
|
+
# A value indicating the general category of error. This should be one of the class constants such
|
128
|
+
# as {#ERROR_FLAG_NOT_FOUND}. If {#kind} is not {#ERROR}, it will be `nil`.
|
129
|
+
attr_reader :error_kind
|
130
|
+
|
131
|
+
# Returns an instance whose {#kind} is {#OFF}.
|
132
|
+
# @return [EvaluationReason]
|
133
|
+
def self.off
|
134
|
+
@@off
|
135
|
+
end
|
136
|
+
|
137
|
+
# Returns an instance whose {#kind} is {#FALLTHROUGH}.
|
138
|
+
# @return [EvaluationReason]
|
139
|
+
def self.fallthrough
|
140
|
+
@@fallthrough
|
141
|
+
end
|
142
|
+
|
143
|
+
# Returns an instance whose {#kind} is {#TARGET_MATCH}.
|
144
|
+
# @return [EvaluationReason]
|
145
|
+
def self.target_match
|
146
|
+
@@target_match
|
147
|
+
end
|
148
|
+
|
149
|
+
# Returns an instance whose {#kind} is {#RULE_MATCH}.
|
150
|
+
#
|
151
|
+
# @param rule_index [Number] the index of the rule that was matched (0 for the first rule in
|
152
|
+
# the feature flag)
|
153
|
+
# @param rule_id [String] unique string identifier for the matched rule
|
154
|
+
# @return [EvaluationReason]
|
155
|
+
# @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)
|
157
|
+
raise ArgumentError.new("rule_index must be a number") if !(rule_index.is_a? Numeric)
|
158
|
+
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)
|
160
|
+
end
|
161
|
+
|
162
|
+
# Returns an instance whose {#kind} is {#PREREQUISITE_FAILED}.
|
163
|
+
#
|
164
|
+
# @param prerequisite_key [String] key of the prerequisite flag that did not return the desired variation
|
165
|
+
# @return [EvaluationReason]
|
166
|
+
# @raise [ArgumentError] if `prerequisite_key` is nil or not a string
|
167
|
+
def self.prerequisite_failed(prerequisite_key)
|
168
|
+
raise ArgumentError.new("prerequisite_key must be a string") if !(prerequisite_key.is_a? String)
|
169
|
+
new(:PREREQUISITE_FAILED, nil, nil, prerequisite_key, nil)
|
170
|
+
end
|
171
|
+
|
172
|
+
# Returns an instance whose {#kind} is {#ERROR}.
|
173
|
+
#
|
174
|
+
# @param error_kind [Symbol] value indicating the general category of error
|
175
|
+
# @return [EvaluationReason]
|
176
|
+
# @raise [ArgumentError] if `error_kind` is not a symbol
|
177
|
+
def self.error(error_kind)
|
178
|
+
raise ArgumentError.new("error_kind must be a symbol") if !(error_kind.is_a? Symbol)
|
179
|
+
e = @@error_instances[error_kind]
|
180
|
+
e.nil? ? make_error(error_kind) : e
|
181
|
+
end
|
182
|
+
|
183
|
+
def ==(other)
|
184
|
+
if other.is_a? EvaluationReason
|
185
|
+
@kind == other.kind && @rule_index == other.rule_index && @rule_id == other.rule_id &&
|
186
|
+
@prerequisite_key == other.prerequisite_key && @error_kind == other.error_kind
|
187
|
+
elsif other.is_a? Hash
|
188
|
+
@kind.to_s == other[:kind] && @rule_index == other[:ruleIndex] && @rule_id == other[:ruleId] &&
|
189
|
+
@prerequisite_key == other[:prerequisiteKey] &&
|
190
|
+
(other[:errorKind] == @error_kind.nil? ? nil : @error_kind.to_s)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Equivalent to {#inspect}.
|
195
|
+
# @return [String]
|
196
|
+
def to_s
|
197
|
+
inspect
|
198
|
+
end
|
199
|
+
|
200
|
+
# Returns a concise string representation of the reason. Examples: `"FALLTHROUGH"`,
|
201
|
+
# `"ERROR(FLAG_NOT_FOUND)"`. The exact syntax is not guaranteed to remain the same; this is meant
|
202
|
+
# for debugging.
|
203
|
+
# @return [String]
|
204
|
+
def inspect
|
205
|
+
case @kind
|
206
|
+
when :RULE_MATCH
|
207
|
+
"RULE_MATCH(#{@rule_index},#{@rule_id})"
|
208
|
+
when :PREREQUISITE_FAILED
|
209
|
+
"PREREQUISITE_FAILED(#{@prerequisite_key})"
|
210
|
+
when :ERROR
|
211
|
+
"ERROR(#{@error_kind})"
|
212
|
+
else
|
213
|
+
@kind.to_s
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Returns a hash that can be used as a JSON representation of the reason, in the format used
|
218
|
+
# in LaunchDarkly analytics events.
|
219
|
+
# @return [Hash]
|
220
|
+
def as_json(*) # parameter is unused, but may be passed if we're using the json gem
|
221
|
+
# Note that this implementation is somewhat inefficient; it allocates a new hash every time.
|
222
|
+
# However, in normal usage the SDK only serializes reasons if 1. full event tracking is
|
223
|
+
# enabled for a flag and the application called variation_detail, or 2. experimentation is
|
224
|
+
# enabled for an evaluation. We can't reuse these hashes because an application could call
|
225
|
+
# as_json and then modify the result.
|
226
|
+
case @kind
|
227
|
+
when :RULE_MATCH
|
228
|
+
{ kind: @kind, ruleIndex: @rule_index, ruleId: @rule_id }
|
229
|
+
when :PREREQUISITE_FAILED
|
230
|
+
{ kind: @kind, prerequisiteKey: @prerequisite_key }
|
231
|
+
when :ERROR
|
232
|
+
{ kind: @kind, errorKind: @error_kind }
|
233
|
+
else
|
234
|
+
{ kind: @kind }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Same as {#as_json}, but converts the JSON structure into a string.
|
239
|
+
# @return [String]
|
240
|
+
def to_json(*a)
|
241
|
+
as_json.to_json(a)
|
242
|
+
end
|
243
|
+
|
244
|
+
# Allows this object to be treated as a hash corresponding to its JSON representation. For
|
245
|
+
# instance, if `reason.kind` is {#RULE_MATCH}, then `reason[:kind]` will be `"RULE_MATCH"` and
|
246
|
+
# `reason[:ruleIndex]` will be equal to `reason.rule_index`.
|
247
|
+
def [](key)
|
248
|
+
case key
|
249
|
+
when :kind
|
250
|
+
@kind.to_s
|
251
|
+
when :ruleIndex
|
252
|
+
@rule_index
|
253
|
+
when :ruleId
|
254
|
+
@rule_id
|
255
|
+
when :prerequisiteKey
|
256
|
+
@prerequisite_key
|
257
|
+
when :errorKind
|
258
|
+
@error_kind.nil? ? nil : @error_kind.to_s
|
259
|
+
else
|
260
|
+
nil
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
private
|
265
|
+
|
266
|
+
def initialize(kind, rule_index, rule_id, prerequisite_key, error_kind)
|
267
|
+
@kind = kind.to_sym
|
268
|
+
@rule_index = rule_index
|
269
|
+
@rule_id = rule_id
|
270
|
+
@rule_id.freeze if !rule_id.nil?
|
271
|
+
@prerequisite_key = prerequisite_key
|
272
|
+
@prerequisite_key.freeze if !prerequisite_key.nil?
|
273
|
+
@error_kind = error_kind
|
274
|
+
end
|
275
|
+
|
276
|
+
private_class_method :new
|
277
|
+
|
278
|
+
def self.make_error(error_kind)
|
279
|
+
new(:ERROR, nil, nil, nil, error_kind)
|
280
|
+
end
|
281
|
+
|
282
|
+
@@fallthrough = new(:FALLTHROUGH, nil, nil, nil, nil)
|
283
|
+
@@off = new(:OFF, nil, nil, nil, nil)
|
284
|
+
@@target_match = new(:TARGET_MATCH, nil, nil, nil, nil)
|
285
|
+
@@error_instances = {
|
286
|
+
ERROR_CLIENT_NOT_READY => make_error(ERROR_CLIENT_NOT_READY),
|
287
|
+
ERROR_FLAG_NOT_FOUND => make_error(ERROR_FLAG_NOT_FOUND),
|
288
|
+
ERROR_MALFORMED_FLAG => make_error(ERROR_MALFORMED_FLAG),
|
289
|
+
ERROR_USER_NOT_SPECIFIED => make_error(ERROR_USER_NOT_SPECIFIED),
|
290
|
+
ERROR_EXCEPTION => make_error(ERROR_EXCEPTION)
|
291
|
+
}
|
292
|
+
end
|
293
|
+
end
|
data/lib/ldclient-rb/events.rb
CHANGED
@@ -238,10 +238,7 @@ module LaunchDarkly
|
|
238
238
|
diagnostic_event_workers.shutdown
|
239
239
|
diagnostic_event_workers.wait_for_termination
|
240
240
|
end
|
241
|
-
|
242
|
-
@client.finish
|
243
|
-
rescue
|
244
|
-
end
|
241
|
+
@event_sender.stop if @event_sender.respond_to?(:stop)
|
245
242
|
end
|
246
243
|
|
247
244
|
def synchronize_for_testing(flush_workers, diagnostic_event_workers)
|
@@ -51,7 +51,7 @@ module LaunchDarkly
|
|
51
51
|
# output as the starting point for your file. In Linux you would do this:
|
52
52
|
#
|
53
53
|
# ```
|
54
|
-
# curl -H "Authorization: YOUR_SDK_KEY" https://
|
54
|
+
# curl -H "Authorization: YOUR_SDK_KEY" https://sdk.launchdarkly.com/sdk/latest-all
|
55
55
|
# ```
|
56
56
|
#
|
57
57
|
# The output will look something like this (but with many more properties):
|
@@ -0,0 +1,225 @@
|
|
1
|
+
require "ldclient-rb/evaluation_detail"
|
2
|
+
require "ldclient-rb/impl/evaluator_bucketing"
|
3
|
+
require "ldclient-rb/impl/evaluator_operators"
|
4
|
+
|
5
|
+
module LaunchDarkly
|
6
|
+
module Impl
|
7
|
+
# Encapsulates the feature flag evaluation logic. The Evaluator has no knowledge of the rest of the SDK environment;
|
8
|
+
# if it needs to retrieve flags or segments that are referenced by a flag, it does so through a simple function that
|
9
|
+
# is provided in the constructor. It also produces feature requests as appropriate for any referenced prerequisite
|
10
|
+
# flags, but does not send them.
|
11
|
+
class Evaluator
|
12
|
+
# A single Evaluator is instantiated for each client instance.
|
13
|
+
#
|
14
|
+
# @param get_flag [Function] called if the Evaluator needs to query a different flag from the one that it is
|
15
|
+
# currently evaluating (i.e. a prerequisite flag); takes a single parameter, the flag key, and returns the
|
16
|
+
# flag data - or nil if the flag is unknown or deleted
|
17
|
+
# @param get_segment [Function] similar to `get_flag`, but is used to query a user segment.
|
18
|
+
# @param logger [Logger] the client's logger
|
19
|
+
def initialize(get_flag, get_segment, logger)
|
20
|
+
@get_flag = get_flag
|
21
|
+
@get_segment = get_segment
|
22
|
+
@logger = logger
|
23
|
+
end
|
24
|
+
|
25
|
+
# Used internally to hold an evaluation result and the events that were generated from prerequisites. The
|
26
|
+
# `detail` property is an EvaluationDetail. The `events` property can be either an array of feature request
|
27
|
+
# events or nil.
|
28
|
+
EvalResult = Struct.new(:detail, :events)
|
29
|
+
|
30
|
+
# Helper function used internally to construct an EvaluationDetail for an error result.
|
31
|
+
def self.error_result(errorKind, value = nil)
|
32
|
+
EvaluationDetail.new(value, nil, EvaluationReason.error(errorKind))
|
33
|
+
end
|
34
|
+
|
35
|
+
# The client's entry point for evaluating a flag. The returned `EvalResult` contains the evaluation result and
|
36
|
+
# any events that were generated for prerequisite flags; its `value` will be `nil` if the flag returns the
|
37
|
+
# default value. Error conditions produce a result with a nil value and an error reason, not an exception.
|
38
|
+
#
|
39
|
+
# @param flag [Object] the flag
|
40
|
+
# @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
|
+
# @return [EvalResult] the evaluation result
|
44
|
+
def evaluate(flag, user, event_factory)
|
45
|
+
if user.nil? || user[:key].nil?
|
46
|
+
return EvalResult.new(Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED), [])
|
47
|
+
end
|
48
|
+
|
49
|
+
# If the flag doesn't have any prerequisites (which most flags don't) then it cannot generate any feature
|
50
|
+
# request events for prerequisites and we can skip allocating an array.
|
51
|
+
if flag[:prerequisites] && !flag[:prerequisites].empty?
|
52
|
+
events = []
|
53
|
+
else
|
54
|
+
events = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
detail = eval_internal(flag, user, events, event_factory)
|
58
|
+
return EvalResult.new(detail, events.nil? || events.empty? ? nil : events)
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def eval_internal(flag, user, events, event_factory)
|
64
|
+
if !flag[:on]
|
65
|
+
return get_off_value(flag, EvaluationReason::off)
|
66
|
+
end
|
67
|
+
|
68
|
+
prereq_failure_reason = check_prerequisites(flag, user, events, event_factory)
|
69
|
+
if !prereq_failure_reason.nil?
|
70
|
+
return get_off_value(flag, prereq_failure_reason)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Check user target matches
|
74
|
+
(flag[:targets] || []).each do |target|
|
75
|
+
(target[:values] || []).each do |value|
|
76
|
+
if value == user[:key]
|
77
|
+
return get_variation(flag, target[:variation], EvaluationReason::target_match)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check custom rules
|
83
|
+
rules = flag[:rules] || []
|
84
|
+
rules.each_index do |i|
|
85
|
+
rule = rules[i]
|
86
|
+
if rule_match_user(rule, user)
|
87
|
+
reason = rule[:_reason] # try to use cached reason for this rule
|
88
|
+
reason = EvaluationReason::rule_match(i, rule[:id]) if reason.nil?
|
89
|
+
return get_value_for_variation_or_rollout(flag, rule, user, reason)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
# Check the fallthrough rule
|
94
|
+
if !flag[:fallthrough].nil?
|
95
|
+
return get_value_for_variation_or_rollout(flag, flag[:fallthrough], user, EvaluationReason::fallthrough)
|
96
|
+
end
|
97
|
+
|
98
|
+
return EvaluationDetail.new(nil, nil, EvaluationReason::fallthrough)
|
99
|
+
end
|
100
|
+
|
101
|
+
def check_prerequisites(flag, user, events, event_factory)
|
102
|
+
(flag[:prerequisites] || []).each do |prerequisite|
|
103
|
+
prereq_ok = true
|
104
|
+
prereq_key = prerequisite[:key]
|
105
|
+
prereq_flag = @get_flag.call(prereq_key)
|
106
|
+
|
107
|
+
if prereq_flag.nil?
|
108
|
+
@logger.error { "[LDClient] Could not retrieve prerequisite flag \"#{prereq_key}\" when evaluating \"#{flag[:key]}\"" }
|
109
|
+
prereq_ok = false
|
110
|
+
else
|
111
|
+
begin
|
112
|
+
prereq_res = eval_internal(prereq_flag, user, events, event_factory)
|
113
|
+
# Note that if the prerequisite flag is off, we don't consider it a match no matter what its
|
114
|
+
# off variation was. But we still need to evaluate it in order to generate an event.
|
115
|
+
if !prereq_flag[:on] || prereq_res.variation_index != prerequisite[:variation]
|
116
|
+
prereq_ok = false
|
117
|
+
end
|
118
|
+
event = event_factory.new_eval_event(prereq_flag, user, prereq_res, nil, flag)
|
119
|
+
events.push(event)
|
120
|
+
rescue => exn
|
121
|
+
Util.log_exception(@logger, "Error evaluating prerequisite flag \"#{prereq_key}\" for flag \"#{flag[:key]}\"", exn)
|
122
|
+
prereq_ok = false
|
123
|
+
end
|
124
|
+
end
|
125
|
+
if !prereq_ok
|
126
|
+
reason = prerequisite[:_reason] # try to use cached reason
|
127
|
+
return reason.nil? ? EvaluationReason::prerequisite_failed(prereq_key) : reason
|
128
|
+
end
|
129
|
+
end
|
130
|
+
nil
|
131
|
+
end
|
132
|
+
|
133
|
+
def rule_match_user(rule, user)
|
134
|
+
return false if !rule[:clauses]
|
135
|
+
|
136
|
+
(rule[:clauses] || []).each do |clause|
|
137
|
+
return false if !clause_match_user(clause, user)
|
138
|
+
end
|
139
|
+
|
140
|
+
return true
|
141
|
+
end
|
142
|
+
|
143
|
+
def clause_match_user(clause, user)
|
144
|
+
# In the case of a segment match operator, we check if the user is in any of the segments,
|
145
|
+
# and possibly negate
|
146
|
+
if clause[:op].to_sym == :segmentMatch
|
147
|
+
result = (clause[:values] || []).any? { |v|
|
148
|
+
segment = @get_segment.call(v)
|
149
|
+
!segment.nil? && segment_match_user(segment, user)
|
150
|
+
}
|
151
|
+
clause[:negate] ? !result : result
|
152
|
+
else
|
153
|
+
clause_match_user_no_segments(clause, user)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def clause_match_user_no_segments(clause, user)
|
158
|
+
user_val = EvaluatorOperators.user_value(user, clause[:attribute])
|
159
|
+
return false if user_val.nil?
|
160
|
+
|
161
|
+
op = clause[:op].to_sym
|
162
|
+
clause_vals = clause[:values]
|
163
|
+
result = if user_val.is_a? Enumerable
|
164
|
+
user_val.any? { |uv| clause_vals.any? { |cv| EvaluatorOperators.apply(op, uv, cv) } }
|
165
|
+
else
|
166
|
+
clause_vals.any? { |cv| EvaluatorOperators.apply(op, user_val, cv) }
|
167
|
+
end
|
168
|
+
clause[:negate] ? !result : result
|
169
|
+
end
|
170
|
+
|
171
|
+
def segment_match_user(segment, user)
|
172
|
+
return false unless user[:key]
|
173
|
+
|
174
|
+
return true if segment[:included].include?(user[:key])
|
175
|
+
return false if segment[:excluded].include?(user[:key])
|
176
|
+
|
177
|
+
(segment[:rules] || []).each do |r|
|
178
|
+
return true if segment_rule_match_user(r, user, segment[:key], segment[:salt])
|
179
|
+
end
|
180
|
+
|
181
|
+
return false
|
182
|
+
end
|
183
|
+
|
184
|
+
def segment_rule_match_user(rule, user, segment_key, salt)
|
185
|
+
(rule[:clauses] || []).each do |c|
|
186
|
+
return false unless clause_match_user_no_segments(c, user)
|
187
|
+
end
|
188
|
+
|
189
|
+
# If the weight is absent, this rule matches
|
190
|
+
return true if !rule[:weight]
|
191
|
+
|
192
|
+
# 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)
|
194
|
+
weight = rule[:weight].to_f / 100000.0
|
195
|
+
return bucket < weight
|
196
|
+
end
|
197
|
+
|
198
|
+
private
|
199
|
+
|
200
|
+
def get_variation(flag, index, reason)
|
201
|
+
if index < 0 || index >= flag[:variations].length
|
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)
|
217
|
+
if index.nil?
|
218
|
+
@logger.error("[LDClient] Data inconsistency in feature flag \"#{flag[:key]}\": variation/rollout object with no variation or rollout")
|
219
|
+
return Evaluator.error_result(EvaluationReason::ERROR_MALFORMED_FLAG)
|
220
|
+
end
|
221
|
+
return get_variation(flag, index, reason)
|
222
|
+
end
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|