sentry-ruby 5.4.0 → 5.4.1

Sign up to get free protection for your applications and to get access to all the features.
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