ldclient-rb 3.0.3 → 4.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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