ldclient-rb 2.3.2 → 2.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 074163fec07d0e90f8f0aadb30bcf43adb132983
4
- data.tar.gz: 5f6a6c1649125810cd59b314d44c83f57a7eb91e
3
+ metadata.gz: a18dfb629edba5ddd3ba6662289a8cf90b54e452
4
+ data.tar.gz: ba95bca6cbea4c808fadb06e04e66d3df39ab0a5
5
5
  SHA512:
6
- metadata.gz: f9f4d473ee40827436698307aec2455d08cf822072a0795752fb824bad9b04100505e3d42665413ae38f07590b7e7e2149887e486103670c16f057cf13790eef
7
- data.tar.gz: 353e502f66a7fcb3e07daf97a629139adb4fcdb76e648fb95e4447d9563f4aa21879816cc1e40c10e50fa3f31241c2757502c7dee365519374dffc35884086d2
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.3.2 - 2017-12-02
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
@@ -2,6 +2,7 @@ require "ldclient-rb/version"
2
2
  require "ldclient-rb/evaluation"
3
3
  require "ldclient-rb/ldclient"
4
4
  require "ldclient-rb/cache_store"
5
+ require "ldclient-rb/memoized_value"
5
6
  require "ldclient-rb/config"
6
7
  require "ldclient-rb/newrelic"
7
8
  require "ldclient-rb/stream"
@@ -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 (1) The number of seconds between polls for flag updates
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] > 1 ? opts[:poll_interval] : Config.default_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
- 1
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
- IGNORED_TOP_LEVEL_KEYS = Set.new([:custom, :key, :privateAttributeNames])
21
- STRIPPED_TOP_LEVEL_KEYS = Set.new([:privateAttributeNames])
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, ignore=[])
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
- !ignore.include?(key) && private_attr?(key, user_private_attrs)
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) && !STRIPPED_TOP_LEVEL_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)
@@ -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
- @update_processor.initialized?
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 !@update_processor.nil? && !@update_processor.initialized?
121
- @config.logger.error("[LDClient] Client has not finished initializing. Returning default value")
122
- @event_processor.add_event(kind: "feature", key: key, value: default, default: default, user: user)
123
- return default
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
- put_cache(INIT_KEY, true)
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
- if @cache[INIT_KEY].nil?
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
@@ -1,3 +1,3 @@
1
1
  module LaunchDarkly
2
- VERSION = "2.3.2"
2
+ VERSION = "2.4.0"
3
3
  end
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.3.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: 2017-12-02 00:00:00.000000000 Z
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