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,77 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
# A thread-safe cache with maximum number of entries and TTL.
|
4
|
+
# Adapted from https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/ttl/cache.rb
|
5
|
+
# under MIT license with the following changes:
|
6
|
+
# * made thread-safe
|
7
|
+
# * removed many unused methods
|
8
|
+
# * reading a key does not reset its expiration time, only writing
|
9
|
+
# @private
|
10
|
+
class ExpiringCache
|
11
|
+
def initialize(max_size, ttl)
|
12
|
+
@max_size = max_size
|
13
|
+
@ttl = ttl
|
14
|
+
@data_lru = {}
|
15
|
+
@data_ttl = {}
|
16
|
+
@lock = Mutex.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def [](key)
|
20
|
+
@lock.synchronize do
|
21
|
+
ttl_evict
|
22
|
+
@data_lru[key]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(key, val)
|
27
|
+
@lock.synchronize do
|
28
|
+
ttl_evict
|
29
|
+
|
30
|
+
@data_lru.delete(key)
|
31
|
+
@data_ttl.delete(key)
|
32
|
+
|
33
|
+
@data_lru[key] = val
|
34
|
+
@data_ttl[key] = Time.now.to_f
|
35
|
+
|
36
|
+
if @data_lru.size > @max_size
|
37
|
+
key, _ = @data_lru.first # hashes have a FIFO ordering in Ruby
|
38
|
+
|
39
|
+
@data_ttl.delete(key)
|
40
|
+
@data_lru.delete(key)
|
41
|
+
end
|
42
|
+
|
43
|
+
val
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def delete(key)
|
48
|
+
@lock.synchronize do
|
49
|
+
ttl_evict
|
50
|
+
|
51
|
+
@data_lru.delete(key)
|
52
|
+
@data_ttl.delete(key)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def clear
|
57
|
+
@lock.synchronize do
|
58
|
+
@data_lru.clear
|
59
|
+
@data_ttl.clear
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
def ttl_evict
|
66
|
+
ttl_horizon = Time.now.to_f - @ttl
|
67
|
+
key, time = @data_ttl.first
|
68
|
+
|
69
|
+
until time.nil? || time > ttl_horizon
|
70
|
+
@data_ttl.delete(key)
|
71
|
+
@data_lru.delete(key)
|
72
|
+
|
73
|
+
key, time = @data_ttl.first
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,312 @@
|
|
1
|
+
require 'concurrent/atomics'
|
2
|
+
require 'json'
|
3
|
+
require 'yaml'
|
4
|
+
require 'pathname'
|
5
|
+
|
6
|
+
module LaunchDarkly
|
7
|
+
# To avoid pulling in 'listen' and its transitive dependencies for people who aren't using the
|
8
|
+
# file data source or who don't need auto-updating, we only enable auto-update if the 'listen'
|
9
|
+
# gem has been provided by the host app.
|
10
|
+
# @private
|
11
|
+
@@have_listen = false
|
12
|
+
begin
|
13
|
+
require 'listen'
|
14
|
+
@@have_listen = true
|
15
|
+
rescue LoadError
|
16
|
+
end
|
17
|
+
|
18
|
+
# @private
|
19
|
+
def self.have_listen?
|
20
|
+
@@have_listen
|
21
|
+
end
|
22
|
+
|
23
|
+
#
|
24
|
+
# Provides a way to use local files as a source of feature flag state. This would typically be
|
25
|
+
# used in a test environment, to operate using a predetermined feature flag state without an
|
26
|
+
# actual LaunchDarkly connection.
|
27
|
+
#
|
28
|
+
# To use this component, call {FileDataSource#factory}, and store its return value in the
|
29
|
+
# {Config#data_source} property of your LaunchDarkly client configuration. In the options
|
30
|
+
# to `factory`, set `paths` to the file path(s) of your data file(s):
|
31
|
+
#
|
32
|
+
# file_source = FileDataSource.factory(paths: [ myFilePath ])
|
33
|
+
# config = LaunchDarkly::Config.new(data_source: file_source)
|
34
|
+
#
|
35
|
+
# This will cause the client not to connect to LaunchDarkly to get feature flags. The
|
36
|
+
# client may still make network connections to send analytics events, unless you have disabled
|
37
|
+
# this with {Config#send_events} or {Config#offline?}.
|
38
|
+
#
|
39
|
+
# Flag data files can be either JSON or YAML. They contain an object with three possible
|
40
|
+
# properties:
|
41
|
+
#
|
42
|
+
# - `flags`: Feature flag definitions.
|
43
|
+
# - `flagValues`: Simplified feature flags that contain only a value.
|
44
|
+
# - `segments`: User segment definitions.
|
45
|
+
#
|
46
|
+
# The format of the data in `flags` and `segments` is defined by the LaunchDarkly application
|
47
|
+
# and is subject to change. Rather than trying to construct these objects yourself, it is simpler
|
48
|
+
# to request existing flags directly from the LaunchDarkly server in JSON format, and use this
|
49
|
+
# output as the starting point for your file. In Linux you would do this:
|
50
|
+
#
|
51
|
+
# ```
|
52
|
+
# curl -H "Authorization: YOUR_SDK_KEY" https://app.launchdarkly.com/sdk/latest-all
|
53
|
+
# ```
|
54
|
+
#
|
55
|
+
# The output will look something like this (but with many more properties):
|
56
|
+
#
|
57
|
+
# {
|
58
|
+
# "flags": {
|
59
|
+
# "flag-key-1": {
|
60
|
+
# "key": "flag-key-1",
|
61
|
+
# "on": true,
|
62
|
+
# "variations": [ "a", "b" ]
|
63
|
+
# }
|
64
|
+
# },
|
65
|
+
# "segments": {
|
66
|
+
# "segment-key-1": {
|
67
|
+
# "key": "segment-key-1",
|
68
|
+
# "includes": [ "user-key-1" ]
|
69
|
+
# }
|
70
|
+
# }
|
71
|
+
# }
|
72
|
+
#
|
73
|
+
# Data in this format allows the SDK to exactly duplicate all the kinds of flag behavior supported
|
74
|
+
# by LaunchDarkly. However, in many cases you will not need this complexity, but will just want to
|
75
|
+
# set specific flag keys to specific values. For that, you can use a much simpler format:
|
76
|
+
#
|
77
|
+
# {
|
78
|
+
# "flagValues": {
|
79
|
+
# "my-string-flag-key": "value-1",
|
80
|
+
# "my-boolean-flag-key": true,
|
81
|
+
# "my-integer-flag-key": 3
|
82
|
+
# }
|
83
|
+
# }
|
84
|
+
#
|
85
|
+
# Or, in YAML:
|
86
|
+
#
|
87
|
+
# flagValues:
|
88
|
+
# my-string-flag-key: "value-1"
|
89
|
+
# my-boolean-flag-key: true
|
90
|
+
# my-integer-flag-key: 1
|
91
|
+
#
|
92
|
+
# It is also possible to specify both "flags" and "flagValues", if you want some flags
|
93
|
+
# to have simple values and others to have complex behavior. However, it is an error to use the
|
94
|
+
# same flag key or segment key more than once, either in a single file or across multiple files.
|
95
|
+
#
|
96
|
+
# If the data source encounters any error in any file-- malformed content, a missing file, or a
|
97
|
+
# duplicate key-- it will not load flags from any of the files.
|
98
|
+
#
|
99
|
+
class FileDataSource
|
100
|
+
#
|
101
|
+
# Returns a factory for the file data source component.
|
102
|
+
#
|
103
|
+
# @param options [Hash] the configuration options
|
104
|
+
# @option options [Array] :paths The paths of the source files for loading flag data. These
|
105
|
+
# may be absolute paths or relative to the current working directory.
|
106
|
+
# @option options [Boolean] :auto_update True if the data source should watch for changes to
|
107
|
+
# the source file(s) and reload flags whenever there is a change. Auto-updating will only
|
108
|
+
# work if all of the files you specified have valid directory paths at startup time.
|
109
|
+
# Note that the default implementation of this feature is based on polling the filesystem,
|
110
|
+
# which may not perform well. If you install the 'listen' gem (not included by default, to
|
111
|
+
# avoid adding unwanted dependencies to the SDK), its native file watching mechanism will be
|
112
|
+
# used instead. However, 'listen' will not be used in JRuby 9.1 due to a known instability.
|
113
|
+
# @option options [Float] :poll_interval The minimum interval, in seconds, between checks for
|
114
|
+
# file modifications - used only if auto_update is true, and if the native file-watching
|
115
|
+
# mechanism from 'listen' is not being used. The default value is 1 second.
|
116
|
+
# @return an object that can be stored in {Config#data_source}
|
117
|
+
#
|
118
|
+
def self.factory(options={})
|
119
|
+
return lambda { |sdk_key, config| FileDataSourceImpl.new(config.feature_store, config.logger, options) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# @private
|
124
|
+
class FileDataSourceImpl
|
125
|
+
def initialize(feature_store, logger, options={})
|
126
|
+
@feature_store = feature_store
|
127
|
+
@logger = logger
|
128
|
+
@paths = options[:paths] || []
|
129
|
+
if @paths.is_a? String
|
130
|
+
@paths = [ @paths ]
|
131
|
+
end
|
132
|
+
@auto_update = options[:auto_update]
|
133
|
+
if @auto_update && LaunchDarkly.have_listen? && !options[:force_polling] # force_polling is used only for tests
|
134
|
+
# We have seen unreliable behavior in the 'listen' gem in JRuby 9.1 (https://github.com/guard/listen/issues/449).
|
135
|
+
# Therefore, on that platform we'll fall back to file polling instead.
|
136
|
+
if defined?(JRUBY_VERSION) && JRUBY_VERSION.start_with?("9.1.")
|
137
|
+
@use_listen = false
|
138
|
+
else
|
139
|
+
@use_listen = true
|
140
|
+
end
|
141
|
+
end
|
142
|
+
@poll_interval = options[:poll_interval] || 1
|
143
|
+
@initialized = Concurrent::AtomicBoolean.new(false)
|
144
|
+
@ready = Concurrent::Event.new
|
145
|
+
end
|
146
|
+
|
147
|
+
def initialized?
|
148
|
+
@initialized.value
|
149
|
+
end
|
150
|
+
|
151
|
+
def start
|
152
|
+
ready = Concurrent::Event.new
|
153
|
+
|
154
|
+
# We will return immediately regardless of whether the file load succeeded or failed -
|
155
|
+
# the difference can be detected by checking "initialized?"
|
156
|
+
ready.set
|
157
|
+
|
158
|
+
load_all
|
159
|
+
|
160
|
+
if @auto_update
|
161
|
+
# If we're going to watch files, then the start event will be set the first time we get
|
162
|
+
# a successful load.
|
163
|
+
@listener = start_listener
|
164
|
+
end
|
165
|
+
|
166
|
+
ready
|
167
|
+
end
|
168
|
+
|
169
|
+
def stop
|
170
|
+
@listener.stop if !@listener.nil?
|
171
|
+
end
|
172
|
+
|
173
|
+
private
|
174
|
+
|
175
|
+
def load_all
|
176
|
+
all_data = {
|
177
|
+
FEATURES => {},
|
178
|
+
SEGMENTS => {}
|
179
|
+
}
|
180
|
+
@paths.each do |path|
|
181
|
+
begin
|
182
|
+
load_file(path, all_data)
|
183
|
+
rescue => exn
|
184
|
+
Util.log_exception(@logger, "Unable to load flag data from \"#{path}\"", exn)
|
185
|
+
return
|
186
|
+
end
|
187
|
+
end
|
188
|
+
@feature_store.init(all_data)
|
189
|
+
@initialized.make_true
|
190
|
+
end
|
191
|
+
|
192
|
+
def load_file(path, all_data)
|
193
|
+
parsed = parse_content(IO.read(path))
|
194
|
+
(parsed[:flags] || {}).each do |key, flag|
|
195
|
+
add_item(all_data, FEATURES, flag)
|
196
|
+
end
|
197
|
+
(parsed[:flagValues] || {}).each do |key, value|
|
198
|
+
add_item(all_data, FEATURES, make_flag_with_value(key.to_s, value))
|
199
|
+
end
|
200
|
+
(parsed[:segments] || {}).each do |key, segment|
|
201
|
+
add_item(all_data, SEGMENTS, segment)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
def parse_content(content)
|
206
|
+
# We can use the Ruby YAML parser for both YAML and JSON (JSON is a subset of YAML and while
|
207
|
+
# not all YAML parsers handle it correctly, we have verified that the Ruby one does, at least
|
208
|
+
# for all the samples of actual flag data that we've tested).
|
209
|
+
symbolize_all_keys(YAML.load(content))
|
210
|
+
end
|
211
|
+
|
212
|
+
def symbolize_all_keys(value)
|
213
|
+
# This is necessary because YAML.load doesn't have an option for parsing keys as symbols, and
|
214
|
+
# the SDK expects all objects to be formatted that way.
|
215
|
+
if value.is_a?(Hash)
|
216
|
+
value.map{ |k, v| [k.to_sym, symbolize_all_keys(v)] }.to_h
|
217
|
+
elsif value.is_a?(Array)
|
218
|
+
value.map{ |v| symbolize_all_keys(v) }
|
219
|
+
else
|
220
|
+
value
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
def add_item(all_data, kind, item)
|
225
|
+
items = all_data[kind]
|
226
|
+
raise ArgumentError, "Received unknown item kind #{kind} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash
|
227
|
+
key = item[:key].to_sym
|
228
|
+
if !items[key].nil?
|
229
|
+
raise ArgumentError, "#{kind[:namespace]} key \"#{item[:key]}\" was used more than once"
|
230
|
+
end
|
231
|
+
items[key] = item
|
232
|
+
end
|
233
|
+
|
234
|
+
def make_flag_with_value(key, value)
|
235
|
+
{
|
236
|
+
key: key,
|
237
|
+
on: true,
|
238
|
+
fallthrough: { variation: 0 },
|
239
|
+
variations: [ value ]
|
240
|
+
}
|
241
|
+
end
|
242
|
+
|
243
|
+
def start_listener
|
244
|
+
resolved_paths = @paths.map { |p| Pathname.new(File.absolute_path(p)).realpath.to_s }
|
245
|
+
if @use_listen
|
246
|
+
start_listener_with_listen_gem(resolved_paths)
|
247
|
+
else
|
248
|
+
FileDataSourcePoller.new(resolved_paths, @poll_interval, self.method(:load_all), @logger)
|
249
|
+
end
|
250
|
+
end
|
251
|
+
|
252
|
+
def start_listener_with_listen_gem(resolved_paths)
|
253
|
+
path_set = resolved_paths.to_set
|
254
|
+
dir_paths = resolved_paths.map{ |p| File.dirname(p) }.uniq
|
255
|
+
opts = { latency: @poll_interval }
|
256
|
+
l = Listen.to(*dir_paths, opts) do |modified, added, removed|
|
257
|
+
paths = modified + added + removed
|
258
|
+
if paths.any? { |p| path_set.include?(p) }
|
259
|
+
load_all
|
260
|
+
end
|
261
|
+
end
|
262
|
+
l.start
|
263
|
+
l
|
264
|
+
end
|
265
|
+
|
266
|
+
#
|
267
|
+
# Used internally by FileDataSource to track data file changes if the 'listen' gem is not available.
|
268
|
+
#
|
269
|
+
class FileDataSourcePoller
|
270
|
+
def initialize(resolved_paths, interval, reloader, logger)
|
271
|
+
@stopped = Concurrent::AtomicBoolean.new(false)
|
272
|
+
get_file_times = Proc.new do
|
273
|
+
ret = {}
|
274
|
+
resolved_paths.each do |path|
|
275
|
+
begin
|
276
|
+
ret[path] = File.mtime(path)
|
277
|
+
rescue Errno::ENOENT
|
278
|
+
ret[path] = nil
|
279
|
+
end
|
280
|
+
end
|
281
|
+
ret
|
282
|
+
end
|
283
|
+
last_times = get_file_times.call
|
284
|
+
@thread = Thread.new do
|
285
|
+
while true
|
286
|
+
sleep interval
|
287
|
+
break if @stopped.value
|
288
|
+
begin
|
289
|
+
new_times = get_file_times.call
|
290
|
+
changed = false
|
291
|
+
last_times.each do |path, old_time|
|
292
|
+
new_time = new_times[path]
|
293
|
+
if !new_time.nil? && new_time != old_time
|
294
|
+
changed = true
|
295
|
+
break
|
296
|
+
end
|
297
|
+
end
|
298
|
+
reloader.call if changed
|
299
|
+
rescue => exn
|
300
|
+
Util.log_exception(logger, "Unexpected exception in FileDataSourcePoller", exn)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def stop
|
307
|
+
@stopped.make_true
|
308
|
+
@thread.run # wakes it up if it's sleeping
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module LaunchDarkly
|
4
|
+
#
|
5
|
+
# A snapshot of the state of all feature flags with regard to a specific user, generated by
|
6
|
+
# calling the {LDClient#all_flags_state}. Serializing this object to JSON using
|
7
|
+
# `JSON.generate` (or the `to_json` method) will produce the appropriate data structure for
|
8
|
+
# bootstrapping the LaunchDarkly JavaScript client.
|
9
|
+
#
|
10
|
+
class FeatureFlagsState
|
11
|
+
def initialize(valid)
|
12
|
+
@flag_values = {}
|
13
|
+
@flag_metadata = {}
|
14
|
+
@valid = valid
|
15
|
+
end
|
16
|
+
|
17
|
+
# Used internally to build the state map.
|
18
|
+
# @private
|
19
|
+
def add_flag(flag, value, variation, reason = nil, details_only_if_tracked = false)
|
20
|
+
key = flag[:key]
|
21
|
+
@flag_values[key] = value
|
22
|
+
meta = {}
|
23
|
+
with_details = !details_only_if_tracked || flag[:trackEvents]
|
24
|
+
if !with_details && flag[:debugEventsUntilDate]
|
25
|
+
with_details = flag[:debugEventsUntilDate] > (Time.now.to_f * 1000).to_i
|
26
|
+
end
|
27
|
+
if with_details
|
28
|
+
meta[:version] = flag[:version]
|
29
|
+
meta[:reason] = reason if !reason.nil?
|
30
|
+
end
|
31
|
+
meta[:variation] = variation if !variation.nil?
|
32
|
+
meta[:trackEvents] = true if flag[:trackEvents]
|
33
|
+
meta[:debugEventsUntilDate] = flag[:debugEventsUntilDate] if flag[:debugEventsUntilDate]
|
34
|
+
@flag_metadata[key] = meta
|
35
|
+
end
|
36
|
+
|
37
|
+
# Returns true if this object contains a valid snapshot of feature flag state, or false if the
|
38
|
+
# state could not be computed (for instance, because the client was offline or there was no user).
|
39
|
+
def valid?
|
40
|
+
@valid
|
41
|
+
end
|
42
|
+
|
43
|
+
# Returns the value of an individual feature flag at the time the state was recorded.
|
44
|
+
# Returns nil if the flag returned the default value, or if there was no such flag.
|
45
|
+
def flag_value(key)
|
46
|
+
@flag_values[key]
|
47
|
+
end
|
48
|
+
|
49
|
+
# Returns a map of flag keys to flag values. If a flag would have evaluated to the default value,
|
50
|
+
# its value will be nil.
|
51
|
+
#
|
52
|
+
# Do not use this method if you are passing data to the front end to "bootstrap" the JavaScript client.
|
53
|
+
# Instead, use as_json.
|
54
|
+
def values_map
|
55
|
+
@flag_values
|
56
|
+
end
|
57
|
+
|
58
|
+
# Returns a hash that can be used as a JSON representation of the entire state map, in the format
|
59
|
+
# used by the LaunchDarkly JavaScript SDK. Use this method if you are passing data to the front end
|
60
|
+
# in order to "bootstrap" the JavaScript client.
|
61
|
+
#
|
62
|
+
# Do not rely on the exact shape of this data, as it may change in future to support the needs of
|
63
|
+
# the JavaScript client.
|
64
|
+
def as_json(*) # parameter is unused, but may be passed if we're using the json gem
|
65
|
+
ret = @flag_values.clone
|
66
|
+
ret['$flagsState'] = @flag_metadata
|
67
|
+
ret['$valid'] = @valid
|
68
|
+
ret
|
69
|
+
end
|
70
|
+
|
71
|
+
# Same as as_json, but converts the JSON structure into a string.
|
72
|
+
def to_json(*a)
|
73
|
+
as_json.to_json(a)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|