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.
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