launchdarkly-server-sdk 5.5.7

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