bugsnag-maze-runner 10.7.3 → 10.8.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: 460723532ac7cdbb236e248fcc171b87641be8c480c190c9ae9d36acf1fe8f0b
4
- data.tar.gz: a5796d5ee2b0e5882c4c8d552a905e08b724e325561d7cda01dbe6c4815c3011
3
+ metadata.gz: 0de79ce31a9a1b66dfca934d16fb84e0c80ac5a4c5e879e993832f53f9d92c06
4
+ data.tar.gz: f92ab806fac107be92713d90b346b5dda5d326d7b1cbbbfc8c8a247acfceeb5d
5
5
  SHA512:
6
- metadata.gz: 776888ccd312f6754f91b2816abc8cc2bbeeb0e21fdee5de6eede8fb67d0dacf0d13de47896d5914b0950445aff259e638ffb9576dac9fed79ce3a5c8beeab49
7
- data.tar.gz: 0337eabade7c9431d0b917f23b416aef835852ad57235d7e910366a239992554cc8d3a83ca11f2cf0cd2ffe15f8972c3d97be518c55262351adbdf5ba0f9fbe1
6
+ metadata.gz: a6473e52c80d43cea5b5ee4f9dd49e5caf6db7f849bc926e032fc080c74ec69180553ab2d3973f6dc90028f6cb4ed8c3f0c7f98a6484e9d2e9110a00f4a87b51
7
+ data.tar.gz: 5ce8ec5e75ef9b22460bae6f767d5a9ecc1394c2b53fe70b4273bd8ce7a77b591a2a71b32ba80d101bebfb30356c73d43a0bc20c58d91c3b40997384f8743dd1
data/bin/maze-runner CHANGED
@@ -18,6 +18,11 @@ require_relative '../lib/maze/api/appium/file_manager'
18
18
  require_relative '../lib/maze/api/appium/ui_manager'
19
19
  require_relative '../lib/maze/api/cucumber/scenario'
20
20
  require_relative '../lib/maze/api/exit_code'
21
+ require_relative '../lib/maze/api/model/span'
22
+ require_relative '../lib/maze/api/model/span_kind'
23
+ require_relative '../lib/maze/api/model/span_set'
24
+ require_relative '../lib/maze/api/model/otel_attribute'
25
+ require_relative '../lib/maze/api/model/otel_attribute_type'
21
26
  require_relative '../lib/maze/error_monitor/selenium_error_middleware'
22
27
  require_relative '../lib/maze/error_monitor/assert_error_middleware'
23
28
  require_relative '../lib/maze/error_monitor/config'
@@ -172,7 +172,7 @@ After do |scenario|
172
172
  $success = !scenario.failed?
173
173
 
174
174
  # Log all received requests to file
175
- Maze::MazeOutput.new(scenario).write_requests if Maze.config.file_log
175
+ Maze::MazeOutput.new(scenario).write_requests_and_spans if Maze.config.file_log
176
176
 
177
177
  # Invoke the internal hook for the mode of operation
178
178
  Maze.internal_hooks.after scenario
@@ -33,25 +33,33 @@ class SpanSupport
33
33
  assert_received_spans(minimum, maximum)
34
34
  end
35
35
 
36
+ def received_spans_names
37
+ spans = spans_from_request_list(Maze::Server.traces)
38
+ names spans.map { |span| span['name'] }
39
+
40
+ end
41
+
36
42
  def assert_received_named_span(span_name)
37
43
  timeout = Maze.config.receive_requests_wait
38
44
  wait = Maze::Wait.new(timeout: timeout)
39
-
40
45
  received = wait.until { SpanSupport.named_span_exists?(span_name) }
41
46
 
47
+ spans = Maze::Api::Model::SpanSet.new
42
48
  list = Maze::Server.traces
43
- received_count = SpanSupport.spans_from_request_list(list).size
49
+ list.remaining.each { |t| spans.add_from_trace_hash(t[:body]) }
50
+ names = spans.size == 0 ? '.' : ", with names:\n#{spans.names.sort.join("\n")}"
44
51
 
45
52
  unless received
46
53
  raise Test::Unit::AssertionFailedError.new <<-MESSAGE
47
- Expected span with name #{span_name} not received within the #{timeout}s timeout (#{received_count} spans were received)}.
54
+ Expected span with name #{span_name} not received within the #{timeout}s timeout. #{spans.size} spans were received#{names}
55
+
48
56
  This could indicate that:
49
57
  - Bugsnag crashed with a fatal error.
50
58
  - Bugsnag did not make the requests that it should have done.
51
59
  - The requests were made, but not deemed to be valid (e.g. missing integrity header).
52
60
  - The requests made were prevented from being received due to a network or other infrastructure issue.
53
61
  Please check the Maze Runner and device logs to confirm.)
54
- MESSAGE
62
+ MESSAGE
55
63
  end
56
64
 
57
65
  Maze::Schemas::Validator.validate_payload_elements(list, 'trace')
@@ -73,7 +81,7 @@ This could indicate that:
73
81
  - The requests were made, but not deemed to be valid (e.g. missing integrity header).
74
82
  - The requests made were prevented from being received due to a network or other infrastructure issue.
75
83
  Please check the Maze Runner and device logs to confirm.)
76
- MESSAGE
84
+ MESSAGE
77
85
  end
78
86
 
79
87
  Maze.check.operator(max_received, :>=, received_count, "#{received_count} spans received") if max_received
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+ require_relative 'otel_attribute_type'
3
+
4
+ module Maze
5
+ module Api
6
+ module Model
7
+ # Element of an OTEL attribute array.
8
+ class OtelAttributeArrayElement
9
+ attr_accessor :type, :value
10
+
11
+ def initialize(type, value)
12
+ @type = type
13
+ @value = value
14
+ end
15
+ end
16
+
17
+ # OTEL attributes used in both spans and resources
18
+ class OtelAttribute
19
+ attr_accessor :type, :key, :value
20
+
21
+ def initialize(type, key, value)
22
+ @key = key
23
+ @type = type
24
+ @value = value
25
+ end
26
+
27
+ class << self
28
+ # Create an array of OtelAttributeArrayElement from a hash array.
29
+ # @param hash_array [Array<Hash>] Array of hashes representing attribute values.
30
+ # @return [Array<OtelAttributeArrayElement>] Array of OtelAttributeArrayElement objects.
31
+ def array_from_hash(hash_array)
32
+ array = []
33
+ hash_array.each do |value_hash|
34
+ type = OtelAttributeType::for_string(value_hash.keys.first)
35
+ value = value_hash.values.first
36
+ array << OtelAttributeArrayElement.new(type, value)
37
+ end
38
+ array
39
+ end
40
+
41
+ # Create an OtelAttribute from a hash.
42
+ # @param hash [Hash] Hash representing an OTEL attribute.
43
+ # @return [OtelAttribute] OtelAttribute object.
44
+ def from_hash(hash)
45
+ type = OtelAttributeType.for_string(hash['value'].keys.first)
46
+ hash_value = hash['value']
47
+ value = if hash_value.has_key?('arrayValue')
48
+ array_from_hash(hash_value['arrayValue']['values'])
49
+ elsif hash_value.has_key?('boolValue')
50
+ hash_value['boolValue']
51
+ elsif hash_value.has_key?('doubleValue')
52
+ hash_value['doubleValue']
53
+ elsif hash_value.has_key?('intValue')
54
+ hash_value['intValue']
55
+ elsif hash_value.has_key?('stringValue')
56
+ hash_value['stringValue']
57
+ end
58
+
59
+ new(
60
+ type,
61
+ hash['key'],
62
+ value
63
+ )
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Maze
4
+ module Api
5
+ module Model
6
+ # OTEL attribute types.
7
+ module OtelAttributeType
8
+ ARRAY = 0
9
+ BOOL = 1
10
+ DOUBLE = 2
11
+ INT = 3
12
+ STRING = 4
13
+
14
+ # Get the OTEL attribute type constant for a given string.
15
+ # @param type_string [String] OTEL attribute type as a string.
16
+ # @return [Integer, nil] OTEL attribute type constant or nil if not found.
17
+ def self.for_string(type_string)
18
+ case type_string
19
+ when 'arrayValue'
20
+ ARRAY
21
+ when 'boolValue'
22
+ BOOL
23
+ when 'doubleValue'
24
+ DOUBLE
25
+ when 'intValue'
26
+ INT
27
+ when 'stringValue'
28
+ STRING
29
+ else
30
+ nil
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'otel_attribute'
4
+
5
+ module Maze
6
+ module Api
7
+ module Model
8
+ # A single OTEL span.
9
+ class Span
10
+ attr_accessor :id, :kind, :name, :trace_id, :start_time, :end_time, :attributes
11
+
12
+ def initialize
13
+ @attributes = {}
14
+
15
+ def add_attribute(attribute)
16
+ @attributes[attribute.key] = attribute
17
+ end
18
+ end
19
+
20
+ class << self
21
+ # Create a Span from a hash.
22
+ # @param hash [Hash] Hash representing an OTEL span.
23
+ # @return [Span] Span object.
24
+ def from_hash(hash)
25
+ span = new
26
+ span.id = hash['spanId']
27
+ span.kind = hash['kind']
28
+ span.name = hash['name']
29
+ span.trace_id = hash['traceId']
30
+ span.start_time = hash['startTimeUnixNano']
31
+ span.end_time = hash['endTimeUnixNano']
32
+
33
+ hash['attributes'].each do |attribute_hash|
34
+ attribute = OtelAttribute.from_hash(attribute_hash)
35
+ span.add_attribute(attribute)
36
+ end
37
+
38
+ span
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module Maze
3
+ module Api
4
+ module Model
5
+ # OTEL span kinds.
6
+ module SpanKind
7
+ UNSPECIFIED = 0
8
+ INTERNAL = 1
9
+ SERVER = 2
10
+ CLIENT = 3
11
+ PRODUCER = 4
12
+ CONSUMER = 5
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'span'
4
+
5
+ module Maze
6
+ module Api
7
+ module Model
8
+ # A collection of spans, typically representing one or more traces.
9
+ class SpanSet
10
+ def initialize
11
+ @spans = {}
12
+ end
13
+
14
+ # @param span [Maze::Api::Model::Span] Span to add to the SpanSet.
15
+ def add(span)
16
+ @spans[span.id] = span
17
+ end
18
+
19
+ # Add spans from a trace hash to the SpanSet.
20
+ # @param trace_hash [Hash] Trace request payload as a hash.
21
+ def add_from_trace_hash(trace_hash)
22
+ SpanSet.add_trace_hash(trace_hash, self)
23
+ end
24
+
25
+ # @param span_id [String] Id of the span to remove from the SpanSet.
26
+ def remove(span_id)
27
+ @spans.delete(span_id)
28
+ end
29
+
30
+ # @return [Integer] Number of spans in the SpanSet.
31
+ def size
32
+ @spans.size
33
+ end
34
+
35
+ # @return [Array<String>] List of span names in the SpanSet.
36
+ def names
37
+ @spans.values.map(&:name)
38
+ end
39
+
40
+ class << self
41
+ # Creates a new SpanSet from a trace hash.
42
+ # @param trace_hash [Hash] Trace request payload as a hash.
43
+ def from_trace_hash(trace_hash)
44
+ span_set = Maze::Api::Model::SpanSet.new
45
+ add_trace_hash(trace_hash, span_set)
46
+ span_set
47
+ end
48
+
49
+ # Adds spans from a trace hash to an existing SpanSet.
50
+ # @param trace_hash [Hash] Trace request payload as a hash.
51
+ # @param span_set [Maze::Api::Model::SpanSet] SpanSet to add spans to.
52
+ def add_trace_hash(trace_hash, span_set)
53
+ spans = trace_hash['resourceSpans'].flat_map { |r| r['scopeSpans'] }
54
+ .flat_map { |s| s['spans'] }
55
+ .select { |s| !s.nil? }
56
+
57
+ spans.each do |span_hash|
58
+ span = Maze::Api::Model::Span.from_hash(span_hash)
59
+ span_set.add(span)
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
@@ -19,7 +19,10 @@ module Maze
19
19
 
20
20
  # @!attribute [r] current_buffer
21
21
  # @return [String] A string representation of the current output present in the terminal
22
- attr_reader :current_buffer
22
+ # attr_reader :current_buffer
23
+ def current_buffer
24
+ strip_nonprintable(@current_buffer)
25
+ end
23
26
 
24
27
  # Creates an InteractiveCLI instance
25
28
  #
@@ -169,5 +172,10 @@ module Maze
169
172
  # @boring.scrub(line.strip)
170
173
  line.strip
171
174
  end
175
+
176
+ def strip_nonprintable(str)
177
+ # keep: space..tilde, Tab, CR, LF
178
+ str.gsub(/[^\x20-\x7E\t\r\n]/, '')
179
+ end
172
180
  end
173
181
  end
@@ -7,6 +7,11 @@ module Maze
7
7
  @scenario = scenario
8
8
  end
9
9
 
10
+ def write_requests_and_spans
11
+ write_requests
12
+ write_spans
13
+ end
14
+
10
15
  # Writes each list of requests to a separate file under, e.g:
11
16
  # maze_output/failed/scenario_name/errors.log
12
17
  def write_requests
@@ -91,6 +96,49 @@ module Maze
91
96
  end
92
97
  end
93
98
 
99
+ # Writes each list of requests to a separate file under, e.g:
100
+ # maze_output/failed/scenario_name/errors.log
101
+ def write_spans
102
+ list = Maze::Server.traces.all
103
+ return if list.empty?
104
+
105
+ path = output_folder
106
+ filepath = File.join(path, 'spans.log')
107
+
108
+ File.open(filepath, 'w+') do |file|
109
+ spans = spans_from_request_list(list)
110
+
111
+ # File summary
112
+ file.puts "=== Spans Summary ==="
113
+ file.puts
114
+ file.puts "Id Name"
115
+ spans.each do |span|
116
+ file.puts "#{span['spanId']} #{span['name']}"
117
+ end
118
+ file.puts
119
+ file.puts
120
+
121
+ # Write the spans
122
+ counter = 1
123
+ spans.each do |span|
124
+ file.puts "=== Span #{counter} of #{spans.size} ==="
125
+ file.puts
126
+ file.puts JSON.pretty_generate(span)
127
+ file.puts
128
+ file.puts
129
+
130
+ counter += 1
131
+ end
132
+ end
133
+ end
134
+
135
+ def spans_from_request_list(list)
136
+ list.flat_map { |req| req[:body]['resourceSpans'] }
137
+ .flat_map { |r| r['scopeSpans'] }
138
+ .flat_map { |s| s['spans'] }
139
+ .select { |s| !s.nil? }
140
+ end
141
+
94
142
  # Determines the output folder for the scenario
95
143
  def output_folder
96
144
  folder1 = File.join(Dir.pwd, 'maze_output')
@@ -18,25 +18,7 @@ module Maze
18
18
  @success = true
19
19
  verify_against_schema
20
20
  validate_headers
21
- regex_comparison('resourceSpans.0.scopeSpans.0.spans.0.spanId', HEX_STRING_16)
22
- regex_comparison('resourceSpans.0.scopeSpans.0.spans.0.traceId', HEX_STRING_32)
23
- element_int_in_range('resourceSpans.0.scopeSpans.0.spans.0.kind', 0..5)
24
- regex_comparison('resourceSpans.0.scopeSpans.0.spans.0.startTimeUnixNano', '^[0-9]+$')
25
- regex_comparison('resourceSpans.0.scopeSpans.0.spans.0.endTimeUnixNano', '^[0-9]+$')
26
- each_span_element_contains('resourceSpans.0.scopeSpans.0.spans', 'attributes', 'bugsnag.sampling.p')
27
- span_element_contains('resourceSpans.0.resource.attributes', 'deployment.environment')
28
- span_element_contains('resourceSpans.0.resource.attributes', 'telemetry.sdk.name')
29
- span_element_contains('resourceSpans.0.resource.attributes', 'telemetry.sdk.version')
30
- validate_timestamp('resourceSpans.0.scopeSpans.0.spans.0.startTimeUnixNano', HOUR_TOLERANCE)
31
- validate_timestamp('resourceSpans.0.scopeSpans.0.spans.0.endTimeUnixNano', HOUR_TOLERANCE)
32
- element_a_greater_or_equal_element_b(
33
- 'resourceSpans.0.scopeSpans.0.spans.0.endTimeUnixNano',
34
- 'resourceSpans.0.scopeSpans.0.spans.0.startTimeUnixNano'
35
- )
36
-
37
- if Maze.config.client_mode_validation
38
- span_element_contains('resourceSpans.0.resource.attributes', 'device.id')
39
- end
21
+ validate_fields
40
22
  end
41
23
 
42
24
  def verify_against_schema
@@ -84,6 +66,46 @@ module Maze
84
66
  end
85
67
  end
86
68
 
69
+ def validate_fields
70
+ resource_spans = Maze::Helper.read_key_path(@body, 'resourceSpans')
71
+
72
+ # Loop through all resource spans
73
+ (0...resource_spans.size).each do |rs|
74
+
75
+ if Maze.config.client_mode_validation
76
+ span_element_contains("resourceSpans.#{rs}.resource.attributes", 'device.id')
77
+ end
78
+ span_element_contains("resourceSpans.#{rs}.resource.attributes", 'deployment.environment')
79
+ span_element_contains("resourceSpans.#{rs}.resource.attributes", 'telemetry.sdk.name')
80
+ span_element_contains("resourceSpans.#{rs}.resource.attributes", 'telemetry.sdk.version')
81
+
82
+ # Loop through all scope spans
83
+ scope_spans = Maze::Helper.read_key_path(@body, "resourceSpans.#{rs}.scopeSpans")
84
+ (0...scope_spans.size).each do |ss|
85
+
86
+ each_span_element_contains("resourceSpans.#{rs}.scopeSpans.#{ss}.spans", 'attributes', 'bugsnag.sampling.p')
87
+
88
+ # Loop through all spans
89
+ spans = Maze::Helper.read_key_path(@body, "resourceSpans.#{rs}.scopeSpans.#{ss}.spans")
90
+ (0...spans.size).each do |s|
91
+
92
+ # Field validations
93
+ regex_comparison("resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.spanId", HEX_STRING_16)
94
+ regex_comparison("resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.traceId", HEX_STRING_32)
95
+ element_int_in_range("resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.kind", 0..5)
96
+ regex_comparison("resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.startTimeUnixNano", '^[0-9]+$')
97
+ regex_comparison("resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.endTimeUnixNano", '^[0-9]+$')
98
+ validate_timestamp("resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.startTimeUnixNano", HOUR_TOLERANCE)
99
+ validate_timestamp("resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.endTimeUnixNano", HOUR_TOLERANCE)
100
+ element_a_greater_or_equal_element_b(
101
+ "resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.endTimeUnixNano",
102
+ "resourceSpans.#{rs}.scopeSpans.#{ss}.spans.#{s}.startTimeUnixNano"
103
+ )
104
+ end
105
+ end
106
+ end
107
+ end
108
+
87
109
  def span_element_contains(path, key_value, value_type=nil, possible_values=nil)
88
110
  container = Maze::Helper.read_key_path(@body, path)
89
111
  if container.nil? || !container.kind_of?(Array)
@@ -129,7 +129,7 @@ module Maze
129
129
  time_in_nanos = Time.now.to_i * 1000000000
130
130
  unless (time_in_nanos - parsed_timestamp).abs < tolerance
131
131
  @success = false
132
- @errors << "Timestamp was expected to be within #{tolerance} nanoseconds of the current time (#{time_in_nanos}), was '#{parsed_timestamp}'"
132
+ @errors << "Timestamp was expected to be within #{tolerance} nanoseconds of the current time (#{time_in_nanos}), was '#{timestamp}'"
133
133
  end
134
134
  end
135
135
 
data/lib/maze.rb CHANGED
@@ -8,7 +8,7 @@ require_relative 'maze/timers'
8
8
  # providing an alternative to the proliferation of global variables or singletons.
9
9
  module Maze
10
10
 
11
- VERSION = '10.7.3'
11
+ VERSION = '10.8.0'
12
12
 
13
13
  class << self
14
14
  attr_accessor :check, :driver, :internal_hooks, :mode, :start_time, :dynamic_retry, :public_address,
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bugsnag-maze-runner
3
3
  version: !ruby/object:Gem::Version
4
- version: 10.7.3
4
+ version: 10.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Steve Kirkland
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2025-11-17 00:00:00.000000000 Z
12
+ date: 2025-12-03 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: cucumber
@@ -221,6 +221,20 @@ dependencies:
221
221
  - - "~>"
222
222
  - !ruby/object:Gem::Version
223
223
  version: 0.2.24
224
+ - !ruby/object:Gem::Dependency
225
+ name: openssl
226
+ requirement: !ruby/object:Gem::Requirement
227
+ requirements:
228
+ - - "~>"
229
+ - !ruby/object:Gem::Version
230
+ version: 3.3.0
231
+ type: :runtime
232
+ prerelease: false
233
+ version_requirements: !ruby/object:Gem::Requirement
234
+ requirements:
235
+ - - "~>"
236
+ - !ruby/object:Gem::Version
237
+ version: 3.3.0
224
238
  - !ruby/object:Gem::Dependency
225
239
  name: rubyzip
226
240
  requirement: !ruby/object:Gem::Requirement
@@ -438,6 +452,11 @@ files:
438
452
  - lib/maze/api/appium/ui_manager.rb
439
453
  - lib/maze/api/cucumber/scenario.rb
440
454
  - lib/maze/api/exit_code.rb
455
+ - lib/maze/api/model/otel_attribute.rb
456
+ - lib/maze/api/model/otel_attribute_type.rb
457
+ - lib/maze/api/model/span.rb
458
+ - lib/maze/api/model/span_kind.rb
459
+ - lib/maze/api/model/span_set.rb
441
460
  - lib/maze/appium_server.rb
442
461
  - lib/maze/assertions/request_set_assertions.rb
443
462
  - lib/maze/aws/sam.rb