ldclient-rb 2.3.2 → 2.4.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/CHANGELOG.md +7 -1
- data/README.md +17 -0
- data/lib/ldclient-rb.rb +1 -0
- data/lib/ldclient-rb/config.rb +4 -3
- data/lib/ldclient-rb/event_serializer.rb +10 -6
- data/lib/ldclient-rb/ldclient.rb +11 -5
- data/lib/ldclient-rb/memoized_value.rb +30 -0
- data/lib/ldclient-rb/redis_feature_store.rb +9 -13
- data/lib/ldclient-rb/version.rb +1 -1
- data/spec/config_spec.rb +8 -0
- data/spec/event_serializer_spec.rb +26 -0
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a18dfb629edba5ddd3ba6662289a8cf90b54e452
|
4
|
+
data.tar.gz: ba95bca6cbea4c808fadb06e04e66d3df39ab0a5
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e5c7b818184d3b961f6894579356f6cd624cf3cf74f109ded02004d5e5ca9c17582a93bfec01316e7fce47de3d6fbfc4618f2967d43d52a94904f70f1db8d67c
|
7
|
+
data.tar.gz: 3d7126f29afd904557ceb9a0f4f6c115659db399894eb6f8bdf69fe65e1f9b9bb468d9d24a09610d1a1f390f2feb165da54f9400edc0b8e53baf09553ef4375c
|
data/CHANGELOG.md
CHANGED
@@ -2,7 +2,13 @@
|
|
2
2
|
|
3
3
|
All notable changes to the LaunchDarkly Ruby SDK will be documented in this file. This project adheres to [Semantic Versioning](http://semver.org).
|
4
4
|
|
5
|
-
## 2.
|
5
|
+
## [2.4.0] - 2018-01-12
|
6
|
+
## Changed
|
7
|
+
- Will use feature store if already initialized even if connection to service could not be established. This is useful when flags have been initialized in redis.
|
8
|
+
- Increase default and minimum polling interval to 30s
|
9
|
+
- Strip out unknown top-level attributes
|
10
|
+
|
11
|
+
## [2.3.2] - 2017-12-02
|
6
12
|
|
7
13
|
### Fixed
|
8
14
|
- Make sure redis store initializations are atomic
|
data/README.md
CHANGED
@@ -77,6 +77,23 @@ Rails.application.config.ld_client.variation('your.flag.key', launchdarkly_setti
|
|
77
77
|
|
78
78
|
Note that this gem will automatically switch to using the Rails logger it is detected.
|
79
79
|
|
80
|
+
|
81
|
+
HTTPS proxy
|
82
|
+
------------
|
83
|
+
The Ruby SDK uses Faraday to handle all of its network traffic. Faraday provides a built-in HTTPS proxy. If the HTTPS_PROXY environment variable is present then the SDK will proxy all network requests through the URL provided.
|
84
|
+
|
85
|
+
How to set the HTTPS_PROXY environment variable on Mac/Linux systems:
|
86
|
+
```
|
87
|
+
export HTTPS_PROXY=https://web-proxy.domain.com:8080
|
88
|
+
```
|
89
|
+
|
90
|
+
|
91
|
+
How to set the HTTPS_PROXY environment variable on Windows systems:
|
92
|
+
```
|
93
|
+
set HTTPS_PROXY=https://web-proxy.domain.com:8080
|
94
|
+
```
|
95
|
+
|
96
|
+
|
80
97
|
Your first feature flag
|
81
98
|
-----------------------
|
82
99
|
|
data/lib/ldclient-rb.rb
CHANGED
data/lib/ldclient-rb/config.rb
CHANGED
@@ -41,9 +41,10 @@ module LaunchDarkly
|
|
41
41
|
# @option opts [Boolean] :offline (false) Whether the client should be initialized in
|
42
42
|
# offline mode. In offline mode, default values are returned for all flags and no
|
43
43
|
# remote network requests are made.
|
44
|
-
# @option opts [Float] :poll_interval (
|
44
|
+
# @option opts [Float] :poll_interval (30) The number of seconds between polls for flag updates
|
45
45
|
# if streaming is off.
|
46
46
|
# @option opts [Boolean] :stream (true) Whether or not the streaming API should be used to receive flag updates.
|
47
|
+
# Streaming should only be disabled on the advice of LaunchDarkly support.
|
47
48
|
# @option opts [Boolean] all_attributes_private (false) If true, all user attributes (other than the key)
|
48
49
|
# will be private, not just the attributes specified in `private_attribute_names`.
|
49
50
|
# @option opts [Array] :private_attribute_names Marks a set of attribute names private. Any users sent to
|
@@ -68,7 +69,7 @@ module LaunchDarkly
|
|
68
69
|
@stream = opts.has_key?(:stream) ? opts[:stream] : Config.default_stream
|
69
70
|
@use_ldd = opts.has_key?(:use_ldd) ? opts[:use_ldd] : Config.default_use_ldd
|
70
71
|
@offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
|
71
|
-
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] >
|
72
|
+
@poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > Config.default_poll_interval ? opts[:poll_interval] : Config.default_poll_interval
|
72
73
|
@proxy = opts[:proxy] || Config.default_proxy
|
73
74
|
@all_attributes_private = opts[:all_attributes_private] || false
|
74
75
|
@private_attribute_names = opts[:private_attribute_names] || []
|
@@ -256,7 +257,7 @@ module LaunchDarkly
|
|
256
257
|
end
|
257
258
|
|
258
259
|
def self.default_poll_interval
|
259
|
-
|
260
|
+
30
|
260
261
|
end
|
261
262
|
|
262
263
|
def self.default_send_events
|
@@ -17,14 +17,18 @@ module LaunchDarkly
|
|
17
17
|
|
18
18
|
private
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
ALLOWED_TOP_LEVEL_KEYS = Set.new([:key, :secondary, :ip, :country, :email,
|
21
|
+
:firstName, :lastName, :avatar, :name, :anonymous, :custom])
|
22
|
+
IGNORED_TOP_LEVEL_KEYS = Set.new([:custom, :key, :anonymous])
|
22
23
|
|
23
|
-
def filter_values(props, user_private_attrs,
|
24
|
+
def filter_values(props, user_private_attrs, allowed_keys = [], keys_to_leave_as_is = [])
|
25
|
+
is_valid_key = lambda { |key| allowed_keys.empty? || allowed_keys.include?(key) }
|
24
26
|
removed_keys = Set.new(props.keys.select { |key|
|
25
|
-
|
27
|
+
# Note that if is_valid_key returns false, we don't explicitly *remove* the key (which would place
|
28
|
+
# it in the privateAttrs list) - we just silently drop it when we calculate filtered_hash.
|
29
|
+
is_valid_key.call(key) && !keys_to_leave_as_is.include?(key) && private_attr?(key, user_private_attrs)
|
26
30
|
})
|
27
|
-
filtered_hash = props.select { |key, value| !removed_keys.include?(key) &&
|
31
|
+
filtered_hash = props.select { |key, value| !removed_keys.include?(key) && is_valid_key.call(key) }
|
28
32
|
[filtered_hash, removed_keys]
|
29
33
|
end
|
30
34
|
|
@@ -35,7 +39,7 @@ module LaunchDarkly
|
|
35
39
|
def transform_user_props(user_props)
|
36
40
|
user_private_attrs = Set.new((user_props[:privateAttributeNames] || []).map(&:to_sym))
|
37
41
|
|
38
|
-
filtered_user_props, removed = filter_values(user_props, user_private_attrs, IGNORED_TOP_LEVEL_KEYS)
|
42
|
+
filtered_user_props, removed = filter_values(user_props, user_private_attrs, ALLOWED_TOP_LEVEL_KEYS, IGNORED_TOP_LEVEL_KEYS)
|
39
43
|
if user_props.has_key?(:custom)
|
40
44
|
filtered_user_props[:custom], removed_custom = filter_values(user_props[:custom], user_private_attrs)
|
41
45
|
removed.merge(removed_custom)
|
data/lib/ldclient-rb/ldclient.rb
CHANGED
@@ -41,6 +41,8 @@ module LaunchDarkly
|
|
41
41
|
if @config.stream?
|
42
42
|
@update_processor = StreamProcessor.new(sdk_key, config, requestor)
|
43
43
|
else
|
44
|
+
@config.logger.info("Disabling streaming API")
|
45
|
+
@config.logger.warn("You should only disable the streaming API if instructed to do so by LaunchDarkly support")
|
44
46
|
@update_processor = PollingProcessor.new(config, requestor)
|
45
47
|
end
|
46
48
|
@update_processor.start
|
@@ -49,7 +51,7 @@ module LaunchDarkly
|
|
49
51
|
if !@config.offline? && wait_for_sec > 0
|
50
52
|
begin
|
51
53
|
WaitUtil.wait_for_condition("LaunchDarkly client initialization", timeout_sec: wait_for_sec, delay_sec: 0.1) do
|
52
|
-
|
54
|
+
initialized?
|
53
55
|
end
|
54
56
|
rescue WaitUtil::TimeoutError
|
55
57
|
@config.logger.error("[LDClient] Timeout encountered waiting for LaunchDarkly client initialization")
|
@@ -117,10 +119,14 @@ module LaunchDarkly
|
|
117
119
|
return default
|
118
120
|
end
|
119
121
|
|
120
|
-
if
|
121
|
-
@
|
122
|
-
|
123
|
-
|
122
|
+
if !initialized?
|
123
|
+
if @store.initialized?
|
124
|
+
@config.logger.warn("[LDClient] Client has not finished initializing; using last known values from feature store")
|
125
|
+
else
|
126
|
+
@config.logger.error("[LDClient] Client has not finished initializing; feature store unavailable, returning default value")
|
127
|
+
@event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
|
128
|
+
return default
|
129
|
+
end
|
124
130
|
end
|
125
131
|
|
126
132
|
sanitize_user(user)
|
@@ -0,0 +1,30 @@
|
|
1
|
+
|
2
|
+
module LaunchDarkly
|
3
|
+
# Simple implementation of a thread-safe memoized value whose generator function will never be
|
4
|
+
# run more than once, and whose value can be overridden by explicit assignment.
|
5
|
+
class MemoizedValue
|
6
|
+
def initialize(&generator)
|
7
|
+
@generator = generator
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@inited = false
|
10
|
+
@value = nil
|
11
|
+
end
|
12
|
+
|
13
|
+
def get
|
14
|
+
@mutex.synchronize do
|
15
|
+
if !@inited
|
16
|
+
@value = @generator.call
|
17
|
+
@inited = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
@value
|
21
|
+
end
|
22
|
+
|
23
|
+
def set(value)
|
24
|
+
@mutex.synchronize do
|
25
|
+
@value = value
|
26
|
+
@inited = true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -13,8 +13,6 @@ module LaunchDarkly
|
|
13
13
|
# property of your client configuration.
|
14
14
|
#
|
15
15
|
class RedisFeatureStore
|
16
|
-
INIT_KEY = :"$initialized"
|
17
|
-
|
18
16
|
begin
|
19
17
|
require "redis"
|
20
18
|
require "connection_pool"
|
@@ -68,6 +66,9 @@ module LaunchDarkly
|
|
68
66
|
end
|
69
67
|
|
70
68
|
@stopped = Concurrent::AtomicBoolean.new(false)
|
69
|
+
@inited = MemoizedValue.new {
|
70
|
+
query_inited
|
71
|
+
}
|
71
72
|
|
72
73
|
with_connection do |redis|
|
73
74
|
@logger.info("RedisFeatureStore: using Redis instance at #{redis.connection[:host]}:#{redis.connection[:port]} \
|
@@ -163,7 +164,7 @@ and prefix: #{@prefix}")
|
|
163
164
|
fs.each { |k, f| put_redis_and_cache(multi, k, f) }
|
164
165
|
end
|
165
166
|
end
|
166
|
-
|
167
|
+
@inited.set(true)
|
167
168
|
@logger.info("RedisFeatureStore: initialized with #{fs.count} feature flags")
|
168
169
|
end
|
169
170
|
|
@@ -180,16 +181,7 @@ and prefix: #{@prefix}")
|
|
180
181
|
end
|
181
182
|
|
182
183
|
def initialized?
|
183
|
-
|
184
|
-
if with_connection { |redis| redis.exists(@features_key) }
|
185
|
-
put_cache(INIT_KEY, true)
|
186
|
-
true
|
187
|
-
else
|
188
|
-
false
|
189
|
-
end
|
190
|
-
else
|
191
|
-
put_cache(INIT_KEY, true) # reset TTL
|
192
|
-
end
|
184
|
+
@inited.get
|
193
185
|
end
|
194
186
|
|
195
187
|
def stop
|
@@ -232,5 +224,9 @@ and prefix: #{@prefix}")
|
|
232
224
|
end
|
233
225
|
put_cache(key.to_sym, feature)
|
234
226
|
end
|
227
|
+
|
228
|
+
def query_inited
|
229
|
+
with_connection { |redis| redis.exists(@features_key) }
|
230
|
+
end
|
235
231
|
end
|
236
232
|
end
|
data/lib/ldclient-rb/version.rb
CHANGED
data/spec/config_spec.rb
CHANGED
@@ -52,4 +52,12 @@ describe LaunchDarkly::Config do
|
|
52
52
|
expect(subject.default_logger).to be_an_instance_of Logger
|
53
53
|
end
|
54
54
|
end
|
55
|
+
describe ".poll_interval" do
|
56
|
+
it "can be set to greater than the default" do
|
57
|
+
expect(subject.new(poll_interval: 31).poll_interval).to eq 31
|
58
|
+
end
|
59
|
+
it "cannot be set to less than the default" do
|
60
|
+
expect(subject.new(poll_interval: 29).poll_interval).to eq 30
|
61
|
+
end
|
62
|
+
end
|
55
63
|
end
|
@@ -19,6 +19,14 @@ describe LaunchDarkly::EventSerializer do
|
|
19
19
|
u
|
20
20
|
}
|
21
21
|
|
22
|
+
let(:user_with_unknown_top_level_attrs) {
|
23
|
+
{ key: 'abc', firstName: 'Sue', species: 'human', hatSize: 6, custom: { bizzle: 'def', dizzle: 'ghi' }}
|
24
|
+
}
|
25
|
+
|
26
|
+
let(:anon_user) {
|
27
|
+
{ key: 'abc', anonymous: 'true', custom: { bizzle: 'def', dizzle: 'ghi' }}
|
28
|
+
}
|
29
|
+
|
22
30
|
# expected results from serializing user
|
23
31
|
|
24
32
|
let(:user_with_all_attrs_hidden) {
|
@@ -33,6 +41,10 @@ describe LaunchDarkly::EventSerializer do
|
|
33
41
|
{ key: 'abc', firstName: 'Sue', custom: { bizzle: 'def' }, privateAttrs: [ 'dizzle' ]}
|
34
42
|
}
|
35
43
|
|
44
|
+
let(:anon_user_with_all_attrs_hidden) {
|
45
|
+
{ key: 'abc', anonymous: 'true', custom: { }, privateAttrs: [ 'bizzle', 'dizzle' ]}
|
46
|
+
}
|
47
|
+
|
36
48
|
|
37
49
|
def make_event(user)
|
38
50
|
{
|
@@ -82,5 +94,19 @@ describe LaunchDarkly::EventSerializer do
|
|
82
94
|
j = es.serialize_events([event])
|
83
95
|
expect(parse_results(j)).to eq [make_event(user_with_all_attrs_hidden)]
|
84
96
|
end
|
97
|
+
|
98
|
+
it "strips out any unknown top-level attributes" do
|
99
|
+
es = LaunchDarkly::EventSerializer.new(base_config)
|
100
|
+
event = make_event(user_with_unknown_top_level_attrs)
|
101
|
+
j = es.serialize_events([event])
|
102
|
+
expect(parse_results(j)).to eq [make_event(user)]
|
103
|
+
end
|
104
|
+
|
105
|
+
it "leaves the anonymous attribute as is" do
|
106
|
+
es = LaunchDarkly::EventSerializer.new(config_with_all_attrs_private)
|
107
|
+
event = make_event(anon_user)
|
108
|
+
j = es.serialize_events([event])
|
109
|
+
expect(parse_results(j)).to eq [make_event(anon_user_with_all_attrs_hidden)]
|
110
|
+
end
|
85
111
|
end
|
86
112
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ldclient-rb
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.
|
4
|
+
version: 2.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- LaunchDarkly
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2018-01-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -293,6 +293,7 @@ files:
|
|
293
293
|
- lib/ldclient-rb/events.rb
|
294
294
|
- lib/ldclient-rb/feature_store.rb
|
295
295
|
- lib/ldclient-rb/ldclient.rb
|
296
|
+
- lib/ldclient-rb/memoized_value.rb
|
296
297
|
- lib/ldclient-rb/newrelic.rb
|
297
298
|
- lib/ldclient-rb/polling.rb
|
298
299
|
- lib/ldclient-rb/redis_feature_store.rb
|