ldclient-rb 3.0.3 → 4.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.
@@ -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,506 @@
1
+ require "spec_helper"
2
+ require "faraday"
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
+
13
+ after(:each) do
14
+ if !@ep.nil?
15
+ @ep.stop
16
+ end
17
+ end
18
+
19
+ it "queues identify event" do
20
+ @ep = subject.new("sdk_key", default_config, hc)
21
+ e = { kind: "identify", key: user[:key], user: user }
22
+ @ep.add_event(e)
23
+
24
+ output = flush_and_get_events
25
+ expect(output).to contain_exactly(e)
26
+ end
27
+
28
+ it "filters user in identify event" do
29
+ config = LaunchDarkly::Config.new(all_attributes_private: true)
30
+ @ep = subject.new("sdk_key", config, hc)
31
+ e = { kind: "identify", key: user[:key], user: user }
32
+ @ep.add_event(e)
33
+
34
+ output = flush_and_get_events
35
+ expect(output).to contain_exactly({
36
+ kind: "identify",
37
+ key: user[:key],
38
+ creationDate: e[:creationDate],
39
+ user: filtered_user
40
+ })
41
+ end
42
+
43
+ it "queues individual feature event with index event" do
44
+ @ep = subject.new("sdk_key", default_config, hc)
45
+ flag = { key: "flagkey", version: 11 }
46
+ fe = {
47
+ kind: "feature", key: "flagkey", version: 11, user: user,
48
+ variation: 1, value: "value", trackEvents: true
49
+ }
50
+ @ep.add_event(fe)
51
+
52
+ output = flush_and_get_events
53
+ expect(output).to contain_exactly(
54
+ eq(index_event(fe, user)),
55
+ eq(feature_event(fe, flag, false, nil)),
56
+ include(:kind => "summary")
57
+ )
58
+ end
59
+
60
+ it "filters user in index event" do
61
+ config = LaunchDarkly::Config.new(all_attributes_private: true)
62
+ @ep = subject.new("sdk_key", config, hc)
63
+ flag = { key: "flagkey", version: 11 }
64
+ fe = {
65
+ kind: "feature", key: "flagkey", version: 11, user: user,
66
+ variation: 1, value: "value", trackEvents: true
67
+ }
68
+ @ep.add_event(fe)
69
+
70
+ output = flush_and_get_events
71
+ expect(output).to contain_exactly(
72
+ eq(index_event(fe, filtered_user)),
73
+ eq(feature_event(fe, flag, false, nil)),
74
+ include(:kind => "summary")
75
+ )
76
+ end
77
+
78
+ it "can include inline user in feature event" do
79
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
80
+ @ep = subject.new("sdk_key", config, hc)
81
+ flag = { key: "flagkey", version: 11 }
82
+ fe = {
83
+ kind: "feature", key: "flagkey", version: 11, user: user,
84
+ variation: 1, value: "value", trackEvents: true
85
+ }
86
+ @ep.add_event(fe)
87
+
88
+ output = flush_and_get_events
89
+ expect(output).to contain_exactly(
90
+ eq(feature_event(fe, flag, false, user)),
91
+ include(:kind => "summary")
92
+ )
93
+ end
94
+
95
+ it "filters user in feature event" do
96
+ config = LaunchDarkly::Config.new(all_attributes_private: true, inline_users_in_events: true)
97
+ @ep = subject.new("sdk_key", config, hc)
98
+ flag = { key: "flagkey", version: 11 }
99
+ fe = {
100
+ kind: "feature", key: "flagkey", version: 11, user: user,
101
+ variation: 1, value: "value", trackEvents: true
102
+ }
103
+ @ep.add_event(fe)
104
+
105
+ output = flush_and_get_events
106
+ expect(output).to contain_exactly(
107
+ eq(feature_event(fe, flag, false, filtered_user)),
108
+ include(:kind => "summary")
109
+ )
110
+ end
111
+
112
+ it "still generates index event if inline_users is true but feature event was not tracked" do
113
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
114
+ @ep = subject.new("sdk_key", config, hc)
115
+ flag = { key: "flagkey", version: 11 }
116
+ fe = {
117
+ kind: "feature", key: "flagkey", version: 11, user: user,
118
+ variation: 1, value: "value", trackEvents: false
119
+ }
120
+ @ep.add_event(fe)
121
+
122
+ output = flush_and_get_events
123
+ expect(output).to contain_exactly(
124
+ eq(index_event(fe, user)),
125
+ include(:kind => "summary")
126
+ )
127
+ end
128
+
129
+ it "sets event kind to debug if flag is temporarily in debug mode" do
130
+ @ep = subject.new("sdk_key", default_config, hc)
131
+ flag = { key: "flagkey", version: 11 }
132
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
133
+ fe = {
134
+ kind: "feature", key: "flagkey", version: 11, user: user,
135
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: future_time
136
+ }
137
+ @ep.add_event(fe)
138
+
139
+ output = flush_and_get_events
140
+ expect(output).to contain_exactly(
141
+ eq(index_event(fe, user)),
142
+ eq(feature_event(fe, flag, true, user)),
143
+ include(:kind => "summary")
144
+ )
145
+ end
146
+
147
+ it "can be both debugging and tracking an event" do
148
+ @ep = subject.new("sdk_key", default_config, hc)
149
+ flag = { key: "flagkey", version: 11 }
150
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
151
+ fe = {
152
+ kind: "feature", key: "flagkey", version: 11, user: user,
153
+ variation: 1, value: "value", trackEvents: true, debugEventsUntilDate: future_time
154
+ }
155
+ @ep.add_event(fe)
156
+
157
+ output = flush_and_get_events
158
+ expect(output).to contain_exactly(
159
+ eq(index_event(fe, user)),
160
+ eq(feature_event(fe, flag, false, nil)),
161
+ eq(feature_event(fe, flag, true, user)),
162
+ include(:kind => "summary")
163
+ )
164
+ end
165
+
166
+ it "ends debug mode based on client time if client time is later than server time" do
167
+ @ep = subject.new("sdk_key", default_config, hc)
168
+
169
+ # Pick a server time that is somewhat behind the client time
170
+ server_time = (Time.now.to_f * 1000).to_i - 20000
171
+
172
+ # Send and flush an event we don't care about, just to set the last server time
173
+ hc.set_server_time(server_time)
174
+ @ep.add_event({ kind: "identify", user: { key: "otherUser" }})
175
+ flush_and_get_events
176
+
177
+ # Now send an event with debug mode on, with a "debug until" time that is further in
178
+ # the future than the server time, but in the past compared to the client.
179
+ flag = { key: "flagkey", version: 11 }
180
+ debug_until = server_time + 1000
181
+ fe = {
182
+ kind: "feature", key: "flagkey", version: 11, user: user,
183
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until
184
+ }
185
+ @ep.add_event(fe)
186
+
187
+ # Should get a summary event only, not a full feature event
188
+ output = flush_and_get_events
189
+ expect(output).to contain_exactly(
190
+ eq(index_event(fe, user)),
191
+ include(:kind => "summary")
192
+ )
193
+ end
194
+
195
+ it "ends debug mode based on server time if server time is later than client time" do
196
+ @ep = subject.new("sdk_key", default_config, hc)
197
+
198
+ # Pick a server time that is somewhat ahead of the client time
199
+ server_time = (Time.now.to_f * 1000).to_i + 20000
200
+
201
+ # Send and flush an event we don't care about, just to set the last server time
202
+ hc.set_server_time(server_time)
203
+ @ep.add_event({ kind: "identify", user: { key: "otherUser" }})
204
+ flush_and_get_events
205
+
206
+ # Now send an event with debug mode on, with a "debug until" time that is further in
207
+ # the future than the server time, but in the past compared to the client.
208
+ flag = { key: "flagkey", version: 11 }
209
+ debug_until = server_time - 1000
210
+ fe = {
211
+ kind: "feature", key: "flagkey", version: 11, user: user,
212
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until
213
+ }
214
+ @ep.add_event(fe)
215
+
216
+ # Should get a summary event only, not a full feature event
217
+ output = flush_and_get_events
218
+ expect(output).to contain_exactly(
219
+ eq(index_event(fe, user)),
220
+ include(:kind => "summary")
221
+ )
222
+ end
223
+
224
+ it "generates only one index event for multiple events with same user" do
225
+ @ep = subject.new("sdk_key", default_config, hc)
226
+ flag1 = { key: "flagkey1", version: 11 }
227
+ flag2 = { key: "flagkey2", version: 22 }
228
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
229
+ fe1 = {
230
+ kind: "feature", key: "flagkey1", version: 11, user: user,
231
+ variation: 1, value: "value", trackEvents: true
232
+ }
233
+ fe2 = {
234
+ kind: "feature", key: "flagkey2", version: 22, user: user,
235
+ variation: 1, value: "value", trackEvents: true
236
+ }
237
+ @ep.add_event(fe1)
238
+ @ep.add_event(fe2)
239
+
240
+ output = flush_and_get_events
241
+ expect(output).to contain_exactly(
242
+ eq(index_event(fe1, user)),
243
+ eq(feature_event(fe1, flag1, false, nil)),
244
+ eq(feature_event(fe2, flag2, false, nil)),
245
+ include(:kind => "summary")
246
+ )
247
+ end
248
+
249
+ it "summarizes non-tracked events" do
250
+ @ep = subject.new("sdk_key", default_config, hc)
251
+ flag1 = { key: "flagkey1", version: 11 }
252
+ flag2 = { key: "flagkey2", version: 22 }
253
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
254
+ fe1 = {
255
+ kind: "feature", key: "flagkey1", version: 11, user: user,
256
+ variation: 1, value: "value1", default: "default1"
257
+ }
258
+ fe2 = {
259
+ kind: "feature", key: "flagkey2", version: 22, user: user,
260
+ variation: 2, value: "value2", default: "default2"
261
+ }
262
+ @ep.add_event(fe1)
263
+ @ep.add_event(fe2)
264
+
265
+ output = flush_and_get_events
266
+ expect(output).to contain_exactly(
267
+ eq(index_event(fe1, user)),
268
+ eq({
269
+ kind: "summary",
270
+ startDate: fe1[:creationDate],
271
+ endDate: fe2[:creationDate],
272
+ features: {
273
+ flagkey1: {
274
+ default: "default1",
275
+ counters: [
276
+ { version: 11, variation: 1, value: "value1", count: 1 }
277
+ ]
278
+ },
279
+ flagkey2: {
280
+ default: "default2",
281
+ counters: [
282
+ { version: 22, variation: 2, value: "value2", count: 1 }
283
+ ]
284
+ }
285
+ }
286
+ })
287
+ )
288
+ end
289
+
290
+ it "queues custom event with user" do
291
+ @ep = subject.new("sdk_key", default_config, hc)
292
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
293
+ @ep.add_event(e)
294
+
295
+ output = flush_and_get_events
296
+ expect(output).to contain_exactly(
297
+ eq(index_event(e, user)),
298
+ eq(custom_event(e, nil))
299
+ )
300
+ end
301
+
302
+ it "can include inline user in custom event" do
303
+ config = LaunchDarkly::Config.new(inline_users_in_events: true)
304
+ @ep = subject.new("sdk_key", config, hc)
305
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
306
+ @ep.add_event(e)
307
+
308
+ output = flush_and_get_events
309
+ expect(output).to contain_exactly(
310
+ eq(custom_event(e, user))
311
+ )
312
+ end
313
+
314
+ it "filters user in custom event" do
315
+ config = LaunchDarkly::Config.new(all_attributes_private: true, inline_users_in_events: true)
316
+ @ep = subject.new("sdk_key", config, hc)
317
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
318
+ @ep.add_event(e)
319
+
320
+ output = flush_and_get_events
321
+ expect(output).to contain_exactly(
322
+ eq(custom_event(e, filtered_user))
323
+ )
324
+ end
325
+
326
+ it "does a final flush when shutting down" do
327
+ @ep = subject.new("sdk_key", default_config, hc)
328
+ e = { kind: "identify", key: user[:key], user: user }
329
+ @ep.add_event(e)
330
+
331
+ @ep.stop
332
+
333
+ output = get_events_from_last_request
334
+ expect(output).to contain_exactly(e)
335
+ end
336
+
337
+ it "sends nothing if there are no events" do
338
+ @ep = subject.new("sdk_key", default_config, hc)
339
+ @ep.flush
340
+ expect(hc.get_request).to be nil
341
+ end
342
+
343
+ it "sends SDK key" do
344
+ @ep = subject.new("sdk_key", default_config, hc)
345
+ e = { kind: "identify", user: user }
346
+ @ep.add_event(e)
347
+
348
+ @ep.flush
349
+ @ep.wait_until_inactive
350
+
351
+ expect(hc.get_request.headers["Authorization"]).to eq "sdk_key"
352
+ end
353
+
354
+ it "stops posting events after getting a 401 error" do
355
+ @ep = subject.new("sdk_key", default_config, hc)
356
+ e = { kind: "identify", user: user }
357
+ @ep.add_event(e)
358
+
359
+ hc.set_response_status(401)
360
+ @ep.flush
361
+ @ep.wait_until_inactive
362
+ expect(hc.get_request).not_to be_nil
363
+ hc.reset
364
+
365
+ @ep.add_event(e)
366
+ @ep.flush
367
+ @ep.wait_until_inactive
368
+ expect(hc.get_request).to be_nil
369
+ end
370
+
371
+ it "retries flush once after 5xx error" do
372
+ @ep = subject.new("sdk_key", default_config, hc)
373
+ e = { kind: "identify", user: user }
374
+ @ep.add_event(e)
375
+
376
+ hc.set_response_status(503)
377
+ @ep.flush
378
+ @ep.wait_until_inactive
379
+
380
+ expect(hc.get_request).not_to be_nil
381
+ expect(hc.get_request).not_to be_nil
382
+ expect(hc.get_request).to be_nil # no 3rd request
383
+ end
384
+
385
+ it "retries flush once after connection error" do
386
+ @ep = subject.new("sdk_key", default_config, hc)
387
+ e = { kind: "identify", user: user }
388
+ @ep.add_event(e)
389
+
390
+ hc.set_exception(Faraday::Error::ConnectionFailed.new("fail"))
391
+ @ep.flush
392
+ @ep.wait_until_inactive
393
+
394
+ expect(hc.get_request).not_to be_nil
395
+ expect(hc.get_request).not_to be_nil
396
+ expect(hc.get_request).to be_nil # no 3rd request
397
+ end
398
+
399
+ def index_event(e, user)
400
+ {
401
+ kind: "index",
402
+ creationDate: e[:creationDate],
403
+ user: user
404
+ }
405
+ end
406
+
407
+ def feature_event(e, flag, debug, inline_user)
408
+ out = {
409
+ kind: debug ? "debug" : "feature",
410
+ creationDate: e[:creationDate],
411
+ key: flag[:key],
412
+ variation: e[:variation],
413
+ version: flag[:version],
414
+ value: e[:value]
415
+ }
416
+ if inline_user.nil?
417
+ out[:userKey] = e[:user][:key]
418
+ else
419
+ out[:user] = inline_user
420
+ end
421
+ out
422
+ end
423
+
424
+ def custom_event(e, inline_user)
425
+ out = {
426
+ kind: "custom",
427
+ creationDate: e[:creationDate],
428
+ key: e[:key]
429
+ }
430
+ out[:data] = e[:data] if e.has_key?(:data)
431
+ if inline_user.nil?
432
+ out[:userKey] = e[:user][:key]
433
+ else
434
+ out[:user] = inline_user
435
+ end
436
+ out
437
+ end
438
+
439
+ def flush_and_get_events
440
+ @ep.flush
441
+ @ep.wait_until_inactive
442
+ get_events_from_last_request
443
+ end
444
+
445
+ def get_events_from_last_request
446
+ req = hc.get_request
447
+ JSON.parse(req.body, symbolize_names: true)
448
+ end
449
+
450
+ class FakeHttpClient
451
+ def initialize
452
+ reset
453
+ end
454
+
455
+ def set_response_status(status)
456
+ @status = status
457
+ end
458
+
459
+ def set_server_time(time_millis)
460
+ @server_time = Time.at(time_millis.to_f / 1000)
461
+ end
462
+
463
+ def set_exception(e)
464
+ @exception = e
465
+ end
466
+
467
+ def reset
468
+ @requests = []
469
+ @status = 200
470
+ end
471
+
472
+ def post(uri)
473
+ req = Faraday::Request.create("POST")
474
+ req.headers = {}
475
+ req.options = Faraday::RequestOptions.new
476
+ yield req
477
+ @requests.push(req)
478
+ if @exception
479
+ raise @exception
480
+ else
481
+ resp = Faraday::Response.new
482
+ headers = {}
483
+ if @server_time
484
+ headers["Date"] = @server_time.httpdate
485
+ end
486
+ resp.finish({
487
+ status: @status ? @status : 200,
488
+ response_headers: headers
489
+ })
490
+ resp
491
+ end
492
+ end
493
+
494
+ def get_request
495
+ @requests.shift
496
+ end
497
+ end
498
+
499
+ class FakeResponse
500
+ def initialize(status)
501
+ @status = status
502
+ end
503
+
504
+ attr_reader :status
505
+ end
506
+ end