launchdarkly-server-sdk 5.5.7

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