sentry-ruby 5.4.0 → 5.4.1

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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +27 -0
  8. data/Makefile +4 -0
  9. data/Rakefile +13 -0
  10. data/bin/console +18 -0
  11. data/bin/setup +8 -0
  12. data/lib/sentry/background_worker.rb +72 -0
  13. data/lib/sentry/backtrace.rb +124 -0
  14. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  15. data/lib/sentry/breadcrumb.rb +70 -0
  16. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  17. data/lib/sentry/client.rb +190 -0
  18. data/lib/sentry/configuration.rb +502 -0
  19. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  20. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  21. data/lib/sentry/dsn.rb +53 -0
  22. data/lib/sentry/envelope.rb +96 -0
  23. data/lib/sentry/error_event.rb +38 -0
  24. data/lib/sentry/event.rb +178 -0
  25. data/lib/sentry/exceptions.rb +9 -0
  26. data/lib/sentry/hub.rb +220 -0
  27. data/lib/sentry/integrable.rb +26 -0
  28. data/lib/sentry/interface.rb +16 -0
  29. data/lib/sentry/interfaces/exception.rb +43 -0
  30. data/lib/sentry/interfaces/request.rb +144 -0
  31. data/lib/sentry/interfaces/single_exception.rb +57 -0
  32. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  33. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  34. data/lib/sentry/interfaces/threads.rb +42 -0
  35. data/lib/sentry/linecache.rb +47 -0
  36. data/lib/sentry/logger.rb +20 -0
  37. data/lib/sentry/net/http.rb +115 -0
  38. data/lib/sentry/rack/capture_exceptions.rb +80 -0
  39. data/lib/sentry/rack.rb +5 -0
  40. data/lib/sentry/rake.rb +41 -0
  41. data/lib/sentry/redis.rb +90 -0
  42. data/lib/sentry/release_detector.rb +39 -0
  43. data/lib/sentry/scope.rb +295 -0
  44. data/lib/sentry/session.rb +35 -0
  45. data/lib/sentry/session_flusher.rb +90 -0
  46. data/lib/sentry/span.rb +226 -0
  47. data/lib/sentry/test_helper.rb +76 -0
  48. data/lib/sentry/transaction.rb +206 -0
  49. data/lib/sentry/transaction_event.rb +29 -0
  50. data/lib/sentry/transport/configuration.rb +25 -0
  51. data/lib/sentry/transport/dummy_transport.rb +21 -0
  52. data/lib/sentry/transport/http_transport.rb +175 -0
  53. data/lib/sentry/transport.rb +210 -0
  54. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  55. data/lib/sentry/utils/custom_inspection.rb +14 -0
  56. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  57. data/lib/sentry/utils/logging_helper.rb +26 -0
  58. data/lib/sentry/utils/real_ip.rb +84 -0
  59. data/lib/sentry/utils/request_id.rb +18 -0
  60. data/lib/sentry/version.rb +5 -0
  61. data/lib/sentry-ruby.rb +505 -0
  62. data/sentry-ruby-core.gemspec +23 -0
  63. data/sentry-ruby.gemspec +24 -0
  64. metadata +63 -1
data/lib/sentry/dsn.rb ADDED
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
5
+ module Sentry
6
+ class DSN
7
+ PORT_MAP = { 'http' => 80, 'https' => 443 }.freeze
8
+ REQUIRED_ATTRIBUTES = %w(host path public_key project_id).freeze
9
+
10
+ attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
11
+
12
+ def initialize(dsn_string)
13
+ @raw_value = dsn_string
14
+
15
+ uri = URI.parse(dsn_string)
16
+ uri_path = uri.path.split('/')
17
+
18
+ if uri.user
19
+ # DSN-style string
20
+ @project_id = uri_path.pop
21
+ @public_key = uri.user
22
+ @secret_key = !(uri.password.nil? || uri.password.empty?) ? uri.password : nil
23
+ end
24
+
25
+ @scheme = uri.scheme
26
+ @host = uri.host
27
+ @port = uri.port if uri.port
28
+ @path = uri_path.join('/')
29
+ end
30
+
31
+ def valid?
32
+ REQUIRED_ATTRIBUTES.all? { |k| public_send(k) }
33
+ end
34
+
35
+ def to_s
36
+ @raw_value
37
+ end
38
+
39
+ def server
40
+ server = "#{scheme}://#{host}"
41
+ server += ":#{port}" unless port == PORT_MAP[scheme]
42
+ server
43
+ end
44
+
45
+ def csp_report_uri
46
+ "#{server}/api/#{project_id}/security/?sentry_key=#{public_key}"
47
+ end
48
+
49
+ def envelope_endpoint
50
+ "#{path}/api/#{project_id}/envelope/"
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class Envelope
6
+ class Item
7
+ STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500
8
+ MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 200
9
+
10
+ attr_accessor :headers, :payload
11
+
12
+ def initialize(headers, payload)
13
+ @headers = headers
14
+ @payload = payload
15
+ end
16
+
17
+ def type
18
+ @headers[:type] || 'event'
19
+ end
20
+
21
+ def to_s
22
+ <<~ITEM
23
+ #{JSON.generate(@headers)}
24
+ #{JSON.generate(@payload)}
25
+ ITEM
26
+ end
27
+
28
+ def serialize
29
+ result = to_s
30
+
31
+ if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
32
+ remove_breadcrumbs!
33
+ result = to_s
34
+ end
35
+
36
+ if result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE
37
+ reduce_stacktrace!
38
+ result = to_s
39
+ end
40
+
41
+ [result, result.bytesize > MAX_SERIALIZED_PAYLOAD_SIZE]
42
+ end
43
+
44
+ def size_breakdown
45
+ payload.map do |key, value|
46
+ "#{key}: #{JSON.generate(value).bytesize}"
47
+ end.join(", ")
48
+ end
49
+
50
+ private
51
+
52
+ def remove_breadcrumbs!
53
+ if payload.key?(:breadcrumbs)
54
+ payload.delete(:breadcrumbs)
55
+ elsif payload.key?("breadcrumbs")
56
+ payload.delete("breadcrumbs")
57
+ end
58
+ end
59
+
60
+ def reduce_stacktrace!
61
+ if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values")
62
+ exceptions.each do |exception|
63
+ # in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much
64
+ traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames")
65
+
66
+ if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD
67
+ size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2
68
+ traces.replace(
69
+ traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1],
70
+ )
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+
77
+ attr_accessor :headers, :items
78
+
79
+ def initialize(headers = {})
80
+ @headers = headers
81
+ @items = []
82
+ end
83
+
84
+ def add_item(headers, payload)
85
+ @items << Item.new(headers, payload)
86
+ end
87
+
88
+ def item_types
89
+ @items.map(&:type)
90
+ end
91
+
92
+ def event_id
93
+ @headers[:event_id]
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # ErrorEvent represents error or normal message events.
5
+ class ErrorEvent < Event
6
+ # @return [ExceptionInterface]
7
+ attr_reader :exception
8
+
9
+ # @return [ThreadsInterface]
10
+ attr_reader :threads
11
+
12
+ # @return [Hash]
13
+ def to_hash
14
+ data = super
15
+ data[:threads] = threads.to_hash if threads
16
+ data[:exception] = exception.to_hash if exception
17
+ data
18
+ end
19
+
20
+ # @!visibility private
21
+ def add_threads_interface(backtrace: nil, **options)
22
+ @threads = ThreadsInterface.build(
23
+ backtrace: backtrace,
24
+ stacktrace_builder: @stacktrace_builder,
25
+ **options
26
+ )
27
+ end
28
+
29
+ # @!visibility private
30
+ def add_exception_interface(exception)
31
+ if exception.respond_to?(:sentry_context)
32
+ @extra.merge!(exception.sentry_context)
33
+ end
34
+
35
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'socket'
4
+ require 'securerandom'
5
+ require 'sentry/interface'
6
+ require 'sentry/backtrace'
7
+ require 'sentry/utils/real_ip'
8
+ require 'sentry/utils/request_id'
9
+ require 'sentry/utils/custom_inspection'
10
+
11
+ module Sentry
12
+ # This is an abstract class that defines the shared attributes of an event.
13
+ # Please don't use it directly. The user-facing classes are its child classes.
14
+ class Event
15
+ TYPE = "event"
16
+ # These are readable attributes.
17
+ SERIALIZEABLE_ATTRIBUTES = %i(
18
+ event_id level timestamp
19
+ release environment server_name modules
20
+ message user tags contexts extra
21
+ fingerprint breadcrumbs transaction
22
+ platform sdk type
23
+ )
24
+
25
+ # These are writable attributes.
26
+ WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
27
+
28
+ MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
29
+
30
+ SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist]
31
+
32
+ include CustomInspection
33
+
34
+ attr_writer(*WRITER_ATTRIBUTES)
35
+ attr_reader(*SERIALIZEABLE_ATTRIBUTES)
36
+
37
+ # @return [RequestInterface]
38
+ attr_reader :request
39
+
40
+ # @param configuration [Configuration]
41
+ # @param integration_meta [Hash, nil]
42
+ # @param message [String, nil]
43
+ def initialize(configuration:, integration_meta: nil, message: nil)
44
+ # Set some simple default values
45
+ @event_id = SecureRandom.uuid.delete("-")
46
+ @timestamp = Sentry.utc_now.iso8601
47
+ @platform = :ruby
48
+ @type = self.class::TYPE
49
+ @sdk = integration_meta || Sentry.sdk_meta
50
+
51
+ @user = {}
52
+ @extra = {}
53
+ @contexts = {}
54
+ @tags = {}
55
+
56
+ @fingerprint = []
57
+
58
+ # configuration data that's directly used by events
59
+ @server_name = configuration.server_name
60
+ @environment = configuration.environment
61
+ @release = configuration.release
62
+ @modules = configuration.gem_specs if configuration.send_modules
63
+
64
+ # configuration options to help events process data
65
+ @send_default_pii = configuration.send_default_pii
66
+ @trusted_proxies = configuration.trusted_proxies
67
+ @stacktrace_builder = configuration.stacktrace_builder
68
+ @rack_env_whitelist = configuration.rack_env_whitelist
69
+
70
+ @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
71
+ end
72
+
73
+ class << self
74
+ # @!visibility private
75
+ def get_log_message(event_hash)
76
+ message = event_hash[:message] || event_hash['message']
77
+
78
+ return message unless message.nil? || message.empty?
79
+
80
+ message = get_message_from_exception(event_hash)
81
+
82
+ return message unless message.nil? || message.empty?
83
+
84
+ message = event_hash[:transaction] || event_hash["transaction"]
85
+
86
+ return message unless message.nil? || message.empty?
87
+
88
+ '<no message value>'
89
+ end
90
+
91
+ # @!visibility private
92
+ def get_message_from_exception(event_hash)
93
+ if exception = event_hash.dig(:exception, :values, 0)
94
+ "#{exception[:type]}: #{exception[:value]}"
95
+ elsif exception = event_hash.dig("exception", "values", 0)
96
+ "#{exception["type"]}: #{exception["value"]}"
97
+ end
98
+ end
99
+ end
100
+
101
+ # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
102
+ # @return [Configuration]
103
+ def configuration
104
+ Sentry.configuration
105
+ end
106
+
107
+ # Sets the event's timestamp.
108
+ # @param time [Time, Float]
109
+ # @return [void]
110
+ def timestamp=(time)
111
+ @timestamp = time.is_a?(Time) ? time.to_f : time
112
+ end
113
+
114
+ # Sets the event's level.
115
+ # @param level [String, Symbol]
116
+ # @return [void]
117
+ def level=(level) # needed to meet the Sentry spec
118
+ @level = level.to_s == "warn" ? :warning : level
119
+ end
120
+
121
+ # Sets the event's request environment data with RequestInterface.
122
+ # @see RequestInterface
123
+ # @param env [Hash]
124
+ # @return [void]
125
+ def rack_env=(env)
126
+ unless request || env.empty?
127
+ add_request_interface(env)
128
+
129
+ if @send_default_pii
130
+ user[:ip_address] = calculate_real_ip_from_rack(env)
131
+ end
132
+
133
+ if request_id = Utils::RequestId.read_from(env)
134
+ tags[:request_id] = request_id
135
+ end
136
+ end
137
+ end
138
+
139
+ # @return [Hash]
140
+ def to_hash
141
+ data = serialize_attributes
142
+ data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
143
+ data[:request] = request.to_hash if request
144
+ data
145
+ end
146
+
147
+ # @return [Hash]
148
+ def to_json_compatible
149
+ JSON.parse(JSON.generate(to_hash))
150
+ end
151
+
152
+ private
153
+
154
+ def add_request_interface(env)
155
+ @request = Sentry::RequestInterface.new(env: env, send_default_pii: @send_default_pii, rack_env_whitelist: @rack_env_whitelist)
156
+ end
157
+
158
+ def serialize_attributes
159
+ self.class::SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |att, memo|
160
+ if value = public_send(att)
161
+ memo[att] = value
162
+ end
163
+ end
164
+ end
165
+
166
+ # When behind a proxy (or if the user is using a proxy), we can't use
167
+ # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
168
+ def calculate_real_ip_from_rack(env)
169
+ Utils::RealIp.new(
170
+ :remote_addr => env["REMOTE_ADDR"],
171
+ :client_ip => env["HTTP_CLIENT_IP"],
172
+ :real_ip => env["HTTP_X_REAL_IP"],
173
+ :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
174
+ :trusted_proxies => @trusted_proxies
175
+ ).calculate_ip
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Error < StandardError
5
+ end
6
+
7
+ class ExternalError < Error
8
+ end
9
+ end
data/lib/sentry/hub.rb ADDED
@@ -0,0 +1,220 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/scope"
4
+ require "sentry/client"
5
+ require "sentry/session"
6
+
7
+ module Sentry
8
+ class Hub
9
+ include ArgumentCheckingHelper
10
+
11
+ attr_reader :last_event_id
12
+
13
+ def initialize(client, scope)
14
+ first_layer = Layer.new(client, scope)
15
+ @stack = [first_layer]
16
+ @last_event_id = nil
17
+ end
18
+
19
+ def new_from_top
20
+ Hub.new(current_client, current_scope)
21
+ end
22
+
23
+ def current_client
24
+ current_layer&.client
25
+ end
26
+
27
+ def configuration
28
+ current_client.configuration
29
+ end
30
+
31
+ def current_scope
32
+ current_layer&.scope
33
+ end
34
+
35
+ def clone
36
+ layer = current_layer
37
+
38
+ if layer
39
+ scope = layer.scope&.dup
40
+
41
+ Hub.new(layer.client, scope)
42
+ end
43
+ end
44
+
45
+ def bind_client(client)
46
+ layer = current_layer
47
+
48
+ if layer
49
+ layer.client = client
50
+ end
51
+ end
52
+
53
+ def configure_scope(&block)
54
+ block.call(current_scope)
55
+ end
56
+
57
+ def with_scope(&block)
58
+ push_scope
59
+ yield(current_scope)
60
+ ensure
61
+ pop_scope
62
+ end
63
+
64
+ def push_scope
65
+ new_scope =
66
+ if current_scope
67
+ current_scope.dup
68
+ else
69
+ Scope.new
70
+ end
71
+
72
+ @stack << Layer.new(current_client, new_scope)
73
+ end
74
+
75
+ def pop_scope
76
+ @stack.pop
77
+ end
78
+
79
+ def start_transaction(transaction: nil, custom_sampling_context: {}, **options)
80
+ return unless configuration.tracing_enabled?
81
+
82
+ transaction ||= Transaction.new(**options.merge(hub: self))
83
+
84
+ sampling_context = {
85
+ transaction_context: transaction.to_hash,
86
+ parent_sampled: transaction.parent_sampled
87
+ }
88
+
89
+ sampling_context.merge!(custom_sampling_context)
90
+
91
+ transaction.set_initial_sample_decision(sampling_context: sampling_context)
92
+ transaction
93
+ end
94
+
95
+ def capture_exception(exception, **options, &block)
96
+ check_argument_type!(exception, ::Exception)
97
+
98
+ return if Sentry.exception_captured?(exception)
99
+
100
+ return unless current_client
101
+
102
+ options[:hint] ||= {}
103
+ options[:hint][:exception] = exception
104
+ event = current_client.event_from_exception(exception, options[:hint])
105
+
106
+ return unless event
107
+
108
+ current_scope.session&.update_from_exception(event.exception)
109
+
110
+ capture_event(event, **options, &block).tap do
111
+ # mark the exception as captured so we can use this information to avoid duplicated capturing
112
+ exception.instance_variable_set(Sentry::CAPTURED_SIGNATURE, true)
113
+ end
114
+ end
115
+
116
+ def capture_message(message, **options, &block)
117
+ check_argument_type!(message, ::String)
118
+
119
+ return unless current_client
120
+
121
+ options[:hint] ||= {}
122
+ options[:hint][:message] = message
123
+ backtrace = options.delete(:backtrace)
124
+ event = current_client.event_from_message(message, options[:hint], backtrace: backtrace)
125
+
126
+ return unless event
127
+
128
+ capture_event(event, **options, &block)
129
+ end
130
+
131
+ def capture_event(event, **options, &block)
132
+ check_argument_type!(event, Sentry::Event)
133
+
134
+ return unless current_client
135
+
136
+ hint = options.delete(:hint) || {}
137
+ scope = current_scope.dup
138
+
139
+ if block
140
+ block.call(scope)
141
+ elsif custom_scope = options[:scope]
142
+ scope.update_from_scope(custom_scope)
143
+ elsif !options.empty?
144
+ scope.update_from_options(**options)
145
+ end
146
+
147
+ event = current_client.capture_event(event, scope, hint)
148
+
149
+ if event && configuration.debug
150
+ configuration.log_debug(event.to_json_compatible)
151
+ end
152
+
153
+ @last_event_id = event&.event_id unless event.is_a?(Sentry::TransactionEvent)
154
+ event
155
+ end
156
+
157
+ def add_breadcrumb(breadcrumb, hint: {})
158
+ return unless configuration.enabled_in_current_env?
159
+
160
+ if before_breadcrumb = current_client.configuration.before_breadcrumb
161
+ breadcrumb = before_breadcrumb.call(breadcrumb, hint)
162
+ end
163
+
164
+ return unless breadcrumb
165
+
166
+ current_scope.add_breadcrumb(breadcrumb)
167
+ end
168
+
169
+ # this doesn't do anything to the already initialized background worker
170
+ # but it temporarily disables dispatching events to it
171
+ def with_background_worker_disabled(&block)
172
+ original_background_worker_threads = configuration.background_worker_threads
173
+ configuration.background_worker_threads = 0
174
+
175
+ block.call
176
+ ensure
177
+ configuration.background_worker_threads = original_background_worker_threads
178
+ end
179
+
180
+ def start_session
181
+ return unless current_scope
182
+ current_scope.set_session(Session.new)
183
+ end
184
+
185
+ def end_session
186
+ return unless current_scope
187
+ session = current_scope.session
188
+ current_scope.set_session(nil)
189
+
190
+ return unless session
191
+ session.close
192
+ Sentry.session_flusher.add_session(session)
193
+ end
194
+
195
+ def with_session_tracking(&block)
196
+ return yield unless configuration.auto_session_tracking
197
+
198
+ start_session
199
+ yield
200
+ ensure
201
+ end_session
202
+ end
203
+
204
+ private
205
+
206
+ def current_layer
207
+ @stack.last
208
+ end
209
+
210
+ class Layer
211
+ attr_accessor :client
212
+ attr_reader :scope
213
+
214
+ def initialize(client, scope)
215
+ @client = client
216
+ @scope = scope
217
+ end
218
+ end
219
+ end
220
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Integrable
5
+ def register_integration(name:, version:)
6
+ Sentry.register_integration(name, version)
7
+ @integration_name = name
8
+ end
9
+
10
+ def integration_name
11
+ @integration_name
12
+ end
13
+
14
+ def capture_exception(exception, **options, &block)
15
+ options[:hint] ||= {}
16
+ options[:hint][:integration] = integration_name
17
+ Sentry.capture_exception(exception, **options, &block)
18
+ end
19
+
20
+ def capture_message(message, **options, &block)
21
+ options[:hint] ||= {}
22
+ options[:hint][:integration] = integration_name
23
+ Sentry.capture_message(message, **options, &block)
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Interface
5
+ # @return [Hash]
6
+ def to_hash
7
+ Hash[instance_variables.map { |name| [name[1..-1].to_sym, instance_variable_get(name)] }]
8
+ end
9
+ end
10
+ end
11
+
12
+ require "sentry/interfaces/exception"
13
+ require "sentry/interfaces/request"
14
+ require "sentry/interfaces/single_exception"
15
+ require "sentry/interfaces/stacktrace"
16
+ require "sentry/interfaces/threads"
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ require "set"
3
+
4
+ module Sentry
5
+ class ExceptionInterface < Interface
6
+ # @return [<Array[SingleExceptionInterface]>]
7
+ attr_reader :values
8
+
9
+ # @param exceptions [Array<SingleExceptionInterface>]
10
+ def initialize(exceptions:)
11
+ @values = exceptions
12
+ end
13
+
14
+ # @return [Hash]
15
+ def to_hash
16
+ data = super
17
+ data[:values] = data[:values].map(&:to_hash) if data[:values]
18
+ data
19
+ end
20
+
21
+ # Builds ExceptionInterface with given exception and stacktrace_builder.
22
+ # @param exception [Exception]
23
+ # @param stacktrace_builder [StacktraceBuilder]
24
+ # @see SingleExceptionInterface#build_with_stacktrace
25
+ # @see SingleExceptionInterface#initialize
26
+ # @return [ExceptionInterface]
27
+ def self.build(exception:, stacktrace_builder:)
28
+ exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
29
+ processed_backtrace_ids = Set.new
30
+
31
+ exceptions = exceptions.map do |e|
32
+ if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id)
33
+ processed_backtrace_ids << e.backtrace.object_id
34
+ SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder)
35
+ else
36
+ SingleExceptionInterface.new(exception: exception)
37
+ end
38
+ end
39
+
40
+ new(exceptions: exceptions)
41
+ end
42
+ end
43
+ end