ldclient-rb 3.0.3 → 4.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +90 -0
- data/CHANGELOG.md +10 -0
- data/README.md +0 -1
- data/ldclient-rb.gemspec +8 -2
- data/lib/ldclient-rb.rb +5 -1
- data/lib/ldclient-rb/config.rb +41 -1
- data/lib/ldclient-rb/evaluation.rb +33 -17
- data/lib/ldclient-rb/event_summarizer.rb +52 -0
- data/lib/ldclient-rb/events.rb +383 -51
- data/lib/ldclient-rb/expiring_cache.rb +76 -0
- data/lib/ldclient-rb/ldclient.rb +44 -23
- data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
- data/lib/ldclient-rb/redis_store.rb +13 -17
- data/lib/ldclient-rb/simple_lru_cache.rb +24 -0
- data/lib/ldclient-rb/{event_serializer.rb → user_filter.rb} +17 -23
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/evaluation_spec.rb +44 -9
- data/spec/event_summarizer_spec.rb +63 -0
- data/spec/events_spec.rb +506 -0
- data/spec/expiring_cache_spec.rb +76 -0
- data/spec/fixtures/feature.json +1 -0
- data/spec/ldclient_spec.rb +94 -17
- data/spec/simple_lru_cache_spec.rb +24 -0
- data/spec/{event_serializer_spec.rb → user_filter_spec.rb} +23 -44
- metadata +49 -23
- data/circle.yml +0 -35
@@ -0,0 +1,76 @@
|
|
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
|
+
class ExpiringCache
|
10
|
+
def initialize(max_size, ttl)
|
11
|
+
@max_size = max_size
|
12
|
+
@ttl = ttl
|
13
|
+
@data_lru = {}
|
14
|
+
@data_ttl = {}
|
15
|
+
@lock = Mutex.new
|
16
|
+
end
|
17
|
+
|
18
|
+
def [](key)
|
19
|
+
@lock.synchronize do
|
20
|
+
ttl_evict
|
21
|
+
@data_lru[key]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def []=(key, val)
|
26
|
+
@lock.synchronize do
|
27
|
+
ttl_evict
|
28
|
+
|
29
|
+
@data_lru.delete(key)
|
30
|
+
@data_ttl.delete(key)
|
31
|
+
|
32
|
+
@data_lru[key] = val
|
33
|
+
@data_ttl[key] = Time.now.to_f
|
34
|
+
|
35
|
+
if @data_lru.size > @max_size
|
36
|
+
key, _ = @data_lru.first # hashes have a FIFO ordering in Ruby
|
37
|
+
|
38
|
+
@data_ttl.delete(key)
|
39
|
+
@data_lru.delete(key)
|
40
|
+
end
|
41
|
+
|
42
|
+
val
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def delete(key)
|
47
|
+
@lock.synchronize do
|
48
|
+
ttl_evict
|
49
|
+
|
50
|
+
@data_lru.delete(key)
|
51
|
+
@data_ttl.delete(key)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def clear
|
56
|
+
@lock.synchronize do
|
57
|
+
@data_lru.clear
|
58
|
+
@data_ttl.clear
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private
|
63
|
+
|
64
|
+
def ttl_evict
|
65
|
+
ttl_horizon = Time.now.to_f - @ttl
|
66
|
+
key, time = @data_ttl.first
|
67
|
+
|
68
|
+
until time.nil? || time > ttl_horizon
|
69
|
+
@data_ttl.delete(key)
|
70
|
+
@data_lru.delete(key)
|
71
|
+
|
72
|
+
key, time = @data_ttl.first
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -28,7 +28,11 @@ module LaunchDarkly
|
|
28
28
|
@config = config
|
29
29
|
@store = config.feature_store
|
30
30
|
|
31
|
-
@
|
31
|
+
if @config.offline? || !@config.send_events
|
32
|
+
@event_processor = NullEventProcessor.new
|
33
|
+
else
|
34
|
+
@event_processor = EventProcessor.new(sdk_key, config)
|
35
|
+
end
|
32
36
|
|
33
37
|
if @config.use_ldd?
|
34
38
|
@config.logger.info { "[LDClient] Started LaunchDarkly Client in LDD mode" }
|
@@ -38,12 +42,16 @@ module LaunchDarkly
|
|
38
42
|
requestor = Requestor.new(sdk_key, config)
|
39
43
|
|
40
44
|
if !@config.offline?
|
41
|
-
if @config.
|
42
|
-
@
|
45
|
+
if @config.update_processor.nil?
|
46
|
+
if @config.stream?
|
47
|
+
@update_processor = StreamProcessor.new(sdk_key, config, requestor)
|
48
|
+
else
|
49
|
+
@config.logger.info { "Disabling streaming API" }
|
50
|
+
@config.logger.warn { "You should only disable the streaming API if instructed to do so by LaunchDarkly support" }
|
51
|
+
@update_processor = PollingProcessor.new(config, requestor)
|
52
|
+
end
|
43
53
|
else
|
44
|
-
@config.
|
45
|
-
@config.logger.warn { "You should only disable the streaming API if instructed to do so by LaunchDarkly support" }
|
46
|
-
@update_processor = PollingProcessor.new(config, requestor)
|
54
|
+
@update_processor = @config.update_processor
|
47
55
|
end
|
48
56
|
@update_processor.start
|
49
57
|
end
|
@@ -113,12 +121,6 @@ module LaunchDarkly
|
|
113
121
|
def variation(key, user, default)
|
114
122
|
return default if @config.offline?
|
115
123
|
|
116
|
-
unless user
|
117
|
-
@config.logger.error { "[LDClient] Must specify user" }
|
118
|
-
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
|
119
|
-
return default
|
120
|
-
end
|
121
|
-
|
122
124
|
if !initialized?
|
123
125
|
if @store.initialized?
|
124
126
|
@config.logger.warn { "[LDClient] Client has not finished initializing; using last known values from feature store" }
|
@@ -129,7 +131,7 @@ module LaunchDarkly
|
|
129
131
|
end
|
130
132
|
end
|
131
133
|
|
132
|
-
sanitize_user(user)
|
134
|
+
sanitize_user(user) if !user.nil?
|
133
135
|
feature = @store.get(FEATURES, key)
|
134
136
|
|
135
137
|
if feature.nil?
|
@@ -138,24 +140,29 @@ module LaunchDarkly
|
|
138
140
|
return default
|
139
141
|
end
|
140
142
|
|
143
|
+
unless user
|
144
|
+
@config.logger.error { "[LDClient] Must specify user" }
|
145
|
+
@event_processor.add_event(make_feature_event(feature, user, nil, default, default))
|
146
|
+
return default
|
147
|
+
end
|
148
|
+
|
141
149
|
begin
|
142
|
-
res = evaluate(feature, user, @store)
|
150
|
+
res = evaluate(feature, user, @store, @config.logger)
|
143
151
|
if !res[:events].nil?
|
144
152
|
res[:events].each do |event|
|
145
153
|
@event_processor.add_event(event)
|
146
154
|
end
|
147
155
|
end
|
148
|
-
|
149
|
-
|
150
|
-
return res[:value]
|
151
|
-
else
|
156
|
+
value = res[:value]
|
157
|
+
if value.nil?
|
152
158
|
@config.logger.debug { "[LDClient] Result value is null in toggle" }
|
153
|
-
|
154
|
-
return default
|
159
|
+
value = default
|
155
160
|
end
|
161
|
+
@event_processor.add_event(make_feature_event(feature, user, res[:variation], value, default))
|
162
|
+
return value
|
156
163
|
rescue => exn
|
157
164
|
@config.logger.warn { "[LDClient] Error evaluating feature flag: #{exn.inspect}. \nTrace: #{exn.backtrace}" }
|
158
|
-
@event_processor.add_event(
|
165
|
+
@event_processor.add_event(make_feature_event(feature, user, nil, default, default))
|
159
166
|
return default
|
160
167
|
end
|
161
168
|
end
|
@@ -200,7 +207,7 @@ module LaunchDarkly
|
|
200
207
|
features = @store.all(FEATURES)
|
201
208
|
|
202
209
|
# TODO rescue if necessary
|
203
|
-
Hash[features.map{ |k, f| [k, evaluate(f, user, @store)[:value]] }]
|
210
|
+
Hash[features.map{ |k, f| [k, evaluate(f, user, @store, @config.logger)[:value]] }]
|
204
211
|
rescue => exn
|
205
212
|
@config.logger.warn { "[LDClient] Error evaluating all flags: #{exn.inspect}. \nTrace: #{exn.backtrace}" }
|
206
213
|
return Hash.new
|
@@ -232,6 +239,20 @@ module LaunchDarkly
|
|
232
239
|
end
|
233
240
|
end
|
234
241
|
|
235
|
-
|
242
|
+
def make_feature_event(flag, user, variation, value, default)
|
243
|
+
{
|
244
|
+
kind: "feature",
|
245
|
+
key: flag[:key],
|
246
|
+
user: user,
|
247
|
+
variation: variation,
|
248
|
+
value: value,
|
249
|
+
default: default,
|
250
|
+
version: flag[:version],
|
251
|
+
trackEvents: flag[:trackEvents],
|
252
|
+
debugEventsUntilDate: flag[:debugEventsUntilDate]
|
253
|
+
}
|
254
|
+
end
|
255
|
+
|
256
|
+
private :evaluate, :log_exception, :sanitize_user, :make_feature_event
|
236
257
|
end
|
237
258
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require "concurrent"
|
2
|
+
require "concurrent/atomics"
|
3
|
+
require "concurrent/executors"
|
4
|
+
require "thread"
|
5
|
+
|
6
|
+
# Simple wrapper for a FixedThreadPool that rejects new jobs if all the threads are busy, rather
|
7
|
+
# than blocking. Also provides a way to wait for all jobs to finish without shutting down.
|
8
|
+
|
9
|
+
module LaunchDarkly
|
10
|
+
class NonBlockingThreadPool
|
11
|
+
def initialize(capacity)
|
12
|
+
@capacity = capacity
|
13
|
+
@pool = Concurrent::FixedThreadPool.new(capacity)
|
14
|
+
@semaphore = Concurrent::Semaphore.new(capacity)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Attempts to submit a job, but only if a worker is available. Unlike the regular post method,
|
18
|
+
# this returns a value: true if the job was submitted, false if all workers are busy.
|
19
|
+
def post
|
20
|
+
if !@semaphore.try_acquire(1)
|
21
|
+
return
|
22
|
+
end
|
23
|
+
@pool.post do
|
24
|
+
begin
|
25
|
+
yield
|
26
|
+
ensure
|
27
|
+
@semaphore.release(1)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# Waits until no jobs are executing, without shutting down the pool.
|
33
|
+
def wait_all
|
34
|
+
@semaphore.acquire(@capacity)
|
35
|
+
@semaphore.release(@capacity)
|
36
|
+
end
|
37
|
+
|
38
|
+
def shutdown
|
39
|
+
@pool.shutdown
|
40
|
+
end
|
41
|
+
|
42
|
+
def wait_for_termination
|
43
|
+
@pool.wait_for_termination
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -9,15 +9,14 @@ module LaunchDarkly
|
|
9
9
|
# streaming API. Feature data can also be further cached in memory to reduce overhead
|
10
10
|
# of calls to Redis.
|
11
11
|
#
|
12
|
-
# To use this class, you must first have the `redis
|
13
|
-
#
|
14
|
-
#
|
12
|
+
# To use this class, you must first have the `redis` and `connection-pool` gems
|
13
|
+
# installed. Then, create an instance and store it in the `feature_store` property
|
14
|
+
# of your client configuration.
|
15
15
|
#
|
16
16
|
class RedisFeatureStore
|
17
17
|
begin
|
18
18
|
require "redis"
|
19
19
|
require "connection_pool"
|
20
|
-
require "moneta"
|
21
20
|
REDIS_ENABLED = true
|
22
21
|
rescue ScriptError, StandardError
|
23
22
|
REDIS_ENABLED = false
|
@@ -38,7 +37,7 @@ module LaunchDarkly
|
|
38
37
|
#
|
39
38
|
def initialize(opts = {})
|
40
39
|
if !REDIS_ENABLED
|
41
|
-
raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool
|
40
|
+
raise RuntimeError.new("can't use RedisFeatureStore because one of these gems is missing: redis, connection_pool")
|
42
41
|
end
|
43
42
|
@redis_opts = opts[:redis_opts] || Hash.new
|
44
43
|
if opts[:redis_url]
|
@@ -54,15 +53,12 @@ module LaunchDarkly
|
|
54
53
|
@prefix = opts[:prefix] || RedisFeatureStore.default_prefix
|
55
54
|
@logger = opts[:logger] || Config.default_logger
|
56
55
|
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
# hash operations that we use.
|
62
|
-
if @expiration_seconds > 0
|
63
|
-
@cache = Moneta.new(:LRUHash, expires: true, threadsafe: true, max_count: @capacity)
|
56
|
+
expiration_seconds = opts[:expiration] || 15
|
57
|
+
capacity = opts[:capacity] || 1000
|
58
|
+
if expiration_seconds > 0
|
59
|
+
@cache = ExpiringCache.new(capacity, expiration_seconds)
|
64
60
|
else
|
65
|
-
@cache =
|
61
|
+
@cache = nil
|
66
62
|
end
|
67
63
|
|
68
64
|
@stopped = Concurrent::AtomicBoolean.new(false)
|
@@ -92,7 +88,7 @@ and prefix: #{@prefix}")
|
|
92
88
|
end
|
93
89
|
|
94
90
|
def get(kind, key)
|
95
|
-
f = @cache[cache_key(kind, key)]
|
91
|
+
f = @cache.nil? ? nil : @cache[cache_key(kind, key)]
|
96
92
|
if f.nil?
|
97
93
|
@logger.debug { "RedisFeatureStore: no cache hit for #{key} in '#{kind[:namespace]}', requesting from Redis" }
|
98
94
|
f = with_connection do |redis|
|
@@ -139,7 +135,7 @@ and prefix: #{@prefix}")
|
|
139
135
|
end
|
140
136
|
|
141
137
|
def init(all_data)
|
142
|
-
@cache.clear
|
138
|
+
@cache.clear if !@cache.nil?
|
143
139
|
count = 0
|
144
140
|
with_connection do |redis|
|
145
141
|
all_data.each do |kind, items|
|
@@ -174,7 +170,7 @@ and prefix: #{@prefix}")
|
|
174
170
|
def stop
|
175
171
|
if @stopped.make_true
|
176
172
|
@pool.shutdown { |redis| redis.close }
|
177
|
-
@cache.clear
|
173
|
+
@cache.clear if !@cache.nil?
|
178
174
|
end
|
179
175
|
end
|
180
176
|
|
@@ -213,7 +209,7 @@ and prefix: #{@prefix}")
|
|
213
209
|
end
|
214
210
|
|
215
211
|
def put_cache(kind, key, value)
|
216
|
-
@cache
|
212
|
+
@cache[cache_key(kind, key)] = value if !@cache.nil?
|
217
213
|
end
|
218
214
|
|
219
215
|
def update_with_versioning(kind, new_item)
|
@@ -0,0 +1,24 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
# A non-thread-safe implementation of a LRU cache set with only add and reset methods.
|
4
|
+
# Based on https://github.com/SamSaffron/lru_redux/blob/master/lib/lru_redux/cache.rb
|
5
|
+
class SimpleLRUCacheSet
|
6
|
+
def initialize(capacity)
|
7
|
+
@values = {}
|
8
|
+
@capacity = capacity
|
9
|
+
end
|
10
|
+
|
11
|
+
# Adds a value to the cache or marks it recent if it was already there. Returns true if already there.
|
12
|
+
def add(value)
|
13
|
+
found = true
|
14
|
+
@values.delete(value) { found = false }
|
15
|
+
@values[value] = true
|
16
|
+
@values.shift if @values.length > @capacity
|
17
|
+
found
|
18
|
+
end
|
19
|
+
|
20
|
+
def clear
|
21
|
+
@values = {}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,18 +1,28 @@
|
|
1
1
|
require "json"
|
2
2
|
|
3
3
|
module LaunchDarkly
|
4
|
-
class
|
4
|
+
class UserFilter
|
5
5
|
def initialize(config)
|
6
6
|
@all_attributes_private = config.all_attributes_private
|
7
7
|
@private_attribute_names = Set.new(config.private_attribute_names.map(&:to_sym))
|
8
8
|
end
|
9
9
|
|
10
|
-
def
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
10
|
+
def transform_user_props(user_props)
|
11
|
+
return nil if user_props.nil?
|
12
|
+
|
13
|
+
user_private_attrs = Set.new((user_props[:privateAttributeNames] || []).map(&:to_sym))
|
14
|
+
|
15
|
+
filtered_user_props, removed = filter_values(user_props, user_private_attrs, ALLOWED_TOP_LEVEL_KEYS, IGNORED_TOP_LEVEL_KEYS)
|
16
|
+
if user_props.has_key?(:custom)
|
17
|
+
filtered_user_props[:custom], removed_custom = filter_values(user_props[:custom], user_private_attrs)
|
18
|
+
removed.merge(removed_custom)
|
19
|
+
end
|
20
|
+
|
21
|
+
unless removed.empty?
|
22
|
+
# note, :privateAttributeNames is what the developer sets; :privateAttrs is what we send to the server
|
23
|
+
filtered_user_props[:privateAttrs] = removed.to_a.sort.map { |s| s.to_s }
|
24
|
+
end
|
25
|
+
return filtered_user_props
|
16
26
|
end
|
17
27
|
|
18
28
|
private
|
@@ -35,21 +45,5 @@ module LaunchDarkly
|
|
35
45
|
def private_attr?(name, user_private_attrs)
|
36
46
|
@all_attributes_private || @private_attribute_names.include?(name) || user_private_attrs.include?(name)
|
37
47
|
end
|
38
|
-
|
39
|
-
def transform_user_props(user_props)
|
40
|
-
user_private_attrs = Set.new((user_props[:privateAttributeNames] || []).map(&:to_sym))
|
41
|
-
|
42
|
-
filtered_user_props, removed = filter_values(user_props, user_private_attrs, ALLOWED_TOP_LEVEL_KEYS, IGNORED_TOP_LEVEL_KEYS)
|
43
|
-
if user_props.has_key?(:custom)
|
44
|
-
filtered_user_props[:custom], removed_custom = filter_values(user_props[:custom], user_private_attrs)
|
45
|
-
removed.merge(removed_custom)
|
46
|
-
end
|
47
|
-
|
48
|
-
unless removed.empty?
|
49
|
-
# note, :privateAttributeNames is what the developer sets; :privateAttrs is what we send to the server
|
50
|
-
filtered_user_props[:privateAttrs] = removed.to_a.sort
|
51
|
-
end
|
52
|
-
return filtered_user_props
|
53
|
-
end
|
54
48
|
end
|
55
49
|
end
|
data/lib/ldclient-rb/version.rb
CHANGED
data/spec/evaluation_spec.rb
CHANGED
@@ -12,6 +12,8 @@ describe LaunchDarkly::Evaluation do
|
|
12
12
|
}
|
13
13
|
}
|
14
14
|
|
15
|
+
let(:logger) { LaunchDarkly::Config.default_logger }
|
16
|
+
|
15
17
|
include LaunchDarkly::Evaluation
|
16
18
|
|
17
19
|
describe "evaluate" do
|
@@ -24,7 +26,7 @@ describe LaunchDarkly::Evaluation do
|
|
24
26
|
variations: ['a', 'b', 'c']
|
25
27
|
}
|
26
28
|
user = { key: 'x' }
|
27
|
-
expect(evaluate(flag, user, features)).to eq({value: 'b', events: []})
|
29
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: 1, value: 'b', events: []})
|
28
30
|
end
|
29
31
|
|
30
32
|
it "returns nil if flag is off and off variation is unspecified" do
|
@@ -35,7 +37,7 @@ describe LaunchDarkly::Evaluation do
|
|
35
37
|
variations: ['a', 'b', 'c']
|
36
38
|
}
|
37
39
|
user = { key: 'x' }
|
38
|
-
expect(evaluate(flag, user, features)).to eq({value: nil, events: []})
|
40
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: nil, value: nil, events: []})
|
39
41
|
end
|
40
42
|
|
41
43
|
it "returns off variation if prerequisite is not found" do
|
@@ -48,7 +50,34 @@ describe LaunchDarkly::Evaluation do
|
|
48
50
|
variations: ['a', 'b', 'c']
|
49
51
|
}
|
50
52
|
user = { key: 'x' }
|
51
|
-
expect(evaluate(flag, user, features)).to eq({value: 'b', events: []})
|
53
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: 1, value: 'b', events: []})
|
54
|
+
end
|
55
|
+
|
56
|
+
it "returns off variation and event if prerequisite of a prerequisite is not found" do
|
57
|
+
flag = {
|
58
|
+
key: 'feature0',
|
59
|
+
on: true,
|
60
|
+
prerequisites: [{key: 'feature1', variation: 1}],
|
61
|
+
fallthrough: { variation: 0 },
|
62
|
+
offVariation: 1,
|
63
|
+
variations: ['a', 'b', 'c'],
|
64
|
+
version: 1
|
65
|
+
}
|
66
|
+
flag1 = {
|
67
|
+
key: 'feature1',
|
68
|
+
on: true,
|
69
|
+
prerequisites: [{key: 'feature2', variation: 1}], # feature2 doesn't exist
|
70
|
+
fallthrough: { variation: 0 },
|
71
|
+
variations: ['d', 'e'],
|
72
|
+
version: 2
|
73
|
+
}
|
74
|
+
features.upsert(LaunchDarkly::FEATURES, flag1)
|
75
|
+
user = { key: 'x' }
|
76
|
+
events_should_be = [{
|
77
|
+
kind: 'feature', key: 'feature1', variation: nil, value: nil, version: 2, prereqOf: 'feature0',
|
78
|
+
trackEvents: nil, debugEventsUntilDate: nil
|
79
|
+
}]
|
80
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: 1, value: 'b', events: events_should_be})
|
52
81
|
end
|
53
82
|
|
54
83
|
it "returns off variation and event if prerequisite is not met" do
|
@@ -70,8 +99,11 @@ describe LaunchDarkly::Evaluation do
|
|
70
99
|
}
|
71
100
|
features.upsert(LaunchDarkly::FEATURES, flag1)
|
72
101
|
user = { key: 'x' }
|
73
|
-
events_should_be = [{
|
74
|
-
|
102
|
+
events_should_be = [{
|
103
|
+
kind: 'feature', key: 'feature1', variation: 0, value: 'd', version: 2, prereqOf: 'feature0',
|
104
|
+
trackEvents: nil, debugEventsUntilDate: nil
|
105
|
+
}]
|
106
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: 1, value: 'b', events: events_should_be})
|
75
107
|
end
|
76
108
|
|
77
109
|
it "returns fallthrough variation and event if prerequisite is met and there are no rules" do
|
@@ -93,8 +125,11 @@ describe LaunchDarkly::Evaluation do
|
|
93
125
|
}
|
94
126
|
features.upsert(LaunchDarkly::FEATURES, flag1)
|
95
127
|
user = { key: 'x' }
|
96
|
-
events_should_be = [{
|
97
|
-
|
128
|
+
events_should_be = [{
|
129
|
+
kind: 'feature', key: 'feature1', variation: 1, value: 'e', version: 2, prereqOf: 'feature0',
|
130
|
+
trackEvents: nil, debugEventsUntilDate: nil
|
131
|
+
}]
|
132
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: 0, value: 'a', events: events_should_be})
|
98
133
|
end
|
99
134
|
|
100
135
|
it "matches user from targets" do
|
@@ -109,7 +144,7 @@ describe LaunchDarkly::Evaluation do
|
|
109
144
|
variations: ['a', 'b', 'c']
|
110
145
|
}
|
111
146
|
user = { key: 'userkey' }
|
112
|
-
expect(evaluate(flag, user, features)).to eq({value: 'c', events: []})
|
147
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: 2, value: 'c', events: []})
|
113
148
|
end
|
114
149
|
|
115
150
|
it "matches user from rules" do
|
@@ -133,7 +168,7 @@ describe LaunchDarkly::Evaluation do
|
|
133
168
|
variations: ['a', 'b', 'c']
|
134
169
|
}
|
135
170
|
user = { key: 'userkey' }
|
136
|
-
expect(evaluate(flag, user, features)).to eq({value: 'c', events: []})
|
171
|
+
expect(evaluate(flag, user, features, logger)).to eq({variation: 2, value: 'c', events: []})
|
137
172
|
end
|
138
173
|
end
|
139
174
|
|