launchdarkly-server-sdk 6.3.0 → 8.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/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
|
|