launchdarkly-server-sdk 5.7.4 → 6.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +28 -122
  3. data/.gitignore +1 -1
  4. data/.ldrelease/build-docs.sh +18 -0
  5. data/.ldrelease/circleci/linux/execute.sh +18 -0
  6. data/.ldrelease/circleci/mac/execute.sh +18 -0
  7. data/.ldrelease/circleci/template/build.sh +29 -0
  8. data/.ldrelease/circleci/template/publish.sh +23 -0
  9. data/.ldrelease/circleci/template/set-gem-home.sh +7 -0
  10. data/.ldrelease/circleci/template/test.sh +10 -0
  11. data/.ldrelease/circleci/template/update-version.sh +8 -0
  12. data/.ldrelease/circleci/windows/execute.ps1 +19 -0
  13. data/.ldrelease/config.yml +14 -2
  14. data/CHANGELOG.md +28 -0
  15. data/CONTRIBUTING.md +1 -1
  16. data/Gemfile.lock +92 -76
  17. data/README.md +4 -3
  18. data/azure-pipelines.yml +1 -1
  19. data/docs/Makefile +26 -0
  20. data/docs/index.md +9 -0
  21. data/launchdarkly-server-sdk.gemspec +20 -13
  22. data/lib/ldclient-rb.rb +0 -1
  23. data/lib/ldclient-rb/config.rb +15 -3
  24. data/lib/ldclient-rb/evaluation_detail.rb +293 -0
  25. data/lib/ldclient-rb/events.rb +3 -4
  26. data/lib/ldclient-rb/file_data_source.rb +1 -1
  27. data/lib/ldclient-rb/impl/evaluator.rb +225 -0
  28. data/lib/ldclient-rb/impl/evaluator_bucketing.rb +74 -0
  29. data/lib/ldclient-rb/impl/evaluator_operators.rb +160 -0
  30. data/lib/ldclient-rb/impl/event_factory.rb +22 -0
  31. data/lib/ldclient-rb/impl/event_sender.rb +56 -40
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +8 -7
  35. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  36. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  37. data/lib/ldclient-rb/integrations/redis.rb +3 -0
  38. data/lib/ldclient-rb/ldclient.rb +33 -11
  39. data/lib/ldclient-rb/polling.rb +1 -4
  40. data/lib/ldclient-rb/redis_store.rb +1 -0
  41. data/lib/ldclient-rb/requestor.rb +25 -23
  42. data/lib/ldclient-rb/stream.rb +10 -30
  43. data/lib/ldclient-rb/util.rb +12 -8
  44. data/lib/ldclient-rb/version.rb +1 -1
  45. data/spec/evaluation_detail_spec.rb +135 -0
  46. data/spec/event_sender_spec.rb +20 -2
  47. data/spec/events_spec.rb +10 -0
  48. data/spec/http_util.rb +11 -1
  49. data/spec/impl/evaluator_bucketing_spec.rb +111 -0
  50. data/spec/impl/evaluator_clause_spec.rb +55 -0
  51. data/spec/impl/evaluator_operators_spec.rb +141 -0
  52. data/spec/impl/evaluator_rule_spec.rb +96 -0
  53. data/spec/impl/evaluator_segment_spec.rb +125 -0
  54. data/spec/impl/evaluator_spec.rb +305 -0
  55. data/spec/impl/evaluator_spec_base.rb +75 -0
  56. data/spec/impl/model/serialization_spec.rb +41 -0
  57. data/spec/launchdarkly-server-sdk_spec.rb +1 -1
  58. data/spec/ldclient_end_to_end_spec.rb +34 -0
  59. data/spec/ldclient_spec.rb +60 -8
  60. data/spec/polling_spec.rb +2 -2
  61. data/spec/redis_feature_store_spec.rb +32 -3
  62. data/spec/requestor_spec.rb +11 -45
  63. data/spec/spec_helper.rb +0 -3
  64. data/spec/stream_spec.rb +1 -16
  65. metadata +110 -60
  66. data/.yardopts +0 -9
  67. data/lib/ldclient-rb/evaluation.rb +0 -462
  68. data/scripts/gendocs.sh +0 -11
  69. data/scripts/release.sh +0 -27
  70. data/spec/evaluation_spec.rb +0 -789
@@ -0,0 +1,305 @@
1
+ require "spec_helper"
2
+ require "impl/evaluator_spec_base"
3
+
4
+ module LaunchDarkly
5
+ module Impl
6
+ describe "Evaluator (general)", :evaluator_spec_base => true do
7
+ subject { Evaluator }
8
+
9
+ describe "evaluate" do
10
+ it "returns off variation if flag is off" do
11
+ flag = {
12
+ key: 'feature',
13
+ on: false,
14
+ offVariation: 1,
15
+ fallthrough: { variation: 0 },
16
+ variations: ['a', 'b', 'c']
17
+ }
18
+ user = { key: 'x' }
19
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::off)
20
+ result = basic_evaluator.evaluate(flag, user, factory)
21
+ expect(result.detail).to eq(detail)
22
+ expect(result.events).to eq(nil)
23
+ end
24
+
25
+ it "returns nil if flag is off and off variation is unspecified" do
26
+ flag = {
27
+ key: 'feature',
28
+ on: false,
29
+ fallthrough: { variation: 0 },
30
+ variations: ['a', 'b', 'c']
31
+ }
32
+ user = { key: 'x' }
33
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::off)
34
+ result = basic_evaluator.evaluate(flag, user, factory)
35
+ expect(result.detail).to eq(detail)
36
+ expect(result.events).to eq(nil)
37
+ end
38
+
39
+ it "returns an error if off variation is too high" do
40
+ flag = {
41
+ key: 'feature',
42
+ on: false,
43
+ offVariation: 999,
44
+ fallthrough: { variation: 0 },
45
+ variations: ['a', 'b', 'c']
46
+ }
47
+ user = { key: 'x' }
48
+ detail = EvaluationDetail.new(nil, nil,
49
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
50
+ result = basic_evaluator.evaluate(flag, user, factory)
51
+ expect(result.detail).to eq(detail)
52
+ expect(result.events).to eq(nil)
53
+ end
54
+
55
+ it "returns an error if off variation is negative" do
56
+ flag = {
57
+ key: 'feature',
58
+ on: false,
59
+ offVariation: -1,
60
+ fallthrough: { variation: 0 },
61
+ variations: ['a', 'b', 'c']
62
+ }
63
+ user = { key: 'x' }
64
+ detail = EvaluationDetail.new(nil, nil,
65
+ EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
66
+ result = basic_evaluator.evaluate(flag, user, factory)
67
+ expect(result.detail).to eq(detail)
68
+ expect(result.events).to eq(nil)
69
+ end
70
+
71
+ it "returns off variation if prerequisite is not found" do
72
+ flag = {
73
+ key: 'feature0',
74
+ on: true,
75
+ prerequisites: [{key: 'badfeature', variation: 1}],
76
+ fallthrough: { variation: 0 },
77
+ offVariation: 1,
78
+ variations: ['a', 'b', 'c']
79
+ }
80
+ user = { key: 'x' }
81
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('badfeature'))
82
+ e = subject.new(get_things( 'badfeature' => nil ), get_nothing, logger)
83
+ result = e.evaluate(flag, user, factory)
84
+ expect(result.detail).to eq(detail)
85
+ expect(result.events).to eq(nil)
86
+ end
87
+
88
+ it "reuses prerequisite-failed reason instances if possible" do
89
+ flag = {
90
+ key: 'feature0',
91
+ on: true,
92
+ prerequisites: [{key: 'badfeature', variation: 1}],
93
+ fallthrough: { variation: 0 },
94
+ offVariation: 1,
95
+ variations: ['a', 'b', 'c']
96
+ }
97
+ Model.postprocess_item_after_deserializing!(FEATURES, flag) # now there's a cached reason
98
+ user = { key: 'x' }
99
+ e = subject.new(get_things( 'badfeature' => nil ), get_nothing, logger)
100
+ result1 = e.evaluate(flag, user, factory)
101
+ expect(result1.detail.reason).to eq EvaluationReason::prerequisite_failed('badfeature')
102
+ result2 = e.evaluate(flag, user, factory)
103
+ expect(result2.detail.reason).to be result1.detail.reason
104
+ end
105
+
106
+ it "returns off variation and event if prerequisite of a prerequisite is not found" do
107
+ flag = {
108
+ key: 'feature0',
109
+ on: true,
110
+ prerequisites: [{key: 'feature1', variation: 1}],
111
+ fallthrough: { variation: 0 },
112
+ offVariation: 1,
113
+ variations: ['a', 'b', 'c'],
114
+ version: 1
115
+ }
116
+ flag1 = {
117
+ key: 'feature1',
118
+ on: true,
119
+ prerequisites: [{key: 'feature2', variation: 1}], # feature2 doesn't exist
120
+ fallthrough: { variation: 0 },
121
+ variations: ['d', 'e'],
122
+ version: 2
123
+ }
124
+ user = { key: 'x' }
125
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
126
+ events_should_be = [{
127
+ kind: 'feature', key: 'feature1', user: user, value: nil, default: nil, variation: nil, version: 2, prereqOf: 'feature0'
128
+ }]
129
+ get_flag = get_things('feature1' => flag1, 'feature2' => nil)
130
+ e = subject.new(get_flag, get_nothing, logger)
131
+ result = e.evaluate(flag, user, factory)
132
+ expect(result.detail).to eq(detail)
133
+ expect(result.events).to eq(events_should_be)
134
+ end
135
+
136
+ it "returns off variation and event if prerequisite is off" do
137
+ flag = {
138
+ key: 'feature0',
139
+ on: true,
140
+ prerequisites: [{key: 'feature1', variation: 1}],
141
+ fallthrough: { variation: 0 },
142
+ offVariation: 1,
143
+ variations: ['a', 'b', 'c'],
144
+ version: 1
145
+ }
146
+ flag1 = {
147
+ key: 'feature1',
148
+ on: false,
149
+ # note that even though it returns the desired variation, it is still off and therefore not a match
150
+ offVariation: 1,
151
+ fallthrough: { variation: 0 },
152
+ variations: ['d', 'e'],
153
+ version: 2
154
+ }
155
+ user = { key: 'x' }
156
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
157
+ events_should_be = [{
158
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
159
+ }]
160
+ get_flag = get_things({ 'feature1' => flag1 })
161
+ e = subject.new(get_flag, get_nothing, logger)
162
+ result = e.evaluate(flag, user, factory)
163
+ expect(result.detail).to eq(detail)
164
+ expect(result.events).to eq(events_should_be)
165
+ end
166
+
167
+ it "returns off variation and event if prerequisite is not met" do
168
+ flag = {
169
+ key: 'feature0',
170
+ on: true,
171
+ prerequisites: [{key: 'feature1', variation: 1}],
172
+ fallthrough: { variation: 0 },
173
+ offVariation: 1,
174
+ variations: ['a', 'b', 'c'],
175
+ version: 1
176
+ }
177
+ flag1 = {
178
+ key: 'feature1',
179
+ on: true,
180
+ fallthrough: { variation: 0 },
181
+ variations: ['d', 'e'],
182
+ version: 2
183
+ }
184
+ user = { key: 'x' }
185
+ detail = EvaluationDetail.new('b', 1, EvaluationReason::prerequisite_failed('feature1'))
186
+ events_should_be = [{
187
+ kind: 'feature', key: 'feature1', user: user, variation: 0, value: 'd', default: nil, version: 2, prereqOf: 'feature0'
188
+ }]
189
+ get_flag = get_things({ 'feature1' => flag1 })
190
+ e = subject.new(get_flag, get_nothing, logger)
191
+ result = e.evaluate(flag, user, factory)
192
+ expect(result.detail).to eq(detail)
193
+ expect(result.events).to eq(events_should_be)
194
+ end
195
+
196
+ it "returns fallthrough variation and event if prerequisite is met and there are no rules" do
197
+ flag = {
198
+ key: 'feature0',
199
+ on: true,
200
+ prerequisites: [{key: 'feature1', variation: 1}],
201
+ fallthrough: { variation: 0 },
202
+ offVariation: 1,
203
+ variations: ['a', 'b', 'c'],
204
+ version: 1
205
+ }
206
+ flag1 = {
207
+ key: 'feature1',
208
+ on: true,
209
+ fallthrough: { variation: 1 },
210
+ variations: ['d', 'e'],
211
+ version: 2
212
+ }
213
+ user = { key: 'x' }
214
+ detail = EvaluationDetail.new('a', 0, EvaluationReason::fallthrough)
215
+ events_should_be = [{
216
+ kind: 'feature', key: 'feature1', user: user, variation: 1, value: 'e', default: nil, version: 2, prereqOf: 'feature0'
217
+ }]
218
+ get_flag = get_things({ 'feature1' => flag1 })
219
+ e = subject.new(get_flag, get_nothing, logger)
220
+ result = e.evaluate(flag, user, factory)
221
+ expect(result.detail).to eq(detail)
222
+ expect(result.events).to eq(events_should_be)
223
+ end
224
+
225
+ it "returns an error if fallthrough variation is too high" do
226
+ flag = {
227
+ key: 'feature',
228
+ on: true,
229
+ fallthrough: { variation: 999 },
230
+ offVariation: 1,
231
+ variations: ['a', 'b', 'c']
232
+ }
233
+ user = { key: 'userkey' }
234
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
235
+ result = basic_evaluator.evaluate(flag, user, factory)
236
+ expect(result.detail).to eq(detail)
237
+ expect(result.events).to eq(nil)
238
+ end
239
+
240
+ it "returns an error if fallthrough variation is negative" do
241
+ flag = {
242
+ key: 'feature',
243
+ on: true,
244
+ fallthrough: { variation: -1 },
245
+ offVariation: 1,
246
+ variations: ['a', 'b', 'c']
247
+ }
248
+ user = { key: 'userkey' }
249
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
250
+ result = basic_evaluator.evaluate(flag, user, factory)
251
+ expect(result.detail).to eq(detail)
252
+ expect(result.events).to eq(nil)
253
+ end
254
+
255
+ it "returns an error if fallthrough has no variation or rollout" do
256
+ flag = {
257
+ key: 'feature',
258
+ on: true,
259
+ fallthrough: { },
260
+ offVariation: 1,
261
+ variations: ['a', 'b', 'c']
262
+ }
263
+ user = { key: 'userkey' }
264
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
265
+ result = basic_evaluator.evaluate(flag, user, factory)
266
+ expect(result.detail).to eq(detail)
267
+ expect(result.events).to eq(nil)
268
+ end
269
+
270
+ it "returns an error if fallthrough has a rollout with no variations" do
271
+ flag = {
272
+ key: 'feature',
273
+ on: true,
274
+ fallthrough: { rollout: { variations: [] } },
275
+ offVariation: 1,
276
+ variations: ['a', 'b', 'c']
277
+ }
278
+ user = { key: 'userkey' }
279
+ detail = EvaluationDetail.new(nil, nil, EvaluationReason::error(EvaluationReason::ERROR_MALFORMED_FLAG))
280
+ result = basic_evaluator.evaluate(flag, user, factory)
281
+ expect(result.detail).to eq(detail)
282
+ expect(result.events).to eq(nil)
283
+ end
284
+
285
+ it "matches user from targets" do
286
+ flag = {
287
+ key: 'feature',
288
+ on: true,
289
+ targets: [
290
+ { values: [ 'whoever', 'userkey' ], variation: 2 }
291
+ ],
292
+ fallthrough: { variation: 0 },
293
+ offVariation: 1,
294
+ variations: ['a', 'b', 'c']
295
+ }
296
+ user = { key: 'userkey' }
297
+ detail = EvaluationDetail.new('c', 2, EvaluationReason::target_match)
298
+ result = basic_evaluator.evaluate(flag, user, factory)
299
+ expect(result.detail).to eq(detail)
300
+ expect(result.events).to eq(nil)
301
+ end
302
+ end
303
+ end
304
+ end
305
+ end
@@ -0,0 +1,75 @@
1
+ require "spec_helper"
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module EvaluatorSpecBase
6
+ def factory
7
+ EventFactory.new(false)
8
+ end
9
+
10
+ def user
11
+ {
12
+ key: "userkey",
13
+ email: "test@example.com",
14
+ name: "Bob"
15
+ }
16
+ end
17
+
18
+ def logger
19
+ ::Logger.new($stdout, level: ::Logger::FATAL)
20
+ end
21
+
22
+ def get_nothing
23
+ lambda { |key| raise "should not have requested #{key}" }
24
+ end
25
+
26
+ def get_things(map)
27
+ lambda { |key|
28
+ raise "should not have requested #{key}" if !map.has_key?(key)
29
+ map[key]
30
+ }
31
+ end
32
+
33
+ def basic_evaluator
34
+ subject.new(get_nothing, get_nothing, logger)
35
+ end
36
+
37
+ def boolean_flag_with_rules(rules)
38
+ { key: 'feature', on: true, rules: rules, fallthrough: { variation: 0 }, variations: [ false, true ] }
39
+ end
40
+
41
+ def boolean_flag_with_clauses(clauses)
42
+ boolean_flag_with_rules([{ id: 'ruleid', clauses: clauses, variation: 1 }])
43
+ end
44
+
45
+ def make_user_matching_clause(user, attr)
46
+ {
47
+ attribute: attr.to_s,
48
+ op: :in,
49
+ values: [ user[attr.to_sym] ],
50
+ negate: false
51
+ }
52
+ end
53
+
54
+ def make_segment(key)
55
+ {
56
+ key: key,
57
+ included: [],
58
+ excluded: [],
59
+ salt: 'abcdef',
60
+ version: 1
61
+ }
62
+ end
63
+
64
+ def make_segment_match_clause(segment)
65
+ {
66
+ op: :segmentMatch,
67
+ values: [ segment[:key] ],
68
+ negate: false
69
+ }
70
+ end
71
+ end
72
+
73
+ RSpec.configure { |c| c.include EvaluatorSpecBase, :evaluator_spec_base => true }
74
+ end
75
+ end
@@ -0,0 +1,41 @@
1
+ require "spec_helper"
2
+
3
+ module LaunchDarkly
4
+ module Impl
5
+ module Model
6
+ describe "model serialization" do
7
+ it "serializes flag" do
8
+ flag = { key: "flagkey", version: 1 }
9
+ json = Model.serialize(FEATURES, flag)
10
+ expect(JSON.parse(json, symbolize_names: true)).to eq flag
11
+ end
12
+
13
+ it "serializes segment" do
14
+ segment = { key: "segkey", version: 1 }
15
+ json = Model.serialize(SEGMENTS, segment)
16
+ expect(JSON.parse(json, symbolize_names: true)).to eq segment
17
+ end
18
+
19
+ it "serializes arbitrary data kind" do
20
+ thing = { key: "thingkey", name: "me" }
21
+ json = Model.serialize({ name: "things" }, thing)
22
+ expect(JSON.parse(json, symbolize_names: true)).to eq thing
23
+ end
24
+
25
+ it "deserializes flag with no rules or prerequisites" do
26
+ flag_in = { key: "flagkey", version: 1 }
27
+ json = Model.serialize(FEATURES, flag_in)
28
+ flag_out = Model.deserialize(FEATURES, json)
29
+ expect(flag_out).to eq flag_in
30
+ end
31
+
32
+ it "deserializes segment" do
33
+ segment_in = { key: "segkey", version: 1 }
34
+ json = Model.serialize(SEGMENTS, segment_in)
35
+ segment_out = Model.deserialize(SEGMENTS, json)
36
+ expect(segment_out).to eq segment_in
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -4,7 +4,7 @@ require "bundler"
4
4
  describe LaunchDarkly do
5
5
  it "can be automatically loaded by Bundler.require" do
6
6
  ldclient_loaded =
7
- Bundler.with_clean_env do
7
+ Bundler.with_unbundled_env do
8
8
  Kernel.system("ruby", "./spec/launchdarkly-server-sdk_spec_autoloadtest.rb")
9
9
  end
10
10
 
@@ -80,6 +80,7 @@ module LaunchDarkly
80
80
 
81
81
  req, body = events_server.await_request_with_body
82
82
  expect(req.header['authorization']).to eq [ SDK_KEY ]
83
+ expect(req.header['connection']).to eq [ "Keep-Alive" ]
83
84
  data = JSON.parse(body)
84
85
  expect(data.length).to eq 1
85
86
  expect(data[0]["kind"]).to eq "identify"
@@ -111,6 +112,7 @@ module LaunchDarkly
111
112
  req = req0.path == "/diagnostic" ? req0 : req1
112
113
  body = req0.path == "/diagnostic" ? body0 : body1
113
114
  expect(req.header['authorization']).to eq [ SDK_KEY ]
115
+ expect(req.header['connection']).to eq [ "Keep-Alive" ]
114
116
  data = JSON.parse(body)
115
117
  expect(data["kind"]).to eq "diagnostic-init"
116
118
  end
@@ -118,6 +120,38 @@ module LaunchDarkly
118
120
  end
119
121
  end
120
122
 
123
+ it "can use socket factory" do
124
+ with_server do |poll_server|
125
+ with_server do |events_server|
126
+ events_server.setup_ok_response("/bulk", "")
127
+ poll_server.setup_ok_response("/sdk/latest-all", '{"flags":{},"segments":{}}', "application/json")
128
+
129
+ config = Config.new(
130
+ stream: false,
131
+ base_uri: "http://polling.com",
132
+ events_uri: "http://events.com",
133
+ diagnostic_opt_out: true,
134
+ logger: NullLogger.new,
135
+ socket_factory: SocketFactoryFromHash.new({
136
+ "polling.com" => poll_server.port,
137
+ "events.com" => events_server.port
138
+ })
139
+ )
140
+ with_client(config) do |client|
141
+ client.identify(USER)
142
+ client.flush
143
+
144
+ req, body = events_server.await_request_with_body
145
+ expect(req.header['authorization']).to eq [ SDK_KEY ]
146
+ expect(req.header['connection']).to eq [ "Keep-Alive" ]
147
+ data = JSON.parse(body)
148
+ expect(data.length).to eq 1
149
+ expect(data[0]["kind"]).to eq "identify"
150
+ end
151
+ end
152
+ end
153
+ end
154
+
121
155
  # TODO: TLS tests with self-signed cert
122
156
  end
123
157
  end