rspec-buildkite-analytics 0.3.5 → 0.6.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d55751f8e88a6ac7ad250c0c1e2b382dcaeb2d23504aa6d110a22a4a61af8d06
4
- data.tar.gz: b21d3d7fe03e1d504870b2d1fba0f188ab94e1f5e1c5c3fa3f11b1536a9f99b1
3
+ metadata.gz: 1ca7801b3e7bc48f52a372df29c553c9e2dad5bffdab1bf8dff0b20f0173b987
4
+ data.tar.gz: 22d6197b37f465afa1a2a309ba4f3780bafef928d626768a5d4d3fa6d214a809
5
5
  SHA512:
6
- metadata.gz: 8bb6efbdc7f9b4f06ab3b0793c9ef5a1c2b41429a755040d0c6b558fbb24b308e59cd862a646b86717bb5bebf4464eec45e3551db043b8332ed37a94b642f789
7
- data.tar.gz: 97089293de5331c1329979fd853ff15297e4460aaec7631867e2031398a31603296e53174da6a398134fe7a54bfa9b1e7082817acb989e6f74978a574afae9fc
6
+ metadata.gz: '0380c462ae87c2dd2dbb830bbcdc93bef621b74f2f516c465b37e633679e9e0c887fe8557356aaf85abd8e7fe179110b86aabceb339057cd89e37c00afcac107'
7
+ data.tar.gz: 9b6810ff8529454f543b70f521b34906223e2ee02ad1f0d4cd261d4bfe6e2c4ed21eb76c8eddfb08818c9c8a07d07cb62c3c206c958c48743a47c1dfde83f9b3
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rspec-buildkite-analytics (0.3.5)
4
+ rspec-buildkite-analytics (0.6.0)
5
5
  activesupport (>= 5.2, <= 7.0)
6
6
  rspec-core (~> 3.10)
7
7
  rspec-expectations (~> 3.10)
@@ -18,10 +18,10 @@ GEM
18
18
  zeitwerk (~> 2.3)
19
19
  concurrent-ruby (1.1.9)
20
20
  diff-lcs (1.4.4)
21
- i18n (1.8.10)
21
+ i18n (1.8.11)
22
22
  concurrent-ruby (~> 1.0)
23
23
  minitest (5.14.4)
24
- rake (13.0.3)
24
+ rake (13.0.6)
25
25
  rspec (3.10.0)
26
26
  rspec-core (~> 3.10.0)
27
27
  rspec-expectations (~> 3.10.0)
@@ -31,10 +31,10 @@ GEM
31
31
  rspec-expectations (3.10.1)
32
32
  diff-lcs (>= 1.2.0, < 2.0)
33
33
  rspec-support (~> 3.10.0)
34
- rspec-mocks (3.10.1)
34
+ rspec-mocks (3.10.2)
35
35
  diff-lcs (>= 1.2.0, < 2.0)
36
36
  rspec-support (~> 3.10.0)
37
- rspec-support (3.10.1)
37
+ rspec-support (3.10.3)
38
38
  tzinfo (2.0.4)
39
39
  concurrent-ruby (~> 1.0)
40
40
  websocket (1.2.9)
data/README.md CHANGED
@@ -15,10 +15,7 @@ end
15
15
 
16
16
  Configure your API key:
17
17
  ```ruby
18
- RSpec::Buildkite::Analytics.configure do |config|
19
- config.suite_key = "........"
20
- # other config
21
- end
18
+ RSpec::Buildkite::Analytics.configure(token: "........")
22
19
  ```
23
20
 
24
21
  Run bundler to install the gem and update your `Gemfile.lock`:
@@ -13,12 +13,14 @@ module RSpec::Buildkite::Analytics::CI
13
13
  "commit_sha" => ENV["BUILDKITE_COMMIT"],
14
14
  "number" => ENV["BUILDKITE_BUILD_NUMBER"],
15
15
  "job_id" => ENV["BUILDKITE_JOB_ID"],
16
- "message" => ENV["BUILDKITE_MESSAGE"]
16
+ "message" => ENV["BUILDKITE_MESSAGE"],
17
+ "debug" => ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"]
17
18
  }
18
19
  else
19
20
  {
20
21
  "CI" => nil,
21
- "key" => SecureRandom.uuid
22
+ "key" => SecureRandom.uuid,
23
+ "debug" => ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"]
22
24
  }
23
25
  end
24
26
  end
@@ -1,6 +1,10 @@
1
+ require "time"
2
+
1
3
  module RSpec::Buildkite::Analytics
2
4
  class Reporter
3
- RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending
5
+ RSpec::Core::Formatters.register self, :example_passed, :example_failed, :example_pending, :dump_summary
6
+
7
+ attr_reader :output
4
8
 
5
9
  def initialize(output)
6
10
  @output = output
@@ -14,12 +18,82 @@ module RSpec::Buildkite::Analytics
14
18
 
15
19
  if trace
16
20
  trace.example = example
21
+ trace.failure_reason, trace.failure_expanded = failure_info(notification) if example.execution_result.status == :failed
17
22
  RSpec::Buildkite::Analytics.session&.write_result(trace)
18
23
  end
19
24
  end
20
25
 
26
+ def dump_summary(notification)
27
+ if RSpec::Buildkite::Analytics.session.present?
28
+ examples_count = {
29
+ examples: notification.examples.count,
30
+ failed: notification.failed_examples.count,
31
+ pending: notification.pending_examples.count,
32
+ errors_outside_examples: notification.errors_outside_of_examples_count
33
+ }
34
+
35
+ RSpec::Buildkite::Analytics.session.close(examples_count)
36
+
37
+ # Write the debug file, if debug mode is enabled
38
+ if RSpec::Buildkite::Analytics.debug_enabled
39
+ filename = "#{RSpec::Buildkite::Analytics.debug_filepath}/bk-analytics-#{Time.now.strftime("%F-%R:%S")}-#{ENV["BUILDKITE_JOB_ID"]}.log.gz"
40
+
41
+ File.open(filename, "wb") do |f|
42
+ gz = Zlib::GzipWriter.new(f)
43
+ gz.puts(RSpec::Buildkite::Analytics.session.logger.to_array)
44
+ gz.close
45
+ end
46
+ end
47
+ end
48
+ end
49
+
21
50
  alias_method :example_passed, :handle_example
22
51
  alias_method :example_failed, :handle_example
23
52
  alias_method :example_pending, :handle_example
53
+
54
+ private
55
+
56
+ def failure_info(notification)
57
+ failure_expanded = []
58
+
59
+ if notification.exception.class == RSpec::Expectations::MultipleExpectationsNotMetError
60
+ failure_reason = notification.exception.summary
61
+ notification.exception.all_exceptions.each do |exception|
62
+ # an example with multiple failures doesn't give us a
63
+ # separate message lines and backtrace object to send, so
64
+ # I've reached into RSpec internals and duplicated the
65
+ # construction of these
66
+ message_lines = RSpec::Core::Formatters::ExceptionPresenter.new(exception, notification.example).colorized_message_lines
67
+
68
+ failure_expanded << {
69
+ expanded: format_message_lines(message_lines),
70
+ backtrace: RSpec.configuration.backtrace_formatter.format_backtrace(exception.backtrace)
71
+ }
72
+ end
73
+ else
74
+ failure_reason = strip_diff_colors(notification.colorized_message_lines[0])
75
+
76
+ # the second line is always whitespace padding
77
+ message_lines = notification.colorized_message_lines[2..]
78
+
79
+ failure_expanded << {
80
+ expanded: format_message_lines(message_lines),
81
+ backtrace: notification.formatted_backtrace
82
+ }
83
+ end
84
+
85
+ return failure_reason, failure_expanded
86
+ end
87
+
88
+ def format_message_lines(message_lines)
89
+ message_lines.map! { |l| strip_diff_colors(l) }
90
+ # the last line is sometimes blank, depending on the error reported
91
+ message_lines.pop if message_lines.last.blank?
92
+ message_lines
93
+ end
94
+
95
+ def strip_diff_colors(string)
96
+ string.gsub(/\e\[([;\d]+)?m/, '')
97
+ end
24
98
  end
25
99
  end
@@ -12,26 +12,52 @@ module RSpec::Buildkite::Analytics
12
12
  class RejectedSubscription < StandardError; end
13
13
  class InitialConnectionFailure < StandardError; end
14
14
 
15
+ class Logger
16
+ def initialize
17
+ @log = Queue.new
18
+ end
19
+
20
+ def write(str)
21
+ @log << "#{Time.now.strftime("%F-%R:%S.%9N")} #{Thread.current} #{str}"
22
+ end
23
+
24
+ def to_array
25
+ # This empty check is important cos calling pop on a Queue is blocking until
26
+ # it's not empty
27
+ if @log.empty?
28
+ []
29
+ else
30
+ Array.new(@log.size) { @log.pop }
31
+ end
32
+ end
33
+ end
34
+
35
+ attr_reader :logger
36
+
15
37
  def initialize(url, authorization_header, channel)
16
- @queue = Queue.new
38
+ @establish_subscription_queue = Queue.new
17
39
  @channel = channel
18
40
 
19
41
  @unconfirmed_idents = {}
20
42
  @idents_mutex = Mutex.new
43
+ @send_queue = Queue.new
21
44
  @empty = ConditionVariable.new
22
45
  @closing = false
46
+ @eot_queued = false
47
+ @eot_queued_mutex = Mutex.new
23
48
  @reconnection_mutex = Mutex.new
24
49
 
25
50
  @url = url
26
51
  @authorization_header = authorization_header
27
52
 
53
+ @logger = Logger.new
54
+
28
55
  connect
29
56
  rescue TimeoutError, InitialConnectionFailure => e
30
57
  $stderr.puts "rspec-buildkite-analytics could not establish an initial connection with Buildkite due to #{e.message}. You may be missing some data for this test suite, please contact support."
31
58
  end
32
59
 
33
60
  def disconnected(connection)
34
- reconnection_count = 0
35
61
  @reconnection_mutex.synchronize do
36
62
  # When the first thread detects a disconnection, it calls the disconnect method
37
63
  # with the current connection. This thread grabs the reconnection mutex and does the
@@ -43,16 +69,21 @@ module RSpec::Buildkite::Analytics
43
69
  # time the mutex is released, the value of @connection has been refreshed, and so
44
70
  # the second thread returns early and does not reattempt the reconnection.
45
71
  return unless connection == @connection
72
+ @logger.write("starting reconnection")
73
+
74
+ reconnection_count = 0
46
75
 
47
76
  begin
48
77
  reconnection_count += 1
49
78
  connect
50
79
  rescue SocketConnection::HandshakeError, RejectedSubscription, TimeoutError, SocketConnection::SocketError => e
80
+ @logger.write("failed reconnection attempt #{reconnection_count} due to #{e}")
51
81
  if reconnection_count > MAX_RECONNECTION_ATTEMPTS
52
82
  $stderr.puts "rspec-buildkite-analytics experienced a disconnection and could not reconnect to Buildkite due to #{e.message}. Please contact support."
53
83
  raise e
54
84
  else
55
85
  sleep(WAIT_BETWEEN_RECONNECTIONS)
86
+ @logger.write("retrying reconnection")
56
87
  retry
57
88
  end
58
89
  end
@@ -60,8 +91,10 @@ module RSpec::Buildkite::Analytics
60
91
  retransmit
61
92
  end
62
93
 
63
- def close()
94
+ def close(examples_count)
64
95
  @closing = true
96
+ @examples_count = examples_count
97
+ @logger.write("closing socket connection")
65
98
 
66
99
  # Because the server only sends us confirmations after every 10mb of
67
100
  # data it uploads to S3, we'll never get confirmation of the
@@ -70,7 +103,10 @@ module RSpec::Buildkite::Analytics
70
103
  send_eot
71
104
 
72
105
  @idents_mutex.synchronize do
73
- # Here, we sleep for 75 seconds while waiting for the server to confirm the last idents.
106
+ @logger.write("waiting for last confirm")
107
+ # Here, we sleep for 75 seconds while waiting for the send
108
+ # queue to be drained and for the server to confirm the last
109
+ # idents.
74
110
  # We are woken up when the unconfirmed_idents is empty, and given back the mutex to
75
111
  # continue operation.
76
112
  @empty.wait(@idents_mutex, CONFIRMATION_TIMEOUT) unless @unconfirmed_idents.empty?
@@ -78,6 +114,9 @@ module RSpec::Buildkite::Analytics
78
114
 
79
115
  # Then we always disconnect cos we can't wait forever? 🤷‍♀️
80
116
  @connection.close
117
+ # We kill the write thread cos it's got a while loop in it, so it won't finish otherwise
118
+ @write_thread&.kill
119
+ @logger.write("socket connection closed")
81
120
  end
82
121
 
83
122
  def handle(_connection, data)
@@ -86,11 +125,14 @@ module RSpec::Buildkite::Analytics
86
125
  when "ping"
87
126
  # In absence of other message, the server sends us a ping every 3 seconds
88
127
  # We are currently not doing anything with these
128
+ @logger.write("received ping")
89
129
  when "welcome", "confirm_subscription"
90
130
  # Push these two messages onto the queue, so that we block on waiting for the
91
131
  # initializing phase to complete
92
- @queue.push(data)
132
+ @establish_subscription_queue.push(data)
133
+ @logger.write("received #{data['type']}")
93
134
  when "reject_subscription"
135
+ @logger.write("received rejected_subscription")
94
136
  raise RejectedSubscription
95
137
  else
96
138
  process_message(data)
@@ -98,11 +140,9 @@ module RSpec::Buildkite::Analytics
98
140
  end
99
141
 
100
142
  def write_result(result)
101
- result_as_json = result.as_json
102
-
103
- add_unconfirmed_idents(result.id, result_as_json)
143
+ queue_and_track_result(result.id, result.as_hash)
104
144
 
105
- transmit_results([result_as_json])
145
+ @logger.write("added #{result.id} to send queue")
106
146
  end
107
147
 
108
148
  def unconfirmed_idents_count
@@ -113,18 +153,9 @@ module RSpec::Buildkite::Analytics
113
153
 
114
154
  private
115
155
 
116
- def transmit_results(results_as_json)
117
- @connection.transmit({
118
- "identifier" => @channel,
119
- "command" => "message",
120
- "data" => {
121
- "action" => "record_results",
122
- "results" => results_as_json
123
- }.to_json
124
- })
125
- end
126
-
127
156
  def connect
157
+ @logger.write("starting socket connection process")
158
+
128
159
  @connection = SocketConnection.new(self, @url, {
129
160
  "Authorization" => @authorization_header,
130
161
  })
@@ -137,11 +168,54 @@ module RSpec::Buildkite::Analytics
137
168
  })
138
169
 
139
170
  wait_for_confirm
171
+
172
+ @logger.write("connected")
173
+
174
+ # As this connect method can be called multiple times in the
175
+ # reconnection process, kill prev write threads (if any) before
176
+ # setting up the new one
177
+ @write_thread&.kill
178
+
179
+ @write_thread = Thread.new do
180
+ @logger.write("hello from write thread")
181
+ # Pretty sure this eternal loop is fine cos the call to queue.pop is blocking
182
+ loop do
183
+ data = @send_queue.pop
184
+ message_type = data["action"]
185
+
186
+ if message_type == "end_of_transmission"
187
+ # Because of the unpredictable sequencing between the test suite finishing
188
+ # (EOT gets queued) and disconnections happening (retransmit results gets
189
+ # queued), we don't want to send an EOT before any retransmits are sent.
190
+ if @send_queue.length > 0
191
+ @send_queue << data
192
+ @logger.write("putting eot at back of queue")
193
+ next
194
+ end
195
+ @eot_queued_mutex.synchronize do
196
+ @eot_queued = false
197
+ end
198
+ end
199
+
200
+ @connection.transmit({
201
+ "identifier" => @channel,
202
+ "command" => "message",
203
+ "data" => data.to_json
204
+ })
205
+
206
+ if RSpec::Buildkite::Analytics.debug_enabled
207
+ ids = if message_type == "record_results"
208
+ data["results"].map { |result| result["id"] }
209
+ end
210
+ @logger.write("transmitted #{message_type} #{ids}")
211
+ end
212
+ end
213
+ end
140
214
  end
141
215
 
142
216
  def pop_with_timeout(message_type)
143
217
  Timeout.timeout(30, RSpec::Buildkite::Analytics::TimeoutError, "Timeout: Waited 30 seconds for #{message_type}") do
144
- @queue.pop
218
+ @establish_subscription_queue.pop
145
219
  end
146
220
  end
147
221
 
@@ -161,9 +235,16 @@ module RSpec::Buildkite::Analytics
161
235
  end
162
236
  end
163
237
 
164
- def add_unconfirmed_idents(ident, data)
238
+ def queue_and_track_result(ident, result_as_hash)
165
239
  @idents_mutex.synchronize do
166
- @unconfirmed_idents[ident] = data
240
+ @unconfirmed_idents[ident] = result_as_hash
241
+
242
+ data = {
243
+ "action" => "record_results",
244
+ "results" => [result_as_hash]
245
+ }
246
+
247
+ @send_queue << data
167
248
  end
168
249
  end
169
250
 
@@ -174,26 +255,37 @@ module RSpec::Buildkite::Analytics
174
255
  # Remove received idents from unconfirmed_idents
175
256
  idents.each { |key| @unconfirmed_idents.delete(key) }
176
257
 
258
+ @logger.write("received confirm for indentifiers: #{idents.join(", ")}")
259
+
177
260
  # This @empty ConditionVariable broadcasts every time that @unconfirmed_idents is
178
261
  # empty, which will happen about every 10mb of data as that's when the server
179
262
  # sends back confirmations.
180
263
  #
181
264
  # However, there aren't any threads waiting on this signal until after we
182
265
  # send the EOT message, so the prior broadcasts shouldn't do anything.
183
- @empty.broadcast if @unconfirmed_idents.empty?
266
+ if @unconfirmed_idents.empty?
267
+ @empty.broadcast
268
+ @logger.write("broadcast empty")
269
+ else
270
+ @logger.write("still waiting on confirm for #{@unconfirmed_idents.keys}")
271
+ end
184
272
  end
185
273
  end
186
274
 
187
275
  def send_eot
188
- # Expect server to respond with data of indentifiers last upload part
189
-
190
- @connection.transmit({
191
- "identifier" => @channel,
192
- "command" => "message",
193
- "data" => {
194
- "action" => "end_of_transmission"
195
- }.to_json
196
- })
276
+ @eot_queued_mutex.synchronize do
277
+ return if @eot_queued
278
+ # Expect server to respond with data of indentifiers last upload part
279
+ data = {
280
+ "action" => "end_of_transmission",
281
+ "examples_count" => @examples_count.to_json
282
+ }
283
+
284
+ @send_queue << data
285
+ @eot_queued = true
286
+
287
+ @logger.write("added EOT to send queue")
288
+ end
197
289
  end
198
290
 
199
291
  def process_message(data)
@@ -205,16 +297,26 @@ module RSpec::Buildkite::Analytics
205
297
  remove_unconfirmed_idents(data["message"]["confirm"])
206
298
  else
207
299
  # unhandled message
300
+ @logger.write("received unhandled message #{data["message"]}")
208
301
  end
209
302
  end
210
303
 
211
304
  def retransmit
212
- data = @idents_mutex.synchronize do
213
- @unconfirmed_idents.values
305
+ @idents_mutex.synchronize do
306
+ results = @unconfirmed_idents.values
307
+
308
+ # queue the contents of the buffer, unless it's empty
309
+ unless results.empty?
310
+ data = {
311
+ "action" => "record_results",
312
+ "results" => results
313
+ }
314
+
315
+ @send_queue << data
316
+ @logger.write("queueing up retransmitted results #{@unconfirmed_idents.keys}")
317
+ end
214
318
  end
215
319
 
216
- # send the contents of the buffer, unless it's empty
217
- transmit_results(data) unless data.empty?
218
320
  # if we were disconnected in the closing phase, then resend the EOT
219
321
  # message so the server can persist the last upload part
220
322
  send_eot if @closing
@@ -66,6 +66,7 @@ module RSpec::Buildkite::Analytics
66
66
  # Setting up a new thread that listens on the socket, and processes incoming
67
67
  # comms from the server
68
68
  @thread = Thread.new do
69
+ @session.logger.write("listening in on socket")
69
70
  frame = WebSocket::Frame::Incoming::Client.new
70
71
 
71
72
  while @socket
@@ -75,20 +76,25 @@ module RSpec::Buildkite::Analytics
75
76
  @session.handle(self, data.data)
76
77
  end
77
78
  end
78
- rescue EOFError
79
+ rescue EOFError, Errno::ECONNRESET => e
80
+ @session.logger.write("#{e}")
79
81
  if @socket
82
+ @session.logger.write("attempting disconnected flow")
80
83
  @session.disconnected(self)
81
84
  disconnect
82
85
  end
83
86
  rescue IOError
84
87
  # This is fine to ignore
88
+ @session.logger.write("IOError")
85
89
  rescue IndexError
86
90
  # I don't like that we're doing this but I think it's the best of the options
87
91
  #
88
92
  # This relates to this issue https://github.com/ruby/openssl/issues/452
89
93
  # A fix for it has been released but the repercussions of overriding
90
94
  # the OpenSSL version in the stdlib seem worse than catching this error here.
95
+ @session.logger.write("IndexError")
91
96
  if @socket
97
+ @session.logger.write("attempting disconnected flow")
92
98
  @session.disconnected(self)
93
99
  disconnect
94
100
  end
@@ -102,8 +108,10 @@ module RSpec::Buildkite::Analytics
102
108
  raw_data = data.to_json
103
109
  frame = WebSocket::Frame::Outgoing::Client.new(data: raw_data, type: :text, version: @version)
104
110
  @socket.write(frame.to_s)
105
- rescue Errno::EPIPE, OpenSSL::SSL::SSLError => e
111
+ rescue Errno::EPIPE, Errno::ECONNRESET, OpenSSL::SSL::SSLError => e
106
112
  return unless @socket
113
+ return if type == :close
114
+ @session.logger.write("got #{e}, attempting disconnected flow")
107
115
  @session.disconnected(self)
108
116
  disconnect
109
117
  rescue IndexError
@@ -112,13 +120,16 @@ module RSpec::Buildkite::Analytics
112
120
  # This relates to this issue https://github.com/ruby/openssl/issues/452
113
121
  # A fix for it has been released but the repercussions of overriding
114
122
  # the OpenSSL version in the stdlib seem worse than catching this error here.
123
+ @session.logger.write("IndexError")
115
124
  if @socket
125
+ @session.logger.write("attempting disconnected flow")
116
126
  @session.disconnected(self)
117
127
  disconnect
118
128
  end
119
129
  end
120
130
 
121
131
  def close
132
+ @session.logger.write("socket close")
122
133
  transmit(nil, type: :close)
123
134
  disconnect
124
135
  end
@@ -126,6 +137,7 @@ module RSpec::Buildkite::Analytics
126
137
  private
127
138
 
128
139
  def disconnect
140
+ @session.logger.write("socket disconnect")
129
141
  socket = @socket
130
142
  @socket = nil
131
143
  socket&.close
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/hash/indifferent_access"
4
+
3
5
  module RSpec::Buildkite::Analytics
4
6
  class Tracer
5
7
  class Span
@@ -13,15 +15,15 @@ module RSpec::Buildkite::Analytics
13
15
  @children = []
14
16
  end
15
17
 
16
- def as_json
18
+ def as_hash
17
19
  {
18
20
  section: section,
19
21
  start_at: start_at,
20
22
  end_at: end_at,
21
23
  duration: end_at - start_at,
22
24
  detail: detail,
23
- children: children.map(&:as_json),
24
- }
25
+ children: children.map(&:as_hash),
26
+ }.with_indifferent_access
25
27
  end
26
28
  end
27
29
 
@@ -57,7 +59,7 @@ module RSpec::Buildkite::Analytics
57
59
  end
58
60
 
59
61
  def history
60
- @top.as_json
62
+ @top.as_hash
61
63
  end
62
64
  end
63
65
  end
@@ -22,22 +22,15 @@ require "securerandom"
22
22
  module RSpec::Buildkite::Analytics
23
23
  class Uploader
24
24
  class Trace
25
- attr_accessor :example
25
+ attr_accessor :example, :failure_reason, :failure_expanded
26
26
  attr_reader :id, :history
27
27
 
28
28
  def initialize(example, history)
29
29
  @id = SecureRandom.uuid
30
30
  @example = example
31
31
  @history = history
32
- end
33
-
34
- def failure_message
35
- case example.exception
36
- when RSpec::Expectations::ExpectationNotMetError
37
- example.exception.message
38
- when Exception
39
- "#{example.exception.class}: #{example.exception.message}"
40
- end
32
+ @failure_reason = nil
33
+ @failure_expanded = []
41
34
  end
42
35
 
43
36
  def result_state
@@ -48,7 +41,7 @@ module RSpec::Buildkite::Analytics
48
41
  end
49
42
  end
50
43
 
51
- def as_json
44
+ def as_hash
52
45
  {
53
46
  id: @id,
54
47
  scope: example.example_group.metadata[:full_description],
@@ -57,15 +50,16 @@ module RSpec::Buildkite::Analytics
57
50
  location: example.location,
58
51
  file_name: generate_file_name(example),
59
52
  result: result_state,
60
- failure: failure_message,
53
+ failure_reason: failure_reason,
54
+ failure_expanded: failure_expanded,
61
55
  history: history,
62
- }
56
+ }.with_indifferent_access.compact
63
57
  end
64
58
 
65
59
  private
66
60
 
67
61
  def generate_file_name(example)
68
- file_path_regex = /^(.*?\.rb)/
62
+ file_path_regex = /^(.*?\.(rb|feature))/
69
63
  identifier_file_name = example.id[file_path_regex]
70
64
  location_file_name = example.location[file_path_regex]
71
65
 
@@ -121,7 +115,8 @@ module RSpec::Buildkite::Analytics
121
115
  "Content-Type" => "application/json",
122
116
  })
123
117
  contact.body = {
124
- run_env: CI.env
118
+ run_env: CI.env,
119
+ format: "websocket"
125
120
  }.to_json
126
121
 
127
122
  response = begin
@@ -141,10 +136,12 @@ module RSpec::Buildkite::Analytics
141
136
  end
142
137
  else
143
138
  request_id = response.to_hash["x-request-id"]
144
- puts "Buildkite Test Analytics: Unknown error. If this error persists, please contact support+analytics@buildkite.com with this request ID `#{request_id}`."
139
+ puts "rspec-buildkite-analytics could not establish an initial connection with Buildkite. You may be missing some data for this test suite, please contact support."
145
140
  end
146
141
  else
147
- puts "Buildkite Test Analytics: No Suite API key provided. You can get the API key from your Suite settings page."
142
+ if !!ENV["BUILDKITE_BUILD_ID"]
143
+ puts "Buildkite Test Analytics: No Suite API key provided. You can get the API key from your Suite settings page."
144
+ end
148
145
  end
149
146
  end
150
147
 
@@ -162,12 +159,6 @@ module RSpec::Buildkite::Analytics
162
159
  trace = RSpec::Buildkite::Analytics::Uploader::Trace.new(example, tracer.history)
163
160
  RSpec::Buildkite::Analytics.uploader.traces << trace
164
161
  end
165
-
166
- config.after(:suite) do
167
- # This needs the lonely operater as the session will be nil
168
- # if auth against the API token fails
169
- RSpec::Buildkite::Analytics.session&.close
170
- end
171
162
  end
172
163
 
173
164
  RSpec::Buildkite::Analytics::Network.configure
@@ -3,7 +3,7 @@
3
3
  module RSpec
4
4
  module Buildkite
5
5
  module Analytics
6
- VERSION = "0.3.5"
6
+ VERSION = "0.6.0"
7
7
  end
8
8
  end
9
9
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "timeout"
4
+ require "tmpdir"
4
5
 
5
6
  require_relative "analytics/version"
6
7
 
@@ -15,11 +16,15 @@ module RSpec::Buildkite::Analytics
15
16
  attr_accessor :url
16
17
  attr_accessor :uploader
17
18
  attr_accessor :session
19
+ attr_accessor :debug_enabled
20
+ attr_accessor :debug_filepath
18
21
  end
19
22
 
20
- def self.configure(token: nil, url: nil)
23
+ def self.configure(token: nil, url: nil, debug_enabled: false, debug_filepath: nil)
21
24
  self.api_token = token || ENV["BUILDKITE_ANALYTICS_TOKEN"]
22
25
  self.url = url || DEFAULT_URL
26
+ self.debug_enabled = debug_enabled || !!(ENV["BUILDKITE_ANALYTICS_DEBUG_ENABLED"])
27
+ self.debug_filepath = debug_filepath || ENV["BUILDKITE_ANALYTICS_DEBUG_FILEPATH"] || Dir.tmpdir
23
28
 
24
29
  require_relative "analytics/uploader"
25
30
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rspec-buildkite-analytics
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Buildkite
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-10-21 00:00:00.000000000 Z
11
+ date: 2021-12-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport