launchdarkly-server-sdk 5.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +15 -0
  6. data/.hound.yml +2 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +600 -0
  9. data/.simplecov +4 -0
  10. data/.yardopts +9 -0
  11. data/CHANGELOG.md +261 -0
  12. data/CODEOWNERS +1 -0
  13. data/CONTRIBUTING.md +37 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +102 -0
  16. data/LICENSE.txt +13 -0
  17. data/README.md +56 -0
  18. data/Rakefile +5 -0
  19. data/azure-pipelines.yml +51 -0
  20. data/ext/mkrf_conf.rb +11 -0
  21. data/launchdarkly-server-sdk.gemspec +40 -0
  22. data/lib/ldclient-rb.rb +29 -0
  23. data/lib/ldclient-rb/cache_store.rb +45 -0
  24. data/lib/ldclient-rb/config.rb +411 -0
  25. data/lib/ldclient-rb/evaluation.rb +455 -0
  26. data/lib/ldclient-rb/event_summarizer.rb +55 -0
  27. data/lib/ldclient-rb/events.rb +468 -0
  28. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  29. data/lib/ldclient-rb/file_data_source.rb +312 -0
  30. data/lib/ldclient-rb/flags_state.rb +76 -0
  31. data/lib/ldclient-rb/impl.rb +13 -0
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  35. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  36. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  37. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  38. data/lib/ldclient-rb/integrations.rb +55 -0
  39. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  40. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  41. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  42. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  43. data/lib/ldclient-rb/interfaces.rb +153 -0
  44. data/lib/ldclient-rb/ldclient.rb +424 -0
  45. data/lib/ldclient-rb/memoized_value.rb +32 -0
  46. data/lib/ldclient-rb/newrelic.rb +17 -0
  47. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  48. data/lib/ldclient-rb/polling.rb +78 -0
  49. data/lib/ldclient-rb/redis_store.rb +87 -0
  50. data/lib/ldclient-rb/requestor.rb +101 -0
  51. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  52. data/lib/ldclient-rb/stream.rb +141 -0
  53. data/lib/ldclient-rb/user_filter.rb +51 -0
  54. data/lib/ldclient-rb/util.rb +50 -0
  55. data/lib/ldclient-rb/version.rb +3 -0
  56. data/scripts/gendocs.sh +11 -0
  57. data/scripts/release.sh +27 -0
  58. data/spec/config_spec.rb +63 -0
  59. data/spec/evaluation_spec.rb +739 -0
  60. data/spec/event_summarizer_spec.rb +63 -0
  61. data/spec/events_spec.rb +642 -0
  62. data/spec/expiring_cache_spec.rb +76 -0
  63. data/spec/feature_store_spec_base.rb +213 -0
  64. data/spec/file_data_source_spec.rb +255 -0
  65. data/spec/fixtures/feature.json +37 -0
  66. data/spec/fixtures/feature1.json +36 -0
  67. data/spec/fixtures/user.json +9 -0
  68. data/spec/flags_state_spec.rb +81 -0
  69. data/spec/http_util.rb +109 -0
  70. data/spec/in_memory_feature_store_spec.rb +12 -0
  71. data/spec/integrations/consul_feature_store_spec.rb +42 -0
  72. data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
  73. data/spec/integrations/store_wrapper_spec.rb +276 -0
  74. data/spec/ldclient_spec.rb +471 -0
  75. data/spec/newrelic_spec.rb +5 -0
  76. data/spec/polling_spec.rb +120 -0
  77. data/spec/redis_feature_store_spec.rb +95 -0
  78. data/spec/requestor_spec.rb +214 -0
  79. data/spec/segment_store_spec_base.rb +95 -0
  80. data/spec/simple_lru_cache_spec.rb +24 -0
  81. data/spec/spec_helper.rb +9 -0
  82. data/spec/store_spec.rb +10 -0
  83. data/spec/stream_spec.rb +60 -0
  84. data/spec/user_filter_spec.rb +91 -0
  85. data/spec/util_spec.rb +17 -0
  86. data/spec/version_spec.rb +7 -0
  87. metadata +375 -0
@@ -0,0 +1,153 @@
1
+
2
+ module LaunchDarkly
3
+ #
4
+ # Mixins that define the required methods of various pluggable components used by the client.
5
+ #
6
+ module Interfaces
7
+ #
8
+ # Mixin that defines the required methods of a feature store implementation. The LaunchDarkly
9
+ # client uses the feature store to persist feature flags and related objects received from
10
+ # the LaunchDarkly service. Implementations must support concurrent access and updates.
11
+ # 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
+ #
14
+ # An entity that can be stored in a feature store is a hash that can be converted to and from
15
+ # JSON, and that has at a minimum the following properties: `:key`, a string that is unique
16
+ # among entities of the same kind; `:version`, an integer that is higher for newer data;
17
+ # `:deleted`, a boolean (optional, defaults to false) that if true means this is a
18
+ # placeholder for a deleted entity.
19
+ #
20
+ # To represent the different kinds of objects that can be stored, such as feature flags and
21
+ # segments, the SDK will provide a "kind" object; this is a hash with a single property,
22
+ # `:namespace`, which is a short string unique to that kind. This string can be used as a
23
+ # collection name or a key prefix.
24
+ #
25
+ # The default implementation is {LaunchDarkly::InMemoryFeatureStore}. Several implementations
26
+ # that use databases can be found in {LaunchDarkly::Integrations}. If you want to write a new
27
+ # implementation, see {LaunchDarkly::Integrations::Util} for tools that can make this task
28
+ # simpler.
29
+ #
30
+ module FeatureStore
31
+ #
32
+ # Initializes (or re-initializes) the store with the specified set of entities. Any
33
+ # existing entries will be removed. Implementations can assume that this data set is up to
34
+ # date-- there is no need to perform individual version comparisons between the existing
35
+ # objects and the supplied features.
36
+ #
37
+ # If possible, the store should update the entire data set atomically. If that is not possible,
38
+ # it should iterate through the outer hash and then the inner hash using the existing iteration
39
+ # order of those hashes (the SDK will ensure that the items were inserted into the hashes in
40
+ # the correct order), storing each item, and then delete any leftover items at the very end.
41
+ #
42
+ # @param all_data [Hash] a hash where each key is one of the data kind objects, and each
43
+ # value is in turn a hash of string keys to entities
44
+ # @return [void]
45
+ #
46
+ def init(all_data)
47
+ end
48
+
49
+ #
50
+ # Returns the entity to which the specified key is mapped, if any.
51
+ #
52
+ # @param kind [Object] the kind of entity to get
53
+ # @param key [String] the unique key of the entity to get
54
+ # @return [Hash] the entity; nil if the key was not found, or if the stored entity's
55
+ # `:deleted` property was true
56
+ #
57
+ def get(kind, key)
58
+ end
59
+
60
+ #
61
+ # Returns all stored entities of the specified kind, not including deleted entities.
62
+ #
63
+ # @param kind [Object] the kind of entity to get
64
+ # @return [Hash] a hash where each key is the entity's `:key` property and each value
65
+ # is the entity
66
+ #
67
+ def all(kind)
68
+ end
69
+
70
+ #
71
+ # Attempt to add an entity, or update an existing entity with the same key. An update
72
+ # should only succeed if the new item's `:version` is greater than the old one;
73
+ # otherwise, the method should do nothing.
74
+ #
75
+ # @param kind [Object] the kind of entity to add or update
76
+ # @param item [Hash] the entity to add or update
77
+ # @return [void]
78
+ #
79
+ def upsert(kind, item)
80
+ end
81
+
82
+ #
83
+ # Attempt to delete an entity if it exists. Deletion should only succeed if the
84
+ # `version` parameter is greater than the existing entity's `:version`; otherwise, the
85
+ # method should do nothing.
86
+ #
87
+ # @param kind [Object] the kind of entity to delete
88
+ # @param key [String] the unique key of the entity
89
+ # @param version [Integer] the entity must have a lower version than this to be deleted
90
+ # @return [void]
91
+ #
92
+ def delete(kind, key, version)
93
+ end
94
+
95
+ #
96
+ # Checks whether this store has been initialized. That means that `init` has been called
97
+ # either by this process, or (if the store can be shared) by another process. This
98
+ # method will be called frequently, so it should be efficient. You can assume that if it
99
+ # has returned true once, it can continue to return true, i.e. a store cannot become
100
+ # uninitialized again.
101
+ #
102
+ # @return [Boolean] true if the store is in an initialized state
103
+ #
104
+ def initialized?
105
+ end
106
+
107
+ #
108
+ # Performs any necessary cleanup to shut down the store when the client is being shut down.
109
+ #
110
+ # @return [void]
111
+ #
112
+ def stop
113
+ end
114
+ end
115
+
116
+ #
117
+ # Mixin that defines the required methods of a data source implementation. This is the
118
+ # component that delivers feature flag data from LaunchDarkly to the LDClient by putting
119
+ # the data in the {FeatureStore}. It is expected to run concurrently on its own thread.
120
+ #
121
+ # The client has its own standard implementation, which uses either a streaming connection or
122
+ # 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
+ #
125
+ module DataSource
126
+ #
127
+ # Checks whether the data source has finished initializing. Initialization is considered done
128
+ # once it has received one complete data set from LaunchDarkly.
129
+ #
130
+ # @return [Boolean] true if initialization is complete
131
+ #
132
+ def initialized?
133
+ end
134
+
135
+ #
136
+ # Puts the data source into an active state. Normally this means it will make its first
137
+ # connection attempt to LaunchDarkly. If `start` has already been called, calling it again
138
+ # should simply return the same value as the first call.
139
+ #
140
+ # @return [Concurrent::Event] an Event which will be set once initialization is complete
141
+ #
142
+ def start
143
+ end
144
+
145
+ #
146
+ # Puts the data source into an inactive state and releases all of its resources.
147
+ # This state should be considered permanent (`start` does not have to work after `stop`).
148
+ #
149
+ def stop
150
+ end
151
+ end
152
+ end
153
+ end
@@ -0,0 +1,424 @@
1
+ require "ldclient-rb/impl/store_client_wrapper"
2
+ require "concurrent/atomics"
3
+ require "digest/sha1"
4
+ require "logger"
5
+ require "benchmark"
6
+ require "json"
7
+ require "openssl"
8
+
9
+ module LaunchDarkly
10
+ #
11
+ # A client for LaunchDarkly. Client instances are thread-safe. Users
12
+ # should create a single client instance for the lifetime of the application.
13
+ #
14
+ class LDClient
15
+ include Evaluation
16
+ #
17
+ # Creates a new client instance that connects to LaunchDarkly. A custom
18
+ # configuration parameter can also supplied to specify advanced options,
19
+ # but for most use cases, the default configuration is appropriate.
20
+ #
21
+ # The client will immediately attempt to connect to LaunchDarkly and retrieve
22
+ # your feature flag data. If it cannot successfully do so within the time limit
23
+ # specified by `wait_for_sec`, the constructor will return a client that is in
24
+ # an uninitialized state. See {#initialized?} for more details.
25
+ #
26
+ # @param sdk_key [String] the SDK key for your LaunchDarkly account
27
+ # @param config [Config] an optional client configuration object
28
+ # @param wait_for_sec [Float] maximum time (in seconds) to wait for initialization
29
+ #
30
+ # @return [LDClient] The LaunchDarkly client instance
31
+ #
32
+ def initialize(sdk_key, config = Config.default, wait_for_sec = 5)
33
+ @sdk_key = sdk_key
34
+
35
+ # We need to wrap the feature store object with a FeatureStoreClientWrapper in order to add
36
+ # some necessary logic around updates. Unfortunately, we have code elsewhere that accesses
37
+ # the feature store through the Config object, so we need to make a new Config that uses
38
+ # the wrapped store.
39
+ @store = Impl::FeatureStoreClientWrapper.new(config.feature_store)
40
+ updated_config = config.clone
41
+ updated_config.instance_variable_set(:@feature_store, @store)
42
+ @config = updated_config
43
+
44
+ if @config.offline? || !@config.send_events
45
+ @event_processor = NullEventProcessor.new
46
+ else
47
+ @event_processor = EventProcessor.new(sdk_key, config)
48
+ end
49
+
50
+ if @config.use_ldd?
51
+ @config.logger.info { "[LDClient] Started LaunchDarkly Client in LDD mode" }
52
+ return # requestor and update processor are not used in this mode
53
+ end
54
+
55
+ data_source_or_factory = @config.data_source || self.method(:create_default_data_source)
56
+ if data_source_or_factory.respond_to? :call
57
+ @data_source = data_source_or_factory.call(sdk_key, @config)
58
+ else
59
+ @data_source = data_source_or_factory
60
+ end
61
+
62
+ ready = @data_source.start
63
+ if wait_for_sec > 0
64
+ ok = ready.wait(wait_for_sec)
65
+ if !ok
66
+ @config.logger.error { "[LDClient] Timeout encountered waiting for LaunchDarkly client initialization" }
67
+ elsif !@data_source.initialized?
68
+ @config.logger.error { "[LDClient] LaunchDarkly client initialization failed" }
69
+ end
70
+ end
71
+ end
72
+
73
+ #
74
+ # Tells the client that all pending analytics events should be delivered as soon as possible.
75
+ #
76
+ # When the LaunchDarkly client generates analytics events (from {#variation}, {#variation_detail},
77
+ # {#identify}, or {#track}), they are queued on a worker thread. The event thread normally
78
+ # sends all queued events to LaunchDarkly at regular intervals, controlled by the
79
+ # {Config#flush_interval} option. Calling `flush` triggers a send without waiting for the
80
+ # next interval.
81
+ #
82
+ # Flushing is asynchronous, so this method will return before it is complete. However, if you
83
+ # call {#close}, events are guaranteed to be sent before that method returns.
84
+ #
85
+ def flush
86
+ @event_processor.flush
87
+ end
88
+
89
+ #
90
+ # @param key [String] the feature flag key
91
+ # @param user [Hash] the user properties
92
+ # @param default [Boolean] (false) the value to use if the flag cannot be evaluated
93
+ # @return [Boolean] the flag value
94
+ # @deprecated Use {#variation} instead.
95
+ #
96
+ def toggle?(key, user, default = false)
97
+ @config.logger.warn { "[LDClient] toggle? is deprecated. Use variation instead" }
98
+ variation(key, user, default)
99
+ end
100
+
101
+ #
102
+ # Creates a hash string that can be used by the JavaScript SDK to identify a user.
103
+ # For more information, see [Secure mode](https://docs.launchdarkly.com/docs/js-sdk-reference#section-secure-mode).
104
+ #
105
+ # @param user [Hash] the user properties
106
+ # @return [String] a hash string
107
+ #
108
+ def secure_mode_hash(user)
109
+ OpenSSL::HMAC.hexdigest("sha256", @sdk_key, user[:key].to_s)
110
+ end
111
+
112
+ #
113
+ # Returns whether the client has been initialized and is ready to serve feature flag requests.
114
+ #
115
+ # If this returns false, it means that the client did not succeed in connecting to
116
+ # LaunchDarkly within the time limit that you specified in the constructor. It could
117
+ # still succeed in connecting at a later time (on another thread), or it could have
118
+ # given up permanently (for instance, if your SDK key is invalid). In the meantime,
119
+ # any call to {#variation} or {#variation_detail} will behave as follows:
120
+ #
121
+ # 1. It will check whether the feature store already contains data (that is, you
122
+ # are using a database-backed store and it was populated by a previous run of this
123
+ # application). If so, it will use the last known feature flag data.
124
+ #
125
+ # 2. Failing that, it will return the value that you specified for the `default`
126
+ # parameter of {#variation} or {#variation_detail}.
127
+ #
128
+ # @return [Boolean] true if the client has been initialized
129
+ #
130
+ def initialized?
131
+ @config.offline? || @config.use_ldd? || @data_source.initialized?
132
+ end
133
+
134
+ #
135
+ # Determines the variation of a feature flag to present to a user.
136
+ #
137
+ # At a minimum, the user hash should contain a `:key`, which should be the unique
138
+ # identifier for your user (or, for an anonymous user, a session identifier or
139
+ # cookie).
140
+ #
141
+ # Other supported user attributes include IP address, country code, and an arbitrary hash of
142
+ # custom attributes. For more about the supported user properties and how they work in
143
+ # LaunchDarkly, see [Targeting users](https://docs.launchdarkly.com/docs/targeting-users).
144
+ #
145
+ # The optional `:privateAttributeNames` user property allows you to specify a list of
146
+ # attribute names that should not be sent back to LaunchDarkly.
147
+ # [Private attributes](https://docs.launchdarkly.com/docs/private-user-attributes)
148
+ # can also be configured globally in {Config}.
149
+ #
150
+ # @example Basic user hash
151
+ # {key: "my-user-id"}
152
+ #
153
+ # @example More complete user hash
154
+ # {key: "my-user-id", ip: "127.0.0.1", country: "US", custom: {customer_rank: 1000}}
155
+ #
156
+ # @example User with a private attribute
157
+ # {key: "my-user-id", email: "email@example.com", privateAttributeNames: ["email"]}
158
+ #
159
+ # @param key [String] the unique feature key for the feature flag, as shown
160
+ # on the LaunchDarkly dashboard
161
+ # @param user [Hash] a hash containing parameters for the end user requesting the flag
162
+ # @param default the default value of the flag; this is used if there is an error
163
+ # condition making it impossible to find or evaluate the flag
164
+ #
165
+ # @return the variation to show the user, or the default value if there's an an error
166
+ #
167
+ def variation(key, user, default)
168
+ evaluate_internal(key, user, default, false).value
169
+ end
170
+
171
+ #
172
+ # Determines the variation of a feature flag for a user, like {#variation}, but also
173
+ # provides additional information about how this value was calculated.
174
+ #
175
+ # The return value of `variation_detail` is an {EvaluationDetail} object, which has
176
+ # three properties: the result value, the positional index of this value in the flag's
177
+ # list of variations, and an object describing the main reason why this value was
178
+ # selected. See {EvaluationDetail} for more on these properties.
179
+ #
180
+ # Calling `variation_detail` instead of `variation` also causes the "reason" data to
181
+ # be included in analytics events, if you are capturing detailed event data for this flag.
182
+ #
183
+ # For more information, see the reference guide on
184
+ # [Evaluation reasons](https://docs.launchdarkly.com/v2.0/docs/evaluation-reasons).
185
+ #
186
+ # @param key [String] the unique feature key for the feature flag, as shown
187
+ # on the LaunchDarkly dashboard
188
+ # @param user [Hash] a hash containing parameters for the end user requesting the flag
189
+ # @param default the default value of the flag; this is used if there is an error
190
+ # condition making it impossible to find or evaluate the flag
191
+ #
192
+ # @return [EvaluationDetail] an object describing the result
193
+ #
194
+ def variation_detail(key, user, default)
195
+ evaluate_internal(key, user, default, true)
196
+ end
197
+
198
+ #
199
+ # Registers the user. This method simply creates an analytics event containing the user
200
+ # properties, so that LaunchDarkly will know about that user if it does not already.
201
+ #
202
+ # Calling {#variation} or {#variation_detail} also sends the user information to
203
+ # LaunchDarkly (if events are enabled), so you only need to use {#identify} if you
204
+ # want to identify the user without evaluating a flag.
205
+ #
206
+ # Note that event delivery is asynchronous, so the event may not actually be sent
207
+ # until later; see {#flush}.
208
+ #
209
+ # @param user [Hash] The user to register; this can have all the same user properties
210
+ # described in {#variation}
211
+ # @return [void]
212
+ #
213
+ def identify(user)
214
+ if !user || user[:key].nil?
215
+ @config.logger.warn("Identify called with nil user or nil user key!")
216
+ return
217
+ end
218
+ @event_processor.add_event(kind: "identify", key: user[:key], user: user)
219
+ end
220
+
221
+ #
222
+ # Tracks that a user performed an event. This method creates a "custom" analytics event
223
+ # containing the specified event name (key), user properties, and optional data.
224
+ #
225
+ # Note that event delivery is asynchronous, so the event may not actually be sent
226
+ # until later; see {#flush}.
227
+ #
228
+ # @param event_name [String] The name of the event
229
+ # @param user [Hash] The user to register; this can have all the same user properties
230
+ # described in {#variation}
231
+ # @param data [Hash] A hash containing any additional data associated with the event
232
+ # @return [void]
233
+ #
234
+ def track(event_name, user, data)
235
+ if !user || user[:key].nil?
236
+ @config.logger.warn("Track called with nil user or nil user key!")
237
+ return
238
+ end
239
+ @event_processor.add_event(kind: "custom", key: event_name, user: user, data: data)
240
+ end
241
+
242
+ #
243
+ # Returns all feature flag values for the given user.
244
+ #
245
+ # @deprecated Please use {#all_flags_state} instead. Current versions of the
246
+ # client-side SDK will not generate analytics events correctly if you pass the
247
+ # result of `all_flags`.
248
+ #
249
+ # @param user [Hash] The end user requesting the feature flags
250
+ # @return [Hash] a hash of feature flag keys to values
251
+ #
252
+ def all_flags(user)
253
+ all_flags_state(user).values_map
254
+ end
255
+
256
+ #
257
+ # Returns a {FeatureFlagsState} object that encapsulates the state of all feature flags for a given user,
258
+ # including the flag values and also metadata that can be used on the front end. This method does not
259
+ # send analytics events back to LaunchDarkly.
260
+ #
261
+ # @param user [Hash] The end user requesting the feature flags
262
+ # @param options [Hash] Optional parameters to control how the state is generated
263
+ # @option options [Boolean] :client_side_only (false) True if only flags marked for use with the
264
+ # client-side SDK should be included in the state. By default, all flags are included.
265
+ # @option options [Boolean] :with_reasons (false) True if evaluation reasons should be included
266
+ # in the state (see {#variation_detail}). By default, they are not included.
267
+ # @option options [Boolean] :details_only_for_tracked_flags (false) True if any flag metadata that is
268
+ # normally only used for event generation - such as flag versions and evaluation reasons - should be
269
+ # omitted for any flag that does not have event tracking or debugging turned on. This reduces the size
270
+ # of the JSON data if you are passing the flag state to the front end.
271
+ # @return [FeatureFlagsState] a {FeatureFlagsState} object which can be serialized to JSON
272
+ #
273
+ def all_flags_state(user, options={})
274
+ return FeatureFlagsState.new(false) if @config.offline?
275
+
276
+ unless user && !user[:key].nil?
277
+ @config.logger.error { "[LDClient] User and user key must be specified in all_flags_state" }
278
+ return FeatureFlagsState.new(false)
279
+ end
280
+
281
+ begin
282
+ features = @store.all(FEATURES)
283
+ rescue => exn
284
+ Util.log_exception(@config.logger, "Unable to read flags for all_flags_state", exn)
285
+ return FeatureFlagsState.new(false)
286
+ end
287
+
288
+ state = FeatureFlagsState.new(true)
289
+ client_only = options[:client_side_only] || false
290
+ with_reasons = options[:with_reasons] || false
291
+ details_only_if_tracked = options[:details_only_for_tracked_flags] || false
292
+ features.each do |k, f|
293
+ if client_only && !f[:clientSide]
294
+ next
295
+ end
296
+ begin
297
+ result = evaluate(f, user, @store, @config.logger)
298
+ state.add_flag(f, result.detail.value, result.detail.variation_index, with_reasons ? result.detail.reason : nil,
299
+ details_only_if_tracked)
300
+ rescue => exn
301
+ Util.log_exception(@config.logger, "Error evaluating flag \"#{k}\" in all_flags_state", exn)
302
+ state.add_flag(f, nil, nil, with_reasons ? { kind: 'ERROR', errorKind: 'EXCEPTION' } : nil, details_only_if_tracked)
303
+ end
304
+ end
305
+
306
+ state
307
+ end
308
+
309
+ #
310
+ # Releases all network connections and other resources held by the client, making it no longer usable.
311
+ #
312
+ # @return [void]
313
+ def close
314
+ @config.logger.info { "[LDClient] Closing LaunchDarkly client..." }
315
+ @data_source.stop
316
+ @event_processor.stop
317
+ @store.stop
318
+ end
319
+
320
+ private
321
+
322
+ def create_default_data_source(sdk_key, config)
323
+ if config.offline?
324
+ return NullUpdateProcessor.new
325
+ end
326
+ requestor = Requestor.new(sdk_key, config)
327
+ if config.stream?
328
+ StreamProcessor.new(sdk_key, config, requestor)
329
+ else
330
+ config.logger.info { "Disabling streaming API" }
331
+ config.logger.warn { "You should only disable the streaming API if instructed to do so by LaunchDarkly support" }
332
+ PollingProcessor.new(config, requestor)
333
+ end
334
+ end
335
+
336
+ # @return [EvaluationDetail]
337
+ def evaluate_internal(key, user, default, include_reasons_in_events)
338
+ if @config.offline?
339
+ return error_result('CLIENT_NOT_READY', default)
340
+ end
341
+
342
+ if !initialized?
343
+ if @store.initialized?
344
+ @config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
345
+ else
346
+ @config.logger.error { "[LDClient] Client has not finished initializing; feature store unavailable, returning default value" }
347
+ @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
348
+ return error_result('CLIENT_NOT_READY', default)
349
+ end
350
+ end
351
+
352
+ feature = @store.get(FEATURES, key)
353
+
354
+ if feature.nil?
355
+ @config.logger.info { "[LDClient] Unknown feature flag \"#{key}\". Returning default value" }
356
+ detail = error_result('FLAG_NOT_FOUND', default)
357
+ @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user,
358
+ reason: include_reasons_in_events ? detail.reason : nil)
359
+ return detail
360
+ end
361
+
362
+ unless user
363
+ @config.logger.error { "[LDClient] Must specify user" }
364
+ detail = error_result('USER_NOT_SPECIFIED', default)
365
+ @event_processor.add_event(make_feature_event(feature, nil, detail, default, include_reasons_in_events))
366
+ return detail
367
+ end
368
+
369
+ begin
370
+ res = evaluate(feature, user, @store, @config.logger) # note, evaluate will do its own sanitization
371
+ if !res.events.nil?
372
+ res.events.each do |event|
373
+ @event_processor.add_event(event)
374
+ end
375
+ end
376
+ detail = res.detail
377
+ if detail.default_value?
378
+ detail = EvaluationDetail.new(default, nil, detail.reason)
379
+ end
380
+ @event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
381
+ return detail
382
+ rescue => exn
383
+ Util.log_exception(@config.logger, "Error evaluating feature flag \"#{key}\"", exn)
384
+ detail = error_result('EXCEPTION', default)
385
+ @event_processor.add_event(make_feature_event(feature, user, detail, default, include_reasons_in_events))
386
+ return detail
387
+ end
388
+ end
389
+
390
+ def make_feature_event(flag, user, detail, default, with_reasons)
391
+ {
392
+ kind: "feature",
393
+ key: flag[:key],
394
+ user: user,
395
+ variation: detail.variation_index,
396
+ value: detail.value,
397
+ default: default,
398
+ version: flag[:version],
399
+ trackEvents: flag[:trackEvents],
400
+ debugEventsUntilDate: flag[:debugEventsUntilDate],
401
+ reason: with_reasons ? detail.reason : nil
402
+ }
403
+ end
404
+ end
405
+
406
+ #
407
+ # Used internally when the client is offline.
408
+ # @private
409
+ #
410
+ class NullUpdateProcessor
411
+ def start
412
+ e = Concurrent::Event.new
413
+ e.set
414
+ e
415
+ end
416
+
417
+ def initialized?
418
+ true
419
+ end
420
+
421
+ def stop
422
+ end
423
+ end
424
+ end