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.
- checksums.yaml +7 -0
- data/.circleci/config.yml +134 -0
- data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- data/.gitignore +15 -0
- data/.hound.yml +2 -0
- data/.rspec +2 -0
- data/.rubocop.yml +600 -0
- data/.simplecov +4 -0
- data/.yardopts +9 -0
- data/CHANGELOG.md +261 -0
- data/CODEOWNERS +1 -0
- data/CONTRIBUTING.md +37 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +102 -0
- data/LICENSE.txt +13 -0
- data/README.md +56 -0
- data/Rakefile +5 -0
- data/azure-pipelines.yml +51 -0
- data/ext/mkrf_conf.rb +11 -0
- data/launchdarkly-server-sdk.gemspec +40 -0
- data/lib/ldclient-rb.rb +29 -0
- data/lib/ldclient-rb/cache_store.rb +45 -0
- data/lib/ldclient-rb/config.rb +411 -0
- data/lib/ldclient-rb/evaluation.rb +455 -0
- data/lib/ldclient-rb/event_summarizer.rb +55 -0
- data/lib/ldclient-rb/events.rb +468 -0
- data/lib/ldclient-rb/expiring_cache.rb +77 -0
- data/lib/ldclient-rb/file_data_source.rb +312 -0
- data/lib/ldclient-rb/flags_state.rb +76 -0
- data/lib/ldclient-rb/impl.rb +13 -0
- data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
- data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
- data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
- data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
- data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
- data/lib/ldclient-rb/in_memory_store.rb +100 -0
- data/lib/ldclient-rb/integrations.rb +55 -0
- data/lib/ldclient-rb/integrations/consul.rb +38 -0
- data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
- data/lib/ldclient-rb/integrations/redis.rb +55 -0
- data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
- data/lib/ldclient-rb/interfaces.rb +153 -0
- data/lib/ldclient-rb/ldclient.rb +424 -0
- data/lib/ldclient-rb/memoized_value.rb +32 -0
- data/lib/ldclient-rb/newrelic.rb +17 -0
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/polling.rb +78 -0
- data/lib/ldclient-rb/redis_store.rb +87 -0
- data/lib/ldclient-rb/requestor.rb +101 -0
- data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
- data/lib/ldclient-rb/stream.rb +141 -0
- data/lib/ldclient-rb/user_filter.rb +51 -0
- data/lib/ldclient-rb/util.rb +50 -0
- data/lib/ldclient-rb/version.rb +3 -0
- data/scripts/gendocs.sh +11 -0
- data/scripts/release.sh +27 -0
- data/spec/config_spec.rb +63 -0
- data/spec/evaluation_spec.rb +739 -0
- data/spec/event_summarizer_spec.rb +63 -0
- data/spec/events_spec.rb +642 -0
- data/spec/expiring_cache_spec.rb +76 -0
- data/spec/feature_store_spec_base.rb +213 -0
- data/spec/file_data_source_spec.rb +255 -0
- data/spec/fixtures/feature.json +37 -0
- data/spec/fixtures/feature1.json +36 -0
- data/spec/fixtures/user.json +9 -0
- data/spec/flags_state_spec.rb +81 -0
- data/spec/http_util.rb +109 -0
- data/spec/in_memory_feature_store_spec.rb +12 -0
- data/spec/integrations/consul_feature_store_spec.rb +42 -0
- data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
- data/spec/integrations/store_wrapper_spec.rb +276 -0
- data/spec/ldclient_spec.rb +471 -0
- data/spec/newrelic_spec.rb +5 -0
- data/spec/polling_spec.rb +120 -0
- data/spec/redis_feature_store_spec.rb +95 -0
- data/spec/requestor_spec.rb +214 -0
- data/spec/segment_store_spec_base.rb +95 -0
- data/spec/simple_lru_cache_spec.rb +24 -0
- data/spec/spec_helper.rb +9 -0
- data/spec/store_spec.rb +10 -0
- data/spec/stream_spec.rb +60 -0
- data/spec/user_filter_spec.rb +91 -0
- data/spec/util_spec.rb +17 -0
- data/spec/version_spec.rb +7 -0
- 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
|