launchdarkly-server-sdk 5.6.2 → 5.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -17,7 +17,7 @@ describe LaunchDarkly::Evaluation do
17
17
  }
18
18
  }
19
19
 
20
- let(:logger) { LaunchDarkly::Config.default_logger }
20
+ let(:logger) { $null_log }
21
21
 
22
22
  def boolean_flag_with_rules(rules)
23
23
  { key: 'feature', on: true, rules: rules, fallthrough: { variation: 0 }, variations: [ false, true ] }
@@ -0,0 +1,179 @@
1
+ require "ldclient-rb/impl/event_sender"
2
+
3
+ require "http_util"
4
+ require "spec_helper"
5
+
6
+ require "time"
7
+
8
+ module LaunchDarkly
9
+ module Impl
10
+ describe EventSender do
11
+ subject { EventSender }
12
+
13
+ let(:sdk_key) { "sdk_key" }
14
+ let(:fake_data) { '{"things":[]}' }
15
+
16
+ def make_sender(server)
17
+ subject.new(sdk_key, Config.new(events_uri: server.base_uri.to_s, logger: $null_log), nil, 0.1)
18
+ end
19
+
20
+ def with_sender_and_server
21
+ with_server do |server|
22
+ yield make_sender(server), server
23
+ end
24
+ end
25
+
26
+ it "sends analytics event data" do
27
+ with_sender_and_server do |es, server|
28
+ server.setup_ok_response("/bulk", "")
29
+
30
+ result = es.send_event_data(fake_data, false)
31
+
32
+ expect(result.success).to be true
33
+ expect(result.must_shutdown).to be false
34
+ expect(result.time_from_server).not_to be_nil
35
+
36
+ req = server.await_request
37
+ expect(req.body).to eq fake_data
38
+ expect(req.header).to include({
39
+ "authorization" => [ sdk_key ],
40
+ "content-type" => [ "application/json" ],
41
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
42
+ "x-launchdarkly-event-schema" => [ "3" ]
43
+ })
44
+ expect(req.header['x-launchdarkly-payload-id']).not_to eq []
45
+ end
46
+ end
47
+
48
+ it "generates a new payload ID for each payload" do
49
+ with_sender_and_server do |es, server|
50
+ server.setup_ok_response("/bulk", "")
51
+
52
+ result1 = es.send_event_data(fake_data, false)
53
+ result2 = es.send_event_data(fake_data, false)
54
+ expect(result1.success).to be true
55
+ expect(result2.success).to be true
56
+
57
+ req1, body1 = server.await_request_with_body
58
+ req2, body2 = server.await_request_with_body
59
+ expect(body1).to eq fake_data
60
+ expect(body2).to eq fake_data
61
+ expect(req1.header['x-launchdarkly-payload-id']).not_to eq req2.header['x-launchdarkly-payload-id']
62
+ end
63
+ end
64
+
65
+ it "sends diagnostic event data" do
66
+ with_sender_and_server do |es, server|
67
+ server.setup_ok_response("/diagnostic", "")
68
+
69
+ result = es.send_event_data(fake_data, true)
70
+
71
+ expect(result.success).to be true
72
+ expect(result.must_shutdown).to be false
73
+ expect(result.time_from_server).not_to be_nil
74
+
75
+ req, body = server.await_request_with_body
76
+ expect(body).to eq fake_data
77
+ expect(req.header).to include({
78
+ "authorization" => [ sdk_key ],
79
+ "content-type" => [ "application/json" ],
80
+ "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
81
+ })
82
+ expect(req.header['x-launchdarkly-event-schema']).to eq []
83
+ expect(req.header['x-launchdarkly-payload-id']).to eq []
84
+ end
85
+ end
86
+
87
+ it "can use a proxy server" do
88
+ with_server do |server|
89
+ server.setup_ok_response("/bulk", "")
90
+
91
+ with_server(StubProxyServer.new) do |proxy|
92
+ begin
93
+ ENV["http_proxy"] = proxy.base_uri.to_s
94
+
95
+ es = make_sender(server)
96
+
97
+ result = es.send_event_data(fake_data, false)
98
+
99
+ expect(result.success).to be true
100
+
101
+ req, body = server.await_request_with_body
102
+ expect(body).to eq fake_data
103
+ ensure
104
+ ENV["http_proxy"] = nil
105
+ end
106
+ end
107
+ end
108
+ end
109
+
110
+ [400, 408, 429, 500].each do |status|
111
+ it "handles recoverable error #{status}" do
112
+ with_sender_and_server do |es, server|
113
+ req_count = 0
114
+ server.setup_response("/bulk") do |req, res|
115
+ req_count = req_count + 1
116
+ res.status = req_count == 2 ? 200 : status
117
+ end
118
+
119
+ result = es.send_event_data(fake_data, false)
120
+
121
+ expect(result.success).to be true
122
+ expect(result.must_shutdown).to be false
123
+ expect(result.time_from_server).not_to be_nil
124
+
125
+ expect(server.requests.count).to eq 2
126
+ req1, body1 = server.await_request_with_body
127
+ req2, body2 = server.await_request_with_body
128
+ expect(body1).to eq fake_data
129
+ expect(body2).to eq fake_data
130
+ expect(req1.header['x-launchdarkly-payload-id']).to eq req2.header['x-launchdarkly-payload-id']
131
+ end
132
+ end
133
+ end
134
+
135
+ [400, 408, 429, 500].each do |status|
136
+ it "only retries error #{status} once" do
137
+ with_sender_and_server do |es, server|
138
+ req_count = 0
139
+ server.setup_response("/bulk") do |req, res|
140
+ req_count = req_count + 1
141
+ res.status = req_count == 3 ? 200 : status
142
+ end
143
+
144
+ result = es.send_event_data(fake_data, false)
145
+
146
+ expect(result.success).to be false
147
+ expect(result.must_shutdown).to be false
148
+ expect(result.time_from_server).to be_nil
149
+
150
+ expect(server.requests.count).to eq 2
151
+ req1, body1 = server.await_request_with_body
152
+ req2, body2 = server.await_request_with_body
153
+ expect(body1).to eq fake_data
154
+ expect(body2).to eq fake_data
155
+ expect(req1.header['x-launchdarkly-payload-id']).to eq req2.header['x-launchdarkly-payload-id']
156
+ end
157
+ end
158
+ end
159
+
160
+ [401, 403].each do |status|
161
+ it "gives up after unrecoverable error #{status}" do
162
+ with_sender_and_server do |es, server|
163
+ server.setup_response("/bulk") do |req, res|
164
+ res.status = status
165
+ end
166
+
167
+ result = es.send_event_data(fake_data, false)
168
+
169
+ expect(result.success).to be false
170
+ expect(result.must_shutdown).to be true
171
+ expect(result.time_from_server).to be_nil
172
+
173
+ expect(server.requests.count).to eq 1
174
+ end
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -5,8 +5,8 @@ require "time"
5
5
  describe LaunchDarkly::EventProcessor do
6
6
  subject { LaunchDarkly::EventProcessor }
7
7
 
8
- let(:default_config) { LaunchDarkly::Config.new }
9
- let(:hc) { FakeHttpClient.new }
8
+ let(:default_config_opts) { { diagnostic_opt_out: true, logger: $null_log } }
9
+ let(:default_config) { LaunchDarkly::Config.new(default_config_opts) }
10
10
  let(:user) { { key: "userkey", name: "Red" } }
11
11
  let(:filtered_user) { { key: "userkey", privateAttrs: [ "name" ] } }
12
12
  let(:numeric_user) { { key: 1, secondary: 2, ip: 3, country: 4, email: 5, firstName: 6, lastName: 7,
@@ -14,546 +14,508 @@ describe LaunchDarkly::EventProcessor do
14
14
  let(:stringified_numeric_user) { { key: '1', secondary: '2', ip: '3', country: '4', email: '5', firstName: '6',
15
15
  lastName: '7', avatar: '8', name: '9', anonymous: false, custom: { age: 99 } } }
16
16
 
17
- after(:each) do
18
- if !@ep.nil?
19
- @ep.stop
17
+ def with_processor_and_sender(config)
18
+ sender = FakeEventSender.new
19
+ ep = subject.new("sdk_key", config, nil, nil, { event_sender: sender })
20
+ begin
21
+ yield ep, sender
22
+ ensure
23
+ ep.stop
20
24
  end
21
25
  end
22
26
 
23
27
  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)
28
+ with_processor_and_sender(default_config) do |ep, sender|
29
+ e = { kind: "identify", key: user[:key], user: user }
30
+ ep.add_event(e)
27
31
 
28
- output = flush_and_get_events
29
- expect(output).to contain_exactly(e)
32
+ output = flush_and_get_events(ep, sender)
33
+ expect(output).to contain_exactly(e)
34
+ end
30
35
  end
31
36
 
32
37
  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
- })
38
+ config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true))
39
+ with_processor_and_sender(config) do |ep, sender|
40
+ e = { kind: "identify", key: user[:key], user: user }
41
+ ep.add_event(e)
42
+
43
+ output = flush_and_get_events(ep, sender)
44
+ expect(output).to contain_exactly({
45
+ kind: "identify",
46
+ key: user[:key],
47
+ creationDate: e[:creationDate],
48
+ user: filtered_user
49
+ })
50
+ end
45
51
  end
46
52
 
47
53
  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
- )
54
+ with_processor_and_sender(default_config) do |ep, sender|
55
+ flag = { key: "flagkey", version: 11 }
56
+ e = { kind: "identify", key: numeric_user[:key], user: numeric_user }
57
+ ep.add_event(e)
58
+
59
+ output = flush_and_get_events(ep, sender)
60
+ expect(output).to contain_exactly(
61
+ kind: "identify",
62
+ key: numeric_user[:key].to_s,
63
+ creationDate: e[:creationDate],
64
+ user: stringified_numeric_user
65
+ )
66
+ end
60
67
  end
61
68
 
62
69
  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
- )
70
+ with_processor_and_sender(default_config) do |ep, sender|
71
+ flag = { key: "flagkey", version: 11 }
72
+ fe = {
73
+ kind: "feature", key: "flagkey", version: 11, user: user,
74
+ variation: 1, value: "value", trackEvents: true
75
+ }
76
+ ep.add_event(fe)
77
+
78
+ output = flush_and_get_events(ep, sender)
79
+ expect(output).to contain_exactly(
80
+ eq(index_event(fe, user)),
81
+ eq(feature_event(fe, flag, false, nil)),
82
+ include(:kind => "summary")
83
+ )
84
+ end
77
85
  end
78
86
 
79
87
  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
- )
88
+ config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true))
89
+ with_processor_and_sender(config) do |ep, sender|
90
+ flag = { key: "flagkey", version: 11 }
91
+ fe = {
92
+ kind: "feature", key: "flagkey", version: 11, user: user,
93
+ variation: 1, value: "value", trackEvents: true
94
+ }
95
+ ep.add_event(fe)
96
+
97
+ output = flush_and_get_events(ep, sender)
98
+ expect(output).to contain_exactly(
99
+ eq(index_event(fe, filtered_user)),
100
+ eq(feature_event(fe, flag, false, nil)),
101
+ include(:kind => "summary")
102
+ )
103
+ end
95
104
  end
96
105
 
97
106
  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
- )
107
+ with_processor_and_sender(default_config) do |ep, sender|
108
+ flag = { key: "flagkey", version: 11 }
109
+ fe = {
110
+ kind: "feature", key: "flagkey", version: 11, user: numeric_user,
111
+ variation: 1, value: "value", trackEvents: true
112
+ }
113
+ ep.add_event(fe)
114
+
115
+ output = flush_and_get_events(ep, sender)
116
+ expect(output).to contain_exactly(
117
+ eq(index_event(fe, stringified_numeric_user)),
118
+ eq(feature_event(fe, flag, false, nil)),
119
+ include(:kind => "summary")
120
+ )
121
+ end
112
122
  end
113
123
 
114
124
  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
- )
125
+ config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true))
126
+ with_processor_and_sender(config) do |ep, sender|
127
+ flag = { key: "flagkey", version: 11 }
128
+ fe = {
129
+ kind: "feature", key: "flagkey", version: 11, user: user,
130
+ variation: 1, value: "value", trackEvents: true
131
+ }
132
+ ep.add_event(fe)
133
+
134
+ output = flush_and_get_events(ep, sender)
135
+ expect(output).to contain_exactly(
136
+ eq(feature_event(fe, flag, false, user)),
137
+ include(:kind => "summary")
138
+ )
139
+ end
129
140
  end
130
141
 
131
142
  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
- )
143
+ config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true))
144
+ with_processor_and_sender(config) do |ep, sender|
145
+ flag = { key: "flagkey", version: 11 }
146
+ fe = {
147
+ kind: "feature", key: "flagkey", version: 11, user: numeric_user,
148
+ variation: 1, value: "value", trackEvents: true
149
+ }
150
+ ep.add_event(fe)
151
+
152
+ output = flush_and_get_events(ep, sender)
153
+ expect(output).to contain_exactly(
154
+ eq(feature_event(fe, flag, false, stringified_numeric_user)),
155
+ include(:kind => "summary")
156
+ )
157
+ end
146
158
  end
147
159
 
148
160
  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
- )
161
+ config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true, inline_users_in_events: true))
162
+ with_processor_and_sender(config) do |ep, sender|
163
+ flag = { key: "flagkey", version: 11 }
164
+ fe = {
165
+ kind: "feature", key: "flagkey", version: 11, user: user,
166
+ variation: 1, value: "value", trackEvents: true
167
+ }
168
+ ep.add_event(fe)
169
+
170
+ output = flush_and_get_events(ep, sender)
171
+ expect(output).to contain_exactly(
172
+ eq(feature_event(fe, flag, false, filtered_user)),
173
+ include(:kind => "summary")
174
+ )
175
+ end
163
176
  end
164
177
 
165
178
  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
- )
179
+ config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true))
180
+ with_processor_and_sender(config) do |ep, sender|
181
+ flag = { key: "flagkey", version: 11 }
182
+ fe = {
183
+ kind: "feature", key: "flagkey", version: 11, user: user,
184
+ variation: 1, value: "value", trackEvents: false
185
+ }
186
+ ep.add_event(fe)
187
+
188
+ output = flush_and_get_events(ep, sender)
189
+ expect(output).to contain_exactly(
190
+ eq(index_event(fe, user)),
191
+ include(:kind => "summary")
192
+ )
193
+ end
180
194
  end
181
195
 
182
196
  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
- )
197
+ with_processor_and_sender(default_config) do |ep, sender|
198
+ flag = { key: "flagkey", version: 11 }
199
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
200
+ fe = {
201
+ kind: "feature", key: "flagkey", version: 11, user: user,
202
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: future_time
203
+ }
204
+ ep.add_event(fe)
205
+
206
+ output = flush_and_get_events(ep, sender)
207
+ expect(output).to contain_exactly(
208
+ eq(index_event(fe, user)),
209
+ eq(feature_event(fe, flag, true, user)),
210
+ include(:kind => "summary")
211
+ )
212
+ end
198
213
  end
199
214
 
200
215
  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
- )
216
+ with_processor_and_sender(default_config) do |ep, sender|
217
+ flag = { key: "flagkey", version: 11 }
218
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
219
+ fe = {
220
+ kind: "feature", key: "flagkey", version: 11, user: user,
221
+ variation: 1, value: "value", trackEvents: true, debugEventsUntilDate: future_time
222
+ }
223
+ ep.add_event(fe)
224
+
225
+ output = flush_and_get_events(ep, sender)
226
+ expect(output).to contain_exactly(
227
+ eq(index_event(fe, user)),
228
+ eq(feature_event(fe, flag, false, nil)),
229
+ eq(feature_event(fe, flag, true, user)),
230
+ include(:kind => "summary")
231
+ )
232
+ end
217
233
  end
218
234
 
219
235
  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
- )
236
+ with_processor_and_sender(default_config) do |ep, sender|
237
+ # Pick a server time that is somewhat behind the client time
238
+ server_time = Time.now - 20
239
+
240
+ # Send and flush an event we don't care about, just to set the last server time
241
+ sender.result = LaunchDarkly::Impl::EventSenderResult.new(true, false, server_time)
242
+ ep.add_event({ kind: "identify", user: user })
243
+ flush_and_get_events(ep, sender)
244
+
245
+ # Now send an event with debug mode on, with a "debug until" time that is further in
246
+ # the future than the server time, but in the past compared to the client.
247
+ flag = { key: "flagkey", version: 11 }
248
+ debug_until = (server_time.to_f * 1000).to_i + 1000
249
+ fe = {
250
+ kind: "feature", key: "flagkey", version: 11, user: user,
251
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until
252
+ }
253
+ ep.add_event(fe)
254
+
255
+ # Should get a summary event only, not a full feature event
256
+ output = flush_and_get_events(ep, sender)
257
+ expect(output).to contain_exactly(
258
+ include(:kind => "summary")
259
+ )
260
+ end
246
261
  end
247
262
 
248
263
  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
- )
264
+ with_processor_and_sender(default_config) do |ep, sender|
265
+ # Pick a server time that is somewhat ahead of the client time
266
+ server_time = Time.now + 20
267
+
268
+ # Send and flush an event we don't care about, just to set the last server time
269
+ sender.result = LaunchDarkly::Impl::EventSenderResult.new(true, false, server_time)
270
+ ep.add_event({ kind: "identify", user: user })
271
+ flush_and_get_events(ep, sender)
272
+
273
+ # Now send an event with debug mode on, with a "debug until" time that is further in
274
+ # the future than the server time, but in the past compared to the client.
275
+ flag = { key: "flagkey", version: 11 }
276
+ debug_until = (server_time.to_f * 1000).to_i - 1000
277
+ fe = {
278
+ kind: "feature", key: "flagkey", version: 11, user: user,
279
+ variation: 1, value: "value", trackEvents: false, debugEventsUntilDate: debug_until
280
+ }
281
+ ep.add_event(fe)
282
+
283
+ # Should get a summary event only, not a full feature event
284
+ output = flush_and_get_events(ep, sender)
285
+ expect(output).to contain_exactly(
286
+ include(:kind => "summary")
287
+ )
288
+ end
275
289
  end
276
290
 
277
291
  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
- )
292
+ with_processor_and_sender(default_config) do |ep, sender|
293
+ flag1 = { key: "flagkey1", version: 11 }
294
+ flag2 = { key: "flagkey2", version: 22 }
295
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
296
+ fe1 = {
297
+ kind: "feature", key: "flagkey1", version: 11, user: user,
298
+ variation: 1, value: "value", trackEvents: true
299
+ }
300
+ fe2 = {
301
+ kind: "feature", key: "flagkey2", version: 22, user: user,
302
+ variation: 1, value: "value", trackEvents: true
303
+ }
304
+ ep.add_event(fe1)
305
+ ep.add_event(fe2)
306
+
307
+ output = flush_and_get_events(ep, sender)
308
+ expect(output).to contain_exactly(
309
+ eq(index_event(fe1, user)),
310
+ eq(feature_event(fe1, flag1, false, nil)),
311
+ eq(feature_event(fe2, flag2, false, nil)),
312
+ include(:kind => "summary")
313
+ )
314
+ end
300
315
  end
301
316
 
302
317
  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
- ]
318
+ with_processor_and_sender(default_config) do |ep, sender|
319
+ flag1 = { key: "flagkey1", version: 11 }
320
+ flag2 = { key: "flagkey2", version: 22 }
321
+ future_time = (Time.now.to_f * 1000).to_i + 1000000
322
+ fe1 = {
323
+ kind: "feature", key: "flagkey1", version: 11, user: user,
324
+ variation: 1, value: "value1", default: "default1"
325
+ }
326
+ fe2 = {
327
+ kind: "feature", key: "flagkey2", version: 22, user: user,
328
+ variation: 2, value: "value2", default: "default2"
329
+ }
330
+ ep.add_event(fe1)
331
+ ep.add_event(fe2)
332
+
333
+ output = flush_and_get_events(ep, sender)
334
+ expect(output).to contain_exactly(
335
+ eq(index_event(fe1, user)),
336
+ eq({
337
+ kind: "summary",
338
+ startDate: fe1[:creationDate],
339
+ endDate: fe2[:creationDate],
340
+ features: {
341
+ flagkey1: {
342
+ default: "default1",
343
+ counters: [
344
+ { version: 11, variation: 1, value: "value1", count: 1 }
345
+ ]
346
+ },
347
+ flagkey2: {
348
+ default: "default2",
349
+ counters: [
350
+ { version: 22, variation: 2, value: "value2", count: 1 }
351
+ ]
352
+ }
337
353
  }
338
- }
339
- })
340
- )
354
+ })
355
+ )
356
+ end
341
357
  end
342
358
 
343
359
  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" }, metricValue: 1.5 }
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
- )
360
+ with_processor_and_sender(default_config) do |ep, sender|
361
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" }, metricValue: 1.5 }
362
+ ep.add_event(e)
363
+
364
+ output = flush_and_get_events(ep, sender)
365
+ expect(output).to contain_exactly(
366
+ eq(index_event(e, user)),
367
+ eq(custom_event(e, nil))
368
+ )
369
+ end
353
370
  end
354
371
 
355
372
  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
- )
373
+ config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true))
374
+ with_processor_and_sender(config) do |ep, sender|
375
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
376
+ ep.add_event(e)
377
+
378
+ output = flush_and_get_events(ep, sender)
379
+ expect(output).to contain_exactly(
380
+ eq(custom_event(e, user))
381
+ )
382
+ end
365
383
  end
366
384
 
367
385
  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
- )
386
+ config = LaunchDarkly::Config.new(default_config_opts.merge(all_attributes_private: true, inline_users_in_events: true))
387
+ with_processor_and_sender(config) do |ep, sender|
388
+ e = { kind: "custom", key: "eventkey", user: user, data: { thing: "stuff" } }
389
+ ep.add_event(e)
390
+
391
+ output = flush_and_get_events(ep, sender)
392
+ expect(output).to contain_exactly(
393
+ eq(custom_event(e, filtered_user))
394
+ )
395
+ end
377
396
  end
378
397
 
379
398
  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
- )
399
+ config = LaunchDarkly::Config.new(default_config_opts.merge(inline_users_in_events: true))
400
+ with_processor_and_sender(config) do |ep, sender|
401
+ e = { kind: "custom", key: "eventkey", user: numeric_user }
402
+ ep.add_event(e)
403
+
404
+ output = flush_and_get_events(ep, sender)
405
+ expect(output).to contain_exactly(
406
+ eq(custom_event(e, stringified_numeric_user))
407
+ )
408
+ end
389
409
  end
390
410
 
391
411
  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)
412
+ with_processor_and_sender(default_config) do |ep, sender|
413
+ e = { kind: "identify", key: user[:key], user: user }
414
+ ep.add_event(e)
415
+
416
+ ep.stop
417
+
418
+ output = sender.analytics_payloads.pop
419
+ expect(output).to contain_exactly(e)
420
+ end
400
421
  end
401
422
 
402
423
  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
- it "sends unique payload IDs" do
420
- @ep = subject.new("sdk_key", default_config, hc)
421
- e = { kind: "identify", user: user }
422
-
423
- @ep.add_event(e)
424
- @ep.flush
425
- @ep.wait_until_inactive
426
- req0 = hc.get_request
427
-
428
- @ep.add_event(e)
429
- @ep.flush
430
- @ep.wait_until_inactive
431
- req1 = hc.get_request
432
-
433
- id0 = req0["x-launchdarkly-payload-id"]
434
- id1 = req1["x-launchdarkly-payload-id"]
435
- expect(id0).not_to be_nil
436
- expect(id0).not_to eq ""
437
- expect(id1).not_to be nil
438
- expect(id1).not_to eq ""
439
- expect(id1).not_to eq id0
440
- end
441
-
442
- def verify_unrecoverable_http_error(status)
443
- @ep = subject.new("sdk_key", default_config, hc)
444
- e = { kind: "identify", user: user }
445
- @ep.add_event(e)
446
-
447
- hc.set_response_status(status)
448
- @ep.flush
449
- @ep.wait_until_inactive
450
- expect(hc.get_request).not_to be_nil
451
- hc.reset
452
-
453
- @ep.add_event(e)
454
- @ep.flush
455
- @ep.wait_until_inactive
456
- expect(hc.get_request).to be_nil
457
- end
458
-
459
- def verify_recoverable_http_error(status)
460
- @ep = subject.new("sdk_key", default_config, hc)
461
- e = { kind: "identify", user: user }
462
- @ep.add_event(e)
463
-
464
- hc.set_response_status(503)
465
- @ep.flush
466
- @ep.wait_until_inactive
467
-
468
- req0 = hc.get_request
469
- expect(req0).not_to be_nil
470
- req1 = hc.get_request
471
- expect(req1).not_to be_nil
472
- id0 = req0["x-launchdarkly-payload-id"]
473
- expect(id0).not_to be_nil
474
- expect(id0).not_to eq ""
475
- expect(req1["x-launchdarkly-payload-id"]).to eq id0
476
-
477
- expect(hc.get_request).to be_nil # no 3rd request
478
-
479
- # now verify that a subsequent flush still generates a request
480
- hc.reset
481
- @ep.add_event(e)
482
- @ep.flush
483
- @ep.wait_until_inactive
484
- expect(hc.get_request).not_to be_nil
424
+ with_processor_and_sender(default_config) do |ep, sender|
425
+ ep.flush
426
+ ep.wait_until_inactive
427
+ expect(sender.analytics_payloads.empty?).to be true
428
+ end
485
429
  end
486
430
 
487
- it "stops posting events after getting a 401 error" do
488
- verify_unrecoverable_http_error(401)
489
- end
431
+ it "stops posting events after unrecoverable error" do
432
+ with_processor_and_sender(default_config) do |ep, sender|
433
+ sender.result = LaunchDarkly::Impl::EventSenderResult.new(false, true, nil)
434
+ e = { kind: "identify", key: user[:key], user: user }
435
+ ep.add_event(e)
436
+ flush_and_get_events(ep, sender)
490
437
 
491
- it "stops posting events after getting a 403 error" do
492
- verify_unrecoverable_http_error(403)
438
+ e = { kind: "identify", key: user[:key], user: user }
439
+ ep.add_event(e)
440
+ ep.flush
441
+ ep.wait_until_inactive
442
+ expect(sender.analytics_payloads.empty?).to be true
443
+ end
493
444
  end
494
445
 
495
- it "retries after 408 error" do
496
- verify_recoverable_http_error(408)
497
- end
446
+ describe "diagnostic events" do
447
+ let(:default_id) { LaunchDarkly::Impl::DiagnosticAccumulator.create_diagnostic_id('sdk_key') }
448
+ let(:diagnostic_config) { LaunchDarkly::Config.new(diagnostic_opt_out: false, logger: $null_log) }
498
449
 
499
- it "retries after 429 error" do
500
- verify_recoverable_http_error(429)
501
- end
450
+ def with_diagnostic_processor_and_sender(config)
451
+ sender = FakeEventSender.new
452
+ acc = LaunchDarkly::Impl::DiagnosticAccumulator.new(default_id)
453
+ ep = subject.new("sdk_key", config, nil, acc,
454
+ { diagnostic_recording_interval: 0.2, event_sender: sender })
455
+ begin
456
+ yield ep, sender
457
+ ensure
458
+ ep.stop
459
+ end
460
+ end
502
461
 
503
- it "retries after 503 error" do
504
- verify_recoverable_http_error(503)
505
- end
462
+ it "sends init event" do
463
+ with_diagnostic_processor_and_sender(diagnostic_config) do |ep, sender|
464
+ event = sender.diagnostic_payloads.pop
465
+ expect(event).to include({
466
+ kind: 'diagnostic-init',
467
+ id: default_id
468
+ })
469
+ end
470
+ end
506
471
 
507
- it "retries flush once after connection error" do
508
- @ep = subject.new("sdk_key", default_config, hc)
509
- e = { kind: "identify", user: user }
510
- @ep.add_event(e)
472
+ it "sends periodic event" do
473
+ with_diagnostic_processor_and_sender(diagnostic_config) do |ep, sender|
474
+ init_event = sender.diagnostic_payloads.pop
475
+ periodic_event = sender.diagnostic_payloads.pop
476
+ expect(periodic_event).to include({
477
+ kind: 'diagnostic',
478
+ id: default_id,
479
+ droppedEvents: 0,
480
+ deduplicatedUsers: 0,
481
+ eventsInLastBatch: 0,
482
+ streamInits: []
483
+ })
484
+ end
485
+ end
511
486
 
512
- hc.set_exception(IOError.new("deliberate error"))
513
- @ep.flush
514
- @ep.wait_until_inactive
487
+ it "counts events in queue from last flush and dropped events" do
488
+ config = LaunchDarkly::Config.new(diagnostic_opt_out: false, capacity: 2, logger: $null_log)
489
+ with_diagnostic_processor_and_sender(config) do |ep, sender|
490
+ init_event = sender.diagnostic_payloads.pop
491
+
492
+ ep.add_event({ kind: 'identify', user: user })
493
+ ep.add_event({ kind: 'identify', user: user })
494
+ ep.add_event({ kind: 'identify', user: user })
495
+ flush_and_get_events(ep, sender)
496
+
497
+ periodic_event = sender.diagnostic_payloads.pop
498
+ expect(periodic_event).to include({
499
+ kind: 'diagnostic',
500
+ droppedEvents: 1,
501
+ eventsInLastBatch: 2
502
+ })
503
+ end
504
+ end
515
505
 
516
- expect(hc.get_request).not_to be_nil
517
- expect(hc.get_request).not_to be_nil
518
- expect(hc.get_request).to be_nil # no 3rd request
519
- end
506
+ it "counts deduplicated users" do
507
+ with_diagnostic_processor_and_sender(diagnostic_config) do |ep, sender|
508
+ init_event = sender.diagnostic_payloads.pop
520
509
 
521
- it "makes actual HTTP request with correct headers" do
522
- e = { kind: "identify", key: user[:key], user: user }
523
- with_server do |server|
524
- server.setup_ok_response("/bulk", "")
525
-
526
- @ep = subject.new("sdk_key", LaunchDarkly::Config.new(events_uri: server.base_uri.to_s))
527
- @ep.add_event(e)
528
- @ep.flush
529
-
530
- req = server.await_request
531
- expect(req.header).to include({
532
- "authorization" => [ "sdk_key" ],
533
- "content-type" => [ "application/json" ],
534
- "user-agent" => [ "RubyClient/" + LaunchDarkly::VERSION ],
535
- "x-launchdarkly-event-schema" => [ "3" ]
536
- })
537
- end
538
- end
510
+ ep.add_event({ kind: 'custom', key: 'event1', user: user })
511
+ ep.add_event({ kind: 'custom', key: 'event2', user: user })
512
+ events = flush_and_get_events(ep, sender)
539
513
 
540
- it "can use a proxy server" do
541
- e = { kind: "identify", key: user[:key], user: user }
542
- with_server do |server|
543
- server.setup_ok_response("/bulk", "")
544
-
545
- with_server(StubProxyServer.new) do |proxy|
546
- begin
547
- ENV["http_proxy"] = proxy.base_uri.to_s
548
- @ep = subject.new("sdk_key", LaunchDarkly::Config.new(events_uri: server.base_uri.to_s))
549
- @ep.add_event(e)
550
- @ep.flush
551
-
552
- req = server.await_request
553
- expect(req["content-type"]).to eq("application/json")
554
- ensure
555
- ENV["http_proxy"] = nil
556
- end
514
+ periodic_event = sender.diagnostic_payloads.pop
515
+ expect(periodic_event).to include({
516
+ kind: 'diagnostic',
517
+ deduplicatedUsers: 1
518
+ })
557
519
  end
558
520
  end
559
521
  end
@@ -599,75 +561,26 @@ describe LaunchDarkly::EventProcessor do
599
561
  out
600
562
  end
601
563
 
602
- def flush_and_get_events
603
- @ep.flush
604
- @ep.wait_until_inactive
605
- get_events_from_last_request
564
+ def flush_and_get_events(ep, sender)
565
+ ep.flush
566
+ ep.wait_until_inactive
567
+ sender.analytics_payloads.pop
606
568
  end
607
569
 
608
- def get_events_from_last_request
609
- req = hc.get_request
610
- JSON.parse(req.body, symbolize_names: true)
611
- end
570
+ class FakeEventSender
571
+ attr_accessor :result
572
+ attr_reader :analytics_payloads
573
+ attr_reader :diagnostic_payloads
612
574
 
613
- class FakeHttpClient
614
575
  def initialize
615
- reset
616
- end
617
-
618
- def set_response_status(status)
619
- @status = status
620
- end
621
-
622
- def set_server_time(time_millis)
623
- @server_time = Time.at(time_millis.to_f / 1000)
624
- end
625
-
626
- def set_exception(e)
627
- @exception = e
628
- end
629
-
630
- def reset
631
- @requests = []
632
- @status = 200
633
- end
634
-
635
- def request(req)
636
- @requests.push(req)
637
- if @exception
638
- raise @exception
639
- else
640
- headers = {}
641
- if @server_time
642
- headers["Date"] = @server_time.httpdate
643
- end
644
- FakeResponse.new(@status ? @status : 200, headers)
645
- end
576
+ @result = LaunchDarkly::Impl::EventSenderResult.new(true, false, nil)
577
+ @analytics_payloads = Queue.new
578
+ @diagnostic_payloads = Queue.new
646
579
  end
647
580
 
648
- def start
649
- end
650
-
651
- def started?
652
- false
653
- end
654
-
655
- def finish
656
- end
657
-
658
- def get_request
659
- @requests.shift
660
- end
661
- end
662
-
663
- class FakeResponse
664
- include Net::HTTPHeader
665
-
666
- attr_reader :code
667
-
668
- def initialize(status, headers)
669
- @code = status.to_s
670
- initialize_http_header(headers)
581
+ def send_event_data(data, is_diagnostic)
582
+ (is_diagnostic ? @diagnostic_payloads : @analytics_payloads).push(JSON.parse(data, symbolize_names: true))
583
+ @result
671
584
  end
672
585
  end
673
586
  end