launchdarkly-server-sdk 5.8.2 → 6.0.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/.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
|