ldclient-rb 3.0.3 → 4.0.0
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 +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
|
|