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