launchdarkly-server-sdk 5.5.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|