launchdarkly-server-sdk 5.6.2 → 5.7.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.
@@ -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