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