launchdarkly-server-sdk 5.7.3 → 6.0.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.
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 +36 -0
  15. data/CONTRIBUTING.md +1 -1
  16. data/Gemfile.lock +92 -76
  17. data/README.md +5 -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 +1 -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_sender.rb +56 -40
  31. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +5 -5
  32. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +5 -5
  33. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +8 -7
  34. data/lib/ldclient-rb/impl/model/serialization.rb +62 -0
  35. data/lib/ldclient-rb/impl/unbounded_pool.rb +34 -0
  36. data/lib/ldclient-rb/integrations/redis.rb +3 -0
  37. data/lib/ldclient-rb/ldclient.rb +16 -11
  38. data/lib/ldclient-rb/polling.rb +1 -4
  39. data/lib/ldclient-rb/redis_store.rb +1 -0
  40. data/lib/ldclient-rb/requestor.rb +25 -23
  41. data/lib/ldclient-rb/stream.rb +10 -30
  42. data/lib/ldclient-rb/user_filter.rb +3 -2
  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 +11 -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 +10 -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