launchdarkly-server-sdk 6.1.1 → 6.4.0

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