launchdarkly-server-sdk 5.5.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +134 -0
  3. data/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  4. data/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  5. data/.gitignore +15 -0
  6. data/.hound.yml +2 -0
  7. data/.rspec +2 -0
  8. data/.rubocop.yml +600 -0
  9. data/.simplecov +4 -0
  10. data/.yardopts +9 -0
  11. data/CHANGELOG.md +261 -0
  12. data/CODEOWNERS +1 -0
  13. data/CONTRIBUTING.md +37 -0
  14. data/Gemfile +3 -0
  15. data/Gemfile.lock +102 -0
  16. data/LICENSE.txt +13 -0
  17. data/README.md +56 -0
  18. data/Rakefile +5 -0
  19. data/azure-pipelines.yml +51 -0
  20. data/ext/mkrf_conf.rb +11 -0
  21. data/launchdarkly-server-sdk.gemspec +40 -0
  22. data/lib/ldclient-rb.rb +29 -0
  23. data/lib/ldclient-rb/cache_store.rb +45 -0
  24. data/lib/ldclient-rb/config.rb +411 -0
  25. data/lib/ldclient-rb/evaluation.rb +455 -0
  26. data/lib/ldclient-rb/event_summarizer.rb +55 -0
  27. data/lib/ldclient-rb/events.rb +468 -0
  28. data/lib/ldclient-rb/expiring_cache.rb +77 -0
  29. data/lib/ldclient-rb/file_data_source.rb +312 -0
  30. data/lib/ldclient-rb/flags_state.rb +76 -0
  31. data/lib/ldclient-rb/impl.rb +13 -0
  32. data/lib/ldclient-rb/impl/integrations/consul_impl.rb +158 -0
  33. data/lib/ldclient-rb/impl/integrations/dynamodb_impl.rb +228 -0
  34. data/lib/ldclient-rb/impl/integrations/redis_impl.rb +155 -0
  35. data/lib/ldclient-rb/impl/store_client_wrapper.rb +47 -0
  36. data/lib/ldclient-rb/impl/store_data_set_sorter.rb +55 -0
  37. data/lib/ldclient-rb/in_memory_store.rb +100 -0
  38. data/lib/ldclient-rb/integrations.rb +55 -0
  39. data/lib/ldclient-rb/integrations/consul.rb +38 -0
  40. data/lib/ldclient-rb/integrations/dynamodb.rb +47 -0
  41. data/lib/ldclient-rb/integrations/redis.rb +55 -0
  42. data/lib/ldclient-rb/integrations/util/store_wrapper.rb +230 -0
  43. data/lib/ldclient-rb/interfaces.rb +153 -0
  44. data/lib/ldclient-rb/ldclient.rb +424 -0
  45. data/lib/ldclient-rb/memoized_value.rb +32 -0
  46. data/lib/ldclient-rb/newrelic.rb +17 -0
  47. data/lib/ldclient-rb/non_blocking_thread_pool.rb +46 -0
  48. data/lib/ldclient-rb/polling.rb +78 -0
  49. data/lib/ldclient-rb/redis_store.rb +87 -0
  50. data/lib/ldclient-rb/requestor.rb +101 -0
  51. data/lib/ldclient-rb/simple_lru_cache.rb +25 -0
  52. data/lib/ldclient-rb/stream.rb +141 -0
  53. data/lib/ldclient-rb/user_filter.rb +51 -0
  54. data/lib/ldclient-rb/util.rb +50 -0
  55. data/lib/ldclient-rb/version.rb +3 -0
  56. data/scripts/gendocs.sh +11 -0
  57. data/scripts/release.sh +27 -0
  58. data/spec/config_spec.rb +63 -0
  59. data/spec/evaluation_spec.rb +739 -0
  60. data/spec/event_summarizer_spec.rb +63 -0
  61. data/spec/events_spec.rb +642 -0
  62. data/spec/expiring_cache_spec.rb +76 -0
  63. data/spec/feature_store_spec_base.rb +213 -0
  64. data/spec/file_data_source_spec.rb +255 -0
  65. data/spec/fixtures/feature.json +37 -0
  66. data/spec/fixtures/feature1.json +36 -0
  67. data/spec/fixtures/user.json +9 -0
  68. data/spec/flags_state_spec.rb +81 -0
  69. data/spec/http_util.rb +109 -0
  70. data/spec/in_memory_feature_store_spec.rb +12 -0
  71. data/spec/integrations/consul_feature_store_spec.rb +42 -0
  72. data/spec/integrations/dynamodb_feature_store_spec.rb +105 -0
  73. data/spec/integrations/store_wrapper_spec.rb +276 -0
  74. data/spec/ldclient_spec.rb +471 -0
  75. data/spec/newrelic_spec.rb +5 -0
  76. data/spec/polling_spec.rb +120 -0
  77. data/spec/redis_feature_store_spec.rb +95 -0
  78. data/spec/requestor_spec.rb +214 -0
  79. data/spec/segment_store_spec_base.rb +95 -0
  80. data/spec/simple_lru_cache_spec.rb +24 -0
  81. data/spec/spec_helper.rb +9 -0
  82. data/spec/store_spec.rb +10 -0
  83. data/spec/stream_spec.rb +60 -0
  84. data/spec/user_filter_spec.rb +91 -0
  85. data/spec/util_spec.rb +17 -0
  86. data/spec/version_spec.rb +7 -0
  87. metadata +375 -0
@@ -0,0 +1,63 @@
1
+ require "spec_helper"
2
+
3
+ describe LaunchDarkly::EventSummarizer do
4
+ subject { LaunchDarkly::EventSummarizer }
5
+
6
+ let(:user) { { key: "key" } }
7
+
8
+ it "does not add identify event to summary" do
9
+ es = subject.new
10
+ snapshot = es.snapshot
11
+ es.summarize_event({ kind: "identify", user: user })
12
+
13
+ expect(es.snapshot).to eq snapshot
14
+ end
15
+
16
+ it "does not add custom event to summary" do
17
+ es = subject.new
18
+ snapshot = es.snapshot
19
+ es.summarize_event({ kind: "custom", key: "whatever", user: user })
20
+
21
+ expect(es.snapshot).to eq snapshot
22
+ end
23
+
24
+ it "tracks start and end dates" do
25
+ es = subject.new
26
+ flag = { key: "key" }
27
+ event1 = { kind: "feature", creationDate: 2000, user: user }
28
+ event2 = { kind: "feature", creationDate: 1000, user: user }
29
+ event3 = { kind: "feature", creationDate: 1500, user: user }
30
+ es.summarize_event(event1)
31
+ es.summarize_event(event2)
32
+ es.summarize_event(event3)
33
+ data = es.snapshot
34
+
35
+ expect(data.start_date).to be 1000
36
+ expect(data.end_date).to be 2000
37
+ end
38
+
39
+ it "counts events" do
40
+ es = subject.new
41
+ flag1 = { key: "key1", version: 11 }
42
+ flag2 = { key: "key2", version: 22 }
43
+ event1 = { kind: "feature", key: "key1", version: 11, user: user, variation: 1, value: "value1", default: "default1" }
44
+ event2 = { kind: "feature", key: "key1", version: 11, user: user, variation: 2, value: "value2", default: "default1" }
45
+ event3 = { kind: "feature", key: "key2", version: 22, user: user, variation: 1, value: "value99", default: "default2" }
46
+ event4 = { kind: "feature", key: "key1", version: 11, user: user, variation: 1, value: "value1", default: "default1" }
47
+ event5 = { kind: "feature", key: "badkey", user: user, variation: nil, value: "default3", default: "default3" }
48
+ [event1, event2, event3, event4, event5].each { |e| es.summarize_event(e) }
49
+ data = es.snapshot
50
+
51
+ expectedCounters = {
52
+ { key: "key1", version: 11, variation: 1 } =>
53
+ { count: 2, value: "value1", default: "default1" },
54
+ { key: "key1", version: 11, variation: 2 } =>
55
+ { count: 1, value: "value2", default: "default1" },
56
+ { key: "key2", version: 22, variation: 1 } =>
57
+ { count: 1, value: "value99", default: "default2" },
58
+ { key: "badkey", version: nil, variation: nil } =>
59
+ { count: 1, value: "default3", default: "default3" }
60
+ }
61
+ expect(data.counters).to eq expectedCounters
62
+ end
63
+ end
@@ -0,0 +1,642 @@
1
+ require "http_util"
2
+ require "spec_helper"
3
+ require "time"
4
+
5
+ describe LaunchDarkly::EventProcessor do
6
+ subject { LaunchDarkly::EventProcessor }
7
+
8
+ let(:default_config) { LaunchDarkly::Config.new }
9
+ let(:hc) { FakeHttpClient.new }
10
+ let(:user) { { key: "userkey", name: "Red" } }
11
+ let(:filtered_user) { { key: "userkey", privateAttrs: [ "name" ] } }
12
+ let(:numeric_user) { { key: 1, secondary: 2, ip: 3, country: 4, email: 5, firstName: 6, lastName: 7,
13
+ avatar: 8, name: 9, anonymous: false, custom: { age: 99 } } }
14
+ let(:stringified_numeric_user) { { key: '1', secondary: '2', ip: '3', country: '4', email: '5', firstName: '6',
15
+ lastName: '7', avatar: '8', name: '9', anonymous: false, custom: { age: 99 } } }
16
+
17
+ after(:each) do
18
+ if !@ep.nil?
19
+ @ep.stop
20
+ end
21
+ end
22
+
23
+ it "queues identify event" do
24
+ @ep = subject.new("sdk_key", default_config, hc)
25
+ e = { kind: "identify", key: user[:key], user: user }
26
+ @ep.add_event(e)
27
+
28
+ output = flush_and_get_events
29
+ expect(output).to contain_exactly(e)
30
+ end
31
+
32
+ it "filters user in identify event" do
33
+ config = LaunchDarkly::Config.new(all_attributes_private: true)
34
+ @ep = subject.new("sdk_key", config, hc)
35
+ e = { kind: "identify", key: user[:key], user: user }
36
+ @ep.add_event(e)
37
+
38
+ output = flush_and_get_events
39
+ expect(output).to contain_exactly({
40
+ kind: "identify",
41
+ key: user[:key],
42
+ creationDate: e[:creationDate],
43
+ user: filtered_user
44
+ })
45
+ end
46
+
47
+ it "stringifies built-in user attributes in identify event" do
48
+ @ep = subject.new("sdk_key", default_config, hc)
49
+ flag = { key: "flagkey", version: 11 }
50
+ e = { kind: "identify", key: numeric_user[:key], user: numeric_user }
51
+ @ep.add_event(e)
52
+
53
+ output = flush_and_get_events
54
+ expect(output).to contain_exactly(
55
+ kind: "identify",
56
+ key: numeric_user[:key].to_s,
57
+ creationDate: e[:creationDate],
58
+ user: stringified_numeric_user
59
+ )
60
+ end
61
+
62
+ it "queues individual feature event with index event" do
63
+ @ep = subject.new("sdk_key", default_config, hc)
64
+ flag = { key: "flagkey", version: 11 }
65
+ fe = {
66
+ kind: "feature", key: "flagkey", version: 11, user: user,
67
+ variation: 1, value: "value", trackEvents: true
68
+ }
69
+ @ep.add_event(fe)
70
+
71
+ output = flush_and_get_events
72
+ expect(output).to contain_exactly(
73
+ eq(index_event(fe, user)),
74
+ eq(feature_event(fe, flag, false, nil)),
75
+ include(:kind => "summary")
76
+ )
77
+ end
78
+
79
+ it "filters user in index event" do
80
+ config = LaunchDarkly::Config.new(all_attributes_private: true)
81
+ @ep = subject.new("sdk_key", config, hc)
82
+ flag = { key: "flagkey", version: 11 }
83
+ fe = {
84
+ kind: "feature", key: "flagkey", version: 11, user: user,
85
+ variation: 1, value: "value", trackEvents: true
86
+ }
87
+ @ep.add_event(fe)
88
+
89
+ output = flush_and_get_events
90
+ expect(output).to contain_exactly(
91
+ eq(index_event(fe, filtered_user)),
92
+ eq(feature_event(fe, flag, false, nil)),
93
+ include(:kind => "summary")
94
+ )
95
+ end
96
+
97
+ it "stringifies built-in user attributes in index event" do
98
+ @ep = subject.new("sdk_key", default_config, hc)
99
+ flag = { key: "flagkey", version: 11 }
100
+ fe = {
101
+ kind: "feature", key: "flagkey", version: 11, user: numeric_user,
102
+ variation: 1, value: "value", trackEvents: true
103
+ }
104
+ @ep.add_event(fe)
105
+
106
+ output = flush_and_get_events
107
+ expect(output).to contain_exactly(
108
+ eq(index_event(fe, stringified_numeric_user)),
109
+ eq(feature_event(fe, flag, false, nil)),
110
+ include(:kind => "summary")
111
+ )
112
+ end
113
+
114
+ it "can include inline user in feature event" do
115
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
116
+ @ep = subject.new("sdk_key", config, hc)
117
+ flag = { key: "flagkey", version: 11 }
118
+ fe = {
119
+ kind: "feature", key: "flagkey", version: 11, user: user,
120
+ variation: 1, value: "value", trackEvents: true
121
+ }
122
+ @ep.add_event(fe)
123
+
124
+ output = flush_and_get_events
125
+ expect(output).to contain_exactly(
126
+ eq(feature_event(fe, flag, false, user)),
127
+ include(:kind => "summary")
128
+ )
129
+ end
130
+
131
+ it "stringifies built-in user attributes in feature event" do
132
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
133
+ @ep = subject.new("sdk_key", config, hc)
134
+ flag = { key: "flagkey", version: 11 }
135
+ fe = {
136
+ kind: "feature", key: "flagkey", version: 11, user: numeric_user,
137
+ variation: 1, value: "value", trackEvents: true
138
+ }
139
+ @ep.add_event(fe)
140
+
141
+ output = flush_and_get_events
142
+ expect(output).to contain_exactly(
143
+ eq(feature_event(fe, flag, false, stringified_numeric_user)),
144
+ include(:kind => "summary")
145
+ )
146
+ end
147
+
148
+ it "filters user in feature event" do
149
+ config = LaunchDarkly::Config.new(all_attributes_private: true, inline_users_in_events: true)
150
+ @ep = subject.new("sdk_key", config, hc)
151
+ flag = { key: "flagkey", version: 11 }
152
+ fe = {
153
+ kind: "feature", key: "flagkey", version: 11, user: user,
154
+ variation: 1, value: "value", trackEvents: true
155
+ }
156
+ @ep.add_event(fe)
157
+
158
+ output = flush_and_get_events
159
+ expect(output).to contain_exactly(
160
+ eq(feature_event(fe, flag, false, filtered_user)),
161
+ include(:kind => "summary")
162
+ )
163
+ end
164
+
165
+ it "still generates index event if inline_users is true but feature event was not tracked" do
166
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
167
+ @ep = subject.new("sdk_key", config, hc)
168
+ flag = { key: "flagkey", version: 11 }
169
+ fe = {
170
+ kind: "feature", key: "flagkey", version: 11, user: user,
171
+ variation: 1, value: "value", trackEvents: false
172
+ }
173
+ @ep.add_event(fe)
174
+
175
+ output = flush_and_get_events
176
+ expect(output).to contain_exactly(
177
+ eq(index_event(fe, user)),
178
+ include(:kind => "summary")
179
+ )
180
+ end
181
+
182
+ it "sets event kind to debug if flag is temporarily in debug mode" do
183
+ @ep = subject.new("sdk_key", default_config, hc)
184
+ flag = { key: "flagkey", version: 11 }
185
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
186
+ fe = {
187
+ kind: "feature", key: "flagkey", version: 11, user: user,
188
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: future_time
189
+ }
190
+ @ep.add_event(fe)
191
+
192
+ output = flush_and_get_events
193
+ expect(output).to contain_exactly(
194
+ eq(index_event(fe, user)),
195
+ eq(feature_event(fe, flag, true, user)),
196
+ include(:kind => "summary")
197
+ )
198
+ end
199
+
200
+ it "can be both debugging and tracking an event" do
201
+ @ep = subject.new("sdk_key", default_config, hc)
202
+ flag = { key: "flagkey", version: 11 }
203
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
204
+ fe = {
205
+ kind: "feature", key: "flagkey", version: 11, user: user,
206
+ variation: 1, value: "value", trackEvents: true, debugEventsUntilDate: future_time
207
+ }
208
+ @ep.add_event(fe)
209
+
210
+ output = flush_and_get_events
211
+ expect(output).to contain_exactly(
212
+ eq(index_event(fe, user)),
213
+ eq(feature_event(fe, flag, false, nil)),
214
+ eq(feature_event(fe, flag, true, user)),
215
+ include(:kind => "summary")
216
+ )
217
+ end
218
+
219
+ it "ends debug mode based on client time if client time is later than server time" do
220
+ @ep = subject.new("sdk_key", default_config, hc)
221
+
222
+ # Pick a server time that is somewhat behind the client time
223
+ server_time = (Time.now.to_f * 1000).to_i - 20000
224
+
225
+ # Send and flush an event we don't care about, just to set the last server time
226
+ hc.set_server_time(server_time)
227
+ @ep.add_event({ kind: "identify", user: { key: "otherUser" }})
228
+ flush_and_get_events
229
+
230
+ # Now send an event with debug mode on, with a "debug until" time that is further in
231
+ # the future than the server time, but in the past compared to the client.
232
+ flag = { key: "flagkey", version: 11 }
233
+ debug_until = server_time + 1000
234
+ fe = {
235
+ kind: "feature", key: "flagkey", version: 11, user: user,
236
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until
237
+ }
238
+ @ep.add_event(fe)
239
+
240
+ # Should get a summary event only, not a full feature event
241
+ output = flush_and_get_events
242
+ expect(output).to contain_exactly(
243
+ eq(index_event(fe, user)),
244
+ include(:kind => "summary")
245
+ )
246
+ end
247
+
248
+ it "ends debug mode based on server time if server time is later than client time" do
249
+ @ep = subject.new("sdk_key", default_config, hc)
250
+
251
+ # Pick a server time that is somewhat ahead of the client time
252
+ server_time = (Time.now.to_f * 1000).to_i + 20000
253
+
254
+ # Send and flush an event we don't care about, just to set the last server time
255
+ hc.set_server_time(server_time)
256
+ @ep.add_event({ kind: "identify", user: { key: "otherUser" }})
257
+ flush_and_get_events
258
+
259
+ # Now send an event with debug mode on, with a "debug until" time that is further in
260
+ # the future than the server time, but in the past compared to the client.
261
+ flag = { key: "flagkey", version: 11 }
262
+ debug_until = server_time - 1000
263
+ fe = {
264
+ kind: "feature", key: "flagkey", version: 11, user: user,
265
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until
266
+ }
267
+ @ep.add_event(fe)
268
+
269
+ # Should get a summary event only, not a full feature event
270
+ output = flush_and_get_events
271
+ expect(output).to contain_exactly(
272
+ eq(index_event(fe, user)),
273
+ include(:kind => "summary")
274
+ )
275
+ end
276
+
277
+ it "generates only one index event for multiple events with same user" do
278
+ @ep = subject.new("sdk_key", default_config, hc)
279
+ flag1 = { key: "flagkey1", version: 11 }
280
+ flag2 = { key: "flagkey2", version: 22 }
281
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
282
+ fe1 = {
283
+ kind: "feature", key: "flagkey1", version: 11, user: user,
284
+ variation: 1, value: "value", trackEvents: true
285
+ }
286
+ fe2 = {
287
+ kind: "feature", key: "flagkey2", version: 22, user: user,
288
+ variation: 1, value: "value", trackEvents: true
289
+ }
290
+ @ep.add_event(fe1)
291
+ @ep.add_event(fe2)
292
+
293
+ output = flush_and_get_events
294
+ expect(output).to contain_exactly(
295
+ eq(index_event(fe1, user)),
296
+ eq(feature_event(fe1, flag1, false, nil)),
297
+ eq(feature_event(fe2, flag2, false, nil)),
298
+ include(:kind => "summary")
299
+ )
300
+ end
301
+
302
+ it "summarizes non-tracked events" do
303
+ @ep = subject.new("sdk_key", default_config, hc)
304
+ flag1 = { key: "flagkey1", version: 11 }
305
+ flag2 = { key: "flagkey2", version: 22 }
306
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
307
+ fe1 = {
308
+ kind: "feature", key: "flagkey1", version: 11, user: user,
309
+ variation: 1, value: "value1", default: "default1"
310
+ }
311
+ fe2 = {
312
+ kind: "feature", key: "flagkey2", version: 22, user: user,
313
+ variation: 2, value: "value2", default: "default2"
314
+ }
315
+ @ep.add_event(fe1)
316
+ @ep.add_event(fe2)
317
+
318
+ output = flush_and_get_events
319
+ expect(output).to contain_exactly(
320
+ eq(index_event(fe1, user)),
321
+ eq({
322
+ kind: "summary",
323
+ startDate: fe1[:creationDate],
324
+ endDate: fe2[:creationDate],
325
+ features: {
326
+ flagkey1: {
327
+ default: "default1",
328
+ counters: [
329
+ { version: 11, variation: 1, value: "value1", count: 1 }
330
+ ]
331
+ },
332
+ flagkey2: {
333
+ default: "default2",
334
+ counters: [
335
+ { version: 22, variation: 2, value: "value2", count: 1 }
336
+ ]
337
+ }
338
+ }
339
+ })
340
+ )
341
+ end
342
+
343
+ it "queues custom event with user" do
344
+ @ep = subject.new("sdk_key", default_config, hc)
345
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
346
+ @ep.add_event(e)
347
+
348
+ output = flush_and_get_events
349
+ expect(output).to contain_exactly(
350
+ eq(index_event(e, user)),
351
+ eq(custom_event(e, nil))
352
+ )
353
+ end
354
+
355
+ it "can include inline user in custom event" do
356
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
357
+ @ep = subject.new("sdk_key", config, hc)
358
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
359
+ @ep.add_event(e)
360
+
361
+ output = flush_and_get_events
362
+ expect(output).to contain_exactly(
363
+ eq(custom_event(e, user))
364
+ )
365
+ end
366
+
367
+ it "filters user in custom event" do
368
+ config = LaunchDarkly::Config.new(all_attributes_private: true, inline_users_in_events: true)
369
+ @ep = subject.new("sdk_key", config, hc)
370
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
371
+ @ep.add_event(e)
372
+
373
+ output = flush_and_get_events
374
+ expect(output).to contain_exactly(
375
+ eq(custom_event(e, filtered_user))
376
+ )
377
+ end
378
+
379
+ it "stringifies built-in user attributes in custom event" do
380
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
381
+ @ep = subject.new("sdk_key", config, hc)
382
+ e = { kind: "custom", key: "eventkey", user: numeric_user }
383
+ @ep.add_event(e)
384
+
385
+ output = flush_and_get_events
386
+ expect(output).to contain_exactly(
387
+ eq(custom_event(e, stringified_numeric_user))
388
+ )
389
+ end
390
+
391
+ it "does a final flush when shutting down" do
392
+ @ep = subject.new("sdk_key", default_config, hc)
393
+ e = { kind: "identify", key: user[:key], user: user }
394
+ @ep.add_event(e)
395
+
396
+ @ep.stop
397
+
398
+ output = get_events_from_last_request
399
+ expect(output).to contain_exactly(e)
400
+ end
401
+
402
+ it "sends nothing if there are no events" do
403
+ @ep = subject.new("sdk_key", default_config, hc)
404
+ @ep.flush
405
+ expect(hc.get_request).to be nil
406
+ end
407
+
408
+ it "sends SDK key" do
409
+ @ep = subject.new("sdk_key", default_config, hc)
410
+ e = { kind: "identify", user: user }
411
+ @ep.add_event(e)
412
+
413
+ @ep.flush
414
+ @ep.wait_until_inactive
415
+
416
+ expect(hc.get_request["authorization"]).to eq "sdk_key"
417
+ end
418
+
419
+ def verify_unrecoverable_http_error(status)
420
+ @ep = subject.new("sdk_key", default_config, hc)
421
+ e = { kind: "identify", user: user }
422
+ @ep.add_event(e)
423
+
424
+ hc.set_response_status(status)
425
+ @ep.flush
426
+ @ep.wait_until_inactive
427
+ expect(hc.get_request).not_to be_nil
428
+ hc.reset
429
+
430
+ @ep.add_event(e)
431
+ @ep.flush
432
+ @ep.wait_until_inactive
433
+ expect(hc.get_request).to be_nil
434
+ end
435
+
436
+ def verify_recoverable_http_error(status)
437
+ @ep = subject.new("sdk_key", default_config, hc)
438
+ e = { kind: "identify", user: user }
439
+ @ep.add_event(e)
440
+
441
+ hc.set_response_status(503)
442
+ @ep.flush
443
+ @ep.wait_until_inactive
444
+
445
+ expect(hc.get_request).not_to be_nil
446
+ expect(hc.get_request).not_to be_nil
447
+ expect(hc.get_request).to be_nil # no 3rd request
448
+
449
+ # now verify that a subsequent flush still generates a request
450
+ hc.reset
451
+ @ep.add_event(e)
452
+ @ep.flush
453
+ @ep.wait_until_inactive
454
+ expect(hc.get_request).not_to be_nil
455
+ end
456
+
457
+ it "stops posting events after getting a 401 error" do
458
+ verify_unrecoverable_http_error(401)
459
+ end
460
+
461
+ it "stops posting events after getting a 403 error" do
462
+ verify_unrecoverable_http_error(403)
463
+ end
464
+
465
+ it "retries after 408 error" do
466
+ verify_recoverable_http_error(408)
467
+ end
468
+
469
+ it "retries after 429 error" do
470
+ verify_recoverable_http_error(429)
471
+ end
472
+
473
+ it "retries after 503 error" do
474
+ verify_recoverable_http_error(503)
475
+ end
476
+
477
+ it "retries flush once after connection error" do
478
+ @ep = subject.new("sdk_key", default_config, hc)
479
+ e = { kind: "identify", user: user }
480
+ @ep.add_event(e)
481
+
482
+ hc.set_exception(IOError.new("deliberate error"))
483
+ @ep.flush
484
+ @ep.wait_until_inactive
485
+
486
+ expect(hc.get_request).not_to be_nil
487
+ expect(hc.get_request).not_to be_nil
488
+ expect(hc.get_request).to be_nil # no 3rd request
489
+ end
490
+
491
+ it "makes actual HTTP request with correct headers" do
492
+ e = { kind: "identify", key: user[:key], user: user }
493
+ with_server do |server|
494
+ server.setup_ok_response("/bulk", "")
495
+
496
+ @ep = subject.new("sdk_key", LaunchDarkly::Config.new(events_uri: server.base_uri.to_s))
497
+ @ep.add_event(e)
498
+ @ep.flush
499
+
500
+ req = server.await_request
501
+ expect(req.header).to include({
502
+ "authorization" => [ "sdk_key" ],
503
+ "content-type" => [ "application/json" ],
504
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
505
+ "x-launchdarkly-event-schema" => [ "3" ]
506
+ })
507
+ end
508
+ end
509
+
510
+ it "can use a proxy server" do
511
+ e = { kind: "identify", key: user[:key], user: user }
512
+ with_server do |server|
513
+ server.setup_ok_response("/bulk", "")
514
+
515
+ with_server(StubProxyServer.new) do |proxy|
516
+ begin
517
+ ENV["http_proxy"] = proxy.base_uri.to_s
518
+ @ep = subject.new("sdk_key", LaunchDarkly::Config.new(events_uri: server.base_uri.to_s))
519
+ @ep.add_event(e)
520
+ @ep.flush
521
+
522
+ req = server.await_request
523
+ expect(req["content-type"]).to eq("application/json")
524
+ ensure
525
+ ENV["http_proxy"] = nil
526
+ end
527
+ end
528
+ end
529
+ end
530
+
531
+ def index_event(e, user)
532
+ {
533
+ kind: "index",
534
+ creationDate: e[:creationDate],
535
+ user: user
536
+ }
537
+ end
538
+
539
+ def feature_event(e, flag, debug, inline_user)
540
+ out = {
541
+ kind: debug ? "debug" : "feature",
542
+ creationDate: e[:creationDate],
543
+ key: flag[:key],
544
+ variation: e[:variation],
545
+ version: flag[:version],
546
+ value: e[:value]
547
+ }
548
+ if inline_user.nil?
549
+ out[:userKey] = e[:user][:key]
550
+ else
551
+ out[:user] = inline_user
552
+ end
553
+ out
554
+ end
555
+
556
+ def custom_event(e, inline_user)
557
+ out = {
558
+ kind: "custom",
559
+ creationDate: e[:creationDate],
560
+ key: e[:key]
561
+ }
562
+ out[:data] = e[:data] if e.has_key?(:data)
563
+ if inline_user.nil?
564
+ out[:userKey] = e[:user][:key]
565
+ else
566
+ out[:user] = inline_user
567
+ end
568
+ out
569
+ end
570
+
571
+ def flush_and_get_events
572
+ @ep.flush
573
+ @ep.wait_until_inactive
574
+ get_events_from_last_request
575
+ end
576
+
577
+ def get_events_from_last_request
578
+ req = hc.get_request
579
+ JSON.parse(req.body, symbolize_names: true)
580
+ end
581
+
582
+ class FakeHttpClient
583
+ def initialize
584
+ reset
585
+ end
586
+
587
+ def set_response_status(status)
588
+ @status = status
589
+ end
590
+
591
+ def set_server_time(time_millis)
592
+ @server_time = Time.at(time_millis.to_f / 1000)
593
+ end
594
+
595
+ def set_exception(e)
596
+ @exception = e
597
+ end
598
+
599
+ def reset
600
+ @requests = []
601
+ @status = 200
602
+ end
603
+
604
+ def request(req)
605
+ @requests.push(req)
606
+ if @exception
607
+ raise @exception
608
+ else
609
+ headers = {}
610
+ if @server_time
611
+ headers["Date"] = @server_time.httpdate
612
+ end
613
+ FakeResponse.new(@status ? @status : 200, headers)
614
+ end
615
+ end
616
+
617
+ def start
618
+ end
619
+
620
+ def started?
621
+ false
622
+ end
623
+
624
+ def finish
625
+ end
626
+
627
+ def get_request
628
+ @requests.shift
629
+ end
630
+ end
631
+
632
+ class FakeResponse
633
+ include Net::HTTPHeader
634
+
635
+ attr_reader :code
636
+
637
+ def initialize(status, headers)
638
+ @code = status.to_s
639
+ initialize_http_header(headers)
640
+ end
641
+ end
642
+ end