launchdarkly-server-sdk 6.3.0 → 8.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +3 -4
- data/lib/ldclient-rb/config.rb +112 -62
- data/lib/ldclient-rb/context.rb +444 -0
- data/lib/ldclient-rb/evaluation_detail.rb +26 -22
- data/lib/ldclient-rb/events.rb +256 -146
- data/lib/ldclient-rb/flags_state.rb +26 -15
- data/lib/ldclient-rb/impl/big_segments.rb +18 -18
- data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
- data/lib/ldclient-rb/impl/context.rb +96 -0
- data/lib/ldclient-rb/impl/context_filter.rb +145 -0
- data/lib/ldclient-rb/impl/data_source.rb +188 -0
- data/lib/ldclient-rb/impl/data_store.rb +59 -0
- data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
- data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
- data/lib/ldclient-rb/impl/evaluator.rb +386 -142
- data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
- data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
- data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
- data/lib/ldclient-rb/impl/event_sender.rb +7 -6
- data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
- data/lib/ldclient-rb/impl/event_types.rb +136 -0
- data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
- data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
- data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
- data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
- data/lib/ldclient-rb/impl/model/clause.rb +45 -0
- data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
- data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
- data/lib/ldclient-rb/impl/model/segment.rb +132 -0
- data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
- data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
- data/lib/ldclient-rb/impl/sampler.rb +25 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
- data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
- data/lib/ldclient-rb/impl/util.rb +59 -1
- data/lib/ldclient-rb/in_memory_store.rb +9 -2
- data/lib/ldclient-rb/integrations/consul.rb +2 -2
- data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
- data/lib/ldclient-rb/integrations/file_data.rb +4 -4
- data/lib/ldclient-rb/integrations/redis.rb +5 -5
- data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
- data/lib/ldclient-rb/integrations/test_data.rb +18 -14
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
- data/lib/ldclient-rb/interfaces.rb +600 -14
- data/lib/ldclient-rb/ldclient.rb +314 -134
- data/lib/ldclient-rb/memoized_value.rb +1 -1
- data/lib/ldclient-rb/migrations.rb +230 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
- data/lib/ldclient-rb/polling.rb +52 -6
- data/lib/ldclient-rb/reference.rb +274 -0
- data/lib/ldclient-rb/requestor.rb +9 -11
- data/lib/ldclient-rb/stream.rb +96 -34
- data/lib/ldclient-rb/util.rb +97 -14
- data/lib/ldclient-rb/version.rb +1 -1
- data/lib/ldclient-rb.rb +3 -4
- metadata +65 -23
- data/lib/ldclient-rb/event_summarizer.rb +0 -55
- data/lib/ldclient-rb/file_data_source.rb +0 -23
- data/lib/ldclient-rb/impl/event_factory.rb +0 -126
- data/lib/ldclient-rb/newrelic.rb +0 -17
- data/lib/ldclient-rb/redis_store.rb +0 -88
- data/lib/ldclient-rb/user_filter.rb +0 -52
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
1
|
require "ldclient-rb/impl/big_segments"
|
2
|
+
require "ldclient-rb/impl/broadcaster"
|
3
|
+
require "ldclient-rb/impl/data_source"
|
4
|
+
require "ldclient-rb/impl/data_store"
|
2
5
|
require "ldclient-rb/impl/diagnostic_events"
|
3
6
|
require "ldclient-rb/impl/evaluator"
|
4
|
-
require "ldclient-rb/impl/
|
7
|
+
require "ldclient-rb/impl/flag_tracker"
|
5
8
|
require "ldclient-rb/impl/store_client_wrapper"
|
9
|
+
require "ldclient-rb/impl/migrations/tracker"
|
6
10
|
require "concurrent/atomics"
|
7
11
|
require "digest/sha1"
|
12
|
+
require "forwardable"
|
8
13
|
require "logger"
|
9
14
|
require "benchmark"
|
10
15
|
require "json"
|
@@ -17,6 +22,10 @@ module LaunchDarkly
|
|
17
22
|
#
|
18
23
|
class LDClient
|
19
24
|
include Impl
|
25
|
+
extend Forwardable
|
26
|
+
|
27
|
+
def_delegators :@config, :logger
|
28
|
+
|
20
29
|
#
|
21
30
|
# Creates a new client instance that connects to LaunchDarkly. A custom
|
22
31
|
# configuration parameter can also supplied to specify advanced options,
|
@@ -46,26 +55,30 @@ module LaunchDarkly
|
|
46
55
|
|
47
56
|
@sdk_key = sdk_key
|
48
57
|
|
49
|
-
@
|
50
|
-
|
58
|
+
@shared_executor = Concurrent::SingleThreadExecutor.new
|
59
|
+
|
60
|
+
data_store_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, config.logger)
|
61
|
+
store_sink = LaunchDarkly::Impl::DataStore::UpdateSink.new(data_store_broadcaster)
|
51
62
|
|
52
63
|
# We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
|
53
64
|
# some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
|
54
65
|
# the feature store through the Config object, so we need to make a new Config that uses
|
55
66
|
# the wrapped store.
|
56
|
-
@store = Impl::FeatureStoreClientWrapper.new(config.feature_store)
|
67
|
+
@store = Impl::FeatureStoreClientWrapper.new(config.feature_store, store_sink, config.logger)
|
57
68
|
updated_config = config.clone
|
58
69
|
updated_config.instance_variable_set(:@feature_store, @store)
|
59
70
|
@config = updated_config
|
60
71
|
|
72
|
+
@data_store_status_provider = LaunchDarkly::Impl::DataStore::StatusProvider.new(@store, store_sink)
|
73
|
+
|
61
74
|
@big_segment_store_manager = Impl::BigSegmentStoreManager.new(config.big_segments, @config.logger)
|
62
75
|
@big_segment_store_status_provider = @big_segment_store_manager.status_provider
|
63
76
|
|
64
77
|
get_flag = lambda { |key| @store.get(FEATURES, key) }
|
65
78
|
get_segment = lambda { |key| @store.get(SEGMENTS, key) }
|
66
|
-
get_big_segments_membership = lambda { |key| @big_segment_store_manager.
|
79
|
+
get_big_segments_membership = lambda { |key| @big_segment_store_manager.get_context_membership(key) }
|
67
80
|
@evaluator = LaunchDarkly::Impl::Evaluator.new(get_flag, get_segment, get_big_segments_membership, @config.logger)
|
68
|
-
|
81
|
+
|
69
82
|
if !@config.offline? && @config.send_events && !@config.diagnostic_opt_out?
|
70
83
|
diagnostic_accumulator = Impl::DiagnosticAccumulator.new(Impl::DiagnosticAccumulator.create_diagnostic_id(sdk_key))
|
71
84
|
else
|
@@ -83,6 +96,16 @@ module LaunchDarkly
|
|
83
96
|
return # requestor and update processor are not used in this mode
|
84
97
|
end
|
85
98
|
|
99
|
+
flag_tracker_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger)
|
100
|
+
@flag_tracker = LaunchDarkly::Impl::FlagTracker.new(flag_tracker_broadcaster, lambda { |key, context| variation(key, context, nil) })
|
101
|
+
|
102
|
+
data_source_broadcaster = LaunchDarkly::Impl::Broadcaster.new(@shared_executor, @config.logger)
|
103
|
+
|
104
|
+
# Make the update sink available on the config so that our data source factory can access the sink with a shared executor.
|
105
|
+
@config.data_source_update_sink = LaunchDarkly::Impl::DataSource::UpdateSink.new(@store, data_source_broadcaster, flag_tracker_broadcaster)
|
106
|
+
|
107
|
+
@data_source_status_provider = LaunchDarkly::Impl::DataSource::StatusProvider.new(data_source_broadcaster, @config.data_source_update_sink)
|
108
|
+
|
86
109
|
data_source_or_factory = @config.data_source || self.method(:create_default_data_source)
|
87
110
|
if data_source_or_factory.respond_to? :call
|
88
111
|
# Currently, data source factories take two parameters unless they need to be aware of diagnostic_accumulator, in
|
@@ -124,26 +147,20 @@ module LaunchDarkly
|
|
124
147
|
end
|
125
148
|
|
126
149
|
#
|
127
|
-
#
|
128
|
-
# @param user [Hash] the user properties
|
129
|
-
# @param default [Boolean] (false) the value to use if the flag cannot be evaluated
|
130
|
-
# @return [Boolean] the flag value
|
131
|
-
# @deprecated Use {#variation} instead.
|
132
|
-
#
|
133
|
-
def toggle?(key, user, default = false)
|
134
|
-
@config.logger.warn { "[LDClient] toggle? is deprecated. Use variation instead" }
|
135
|
-
variation(key, user, default)
|
136
|
-
end
|
137
|
-
|
138
|
-
#
|
139
|
-
# Creates a hash string that can be used by the JavaScript SDK to identify a user.
|
150
|
+
# Creates a hash string that can be used by the JavaScript SDK to identify a context.
|
140
151
|
# For more information, see [Secure mode](https://docs.launchdarkly.com/sdk/features/secure-mode#ruby).
|
141
152
|
#
|
142
|
-
# @param
|
143
|
-
# @return [String] a hash string
|
153
|
+
# @param context [Hash, LDContext]
|
154
|
+
# @return [String, nil] a hash string or nil if the provided context was invalid
|
144
155
|
#
|
145
|
-
def secure_mode_hash(
|
146
|
-
|
156
|
+
def secure_mode_hash(context)
|
157
|
+
context = Impl::Context.make_context(context)
|
158
|
+
unless context.valid?
|
159
|
+
@config.logger.warn("secure_mode_hash called with invalid context: #{context.error}")
|
160
|
+
return nil
|
161
|
+
end
|
162
|
+
|
163
|
+
OpenSSL::HMAC.hexdigest("sha256", @sdk_key, context.fully_qualified_key)
|
147
164
|
end
|
148
165
|
|
149
166
|
#
|
@@ -169,44 +186,23 @@ module LaunchDarkly
|
|
169
186
|
end
|
170
187
|
|
171
188
|
#
|
172
|
-
# Determines the variation of a feature flag to present
|
173
|
-
#
|
174
|
-
# At a minimum, the user hash should contain a `:key`, which should be the unique
|
175
|
-
# identifier for your user (or, for an anonymous user, a session identifier or
|
176
|
-
# cookie).
|
177
|
-
#
|
178
|
-
# Other supported user attributes include IP address, country code, and an arbitrary hash of
|
179
|
-
# custom attributes. For more about the supported user properties and how they work in
|
180
|
-
# LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/home/flags/targeting-users).
|
181
|
-
#
|
182
|
-
# The optional `:privateAttributeNames` user property allows you to specify a list of
|
183
|
-
# attribute names that should not be sent back to LaunchDarkly.
|
184
|
-
# [Private attributes](https://docs.launchdarkly.com/home/users/attributes#creating-private-user-attributes)
|
185
|
-
# can also be configured globally in {Config}.
|
186
|
-
#
|
187
|
-
# @example Basic user hash
|
188
|
-
# {key: "my-user-id"}
|
189
|
-
#
|
190
|
-
# @example More complete user hash
|
191
|
-
# {key: "my-user-id", ip: "127.0.0.1", country: "US", custom: {customer_rank: 1000}}
|
192
|
-
#
|
193
|
-
# @example User with a private attribute
|
194
|
-
# {key: "my-user-id", email: "email@example.com", privateAttributeNames: ["email"]}
|
189
|
+
# Determines the variation of a feature flag to present for a context.
|
195
190
|
#
|
196
191
|
# @param key [String] the unique feature key for the feature flag, as shown
|
197
192
|
# on the LaunchDarkly dashboard
|
198
|
-
# @param
|
193
|
+
# @param context [Hash, LDContext] a hash or LDContext instance describing the context requesting the flag
|
199
194
|
# @param default the default value of the flag; this is used if there is an error
|
200
195
|
# condition making it impossible to find or evaluate the flag
|
201
196
|
#
|
202
|
-
# @return the variation
|
197
|
+
# @return the variation for the provided context, or the default value if there's an error
|
203
198
|
#
|
204
|
-
def variation(key,
|
205
|
-
|
199
|
+
def variation(key, context, default)
|
200
|
+
detail, _, _, = variation_with_flag(key, context, default)
|
201
|
+
detail.value
|
206
202
|
end
|
207
203
|
|
208
204
|
#
|
209
|
-
# Determines the variation of a feature flag for a
|
205
|
+
# Determines the variation of a feature flag for a context, like {#variation}, but also
|
210
206
|
# provides additional information about how this value was calculated.
|
211
207
|
#
|
212
208
|
# The return value of `variation_detail` is an {EvaluationDetail} object, which has
|
@@ -222,43 +218,84 @@ module LaunchDarkly
|
|
222
218
|
#
|
223
219
|
# @param key [String] the unique feature key for the feature flag, as shown
|
224
220
|
# on the LaunchDarkly dashboard
|
225
|
-
# @param
|
221
|
+
# @param context [Hash, LDContext] a hash or object describing the context requesting the flag,
|
226
222
|
# @param default the default value of the flag; this is used if there is an error
|
227
223
|
# condition making it impossible to find or evaluate the flag
|
228
224
|
#
|
229
225
|
# @return [EvaluationDetail] an object describing the result
|
230
226
|
#
|
231
|
-
def variation_detail(key,
|
232
|
-
evaluate_internal(key,
|
227
|
+
def variation_detail(key, context, default)
|
228
|
+
detail, _, _ = evaluate_internal(key, context, default, true)
|
229
|
+
detail
|
233
230
|
end
|
234
231
|
|
235
232
|
#
|
236
|
-
#
|
237
|
-
#
|
233
|
+
# This method returns the migration stage of the migration feature flag for the given evaluation context.
|
234
|
+
#
|
235
|
+
# This method returns the default stage if there is an error or the flag does not exist. If the default stage is not
|
236
|
+
# a valid stage, then a default stage of 'off' will be used instead.
|
238
237
|
#
|
239
|
-
#
|
238
|
+
# @param key [String]
|
239
|
+
# @param context [LDContext]
|
240
|
+
# @param default_stage [Symbol]
|
241
|
+
#
|
242
|
+
# @return [Array<Symbol, Interfaces::Migrations::OpTracker>]
|
243
|
+
#
|
244
|
+
def migration_variation(key, context, default_stage)
|
245
|
+
unless Migrations::VALID_STAGES.include? default_stage
|
246
|
+
@config.logger.error { "[LDClient] default_stage #{default_stage} is not a valid stage; continuing with 'off' as default" }
|
247
|
+
default_stage = Migrations::STAGE_OFF
|
248
|
+
end
|
249
|
+
|
250
|
+
context = Impl::Context::make_context(context)
|
251
|
+
detail, flag, _ = variation_with_flag(key, context, default_stage.to_s)
|
252
|
+
|
253
|
+
stage = detail.value
|
254
|
+
stage = stage.to_sym if stage.respond_to? :to_sym
|
255
|
+
|
256
|
+
if Migrations::VALID_STAGES.include?(stage)
|
257
|
+
tracker = Impl::Migrations::OpTracker.new(@config.logger, key, flag, context, detail, default_stage)
|
258
|
+
return stage, tracker
|
259
|
+
end
|
260
|
+
|
261
|
+
detail = LaunchDarkly::Impl::Evaluator.error_result(LaunchDarkly::EvaluationReason::ERROR_WRONG_TYPE, default_stage.to_s)
|
262
|
+
tracker = Impl::Migrations::OpTracker.new(@config.logger, key, flag, context, detail, default_stage)
|
263
|
+
|
264
|
+
[default_stage, tracker]
|
265
|
+
end
|
266
|
+
|
267
|
+
#
|
268
|
+
# Registers the context. This method simply creates an analytics event containing the context
|
269
|
+
# properties, so that LaunchDarkly will know about that context if it does not already.
|
270
|
+
#
|
271
|
+
# Calling {#variation} or {#variation_detail} also sends the context information to
|
240
272
|
# LaunchDarkly (if events are enabled), so you only need to use {#identify} if you
|
241
|
-
# want to identify the
|
273
|
+
# want to identify the context without evaluating a flag.
|
242
274
|
#
|
243
275
|
# Note that event delivery is asynchronous, so the event may not actually be sent
|
244
276
|
# until later; see {#flush}.
|
245
277
|
#
|
246
|
-
# @param
|
247
|
-
# described in {#variation}
|
278
|
+
# @param context [Hash, LDContext] a hash or object describing the context to register
|
248
279
|
# @return [void]
|
249
280
|
#
|
250
|
-
def identify(
|
251
|
-
|
252
|
-
|
281
|
+
def identify(context)
|
282
|
+
context = LaunchDarkly::Impl::Context.make_context(context)
|
283
|
+
unless context.valid?
|
284
|
+
@config.logger.warn("Identify called with invalid context: #{context.error}")
|
253
285
|
return
|
254
286
|
end
|
255
|
-
|
256
|
-
|
287
|
+
|
288
|
+
if context.key == ""
|
289
|
+
@config.logger.warn("Identify called with empty key")
|
290
|
+
return
|
291
|
+
end
|
292
|
+
|
293
|
+
@event_processor.record_identify_event(context)
|
257
294
|
end
|
258
295
|
|
259
296
|
#
|
260
|
-
# Tracks that a
|
261
|
-
# containing the specified event name (key),
|
297
|
+
# Tracks that a context performed an event. This method creates a "custom" analytics event
|
298
|
+
# containing the specified event name (key), context properties, and optional data.
|
262
299
|
#
|
263
300
|
# Note that event delivery is asynchronous, so the event may not actually be sent
|
264
301
|
# until later; see {#flush}.
|
@@ -269,8 +306,7 @@ module LaunchDarkly
|
|
269
306
|
# for the latest status.
|
270
307
|
#
|
271
308
|
# @param event_name [String] The name of the event
|
272
|
-
# @param
|
273
|
-
# described in {#variation}
|
309
|
+
# @param context [Hash, LDContext] a hash or object describing the context to track
|
274
310
|
# @param data [Hash] An optional hash containing any additional data associated with the event
|
275
311
|
# @param metric_value [Number] A numeric value used by the LaunchDarkly experimentation
|
276
312
|
# feature in numeric custom metrics. Can be omitted if this event is used by only
|
@@ -278,52 +314,48 @@ module LaunchDarkly
|
|
278
314
|
# for Data Export.
|
279
315
|
# @return [void]
|
280
316
|
#
|
281
|
-
def track(event_name,
|
282
|
-
|
283
|
-
|
317
|
+
def track(event_name, context, data = nil, metric_value = nil)
|
318
|
+
context = LaunchDarkly::Impl::Context.make_context(context)
|
319
|
+
unless context.valid?
|
320
|
+
@config.logger.warn("Track called with invalid context: #{context.error}")
|
284
321
|
return
|
285
322
|
end
|
286
|
-
|
287
|
-
@event_processor.
|
323
|
+
|
324
|
+
@event_processor.record_custom_event(context, event_name, data, metric_value)
|
288
325
|
end
|
289
326
|
|
290
327
|
#
|
291
|
-
#
|
328
|
+
# Tracks the results of a migrations operation. This event includes measurements which can be used to enhance the
|
329
|
+
# observability of a migration within the LaunchDarkly UI.
|
292
330
|
#
|
293
|
-
#
|
294
|
-
#
|
295
|
-
#
|
331
|
+
# This event should be generated through {Interfaces::Migrations::OpTracker}. If you are using the
|
332
|
+
# {Interfaces::Migrations::Migrator} to handle migrations, this event will be created and emitted
|
333
|
+
# automatically.
|
334
|
+
#
|
335
|
+
# @param tracker [LaunchDarkly::Interfaces::Migrations::OpTracker]
|
296
336
|
#
|
297
|
-
def
|
298
|
-
|
299
|
-
@config.logger.
|
337
|
+
def track_migration_op(tracker)
|
338
|
+
unless tracker.is_a? LaunchDarkly::Interfaces::Migrations::OpTracker
|
339
|
+
@config.logger.error { "invalid op tracker received in track_migration_op" }
|
300
340
|
return
|
301
341
|
end
|
302
|
-
sanitize_user(current_context)
|
303
|
-
sanitize_user(previous_context)
|
304
|
-
@event_processor.add_event(@event_factory_default.new_alias_event(current_context, previous_context))
|
305
|
-
end
|
306
342
|
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
# @return [Hash] a hash of feature flag keys to values
|
316
|
-
#
|
317
|
-
def all_flags(user)
|
318
|
-
all_flags_state(user).values_map
|
343
|
+
event = tracker.build
|
344
|
+
if event.is_a? String
|
345
|
+
@config.logger.error { "[LDClient] Error occurred generating migration op event; #{event}" }
|
346
|
+
return
|
347
|
+
end
|
348
|
+
|
349
|
+
|
350
|
+
@event_processor.record_migration_op_event(event)
|
319
351
|
end
|
320
352
|
|
321
353
|
#
|
322
|
-
# Returns a {FeatureFlagsState} object that encapsulates the state of all feature flags for a given
|
354
|
+
# Returns a {FeatureFlagsState} object that encapsulates the state of all feature flags for a given context,
|
323
355
|
# including the flag values and also metadata that can be used on the front end. This method does not
|
324
356
|
# send analytics events back to LaunchDarkly.
|
325
357
|
#
|
326
|
-
# @param
|
358
|
+
# @param context [Hash, LDContext] a hash or object describing the context requesting the flags,
|
327
359
|
# @param options [Hash] Optional parameters to control how the state is generated
|
328
360
|
# @option options [Boolean] :client_side_only (false) True if only flags marked for use with the
|
329
361
|
# client-side SDK should be included in the state. By default, all flags are included.
|
@@ -335,11 +367,21 @@ module LaunchDarkly
|
|
335
367
|
# of the JSON data if you are passing the flag state to the front end.
|
336
368
|
# @return [FeatureFlagsState] a {FeatureFlagsState} object which can be serialized to JSON
|
337
369
|
#
|
338
|
-
def all_flags_state(
|
370
|
+
def all_flags_state(context, options={})
|
339
371
|
return FeatureFlagsState.new(false) if @config.offline?
|
340
372
|
|
341
|
-
unless
|
342
|
-
@
|
373
|
+
unless initialized?
|
374
|
+
if @store.initialized?
|
375
|
+
@config.logger.warn { "Called all_flags_state before client initialization; using last known values from data store" }
|
376
|
+
else
|
377
|
+
@config.logger.warn { "Called all_flags_state before client initialization. Data store not available; returning empty state" }
|
378
|
+
return FeatureFlagsState.new(false)
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
context = Impl::Context::make_context(context)
|
383
|
+
unless context.valid?
|
384
|
+
@config.logger.error { "[LDClient] Context was invalid for all_flags_state (#{context.error})" }
|
343
385
|
return FeatureFlagsState.new(false)
|
344
386
|
end
|
345
387
|
|
@@ -359,14 +401,25 @@ module LaunchDarkly
|
|
359
401
|
next
|
360
402
|
end
|
361
403
|
begin
|
362
|
-
|
363
|
-
state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
|
364
|
-
details_only_if_tracked)
|
404
|
+
detail = @evaluator.evaluate(f, context).detail
|
365
405
|
rescue => exn
|
406
|
+
detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION))
|
366
407
|
Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
|
367
|
-
state.add_flag(f, nil, nil, with_reasons ? EvaluationReason::error(EvaluationReason::ERROR_EXCEPTION) : nil,
|
368
|
-
details_only_if_tracked)
|
369
408
|
end
|
409
|
+
|
410
|
+
requires_experiment_data = experiment?(f, detail.reason)
|
411
|
+
flag_state = {
|
412
|
+
key: f[:key],
|
413
|
+
value: detail.value,
|
414
|
+
variation: detail.variation_index,
|
415
|
+
reason: detail.reason,
|
416
|
+
version: f[:version],
|
417
|
+
trackEvents: f[:trackEvents] || requires_experiment_data,
|
418
|
+
trackReason: requires_experiment_data,
|
419
|
+
debugEventsUntilDate: f[:debugEventsUntilDate],
|
420
|
+
}
|
421
|
+
|
422
|
+
state.add_flag(flag_state, with_reasons, details_only_if_tracked)
|
370
423
|
end
|
371
424
|
|
372
425
|
state
|
@@ -382,16 +435,53 @@ module LaunchDarkly
|
|
382
435
|
@event_processor.stop
|
383
436
|
@big_segment_store_manager.stop
|
384
437
|
@store.stop
|
438
|
+
@shared_executor.shutdown
|
385
439
|
end
|
386
440
|
|
387
441
|
#
|
388
442
|
# Returns an interface for tracking the status of a Big Segment store.
|
389
443
|
#
|
390
|
-
# The {BigSegmentStoreStatusProvider} has methods for checking whether the Big Segment store
|
444
|
+
# The {Interfaces::BigSegmentStoreStatusProvider} has methods for checking whether the Big Segment store
|
391
445
|
# is (as far as the SDK knows) currently operational and tracking changes in this status.
|
392
446
|
#
|
393
447
|
attr_reader :big_segment_store_status_provider
|
394
448
|
|
449
|
+
#
|
450
|
+
# Returns an interface for tracking the status of a persistent data store.
|
451
|
+
#
|
452
|
+
# The {LaunchDarkly::Interfaces::DataStore::StatusProvider} has methods for
|
453
|
+
# checking whether the data store is (as far as the SDK knows) currently
|
454
|
+
# operational, tracking changes in this status, and getting cache
|
455
|
+
# statistics. These are only relevant for a persistent data store; if you
|
456
|
+
# are using an in-memory data store, then this method will return a stub
|
457
|
+
# object that provides no information.
|
458
|
+
#
|
459
|
+
# @return [LaunchDarkly::Interfaces::DataStore::StatusProvider]
|
460
|
+
#
|
461
|
+
attr_reader :data_store_status_provider
|
462
|
+
|
463
|
+
#
|
464
|
+
# Returns an interface for tracking the status of the data source.
|
465
|
+
#
|
466
|
+
# The data source is the mechanism that the SDK uses to get feature flag
|
467
|
+
# configurations, such as a streaming connection (the default) or poll
|
468
|
+
# requests. The {LaunchDarkly::Interfaces::DataSource::StatusProvider} has
|
469
|
+
# methods for checking whether the data source is (as far as the SDK knows)
|
470
|
+
# currently operational and tracking changes in this status.
|
471
|
+
#
|
472
|
+
# @return [LaunchDarkly::Interfaces::DataSource::StatusProvider]
|
473
|
+
#
|
474
|
+
attr_reader :data_source_status_provider
|
475
|
+
|
476
|
+
#
|
477
|
+
# Returns an interface for tracking changes in feature flag configurations.
|
478
|
+
#
|
479
|
+
# The {LaunchDarkly::Interfaces::FlagTracker} contains methods for
|
480
|
+
# requesting notifications about feature flag changes using an event
|
481
|
+
# listener model.
|
482
|
+
#
|
483
|
+
attr_reader :flag_tracker
|
484
|
+
|
395
485
|
private
|
396
486
|
|
397
487
|
def create_default_data_source(sdk_key, config, diagnostic_accumulator)
|
@@ -409,69 +499,159 @@ module LaunchDarkly
|
|
409
499
|
end
|
410
500
|
end
|
411
501
|
|
412
|
-
#
|
413
|
-
|
502
|
+
#
|
503
|
+
# @param key [String]
|
504
|
+
# @param context [Hash, LDContext]
|
505
|
+
# @param default [Object]
|
506
|
+
#
|
507
|
+
# @return [Array<EvaluationDetail, [LaunchDarkly::Impl::Model::FeatureFlag, nil], [String, nil]>]
|
508
|
+
#
|
509
|
+
def variation_with_flag(key, context, default)
|
510
|
+
evaluate_internal(key, context, default, false)
|
511
|
+
end
|
512
|
+
|
513
|
+
#
|
514
|
+
# @param key [String]
|
515
|
+
# @param context [Hash, LDContext]
|
516
|
+
# @param default [Object]
|
517
|
+
# @param with_reasons [Boolean]
|
518
|
+
#
|
519
|
+
# @return [Array<EvaluationDetail, [LaunchDarkly::Impl::Model::FeatureFlag, nil], [String, nil]>]
|
520
|
+
#
|
521
|
+
def evaluate_internal(key, context, default, with_reasons)
|
414
522
|
if @config.offline?
|
415
|
-
return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
523
|
+
return Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default), nil, nil
|
416
524
|
end
|
417
525
|
|
418
|
-
|
419
|
-
@config.logger.error { "[LDClient] Must specify
|
526
|
+
if context.nil?
|
527
|
+
@config.logger.error { "[LDClient] Must specify context" }
|
420
528
|
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
421
|
-
return detail
|
529
|
+
return detail, nil, "no context provided"
|
422
530
|
end
|
423
531
|
|
424
|
-
|
425
|
-
|
532
|
+
context = Impl::Context::make_context(context)
|
533
|
+
unless context.valid?
|
534
|
+
@config.logger.error { "[LDClient] Context was invalid for evaluation of flag '#{key}' (#{context.error}); returning default value" }
|
426
535
|
detail = Evaluator.error_result(EvaluationReason::ERROR_USER_NOT_SPECIFIED, default)
|
427
|
-
return detail
|
536
|
+
return detail, nil, context.error
|
428
537
|
end
|
429
538
|
|
430
|
-
|
539
|
+
unless initialized?
|
431
540
|
if @store.initialized?
|
432
541
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
433
542
|
else
|
434
543
|
@config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
|
435
544
|
detail = Evaluator.error_result(EvaluationReason::ERROR_CLIENT_NOT_READY, default)
|
436
|
-
|
437
|
-
return
|
545
|
+
record_unknown_flag_eval(key, context, default, detail.reason, with_reasons)
|
546
|
+
return detail, nil, "client not initialized"
|
438
547
|
end
|
439
548
|
end
|
440
549
|
|
441
|
-
|
550
|
+
begin
|
551
|
+
feature = @store.get(FEATURES, key)
|
552
|
+
rescue
|
553
|
+
# Ignored
|
554
|
+
end
|
442
555
|
|
443
556
|
if feature.nil?
|
444
557
|
@config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
|
445
558
|
detail = Evaluator.error_result(EvaluationReason::ERROR_FLAG_NOT_FOUND, default)
|
446
|
-
|
447
|
-
return detail
|
559
|
+
record_unknown_flag_eval(key, context, default, detail.reason, with_reasons)
|
560
|
+
return detail, nil, "feature flag not found"
|
448
561
|
end
|
449
562
|
|
450
563
|
begin
|
451
|
-
res = @evaluator.evaluate(feature,
|
452
|
-
|
453
|
-
res.
|
454
|
-
|
564
|
+
res = @evaluator.evaluate(feature, context)
|
565
|
+
unless res.prereq_evals.nil?
|
566
|
+
res.prereq_evals.each do |prereq_eval|
|
567
|
+
record_prereq_flag_eval(prereq_eval.prereq_flag, prereq_eval.prereq_of_flag, context, prereq_eval.detail, with_reasons)
|
455
568
|
end
|
456
569
|
end
|
457
570
|
detail = res.detail
|
458
571
|
if detail.default_value?
|
459
572
|
detail = EvaluationDetail.new(default, nil, detail.reason)
|
460
573
|
end
|
461
|
-
|
462
|
-
|
574
|
+
record_flag_eval(feature, context, detail, default, with_reasons)
|
575
|
+
[detail, feature, nil]
|
463
576
|
rescue => exn
|
464
577
|
Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
|
465
578
|
detail = Evaluator.error_result(EvaluationReason::ERROR_EXCEPTION, default)
|
466
|
-
|
467
|
-
|
579
|
+
record_flag_eval_error(feature, context, default, detail.reason, with_reasons)
|
580
|
+
[detail, feature, exn.to_s]
|
468
581
|
end
|
469
582
|
end
|
470
583
|
|
471
|
-
def
|
472
|
-
|
473
|
-
|
584
|
+
private def record_flag_eval(flag, context, detail, default, with_reasons)
|
585
|
+
add_experiment_data = experiment?(flag, detail.reason)
|
586
|
+
@event_processor.record_eval_event(
|
587
|
+
context,
|
588
|
+
flag[:key],
|
589
|
+
flag[:version],
|
590
|
+
detail.variation_index,
|
591
|
+
detail.value,
|
592
|
+
(add_experiment_data || with_reasons) ? detail.reason : nil,
|
593
|
+
default,
|
594
|
+
add_experiment_data || flag[:trackEvents] || false,
|
595
|
+
flag[:debugEventsUntilDate],
|
596
|
+
nil,
|
597
|
+
flag[:samplingRatio],
|
598
|
+
!!flag[:excludeFromSummaries]
|
599
|
+
)
|
600
|
+
end
|
601
|
+
|
602
|
+
private def record_prereq_flag_eval(prereq_flag, prereq_of_flag, context, detail, with_reasons)
|
603
|
+
add_experiment_data = experiment?(prereq_flag, detail.reason)
|
604
|
+
@event_processor.record_eval_event(
|
605
|
+
context,
|
606
|
+
prereq_flag[:key],
|
607
|
+
prereq_flag[:version],
|
608
|
+
detail.variation_index,
|
609
|
+
detail.value,
|
610
|
+
(add_experiment_data || with_reasons) ? detail.reason : nil,
|
611
|
+
nil,
|
612
|
+
add_experiment_data || prereq_flag[:trackEvents] || false,
|
613
|
+
prereq_flag[:debugEventsUntilDate],
|
614
|
+
prereq_of_flag[:key],
|
615
|
+
prereq_flag[:samplingRatio],
|
616
|
+
!!prereq_flag[:excludeFromSummaries]
|
617
|
+
)
|
618
|
+
end
|
619
|
+
|
620
|
+
private def record_flag_eval_error(flag, context, default, reason, with_reasons)
|
621
|
+
@event_processor.record_eval_event(context, flag[:key], flag[:version], nil, default, with_reasons ? reason : nil, default,
|
622
|
+
flag[:trackEvents], flag[:debugEventsUntilDate], nil, flag[:samplingRatio], !!flag[:excludeFromSummaries])
|
623
|
+
end
|
624
|
+
|
625
|
+
#
|
626
|
+
# @param flag_key [String]
|
627
|
+
# @param context [LaunchDarkly::LDContext]
|
628
|
+
# @param default [any]
|
629
|
+
# @param reason [LaunchDarkly::EvaluationReason]
|
630
|
+
# @param with_reasons [Boolean]
|
631
|
+
#
|
632
|
+
private def record_unknown_flag_eval(flag_key, context, default, reason, with_reasons)
|
633
|
+
@event_processor.record_eval_event(context, flag_key, nil, nil, default, with_reasons ? reason : nil, default,
|
634
|
+
false, nil, nil, 1, false)
|
635
|
+
end
|
636
|
+
|
637
|
+
private def experiment?(flag, reason)
|
638
|
+
return false unless reason
|
639
|
+
|
640
|
+
if reason.in_experiment
|
641
|
+
return true
|
642
|
+
end
|
643
|
+
|
644
|
+
case reason[:kind]
|
645
|
+
when 'RULE_MATCH'
|
646
|
+
index = reason[:ruleIndex]
|
647
|
+
unless index.nil?
|
648
|
+
rules = flag[:rules] || []
|
649
|
+
return index >= 0 && index < rules.length && rules[index][:trackEvents]
|
650
|
+
end
|
651
|
+
when 'FALLTHROUGH'
|
652
|
+
return !!flag[:trackEventsFallthrough]
|
474
653
|
end
|
654
|
+
false
|
475
655
|
end
|
476
656
|
end
|
477
657
|
|