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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +3 -4
  3. data/lib/ldclient-rb/config.rb +112 -62
  4. data/lib/ldclient-rb/context.rb +444 -0
  5. data/lib/ldclient-rb/evaluation_detail.rb +26 -22
  6. data/lib/ldclient-rb/events.rb +256 -146
  7. data/lib/ldclient-rb/flags_state.rb +26 -15
  8. data/lib/ldclient-rb/impl/big_segments.rb +18 -18
  9. data/lib/ldclient-rb/impl/broadcaster.rb +78 -0
  10. data/lib/ldclient-rb/impl/context.rb +96 -0
  11. data/lib/ldclient-rb/impl/context_filter.rb +145 -0
  12. data/lib/ldclient-rb/impl/data_source.rb +188 -0
  13. data/lib/ldclient-rb/impl/data_store.rb +59 -0
  14. data/lib/ldclient-rb/impl/dependency_tracker.rb +102 -0
  15. data/lib/ldclient-rb/impl/diagnostic_events.rb +9 -10
  16. data/lib/ldclient-rb/impl/evaluator.rb +386 -142
  17. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +40 -41
  18. data/lib/ldclient-rb/impl/evaluator_helpers.rb +50 -0
  19. data/lib/ldclient-rb/impl/evaluator_operators.rb +26 -55
  20. data/lib/ldclient-rb/impl/event_sender.rb +7 -6
  21. data/lib/ldclient-rb/impl/event_summarizer.rb +68 -0
  22. data/lib/ldclient-rb/impl/event_types.rb +136 -0
  23. data/lib/ldclient-rb/impl/flag_tracker.rb +58 -0
  24. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +19 -7
  25. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +38 -30
  26. data/lib/ldclient-rb/impl/integrations/file_data_source.rb +24 -11
  27. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +109 -12
  28. data/lib/ldclient-rb/impl/migrations/migrator.rb +287 -0
  29. data/lib/ldclient-rb/impl/migrations/tracker.rb +136 -0
  30. data/lib/ldclient-rb/impl/model/clause.rb +45 -0
  31. data/lib/ldclient-rb/impl/model/feature_flag.rb +255 -0
  32. data/lib/ldclient-rb/impl/model/preprocessed_data.rb +64 -0
  33. data/lib/ldclient-rb/impl/model/segment.rb +132 -0
  34. data/lib/ldclient-rb/impl/model/serialization.rb +54 -44
  35. data/lib/ldclient-rb/impl/repeating_task.rb +3 -4
  36. data/lib/ldclient-rb/impl/sampler.rb +25 -0
  37. data/lib/ldclient-rb/impl/store_client_wrapper.rb +102 -8
  38. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +2 -2
  39. data/lib/ldclient-rb/impl/unbounded_pool.rb +1 -1
  40. data/lib/ldclient-rb/impl/util.rb +59 -1
  41. data/lib/ldclient-rb/in_memory_store.rb +9 -2
  42. data/lib/ldclient-rb/integrations/consul.rb +2 -2
  43. data/lib/ldclient-rb/integrations/dynamodb.rb +2 -2
  44. data/lib/ldclient-rb/integrations/file_data.rb +4 -4
  45. data/lib/ldclient-rb/integrations/redis.rb +5 -5
  46. data/lib/ldclient-rb/integrations/test_data/flag_builder.rb +287 -62
  47. data/lib/ldclient-rb/integrations/test_data.rb +18 -14
  48. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +20 -9
  49. data/lib/ldclient-rb/interfaces.rb +600 -14
  50. data/lib/ldclient-rb/ldclient.rb +314 -134
  51. data/lib/ldclient-rb/memoized_value.rb +1 -1
  52. data/lib/ldclient-rb/migrations.rb +230 -0
  53. data/lib/ldclient-rb/non_blocking_thread_pool.rb +1 -1
  54. data/lib/ldclient-rb/polling.rb +52 -6
  55. data/lib/ldclient-rb/reference.rb +274 -0
  56. data/lib/ldclient-rb/requestor.rb +9 -11
  57. data/lib/ldclient-rb/stream.rb +96 -34
  58. data/lib/ldclient-rb/util.rb +97 -14
  59. data/lib/ldclient-rb/version.rb +1 -1
  60. data/lib/ldclient-rb.rb +3 -4
  61. metadata +65 -23
  62. data/lib/ldclient-rb/event_summarizer.rb +0 -55
  63. data/lib/ldclient-rb/file_data_source.rb +0 -23
  64. data/lib/ldclient-rb/impl/event_factory.rb +0 -126
  65. data/lib/ldclient-rb/newrelic.rb +0 -17
  66. data/lib/ldclient-rb/redis_store.rb +0 -88
  67. data/lib/ldclient-rb/user_filter.rb +0 -52
@@ -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/event_factory"
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
- @event_factory_default = EventFactory.new(false)
50
- @event_factory_with_reasons = EventFactory.new(true)
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.get_user_membership(key) }
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
- # @param key [String] the feature flag key
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 user [Hash] the user properties
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(user)
146
- OpenSSL::HMAC.hexdigest("sha256", @sdk_key, user[:key].to_s)
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 to a user.
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 user [Hash] a hash containing parameters for the end user requesting the flag
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 to show the user, or the default value if there's an an error
197
+ # @return the variation for the provided context, or the default value if there's an error
203
198
  #
204
- def variation(key, user, default)
205
- evaluate_internal(key, user, default, @event_factory_default).value
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 user, like {#variation}, but also
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 user [Hash] a hash containing parameters for the end user requesting the flag
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, user, default)
232
- evaluate_internal(key, user, default, @event_factory_with_reasons)
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
- # Registers the user. This method simply creates an analytics event containing the user
237
- # properties, so that LaunchDarkly will know about that user if it does not already.
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
- # Calling {#variation} or {#variation_detail} also sends the user information to
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 user without evaluating a flag.
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 user [Hash] The user to register; this can have all the same user properties
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(user)
251
- if !user || user[:key].nil?
252
- @config.logger.warn("Identify called with nil user or nil user key!")
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
- sanitize_user(user)
256
- @event_processor.add_event(@event_factory_default.new_identify_event(user))
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 user performed an event. This method creates a "custom" analytics event
261
- # containing the specified event name (key), user properties, and optional data.
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 user [Hash] The user to register; this can have all the same user properties
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, user, data = nil, metric_value = nil)
282
- if !user || user[:key].nil?
283
- @config.logger.warn("Track called with nil user or nil user key!")
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
- sanitize_user(user)
287
- @event_processor.add_event(@event_factory_default.new_custom_event(event_name, user, data, metric_value))
323
+
324
+ @event_processor.record_custom_event(context, event_name, data, metric_value)
288
325
  end
289
326
 
290
327
  #
291
- # Associates a new and old user object for analytics purposes via an alias event.
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
- # @param current_context [Hash] The current version of a user.
294
- # @param previous_context [Hash] The previous version of a user.
295
- # @return [void]
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 alias(current_context, previous_context)
298
- if !current_context || current_context[:key].nil? || !previous_context || previous_context[:key].nil?
299
- @config.logger.warn("Alias called with nil user or nil user key!")
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
- # Returns all feature flag values for the given user.
309
- #
310
- # @deprecated Please use {#all_flags_state} instead. Current versions of the
311
- # client-side SDK will not generate analytics events correctly if you pass the
312
- # result of `all_flags`.
313
- #
314
- # @param user [Hash] The end user requesting the feature flags
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 user,
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 user [Hash] The end user requesting the feature flags
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(user, options={})
370
+ def all_flags_state(context, options={})
339
371
  return FeatureFlagsState.new(false) if @config.offline?
340
372
 
341
- unless user && !user[:key].nil?
342
- @config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" }
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
- result = @evaluator.evaluate(f, user, @event_factory_default)
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
- # @return [EvaluationDetail]
413
- def evaluate_internal(key, user, default, event_factory)
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
- unless user
419
- @config.logger.error { "[LDClient] Must specify user" }
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
- if user[:key].nil?
425
- @config.logger.warn { "[LDClient] Variation called with nil user key; returning default value" }
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
- if !initialized?
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
- @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
437
- return detail
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
- feature = @store.get(FEATURES, key)
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
- @event_processor.add_event(event_factory.new_unknown_flag_event(key, user, default, detail.reason))
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, user, event_factory)
452
- if !res.events.nil?
453
- res.events.each do |event|
454
- @event_processor.add_event(event)
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
- @event_processor.add_event(event_factory.new_eval_event(feature, user, detail, default))
462
- return detail
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
- @event_processor.add_event(event_factory.new_default_event(feature, user, default, detail.reason))
467
- return detail
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 sanitize_user(user)
472
- if user[:key]
473
- user[:key] = user[:key].to_s
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
 
@@ -14,7 +14,7 @@ module LaunchDarkly
14
14
 
15
15
  def get
16
16
  @mutex.synchronize do
17
- if !@inited
17
+ unless @inited
18
18
  @value = @generator.call
19
19
  @inited = true
20
20
  end