ldclient-rb 0.8.0 → 2.0.1

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: 0c8a2f8c558a103c5d35bac43f65bcec2547c600
4
- data.tar.gz: 2d99a0b8cf0c6a8c7132bad6b44d9f7608a19221
3
+ metadata.gz: 3e6c80abac366d30d919bd5fe4cc1e4457adc55f
4
+ data.tar.gz: 6c2a232ee5f1c33ef3f86f1511e37a3f8dd34579
5
5
  SHA512:
6
- metadata.gz: 76099078850fa826d91024c5d8b9909ec3b91169a4fb3718a97116bbbe89c27aa1a6dc555f45b153c739a83d11b2d62252200c7c66870ca476b9d2b0ae25df34
7
- data.tar.gz: e8b0477b6e0355636b220f67fcc27e72ca14430a6c0750b0b7252ac040a362d6878854f0e78da4800102a7299bf258a6fdf0f10856cca8a4e608d79ca99fbc98
6
+ metadata.gz: 8d9d45ecf32ff2580a8c716646f2b685b66c2786a0e66cd93eb914044bb7a4d67add695ff8d06308d1d51e618962587ddded79cd3287b58d65ef1eec5635b23f
7
+ data.tar.gz: 2b28b6e04bf7ecc6ad677ffad40421c06428b3c2b15334dd996dfe21f327694c8f27406d1ee057316ea6cbbd8b9ce84831f0165d27dd5eeb4d03bada8b96e195
@@ -0,0 +1,25 @@
1
+ # Change log
2
+
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
+
5
+ ## [2.0.0] - 2016-08-08
6
+ ### Added
7
+ - Support for multivariate feature flags. In addition to booleans, feature flags can now return numbers, strings, dictionaries, or arrays via the `variation` method.
8
+ - New `all_flags` method returns all flag values for a specified user.
9
+ - If streaming is disabled, the client polls for feature flag changes. If streaming is disabled, the client will default to polling LaunchDarkly every second for updates. The poll interval is configurable via `poll_interval`.
10
+ - New `secure_mode_hash` function computes a hash suitable for the new LaunchDarkly JavaScript client's secure mode feature.
11
+ - Support for extremely large feature flags. When a large feature flag changes, the stream will include a directive to fetch the updated flag.
12
+
13
+ ### Changed
14
+ - You can now initialize the LaunchDarkly client with an optional timeout (specified in seconds). This will block initialization until the client has finished bootstrapping and is able to serve feature flags.
15
+ - The streaming implementation (`StreamProcessor`) uses [Celluloid](https://github.com/celluloid/celluloid) under the hood instead of [EventMachine](https://github.com/eventmachine/eventmachine). The dependency on EventMachine has been removed.
16
+ - The `store` option has been renamed to `cache_store`.
17
+ - Offline mode can no longer be set dynamically. Instead, at configuration time, the `offline` parameter can be set to put the client in offline mode. It is no longer possible to dynamically change whether the client is online and offline (via `set_online` and `set_offline`). Call `offline?` to determine whether or not the client is offline.
18
+ - The `debug_stream` configuration option has been removed.
19
+ - The `log_timings` configuration option has been removed.
20
+
21
+ ### Deprecated
22
+ - The `toggle` call has been deprecated in favor of `variation`.
23
+
24
+ ### Removed
25
+ - `update_user_flag_setting` has been removed. To change user settings, use the LaunchDarkly REST API.
data/README.md CHANGED
@@ -23,10 +23,10 @@ gem install ldclient-rb
23
23
  require 'ldclient-rb'
24
24
  ```
25
25
 
26
- 2. Create a new LDClient with your API key:
26
+ 2. Create a new LDClient with your SDK key:
27
27
 
28
28
  ```ruby
29
- client = LaunchDarkly::LDClient.new("your_api_key")
29
+ client = LaunchDarkly::LDClient.new("your_sdk_key")
30
30
  ```
31
31
 
32
32
  ### Ruby on Rails
@@ -36,7 +36,7 @@ client = LaunchDarkly::LDClient.new("your_api_key")
36
36
  1. Initialize the launchdarkly client in `config/initializers/launchdarkly.rb`:
37
37
 
38
38
  ```ruby
39
- Rails.configuration.ld_client = LaunchDarkly::LDClient.new("your_api_key")
39
+ Rails.configuration.ld_client = LaunchDarkly::LDClient.new("your_sdk_key")
40
40
  ```
41
41
 
42
42
  2. You may want to include a function in your ApplicationController
@@ -0,0 +1,11 @@
1
+ require 'rubygems'
2
+
3
+
4
+ # From http://stackoverflow.com/questions/5830835/how-to-add-openssl-dependency-to-gemspec
5
+ # the whole reason this file exists: to return an error if openssl
6
+ # isn't installed.
7
+ require 'openssl'
8
+
9
+ f = File.open(File.join(File.dirname(__FILE__), "Rakefile"), "w") # create dummy rakefile to indicate success
10
+ f.write("task :default\n")
11
+ f.close
@@ -17,6 +17,7 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = ["lib"]
20
+ spec.extensions = 'ext/mkrf_conf.rb'
20
21
 
21
22
  spec.add_development_dependency "bundler", "~> 1.7"
22
23
  spec.add_development_dependency "rake", "~> 10.0"
@@ -30,5 +31,6 @@ Gem::Specification.new do |spec|
30
31
  spec.add_runtime_dependency "net-http-persistent", "~> 2.9"
31
32
  spec.add_runtime_dependency "concurrent-ruby", "~> 1.0.0"
32
33
  spec.add_runtime_dependency "hashdiff", "~> 0.2"
33
- spec.add_runtime_dependency "ld-em-eventsource", "~> 0.2" #https://github.com/launchdarkly/em-eventsource
34
+ spec.add_runtime_dependency "ld-celluloid-eventsource", "~> 0.5"
35
+ spec.add_runtime_dependency "waitutil", "0.2"
34
36
  end
@@ -1,7 +1,11 @@
1
1
  require "ldclient-rb/version"
2
- require "ldclient-rb/settings"
2
+ require "ldclient-rb/evaluation"
3
3
  require "ldclient-rb/ldclient"
4
- require "ldclient-rb/store"
4
+ require "ldclient-rb/cache_store"
5
5
  require "ldclient-rb/config"
6
6
  require "ldclient-rb/newrelic"
7
7
  require "ldclient-rb/stream"
8
+ require "ldclient-rb/polling"
9
+ require "ldclient-rb/events"
10
+ require "ldclient-rb/feature_store"
11
+ require "ldclient-rb/requestor"
@@ -31,9 +31,15 @@ module LaunchDarkly
31
31
  # connections in seconds.
32
32
  # @option opts [Float] :connect_timeout (2) The connect timeout for network
33
33
  # connections in seconds.
34
- # @option opts [Object] :store A cache store for the Faraday HTTP caching
34
+ # @option opts [Object] :cache_store A cache store for the Faraday HTTP caching
35
35
  # library. Defaults to the Rails cache in a Rails environment, or a
36
36
  # thread-safe in-memory store otherwise.
37
+ # @option opts [Boolean] :offline (false) Whether the client should be initialized in
38
+ # offline mode. In offline mode, default values are returned for all flags and no
39
+ # remote network requests are made.
40
+ # @option opts [Float] :poll_interval (30) The number of seconds between polls for flag updates
41
+ # if streaming is off.
42
+ # @option opts [Boolean] :stream (true) Whether or not the streaming API should be used to receive flag updates.
37
43
  #
38
44
  # @return [type] [description]
39
45
  def initialize(opts = {})
@@ -42,14 +48,14 @@ module LaunchDarkly
42
48
  @events_uri = (opts[:events_uri] || Config.default_events_uri).chomp("/")
43
49
  @capacity = opts[:capacity] || Config.default_capacity
44
50
  @logger = opts[:logger] || Config.default_logger
45
- @store = opts[:store] || Config.default_store
51
+ @cache_store = opts[:cache_store] || Config.default_cache_store
46
52
  @flush_interval = opts[:flush_interval] || Config.default_flush_interval
47
53
  @connect_timeout = opts[:connect_timeout] || Config.default_connect_timeout
48
54
  @read_timeout = opts[:read_timeout] || Config.default_read_timeout
49
55
  @feature_store = opts[:feature_store] || Config.default_feature_store
50
56
  @stream = opts.has_key?(:stream) ? opts[:stream] : Config.default_stream
51
- @log_timings = opts.has_key?(:log_timings) ? opts[:log_timings] : Config.default_log_timings
52
- @debug_stream = opts.has_key?(:debug_stream) ? opts[:debug_stream] : Config.default_debug_stream
57
+ @offline = opts.has_key?(:offline) ? opts[:offline] : Config.default_offline
58
+ @poll_interval = opts.has_key?(:poll_interval) && opts[:poll_interval] > 1 ? opts[:poll_interval] : Config.default_poll_interval
53
59
  end
54
60
 
55
61
  #
@@ -79,13 +85,9 @@ module LaunchDarkly
79
85
  @stream
80
86
  end
81
87
 
82
- #
83
- # Whether we should debug streaming mode. If set, the client will fetch features via polling
84
- # and compare the retrieved feature with the value in the feature store
85
- #
86
- # @return [Boolean] True if we should debug streaming mode
87
- def debug_stream?
88
- @debug_stream
88
+ # TODO docs
89
+ def offline?
90
+ @offline
89
91
  end
90
92
 
91
93
  #
@@ -95,6 +97,11 @@ module LaunchDarkly
95
97
  # @return [Float] The configured number of seconds between flushes of the event buffer.
96
98
  attr_reader :flush_interval
97
99
 
100
+ #
101
+ # The number of seconds to wait before polling for feature flag updates. This option has no
102
+ # effect unless streaming is disabled
103
+ attr_reader :poll_interval
104
+
98
105
  #
99
106
  # The configured logger for the LaunchDarkly client. The client library uses the log to
100
107
  # print warning and error messages.
@@ -117,7 +124,7 @@ module LaunchDarkly
117
124
  # 'read' and 'write' requests.
118
125
  #
119
126
  # @return [Object] The configured store for the Faraday HTTP caching library.
120
- attr_reader :store
127
+ attr_reader :cache_store
121
128
 
122
129
  #
123
130
  # The read timeout for network connections in seconds.
@@ -132,16 +139,7 @@ module LaunchDarkly
132
139
  attr_reader :connect_timeout
133
140
 
134
141
  #
135
- # Whether timing information should be logged. If it is logged, it will be logged to the DEBUG
136
- # level on the configured logger. This can be very verbose.
137
- #
138
- # @return [Boolean] True if timing information should be logged.
139
- def log_timings?
140
- @log_timings
141
- end
142
-
143
- #
144
- # TODO docs
142
+ # A store for feature flag configuration rules.
145
143
  #
146
144
  attr_reader :feature_store
147
145
 
@@ -170,7 +168,7 @@ module LaunchDarkly
170
168
  "https://events.launchdarkly.com"
171
169
  end
172
170
 
173
- def self.default_store
171
+ def self.default_cache_store
174
172
  defined?(Rails) && Rails.respond_to?(:cache) ? Rails.cache : ThreadSafeMemoryStore.new
175
173
  end
176
174
 
@@ -190,20 +188,20 @@ module LaunchDarkly
190
188
  defined?(Rails) && Rails.respond_to?(:logger) ? Rails.logger : ::Logger.new($stdout)
191
189
  end
192
190
 
193
- def self.default_log_timings
194
- false
195
- end
196
-
197
191
  def self.default_stream
198
192
  true
199
193
  end
200
194
 
201
195
  def self.default_feature_store
202
- nil
196
+ InMemoryFeatureStore.new
203
197
  end
204
198
 
205
- def self.default_debug_stream
199
+ def self.default_offline
206
200
  false
207
201
  end
202
+
203
+ def self.default_poll_interval
204
+ 1
205
+ end
208
206
  end
209
207
  end
@@ -0,0 +1,265 @@
1
+ require "date"
2
+
3
+ module LaunchDarkly
4
+
5
+ module Evaluation
6
+ BUILTINS = [:key, :ip, :country, :email, :firstName, :lastName, :avatar, :name, :anonymous]
7
+
8
+ OPERATORS = {
9
+ in:
10
+ lambda do |a, b|
11
+ a == b
12
+ end,
13
+ endsWith:
14
+ lambda do |a, b|
15
+ (a.is_a? String) && (a.end_with? b)
16
+ end,
17
+ startsWith:
18
+ lambda do |a, b|
19
+ (a.is_a? String) && (a.start_with? b)
20
+ end,
21
+ matches:
22
+ lambda do |a, b|
23
+ (b.is_a? String) && !(Regexp.new b).match(a).nil?
24
+ end,
25
+ contains:
26
+ lambda do |a, b|
27
+ (a.is_a? String) && (a.include? b)
28
+ end,
29
+ lessThan:
30
+ lambda do |a, b|
31
+ (a.is_a? Numeric) && (a < b)
32
+ end,
33
+ lessThanOrEqual:
34
+ lambda do |a, b|
35
+ (a.is_a? Numeric) && (a <= b)
36
+ end,
37
+ greaterThan:
38
+ lambda do |a, b|
39
+ (a.is_a? Numeric) && (a > b)
40
+ end,
41
+ greaterThanOrEqual:
42
+ lambda do |a, b|
43
+ (a.is_a? Numeric) && (a >= b)
44
+ end,
45
+ before:
46
+ lambda do |a, b|
47
+ begin
48
+ if a.is_a? String
49
+ a = DateTime.rfc3339(a).strftime('%Q').to_i
50
+ end
51
+ if b.is_a? String
52
+ b = DateTime.rfc3339(b).strftime('%Q').to_i
53
+ end
54
+ (a.is_a? Numeric) ? a < b : false
55
+ rescue => e
56
+ false
57
+ end
58
+ end,
59
+ after:
60
+ lambda do |a, b|
61
+ begin
62
+ if a.is_a? String
63
+ a = DateTime.rfc3339(a).strftime('%Q').to_i
64
+ end
65
+ if b.is_a? String
66
+ b = DateTime.rfc3339(b).strftime('%Q').to_i
67
+ end
68
+ (a.is_a? Numeric) ? a > b : false
69
+ rescue => e
70
+ false
71
+ end
72
+ end
73
+ }
74
+
75
+ class EvaluationError < StandardError
76
+ end
77
+
78
+ # Evaluates a feature flag, returning a hash containing the evaluation result and any events
79
+ # generated during prerequisite evaluation. Raises EvaluationError if the flag is not well-formed
80
+ # Will return nil, but not raise an exception, indicating that the rules (including fallthrough) did not match
81
+ # In that case, the caller should return the default value.
82
+ def evaluate(flag, user, store)
83
+ if flag.nil?
84
+ raise EvaluationError, "Flag does not exist"
85
+ end
86
+
87
+ if user.nil? || user[:key].nil?
88
+ raise EvaluationError, "Invalid user"
89
+ end
90
+
91
+ events = []
92
+
93
+ if flag[:on]
94
+ res = eval_internal(flag, user, store, events)
95
+
96
+ return {value: res, events: events} if !res.nil?
97
+ end
98
+
99
+ if !flag[:offVariation].nil? && flag[:offVariation] < flag[:variations].length
100
+ value = flag[:variations][flag[:offVariation]]
101
+ return {value: value, events: events}
102
+ end
103
+
104
+ {value: nil, events: events}
105
+ end
106
+
107
+ def eval_internal(flag, user, store, events)
108
+ failed_prereq = false
109
+ # Evaluate prerequisites, if any
110
+ if !flag[:prerequisites].nil?
111
+ flag[:prerequisites].each do |prerequisite|
112
+
113
+ prereq_flag = store.get(prerequisite[:key])
114
+
115
+ if prereq_flag.nil? || !prereq_flag[:on]
116
+ failed_prereq = true
117
+ else
118
+ begin
119
+ prereq_res = eval_internal(prereq_flag, user, store, events)
120
+ variation = get_variation(prereq_flag, prerequisite[:variation])
121
+ events.push(kind: "feature", key: prereq_flag[:key], value: prereq_res, version: prereq_flag[:version], prereqOf: flag[:key])
122
+ if prereq_res.nil? || prereq_res!= variation
123
+ failed_prereq = true
124
+ end
125
+ rescue => exn
126
+ @config.logger.error("[LDClient] Error evaluating prerequisite: #{exn.inspect}")
127
+ failed_prereq = true
128
+ end
129
+ end
130
+ end
131
+
132
+ if failed_prereq
133
+ return nil
134
+ end
135
+ end
136
+ # The prerequisites were satisfied.
137
+ # Now walk through the evaluation steps and get the correct
138
+ # variation index
139
+ eval_rules(flag, user)
140
+ end
141
+
142
+ def eval_rules(flag, user)
143
+ # Check user target matches
144
+ if !flag[:targets].nil?
145
+ flag[:targets].each do |target|
146
+ if !target[:values].nil?
147
+ target[:values].each do |value|
148
+ return get_variation(flag, target[:variation]) if value == user[:key]
149
+ end
150
+ end
151
+ end
152
+ end
153
+
154
+ # Check custom rules
155
+ if !flag[:rules].nil?
156
+ flag[:rules].each do |rule|
157
+ return variation_for_user(rule, user, flag) if rule_match_user(rule, user)
158
+ end
159
+ end
160
+
161
+ # Check the fallthrough rule
162
+ if !flag[:fallthrough].nil?
163
+ return variation_for_user(flag[:fallthrough], user, flag)
164
+ end
165
+
166
+ # Not even the fallthrough matched-- return the off variation or default
167
+ nil
168
+ end
169
+
170
+ def get_variation(flag, index)
171
+ if index >= flag[:variations].length
172
+ raise EvaluationError, "Invalid variation index"
173
+ end
174
+ flag[:variations][index]
175
+ end
176
+
177
+ def rule_match_user(rule, user)
178
+ return false if !rule[:clauses]
179
+
180
+ rule[:clauses].each do |clause|
181
+ return false if !clause_match_user(clause, user)
182
+ end
183
+
184
+ return true
185
+ end
186
+
187
+ def clause_match_user(clause, user)
188
+ val = user_value(user, clause[:attribute])
189
+ return false if val.nil?
190
+
191
+ op = OPERATORS[clause[:op].to_sym]
192
+
193
+ if op.nil?
194
+ raise EvaluationError, "Unsupported operator #{clause[:op]} in evaluation"
195
+ end
196
+
197
+ if val.is_a? Enumerable
198
+ val.each do |v|
199
+ return maybe_negate(clause, true) if match_any(op, v, clause[:values])
200
+ end
201
+ return maybe_negate(clause, false)
202
+ end
203
+
204
+ maybe_negate(clause, match_any(op, val, clause[:values]))
205
+ end
206
+
207
+ def variation_for_user(rule, user, flag)
208
+ if !rule[:variation].nil? # fixed variation
209
+ return get_variation(flag, rule[:variation])
210
+ elsif !rule[:rollout].nil? # percentage rollout
211
+ rollout = rule[:rollout]
212
+ bucket_by = rollout[:bucketBy].nil? ? "key" : rollout[:bucketBy]
213
+ bucket = bucket_user(user, flag[:key], bucket_by, flag[:salt])
214
+ sum = 0;
215
+ rollout[:variations].each do |variate|
216
+ sum += variate[:weight].to_f / 100000.0
217
+ return get_variation(flag, variate[:variation]) if bucket < sum
218
+ end
219
+ nil
220
+ else # the rule isn't well-formed
221
+ raise EvaluationError, "Rule does not define a variation or rollout"
222
+ end
223
+ end
224
+
225
+ def bucket_user(user, key, bucket_by, salt)
226
+ return nil unless user[:key]
227
+
228
+ id_hash = user_value(user, bucket_by)
229
+
230
+ if user[:secondary]
231
+ id_hash += "." + user[:secondary]
232
+ end
233
+
234
+ hash_key = "%s.%s.%s" % [key, salt, id_hash]
235
+
236
+ hash_val = (Digest::SHA1.hexdigest(hash_key))[0..14]
237
+ hash_val.to_i(16) / Float(0xFFFFFFFFFFFFFFF)
238
+ end
239
+
240
+ def user_value(user, attribute)
241
+ attribute = attribute.to_sym
242
+
243
+ if BUILTINS.include? attribute
244
+ user[attribute]
245
+ elsif !user[:custom].nil?
246
+ user[:custom][attribute]
247
+ else
248
+ nil
249
+ end
250
+ end
251
+
252
+ def maybe_negate(clause, b)
253
+ clause[:negate] ? !b : b
254
+ end
255
+
256
+ def match_any(op, value, values)
257
+ values.each do |v|
258
+ return true if op.call(value, v)
259
+ end
260
+ return false
261
+ end
262
+ end
263
+
264
+ end
265
+