launchdarkly-server-sdk 6.1.1 → 6.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +4 -5
- data/lib/ldclient-rb/config.rb +118 -4
- data/lib/ldclient-rb/evaluation_detail.rb +104 -14
- data/lib/ldclient-rb/events.rb +201 -107
- data/lib/ldclient-rb/file_data_source.rb +9 -300
- data/lib/ldclient-rb/flags_state.rb +23 -12
- data/lib/ldclient-rb/impl/big_segments.rb +117 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +1 -1
- data/lib/ldclient-rb/impl/evaluator.rb +116 -62
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +22 -9
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +53 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +1 -1
- data/lib/ldclient-rb/impl/event_summarizer.rb +63 -0
- data/lib/ldclient-rb/impl/event_types.rb +90 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +82 -18
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +212 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +84 -31
- data/lib/ldclient-rb/impl/integrations/test_data/test_data_source.rb +40 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +177 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +7 -37
- data/lib/ldclient-rb/impl/repeating_task.rb +47 -0
- data/lib/ldclient-rb/impl/util.rb +62 -1
- data/lib/ldclient-rb/integrations/consul.rb +8 -1
- data/lib/ldclient-rb/integrations/dynamodb.rb +48 -3
- data/lib/ldclient-rb/integrations/file_data.rb +108 -0
- data/lib/ldclient-rb/integrations/redis.rb +42 -2
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +438 -0
- data/lib/ldclient-rb/integrations/test_data.rb +209 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +5 -0
- data/lib/ldclient-rb/integrations.rb +2 -51
- data/lib/ldclient-rb/interfaces.rb +152 -2
- data/lib/ldclient-rb/ldclient.rb +131 -33
- data/lib/ldclient-rb/polling.rb +22 -41
- data/lib/ldclient-rb/requestor.rb +3 -3
- data/lib/ldclient-rb/stream.rb +4 -3
- data/lib/ldclient-rb/util.rb +10 -1
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +0 -1
- metadata +35 -132
- data/.circleci/config.yml +0 -40
- data/.github/ISSUE_TEMPLATE/bug_report.md +0 -37
- data/.github/ISSUE_TEMPLATE/config.yml +0 -5
- data/.github/ISSUE_TEMPLATE/feature_request.md +0 -20
- data/.github/pull_request_template.md +0 -21
- data/.gitignore +0 -16
- data/.hound.yml +0 -2
- data/.ldrelease/build-docs.sh +0 -18
- data/.ldrelease/circleci/linux/execute.sh +0 -18
- data/.ldrelease/circleci/mac/execute.sh +0 -18
- data/.ldrelease/circleci/template/build.sh +0 -29
- data/.ldrelease/circleci/template/publish.sh +0 -23
- data/.ldrelease/circleci/template/set-gem-home.sh +0 -7
- data/.ldrelease/circleci/template/test.sh +0 -10
- data/.ldrelease/circleci/template/update-version.sh +0 -8
- data/.ldrelease/circleci/windows/execute.ps1 +0 -19
- data/.ldrelease/config.yml +0 -29
- data/.rspec +0 -2
- data/.rubocop.yml +0 -600
- data/.simplecov +0 -4
- data/CHANGELOG.md +0 -351
- data/CODEOWNERS +0 -1
- data/CONTRIBUTING.md +0 -37
- data/Gemfile +0 -3
- data/azure-pipelines.yml +0 -51
- data/docs/Makefile +0 -26
- data/docs/index.md +0 -9
- data/launchdarkly-server-sdk.gemspec +0 -45
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/impl/event_factory.rb +0 -120
- data/spec/config_spec.rb +0 -63
- data/spec/diagnostic_events_spec.rb +0 -163
- data/spec/evaluation_detail_spec.rb +0 -135
- data/spec/event_sender_spec.rb +0 -197
- data/spec/event_summarizer_spec.rb +0 -63
- data/spec/events_spec.rb +0 -607
- data/spec/expiring_cache_spec.rb +0 -76
- data/spec/feature_store_spec_base.rb +0 -213
- data/spec/file_data_source_spec.rb +0 -283
- data/spec/fixtures/feature.json +0 -37
- data/spec/fixtures/feature1.json +0 -36
- data/spec/fixtures/user.json +0 -9
- data/spec/flags_state_spec.rb +0 -81
- data/spec/http_util.rb +0 -132
- data/spec/impl/evaluator_bucketing_spec.rb +0 -111
- data/spec/impl/evaluator_clause_spec.rb +0 -55
- data/spec/impl/evaluator_operators_spec.rb +0 -141
- data/spec/impl/evaluator_rule_spec.rb +0 -96
- data/spec/impl/evaluator_segment_spec.rb +0 -125
- data/spec/impl/evaluator_spec.rb +0 -305
- data/spec/impl/evaluator_spec_base.rb +0 -75
- data/spec/impl/model/serialization_spec.rb +0 -41
- data/spec/in_memory_feature_store_spec.rb +0 -12
- data/spec/integrations/consul_feature_store_spec.rb +0 -40
- data/spec/integrations/dynamodb_feature_store_spec.rb +0 -103
- data/spec/integrations/store_wrapper_spec.rb +0 -276
- data/spec/launchdarkly-server-sdk_spec.rb +0 -13
- data/spec/launchdarkly-server-sdk_spec_autoloadtest.rb +0 -9
- data/spec/ldclient_end_to_end_spec.rb +0 -157
- data/spec/ldclient_spec.rb +0 -643
- data/spec/newrelic_spec.rb +0 -5
- data/spec/polling_spec.rb +0 -120
- data/spec/redis_feature_store_spec.rb +0 -121
- data/spec/requestor_spec.rb +0 -196
- data/spec/segment_store_spec_base.rb +0 -95
- data/spec/simple_lru_cache_spec.rb +0 -24
- data/spec/spec_helper.rb +0 -9
- data/spec/store_spec.rb +0 -10
- data/spec/stream_spec.rb +0 -45
- data/spec/user_filter_spec.rb +0 -91
- data/spec/util_spec.rb +0 -17
- data/spec/version_spec.rb +0 -7
@@ -1,55 +1,6 @@
|
|
1
1
|
require "ldclient-rb/integrations/consul"
|
2
2
|
require "ldclient-rb/integrations/dynamodb"
|
3
|
+
require "ldclient-rb/integrations/file_data"
|
3
4
|
require "ldclient-rb/integrations/redis"
|
5
|
+
require "ldclient-rb/integrations/test_data"
|
4
6
|
require "ldclient-rb/integrations/util/store_wrapper"
|
5
|
-
|
6
|
-
module LaunchDarkly
|
7
|
-
#
|
8
|
-
# Tools for connecting the LaunchDarkly client to other software.
|
9
|
-
#
|
10
|
-
module Integrations
|
11
|
-
#
|
12
|
-
# Integration with [Consul](https://www.consul.io/).
|
13
|
-
#
|
14
|
-
# Note that in order to use this integration, you must first install the gem `diplomat`.
|
15
|
-
#
|
16
|
-
# @since 5.5.0
|
17
|
-
#
|
18
|
-
module Consul
|
19
|
-
# code is in ldclient-rb/impl/integrations/consul_impl
|
20
|
-
end
|
21
|
-
|
22
|
-
#
|
23
|
-
# Integration with [DynamoDB](https://aws.amazon.com/dynamodb/).
|
24
|
-
#
|
25
|
-
# Note that in order to use this integration, you must first install one of the AWS SDK gems: either
|
26
|
-
# `aws-sdk-dynamodb`, or the full `aws-sdk`.
|
27
|
-
#
|
28
|
-
# @since 5.5.0
|
29
|
-
#
|
30
|
-
module DynamoDB
|
31
|
-
# code is in ldclient-rb/impl/integrations/dynamodb_impl
|
32
|
-
end
|
33
|
-
|
34
|
-
#
|
35
|
-
# Integration with [Redis](https://redis.io/).
|
36
|
-
#
|
37
|
-
# Note that in order to use this integration, you must first install the `redis` and `connection-pool`
|
38
|
-
# gems.
|
39
|
-
#
|
40
|
-
# @since 5.5.0
|
41
|
-
#
|
42
|
-
module Redis
|
43
|
-
# code is in ldclient-rb/impl/integrations/redis_impl
|
44
|
-
end
|
45
|
-
|
46
|
-
#
|
47
|
-
# Support code that may be helpful in creating integrations.
|
48
|
-
#
|
49
|
-
# @since 5.5.0
|
50
|
-
#
|
51
|
-
module Util
|
52
|
-
# code is in ldclient-rb/integrations/util/
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require "observer"
|
1
2
|
|
2
3
|
module LaunchDarkly
|
3
4
|
#
|
@@ -9,7 +10,7 @@ module LaunchDarkly
|
|
9
10
|
# client uses the feature store to persist feature flags and related objects received from
|
10
11
|
# the LaunchDarkly service. Implementations must support concurrent access and updates.
|
11
12
|
# For more about how feature stores can be used, see:
|
12
|
-
# [Using a persistent feature store](https://docs.launchdarkly.com/
|
13
|
+
# [Using a persistent feature store](https://docs.launchdarkly.com/sdk/features/storing-data#ruby).
|
13
14
|
#
|
14
15
|
# An entity that can be stored in a feature store is a hash that can be converted to and from
|
15
16
|
# JSON, and that has at a minimum the following properties: `:key`, a string that is unique
|
@@ -120,7 +121,8 @@ module LaunchDarkly
|
|
120
121
|
#
|
121
122
|
# The client has its own standard implementation, which uses either a streaming connection or
|
122
123
|
# polling depending on your configuration. Normally you will not need to use another one
|
123
|
-
# except for testing purposes.
|
124
|
+
# except for testing purposes. Two such test fixtures are {LaunchDarkly::Integrations::FileData}
|
125
|
+
# and {LaunchDarkly::Integrations::TestData}.
|
124
126
|
#
|
125
127
|
module DataSource
|
126
128
|
#
|
@@ -149,5 +151,153 @@ module LaunchDarkly
|
|
149
151
|
def stop
|
150
152
|
end
|
151
153
|
end
|
154
|
+
|
155
|
+
module BigSegmentStore
|
156
|
+
#
|
157
|
+
# Returns information about the overall state of the store. This method will be called only
|
158
|
+
# when the SDK needs the latest state, so it should not be cached.
|
159
|
+
#
|
160
|
+
# @return [BigSegmentStoreMetadata]
|
161
|
+
#
|
162
|
+
def get_metadata
|
163
|
+
end
|
164
|
+
|
165
|
+
#
|
166
|
+
# Queries the store for a snapshot of the current segment state for a specific user.
|
167
|
+
#
|
168
|
+
# The user_hash is a base64-encoded string produced by hashing the user key as defined by
|
169
|
+
# the Big Segments specification; the store implementation does not need to know the details
|
170
|
+
# of how this is done, because it deals only with already-hashed keys, but the string can be
|
171
|
+
# assumed to only contain characters that are valid in base64.
|
172
|
+
#
|
173
|
+
# The return value should be either a Hash, or nil if the user is not referenced in any big
|
174
|
+
# segments. Each key in the Hash is a "segment reference", which is how segments are
|
175
|
+
# identified in Big Segment data. This string is not identical to the segment key-- the SDK
|
176
|
+
# will add other information. The store implementation should not be concerned with the
|
177
|
+
# format of the string. Each value in the Hash is true if the user is explicitly included in
|
178
|
+
# the segment, false if the user is explicitly excluded from the segment-- and is not also
|
179
|
+
# explicitly included (that is, if both an include and an exclude existed in the data, the
|
180
|
+
# include would take precedence). If the user's status in a particular segment is undefined,
|
181
|
+
# there should be no key or value for that segment.
|
182
|
+
#
|
183
|
+
# This Hash may be cached by the SDK, so it should not be modified after it is created. It
|
184
|
+
# is a snapshot of the segment membership state at one point in time.
|
185
|
+
#
|
186
|
+
# @param user_hash [String]
|
187
|
+
# @return [Hash] true/false values for Big Segments that reference this user
|
188
|
+
#
|
189
|
+
def get_membership(user_hash)
|
190
|
+
end
|
191
|
+
|
192
|
+
#
|
193
|
+
# Performs any necessary cleanup to shut down the store when the client is being shut down.
|
194
|
+
#
|
195
|
+
# @return [void]
|
196
|
+
#
|
197
|
+
def stop
|
198
|
+
end
|
199
|
+
end
|
200
|
+
|
201
|
+
#
|
202
|
+
# Values returned by {BigSegmentStore#get_metadata}.
|
203
|
+
#
|
204
|
+
class BigSegmentStoreMetadata
|
205
|
+
def initialize(last_up_to_date)
|
206
|
+
@last_up_to_date = last_up_to_date
|
207
|
+
end
|
208
|
+
|
209
|
+
# The Unix epoch millisecond timestamp of the last update to the {BigSegmentStore}. It is
|
210
|
+
# nil if the store has never been updated.
|
211
|
+
#
|
212
|
+
# @return [Integer|nil]
|
213
|
+
attr_reader :last_up_to_date
|
214
|
+
end
|
215
|
+
|
216
|
+
#
|
217
|
+
# Information about the status of a Big Segment store, provided by {BigSegmentStoreStatusProvider}.
|
218
|
+
#
|
219
|
+
# Big Segments are a specific type of user segments. For more information, read the LaunchDarkly
|
220
|
+
# documentation: https://docs.launchdarkly.com/home/users/big-segments
|
221
|
+
#
|
222
|
+
class BigSegmentStoreStatus
|
223
|
+
def initialize(available, stale)
|
224
|
+
@available = available
|
225
|
+
@stale = stale
|
226
|
+
end
|
227
|
+
|
228
|
+
# True if the Big Segment store is able to respond to queries, so that the SDK can evaluate
|
229
|
+
# whether a user is in a segment or not.
|
230
|
+
#
|
231
|
+
# If this property is false, the store is not able to make queries (for instance, it may not have
|
232
|
+
# a valid database connection). In this case, the SDK will treat any reference to a Big Segment
|
233
|
+
# as if no users are included in that segment. Also, the {EvaluationReason} associated with
|
234
|
+
# with any flag evaluation that references a Big Segment when the store is not available will
|
235
|
+
# have a `big_segments_status` of `STORE_ERROR`.
|
236
|
+
#
|
237
|
+
# @return [Boolean]
|
238
|
+
attr_reader :available
|
239
|
+
|
240
|
+
# True if the Big Segment store is available, but has not been updated within the amount of time
|
241
|
+
# specified by {BigSegmentsConfig#stale_after}.
|
242
|
+
#
|
243
|
+
# This may indicate that the LaunchDarkly Relay Proxy, which populates the store, has stopped
|
244
|
+
# running or has become unable to receive fresh data from LaunchDarkly. Any feature flag
|
245
|
+
# evaluations that reference a Big Segment will be using the last known data, which may be out
|
246
|
+
# of date. Also, the {EvaluationReason} associated with those evaluations will have a
|
247
|
+
# `big_segments_status` of `STALE`.
|
248
|
+
#
|
249
|
+
# @return [Boolean]
|
250
|
+
attr_reader :stale
|
251
|
+
|
252
|
+
def ==(other)
|
253
|
+
self.available == other.available && self.stale == other.stale
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
#
|
258
|
+
# An interface for querying the status of a Big Segment store.
|
259
|
+
#
|
260
|
+
# The Big Segment store is the component that receives information about Big Segments, normally
|
261
|
+
# from a database populated by the LaunchDarkly Relay Proxy. Big Segments are a specific type
|
262
|
+
# of user segments. For more information, read the LaunchDarkly documentation:
|
263
|
+
# https://docs.launchdarkly.com/home/users/big-segments
|
264
|
+
#
|
265
|
+
# An implementation of this interface is returned by {LDClient#big_segment_store_status_provider}.
|
266
|
+
# Application code never needs to implement this interface.
|
267
|
+
#
|
268
|
+
# There are two ways to interact with the status. One is to simply get the current status; if its
|
269
|
+
# `available` property is true, then the SDK is able to evaluate user membership in Big Segments,
|
270
|
+
# and the `stale`` property indicates whether the data might be out of date.
|
271
|
+
#
|
272
|
+
# The other way is to subscribe to status change notifications. Applications may wish to know if
|
273
|
+
# there is an outage in the Big Segment store, or if it has become stale (the Relay Proxy has
|
274
|
+
# stopped updating it with new data), since then flag evaluations that reference a Big Segment
|
275
|
+
# might return incorrect values. To allow finding out about status changes as soon as possible,
|
276
|
+
# `BigSegmentStoreStatusProvider` mixes in Ruby's
|
277
|
+
# [Observable](https://docs.ruby-lang.org/en/2.5.0/Observable.html) module to provide standard
|
278
|
+
# methods such as `add_observer`. Observers will be called with a new {BigSegmentStoreStatus}
|
279
|
+
# value whenever the status changes.
|
280
|
+
#
|
281
|
+
# @example Getting the current status
|
282
|
+
# status = client.big_segment_store_status_provider.status
|
283
|
+
#
|
284
|
+
# @example Subscribing to status notifications
|
285
|
+
# client.big_segment_store_status_provider.add_observer(self, :big_segments_status_changed)
|
286
|
+
#
|
287
|
+
# def big_segments_status_changed(new_status)
|
288
|
+
# puts "Big segment store status is now: #{new_status}"
|
289
|
+
# end
|
290
|
+
#
|
291
|
+
module BigSegmentStoreStatusProvider
|
292
|
+
include Observable
|
293
|
+
#
|
294
|
+
# Gets the current status of the store, if known.
|
295
|
+
#
|
296
|
+
# @return [BigSegmentStoreStatus] the status, or nil if the SDK has not yet queried the Big
|
297
|
+
# Segment store status
|
298
|
+
#
|
299
|
+
def status
|
300
|
+
end
|
301
|
+
end
|
152
302
|
end
|
153
303
|
end
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
|
+
require "ldclient-rb/impl/big_segments"
|
1
2
|
require "ldclient-rb/impl/diagnostic_events"
|
2
3
|
require "ldclient-rb/impl/evaluator"
|
3
|
-
require "ldclient-rb/impl/event_factory"
|
4
4
|
require "ldclient-rb/impl/store_client_wrapper"
|
5
5
|
require "concurrent/atomics"
|
6
6
|
require "digest/sha1"
|
@@ -45,9 +45,6 @@ module LaunchDarkly
|
|
45
45
|
|
46
46
|
@sdk_key = sdk_key
|
47
47
|
|
48
|
-
@event_factory_default = EventFactory.new(false)
|
49
|
-
@event_factory_with_reasons = EventFactory.new(true)
|
50
|
-
|
51
48
|
# We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
|
52
49
|
# some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
|
53
50
|
# the feature store through the Config object, so we need to make a new Config that uses
|
@@ -57,9 +54,13 @@ module LaunchDarkly
|
|
57
54
|
updated_config.instance_variable_set(:@feature_store, @store)
|
58
55
|
@config = updated_config
|
59
56
|
|
57
|
+
@big_segment_store_manager = Impl::BigSegmentStoreManager.new(config.big_segments, @config.logger)
|
58
|
+
@big_segment_store_status_provider = @big_segment_store_manager.status_provider
|
59
|
+
|
60
60
|
get_flag = lambda { |key| @store.get(FEATURES, key) }
|
61
61
|
get_segment = lambda { |key| @store.get(SEGMENTS, key) }
|
62
|
-
|
62
|
+
get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_user_membership(key) }
|
63
|
+
@evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger)
|
63
64
|
|
64
65
|
if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
|
65
66
|
diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
|
@@ -132,7 +133,7 @@ module LaunchDarkly
|
|
132
133
|
|
133
134
|
#
|
134
135
|
# Creates a hash string that can be used by the JavaScript SDK to identify a user.
|
135
|
-
# For more information, see [Secure mode](https://docs.launchdarkly.com/
|
136
|
+
# For more information, see [Secure mode](https://docs.launchdarkly.com/sdk/features/secure-mode#ruby).
|
136
137
|
#
|
137
138
|
# @param user [Hash] the user properties
|
138
139
|
# @return [String] a hash string
|
@@ -172,11 +173,11 @@ module LaunchDarkly
|
|
172
173
|
#
|
173
174
|
# Other supported user attributes include IP address, country code, and an arbitrary hash of
|
174
175
|
# custom attributes. For more about the supported user properties and how they work in
|
175
|
-
# LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/
|
176
|
-
#
|
176
|
+
# LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users).
|
177
|
+
#
|
177
178
|
# The optional `:privateAttributeNames` user property allows you to specify a list of
|
178
179
|
# attribute names that should not be sent back to LaunchDarkly.
|
179
|
-
# [Private attributes](https://docs.launchdarkly.com/
|
180
|
+
# [Private attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes)
|
180
181
|
# can also be configured globally in {Config}.
|
181
182
|
#
|
182
183
|
# @example Basic user hash
|
@@ -197,7 +198,7 @@ module LaunchDarkly
|
|
197
198
|
# @return the variation to show the user, or the default value if there's an an error
|
198
199
|
#
|
199
200
|
def variation(key, user, default)
|
200
|
-
evaluate_internal(key, user, default,
|
201
|
+
evaluate_internal(key, user, default, false).value
|
201
202
|
end
|
202
203
|
|
203
204
|
#
|
@@ -213,7 +214,7 @@ module LaunchDarkly
|
|
213
214
|
# be included in analytics events, if you are capturing detailed event data for this flag.
|
214
215
|
#
|
215
216
|
# For more information, see the reference guide on
|
216
|
-
# [Evaluation reasons](https://docs.launchdarkly.com/
|
217
|
+
# [Evaluation reasons](https://docs.launchdarkly.com/sdk/concepts/evaluation-reasons).
|
217
218
|
#
|
218
219
|
# @param key [String] the unique feature key for the feature flag, as shown
|
219
220
|
# on the LaunchDarkly dashboard
|
@@ -224,7 +225,7 @@ module LaunchDarkly
|
|
224
225
|
# @return [EvaluationDetail] an object describing the result
|
225
226
|
#
|
226
227
|
def variation_detail(key, user, default)
|
227
|
-
evaluate_internal(key, user, default,
|
228
|
+
evaluate_internal(key, user, default, true)
|
228
229
|
end
|
229
230
|
|
230
231
|
#
|
@@ -243,12 +244,12 @@ module LaunchDarkly
|
|
243
244
|
# @return [void]
|
244
245
|
#
|
245
246
|
def identify(user)
|
246
|
-
if !user || user[:key].nil?
|
247
|
-
@config.logger.warn("Identify called with nil user or
|
247
|
+
if !user || user[:key].nil? || user[:key].empty?
|
248
|
+
@config.logger.warn("Identify called with nil user or empty user key!")
|
248
249
|
return
|
249
250
|
end
|
250
251
|
sanitize_user(user)
|
251
|
-
@event_processor.
|
252
|
+
@event_processor.record_identify_event(user)
|
252
253
|
end
|
253
254
|
|
254
255
|
#
|
@@ -260,7 +261,7 @@ module LaunchDarkly
|
|
260
261
|
#
|
261
262
|
# As of this version’s release date, the LaunchDarkly service does not support the `metricValue`
|
262
263
|
# parameter. As a result, specifying `metricValue` will not yet produce any different behavior
|
263
|
-
# from omitting it. Refer to the [SDK reference guide](https://docs.launchdarkly.com/
|
264
|
+
# from omitting it. Refer to the [SDK reference guide](https://docs.launchdarkly.com/sdk/features/events#ruby)
|
264
265
|
# for the latest status.
|
265
266
|
#
|
266
267
|
# @param event_name [String] The name of the event
|
@@ -279,7 +280,7 @@ module LaunchDarkly
|
|
279
280
|
return
|
280
281
|
end
|
281
282
|
sanitize_user(user)
|
282
|
-
@event_processor.
|
283
|
+
@event_processor.record_custom_event(user, event_name, data, metric_value)
|
283
284
|
end
|
284
285
|
|
285
286
|
#
|
@@ -296,7 +297,7 @@ module LaunchDarkly
|
|
296
297
|
end
|
297
298
|
sanitize_user(current_context)
|
298
299
|
sanitize_user(previous_context)
|
299
|
-
@event_processor.
|
300
|
+
@event_processor.record_alias_event(current_context, previous_context)
|
300
301
|
end
|
301
302
|
|
302
303
|
#
|
@@ -333,6 +334,15 @@ module LaunchDarkly
|
|
333
334
|
def all_flags_state(user, options={})
|
334
335
|
return FeatureFlagsState.new(false) if @config.offline?
|
335
336
|
|
337
|
+
if !initialized?
|
338
|
+
if @store.initialized?
|
339
|
+
@config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" }
|
340
|
+
else
|
341
|
+
@config.logger.warn { "Called all_flags_state before client initialization. Data store not available; returning empty state" }
|
342
|
+
return FeatureFlagsState.new(false)
|
343
|
+
end
|
344
|
+
end
|
345
|
+
|
336
346
|
unless user && !user[:key].nil?
|
337
347
|
@config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" }
|
338
348
|
return FeatureFlagsState.new(false)
|
@@ -354,14 +364,25 @@ module LaunchDarkly
|
|
354
364
|
next
|
355
365
|
end
|
356
366
|
begin
|
357
|
-
|
358
|
-
state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
|
359
|
-
details_only_if_tracked)
|
367
|
+
detail = @evaluator.evaluate(f, user).detail
|
360
368
|
rescue => exn
|
369
|
+
detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
|
361
370
|
Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
|
362
|
-
state.add_flag(f, nil, nil, with_reasons ? EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION) : nil,
|
363
|
-
details_only_if_tracked)
|
364
371
|
end
|
372
|
+
|
373
|
+
requires_experiment_data = is_experiment(f, detail.reason)
|
374
|
+
flag_state = {
|
375
|
+
key: f[:key],
|
376
|
+
value: detail.value,
|
377
|
+
variation: detail.variation_index,
|
378
|
+
reason: detail.reason,
|
379
|
+
version: f[:version],
|
380
|
+
trackEvents: f[:trackEvents] || requires_experiment_data,
|
381
|
+
trackReason: requires_experiment_data,
|
382
|
+
debugEventsUntilDate: f[:debugEventsUntilDate],
|
383
|
+
}
|
384
|
+
|
385
|
+
state.add_flag(flag_state, with_reasons, details_only_if_tracked)
|
365
386
|
end
|
366
387
|
|
367
388
|
state
|
@@ -375,9 +396,18 @@ module LaunchDarkly
|
|
375
396
|
@config.logger.info { "[LDClient] Closing LaunchDarkly client..." }
|
376
397
|
@data_source.stop
|
377
398
|
@event_processor.stop
|
399
|
+
@big_segment_store_manager.stop
|
378
400
|
@store.stop
|
379
401
|
end
|
380
402
|
|
403
|
+
#
|
404
|
+
# Returns an interface for tracking the status of a Big Segment store.
|
405
|
+
#
|
406
|
+
# The {BigSegmentStoreStatusProvider} has methods for checking whether the Big Segment store
|
407
|
+
# is (as far as the SDK knows) currently operational and tracking changes in this status.
|
408
|
+
#
|
409
|
+
attr_reader :big_segment_store_status_provider
|
410
|
+
|
381
411
|
private
|
382
412
|
|
383
413
|
def create_default_data_source(sdk_key, config, diagnostic_accumulator)
|
@@ -396,7 +426,7 @@ module LaunchDarkly
|
|
396
426
|
end
|
397
427
|
|
398
428
|
# @return [EvaluationDetail]
|
399
|
-
def evaluate_internal(key, user, default,
|
429
|
+
def evaluate_internal(key, user, default, with_reasons)
|
400
430
|
if @config.offline?
|
401
431
|
return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
402
432
|
end
|
@@ -407,13 +437,19 @@ module LaunchDarkly
|
|
407
437
|
return detail
|
408
438
|
end
|
409
439
|
|
440
|
+
if user[:key].nil?
|
441
|
+
@config.logger.warn { "[LDClient] Variation called with nil user key; returning default value" }
|
442
|
+
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
443
|
+
return detail
|
444
|
+
end
|
445
|
+
|
410
446
|
if !initialized?
|
411
447
|
if @store.initialized?
|
412
448
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
413
449
|
else
|
414
450
|
@config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
|
415
451
|
detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
416
|
-
|
452
|
+
record_unknown_flag_eval(key, user, default, detail.reason, with_reasons)
|
417
453
|
return detail
|
418
454
|
end
|
419
455
|
end
|
@@ -423,32 +459,94 @@ module LaunchDarkly
|
|
423
459
|
if feature.nil?
|
424
460
|
@config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
|
425
461
|
detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
|
426
|
-
|
462
|
+
record_unknown_flag_eval(key, user, default, detail.reason, with_reasons)
|
427
463
|
return detail
|
428
464
|
end
|
429
465
|
|
430
466
|
begin
|
431
|
-
res = @evaluator.evaluate(feature, user
|
432
|
-
if !res.
|
433
|
-
res.
|
434
|
-
|
467
|
+
res = @evaluator.evaluate(feature, user)
|
468
|
+
if !res.prereq_evals.nil?
|
469
|
+
res.prereq_evals.each do |prereq_eval|
|
470
|
+
record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, user, prereq_eval.detail, with_reasons)
|
435
471
|
end
|
436
472
|
end
|
437
473
|
detail = res.detail
|
438
474
|
if detail.default_value?
|
439
475
|
detail = EvaluationDetail.new(default, nil, detail.reason)
|
440
476
|
end
|
441
|
-
|
477
|
+
record_flag_eval(feature, user, detail, default, with_reasons)
|
442
478
|
return detail
|
443
479
|
rescue => exn
|
444
480
|
Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
|
445
481
|
detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
|
446
|
-
|
482
|
+
record_flag_eval_error(feature, user, default, detail.reason, with_reasons)
|
447
483
|
return detail
|
448
484
|
end
|
449
485
|
end
|
450
486
|
|
451
|
-
def
|
487
|
+
private def record_flag_eval(flag, user, detail, default, with_reasons)
|
488
|
+
add_experiment_data = is_experiment(flag, detail.reason)
|
489
|
+
@event_processor.record_eval_event(
|
490
|
+
user,
|
491
|
+
flag[:key],
|
492
|
+
flag[:version],
|
493
|
+
detail.variation_index,
|
494
|
+
detail.value,
|
495
|
+
(add_experiment_data || with_reasons) ? detail.reason : nil,
|
496
|
+
default,
|
497
|
+
add_experiment_data || flag[:trackEvents] || false,
|
498
|
+
flag[:debugEventsUntilDate],
|
499
|
+
nil
|
500
|
+
)
|
501
|
+
end
|
502
|
+
|
503
|
+
private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, user, detail, with_reasons)
|
504
|
+
add_experiment_data = is_experiment(prereq_flag, detail.reason)
|
505
|
+
@event_processor.record_eval_event(
|
506
|
+
user,
|
507
|
+
prereq_flag[:key],
|
508
|
+
prereq_flag[:version],
|
509
|
+
detail.variation_index,
|
510
|
+
detail.value,
|
511
|
+
(add_experiment_data || with_reasons) ? detail.reason : nil,
|
512
|
+
nil,
|
513
|
+
add_experiment_data || prereq_flag[:trackEvents] || false,
|
514
|
+
prereq_flag[:debugEventsUntilDate],
|
515
|
+
prereq_of_flag[:key]
|
516
|
+
)
|
517
|
+
end
|
518
|
+
|
519
|
+
private def record_flag_eval_error(flag, user, default, reason, with_reasons)
|
520
|
+
@event_processor.record_eval_event(user, flag[:key], flag[:version], nil, default, with_reasons ? reason : nil, default,
|
521
|
+
flag[:trackEvents], flag[:debugEventsUntilDate], nil)
|
522
|
+
end
|
523
|
+
|
524
|
+
private def record_unknown_flag_eval(flag_key, user, default, reason, with_reasons)
|
525
|
+
@event_processor.record_eval_event(user, flag_key, nil, nil, default, with_reasons ? reason : nil, default,
|
526
|
+
false, nil, nil)
|
527
|
+
end
|
528
|
+
|
529
|
+
private def is_experiment(flag, reason)
|
530
|
+
return false if !reason
|
531
|
+
|
532
|
+
if reason.in_experiment
|
533
|
+
return true
|
534
|
+
end
|
535
|
+
|
536
|
+
case reason[:kind]
|
537
|
+
when 'RULE_MATCH'
|
538
|
+
index = reason[:ruleIndex]
|
539
|
+
if !index.nil?
|
540
|
+
rules = flag[:rules] || []
|
541
|
+
return index >= 0 && index < rules.length && rules[index][:trackEvents]
|
542
|
+
end
|
543
|
+
when 'FALLTHROUGH'
|
544
|
+
return !!flag[:trackEventsFallthrough]
|
545
|
+
end
|
546
|
+
false
|
547
|
+
end
|
548
|
+
|
549
|
+
private def sanitize_user(user)
|
452
550
|
if user[:key]
|
453
551
|
user[:key] = user[:key].to_s
|
454
552
|
end
|
data/lib/ldclient-rb/polling.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "ldclient-rb/impl/repeating_task"
|
2
|
+
|
1
3
|
require "concurrent/atomics"
|
2
4
|
require "thread"
|
3
5
|
|
@@ -9,8 +11,8 @@ module LaunchDarkly
|
|
9
11
|
@requestor = requestor
|
10
12
|
@initialized = Concurrent::AtomicBoolean.new(false)
|
11
13
|
@started = Concurrent::AtomicBoolean.new(false)
|
12
|
-
@stopped = Concurrent::AtomicBoolean.new(false)
|
13
14
|
@ready = Concurrent::Event.new
|
15
|
+
@task = Impl::RepeatingTask.new(@config.poll_interval, 0, -> { self.poll }, @config.logger)
|
14
16
|
end
|
15
17
|
|
16
18
|
def initialized?
|
@@ -20,56 +22,35 @@ module LaunchDarkly
|
|
20
22
|
def start
|
21
23
|
return @ready unless @started.make_true
|
22
24
|
@config.logger.info { "[LDClient] Initializing polling connection" }
|
23
|
-
|
25
|
+
@task.start
|
24
26
|
@ready
|
25
27
|
end
|
26
28
|
|
27
29
|
def stop
|
28
|
-
|
29
|
-
|
30
|
-
@worker.run # causes the thread to wake up if it's currently in a sleep
|
31
|
-
@worker.join
|
32
|
-
end
|
33
|
-
@config.logger.info { "[LDClient] Polling connection stopped" }
|
34
|
-
end
|
30
|
+
@task.stop
|
31
|
+
@config.logger.info { "[LDClient] Polling connection stopped" }
|
35
32
|
end
|
36
33
|
|
37
34
|
def poll
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
@
|
43
|
-
|
44
|
-
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
def create_worker
|
49
|
-
@worker = Thread.new do
|
50
|
-
@config.logger.debug { "[LDClient] Starting polling worker" }
|
51
|
-
while !@stopped.value do
|
52
|
-
started_at = Time.now
|
53
|
-
begin
|
54
|
-
poll
|
55
|
-
rescue UnexpectedResponseError => e
|
56
|
-
message = Util.http_error_message(e.status, "polling request", "will retry")
|
57
|
-
@config.logger.error { "[LDClient] #{message}" };
|
58
|
-
if !Util.http_error_recoverable?(e.status)
|
59
|
-
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
60
|
-
stop
|
61
|
-
end
|
62
|
-
rescue StandardError => exn
|
63
|
-
Util.log_exception(@config.logger, "Exception while polling", exn)
|
64
|
-
end
|
65
|
-
delta = @config.poll_interval - (Time.now - started_at)
|
66
|
-
if delta > 0
|
67
|
-
sleep(delta)
|
35
|
+
begin
|
36
|
+
all_data = @requestor.request_all_data
|
37
|
+
if all_data
|
38
|
+
@config.feature_store.init(all_data)
|
39
|
+
if @initialized.make_true
|
40
|
+
@config.logger.info { "[LDClient] Polling connection initialized" }
|
41
|
+
@ready.set
|
68
42
|
end
|
69
43
|
end
|
44
|
+
rescue UnexpectedResponseError => e
|
45
|
+
message = Util.http_error_message(e.status, "polling request", "will retry")
|
46
|
+
@config.logger.error { "[LDClient] #{message}" };
|
47
|
+
if !Util.http_error_recoverable?(e.status)
|
48
|
+
@ready.set # if client was waiting on us, make it stop waiting - has no effect if already set
|
49
|
+
stop
|
50
|
+
end
|
51
|
+
rescue StandardError => e
|
52
|
+
Util.log_exception(@config.logger, "Exception while polling", e)
|
70
53
|
end
|
71
54
|
end
|
72
|
-
|
73
|
-
private :poll, :create_worker
|
74
55
|
end
|
75
56
|
end
|
@@ -31,7 +31,7 @@ module LaunchDarkly
|
|
31
31
|
|
32
32
|
def request_all_data()
|
33
33
|
all_data = JSON.parse(make_request("/sdk/latest-all"), symbolize_names: true)
|
34
|
-
Impl::Model.make_all_store_data(all_data)
|
34
|
+
Impl::Model.make_all_store_data(all_data, @config.logger)
|
35
35
|
end
|
36
36
|
|
37
37
|
def stop
|
@@ -44,7 +44,7 @@ module LaunchDarkly
|
|
44
44
|
private
|
45
45
|
|
46
46
|
def request_single_item(kind, path)
|
47
|
-
Impl::Model.deserialize(kind, make_request(path))
|
47
|
+
Impl::Model.deserialize(kind, make_request(path), @config.logger)
|
48
48
|
end
|
49
49
|
|
50
50
|
def make_request(path)
|
@@ -60,9 +60,9 @@ module LaunchDarkly
|
|
60
60
|
headers: headers
|
61
61
|
})
|
62
62
|
status = response.status.code
|
63
|
-
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers}\n\tbody: #{res.to_s}" }
|
64
63
|
# must fully read body for persistent connections
|
65
64
|
body = response.to_s
|
65
|
+
@config.logger.debug { "[LDClient] Got response from uri: #{uri}\n\tstatus code: #{status}\n\theaders: #{response.headers.to_h}\n\tbody: #{body}" }
|
66
66
|
if status == 304 && !cached.nil?
|
67
67
|
body = cached.body
|
68
68
|
else
|