sentry-ruby 5.3.0 → 5.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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +31 -0
  8. data/Makefile +4 -0
  9. data/README.md +10 -6
  10. data/Rakefile +13 -0
  11. data/bin/console +18 -0
  12. data/bin/setup +8 -0
  13. data/lib/sentry/background_worker.rb +72 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/baggage.rb +81 -0
  16. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  17. data/lib/sentry/breadcrumb.rb +70 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  19. data/lib/sentry/client.rb +207 -0
  20. data/lib/sentry/configuration.rb +543 -0
  21. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  22. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  23. data/lib/sentry/dsn.rb +53 -0
  24. data/lib/sentry/envelope.rb +96 -0
  25. data/lib/sentry/error_event.rb +38 -0
  26. data/lib/sentry/event.rb +178 -0
  27. data/lib/sentry/exceptions.rb +9 -0
  28. data/lib/sentry/hub.rb +241 -0
  29. data/lib/sentry/integrable.rb +26 -0
  30. data/lib/sentry/interface.rb +16 -0
  31. data/lib/sentry/interfaces/exception.rb +43 -0
  32. data/lib/sentry/interfaces/request.rb +134 -0
  33. data/lib/sentry/interfaces/single_exception.rb +65 -0
  34. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  35. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  36. data/lib/sentry/interfaces/threads.rb +42 -0
  37. data/lib/sentry/linecache.rb +47 -0
  38. data/lib/sentry/logger.rb +20 -0
  39. data/lib/sentry/net/http.rb +103 -0
  40. data/lib/sentry/rack/capture_exceptions.rb +82 -0
  41. data/lib/sentry/rack.rb +5 -0
  42. data/lib/sentry/rake.rb +41 -0
  43. data/lib/sentry/redis.rb +107 -0
  44. data/lib/sentry/release_detector.rb +39 -0
  45. data/lib/sentry/scope.rb +339 -0
  46. data/lib/sentry/session.rb +33 -0
  47. data/lib/sentry/session_flusher.rb +90 -0
  48. data/lib/sentry/span.rb +236 -0
  49. data/lib/sentry/test_helper.rb +78 -0
  50. data/lib/sentry/transaction.rb +345 -0
  51. data/lib/sentry/transaction_event.rb +53 -0
  52. data/lib/sentry/transport/configuration.rb +25 -0
  53. data/lib/sentry/transport/dummy_transport.rb +21 -0
  54. data/lib/sentry/transport/http_transport.rb +175 -0
  55. data/lib/sentry/transport.rb +214 -0
  56. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  57. data/lib/sentry/utils/custom_inspection.rb +14 -0
  58. data/lib/sentry/utils/encoding_helper.rb +22 -0
  59. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  60. data/lib/sentry/utils/logging_helper.rb +26 -0
  61. data/lib/sentry/utils/real_ip.rb +84 -0
  62. data/lib/sentry/utils/request_id.rb +18 -0
  63. data/lib/sentry/version.rb +5 -0
  64. data/lib/sentry-ruby.rb +511 -0
  65. data/sentry-ruby-core.gemspec +23 -0
  66. data/sentry-ruby.gemspec +24 -0
  67. metadata +66 -16
data/lib/sentry/hub.rb ADDED
@@ -0,0 +1,241 @@
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: {}, instrumenter: :sentry, **options)
80
+ return unless configuration.tracing_enabled?
81
+ return unless instrumenter == configuration.instrumenter
82
+
83
+ transaction ||= Transaction.new(**options.merge(hub: self))
84
+
85
+ sampling_context = {
86
+ transaction_context: transaction.to_hash,
87
+ parent_sampled: transaction.parent_sampled
88
+ }
89
+
90
+ sampling_context.merge!(custom_sampling_context)
91
+
92
+ transaction.set_initial_sample_decision(sampling_context: sampling_context)
93
+ transaction
94
+ end
95
+
96
+ def with_child_span(instrumenter: :sentry, **attributes, &block)
97
+ return yield(nil) unless instrumenter == configuration.instrumenter
98
+
99
+ current_span = current_scope.get_span
100
+ return yield(nil) unless current_span
101
+
102
+ result = nil
103
+
104
+ begin
105
+ current_span.with_child_span(**attributes) do |child_span|
106
+ current_scope.set_span(child_span)
107
+ result = yield(child_span)
108
+ end
109
+ ensure
110
+ current_scope.set_span(current_span)
111
+ end
112
+
113
+ result
114
+ end
115
+
116
+ def capture_exception(exception, **options, &block)
117
+ check_argument_type!(exception, ::Exception)
118
+
119
+ return if Sentry.exception_captured?(exception)
120
+
121
+ return unless current_client
122
+
123
+ options[:hint] ||= {}
124
+ options[:hint][:exception] = exception
125
+ event = current_client.event_from_exception(exception, options[:hint])
126
+
127
+ return unless event
128
+
129
+ current_scope.session&.update_from_exception(event.exception)
130
+
131
+ capture_event(event, **options, &block).tap do
132
+ # mark the exception as captured so we can use this information to avoid duplicated capturing
133
+ exception.instance_variable_set(Sentry::CAPTURED_SIGNATURE, true)
134
+ end
135
+ end
136
+
137
+ def capture_message(message, **options, &block)
138
+ check_argument_type!(message, ::String)
139
+
140
+ return unless current_client
141
+
142
+ options[:hint] ||= {}
143
+ options[:hint][:message] = message
144
+ backtrace = options.delete(:backtrace)
145
+ event = current_client.event_from_message(message, options[:hint], backtrace: backtrace)
146
+
147
+ return unless event
148
+
149
+ capture_event(event, **options, &block)
150
+ end
151
+
152
+ def capture_event(event, **options, &block)
153
+ check_argument_type!(event, Sentry::Event)
154
+
155
+ return unless current_client
156
+
157
+ hint = options.delete(:hint) || {}
158
+ scope = current_scope.dup
159
+
160
+ if block
161
+ block.call(scope)
162
+ elsif custom_scope = options[:scope]
163
+ scope.update_from_scope(custom_scope)
164
+ elsif !options.empty?
165
+ scope.update_from_options(**options)
166
+ end
167
+
168
+ event = current_client.capture_event(event, scope, hint)
169
+
170
+ if event && configuration.debug
171
+ configuration.log_debug(event.to_json_compatible)
172
+ end
173
+
174
+ @last_event_id = event&.event_id unless event.is_a?(Sentry::TransactionEvent)
175
+ event
176
+ end
177
+
178
+ def add_breadcrumb(breadcrumb, hint: {})
179
+ return unless configuration.enabled_in_current_env?
180
+
181
+ if before_breadcrumb = current_client.configuration.before_breadcrumb
182
+ breadcrumb = before_breadcrumb.call(breadcrumb, hint)
183
+ end
184
+
185
+ return unless breadcrumb
186
+
187
+ current_scope.add_breadcrumb(breadcrumb)
188
+ end
189
+
190
+ # this doesn't do anything to the already initialized background worker
191
+ # but it temporarily disables dispatching events to it
192
+ def with_background_worker_disabled(&block)
193
+ original_background_worker_threads = configuration.background_worker_threads
194
+ configuration.background_worker_threads = 0
195
+
196
+ block.call
197
+ ensure
198
+ configuration.background_worker_threads = original_background_worker_threads
199
+ end
200
+
201
+ def start_session
202
+ return unless current_scope
203
+ current_scope.set_session(Session.new)
204
+ end
205
+
206
+ def end_session
207
+ return unless current_scope
208
+ session = current_scope.session
209
+ current_scope.set_session(nil)
210
+
211
+ return unless session
212
+ session.close
213
+ Sentry.session_flusher.add_session(session)
214
+ end
215
+
216
+ def with_session_tracking(&block)
217
+ return yield unless configuration.auto_session_tracking
218
+
219
+ start_session
220
+ yield
221
+ ensure
222
+ end_session
223
+ end
224
+
225
+ private
226
+
227
+ def current_layer
228
+ @stack.last
229
+ end
230
+
231
+ class Layer
232
+ attr_accessor :client
233
+ attr_reader :scope
234
+
235
+ def initialize(client, scope)
236
+ @client = client
237
+ @scope = scope
238
+ end
239
+ end
240
+ end
241
+ 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
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class RequestInterface < Interface
5
+ REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
6
+ CONTENT_HEADERS = %w(CONTENT_TYPE CONTENT_LENGTH).freeze
7
+ IP_HEADERS = [
8
+ "REMOTE_ADDR",
9
+ "HTTP_CLIENT_IP",
10
+ "HTTP_X_REAL_IP",
11
+ "HTTP_X_FORWARDED_FOR"
12
+ ].freeze
13
+
14
+ # See Sentry server default limits at
15
+ # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
16
+ MAX_BODY_LIMIT = 4096 * 4
17
+
18
+ # @return [String]
19
+ attr_accessor :url
20
+
21
+ # @return [String]
22
+ attr_accessor :method
23
+
24
+ # @return [Hash]
25
+ attr_accessor :data
26
+
27
+ # @return [String]
28
+ attr_accessor :query_string
29
+
30
+ # @return [String]
31
+ attr_accessor :cookies
32
+
33
+ # @return [Hash]
34
+ attr_accessor :headers
35
+
36
+ # @return [Hash]
37
+ attr_accessor :env
38
+
39
+ # @param env [Hash]
40
+ # @param send_default_pii [Boolean]
41
+ # @param rack_env_whitelist [Array]
42
+ # @see Configuration#send_default_pii
43
+ # @see Configuration#rack_env_whitelist
44
+ def initialize(env:, send_default_pii:, rack_env_whitelist:)
45
+ env = env.dup
46
+
47
+ unless send_default_pii
48
+ # need to completely wipe out ip addresses
49
+ RequestInterface::IP_HEADERS.each do |header|
50
+ env.delete(header)
51
+ end
52
+ end
53
+
54
+ request = ::Rack::Request.new(env)
55
+
56
+ if send_default_pii
57
+ self.data = read_data_from(request)
58
+ self.cookies = request.cookies
59
+ self.query_string = request.query_string
60
+ end
61
+
62
+ self.url = request.scheme && request.url.split('?').first
63
+ self.method = request.request_method
64
+
65
+ self.headers = filter_and_format_headers(env, send_default_pii)
66
+ self.env = filter_and_format_env(env, rack_env_whitelist)
67
+ end
68
+
69
+ private
70
+
71
+ def read_data_from(request)
72
+ if request.form_data?
73
+ request.POST
74
+ elsif request.body # JSON requests, etc
75
+ data = request.body.read(MAX_BODY_LIMIT)
76
+ data = Utils::EncodingHelper.encode_to_utf_8(data.to_s)
77
+ request.body.rewind
78
+ data
79
+ end
80
+ rescue IOError => e
81
+ e.message
82
+ end
83
+
84
+ def filter_and_format_headers(env, send_default_pii)
85
+ env.each_with_object({}) do |(key, value), memo|
86
+ begin
87
+ key = key.to_s # rack env can contain symbols
88
+ next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
89
+ next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
90
+ next if is_skippable_header?(key)
91
+ next if key == "HTTP_AUTHORIZATION" && !send_default_pii
92
+
93
+ # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
94
+ key = key.sub(/^HTTP_/, "")
95
+ key = key.split('_').map(&:capitalize).join('-')
96
+
97
+ memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
98
+ rescue StandardError => e
99
+ # Rails adds objects to the Rack env that can sometimes raise exceptions
100
+ # when `to_s` is called.
101
+ # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134
102
+ Sentry.logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
103
+ next
104
+ end
105
+ end
106
+ end
107
+
108
+ def is_skippable_header?(key)
109
+ key.upcase != key || # lower-case envs aren't real http headers
110
+ key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
111
+ !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
112
+ end
113
+
114
+ # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream
115
+ # to think this is a Version header. Instead, this is mapped to
116
+ # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
117
+ # if the request has legitimately sent a Version header themselves.
118
+ # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
119
+ def is_server_protocol?(key, value, protocol_version)
120
+ rack_version = Gem::Version.new(::Rack.release)
121
+ return false if rack_version >= Gem::Version.new("3.0")
122
+
123
+ key == 'HTTP_VERSION' && value == protocol_version
124
+ end
125
+
126
+ def filter_and_format_env(env, rack_env_whitelist)
127
+ return env if rack_env_whitelist.empty?
128
+
129
+ env.select do |k, _v|
130
+ rack_env_whitelist.include? k.to_s
131
+ end
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/utils/exception_cause_chain"
4
+
5
+ module Sentry
6
+ class SingleExceptionInterface < Interface
7
+ include CustomInspection
8
+
9
+ SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace]
10
+ PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]".freeze
11
+ OMISSION_MARK = "...".freeze
12
+ MAX_LOCAL_BYTES = 1024
13
+
14
+ attr_reader :type, :value, :module, :thread_id, :stacktrace
15
+
16
+ def initialize(exception:, stacktrace: nil)
17
+ @type = exception.class.to_s
18
+ exception_message =
19
+ if exception.respond_to?(:detailed_message)
20
+ exception.detailed_message(highlight: false)
21
+ else
22
+ exception.message || ""
23
+ end
24
+
25
+ @value = exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
26
+
27
+ @module = exception.class.to_s.split('::')[0...-1].join('::')
28
+ @thread_id = Thread.current.object_id
29
+ @stacktrace = stacktrace
30
+ end
31
+
32
+ def to_hash
33
+ data = super
34
+ data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
35
+ data
36
+ end
37
+
38
+ # patch this method if you want to change an exception's stacktrace frames
39
+ # also see `StacktraceBuilder.build`.
40
+ def self.build_with_stacktrace(exception:, stacktrace_builder:)
41
+ stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
42
+
43
+ if locals = exception.instance_variable_get(:@sentry_locals)
44
+ locals.each do |k, v|
45
+ locals[k] =
46
+ begin
47
+ v = v.inspect unless v.is_a?(String)
48
+
49
+ if v.length >= MAX_LOCAL_BYTES
50
+ v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK
51
+ end
52
+
53
+ v
54
+ rescue StandardError
55
+ PROBLEMATIC_LOCAL_VALUE_REPLACEMENT
56
+ end
57
+ end
58
+
59
+ stacktrace.frames.last.vars = locals
60
+ end
61
+
62
+ new(exception: exception, stacktrace: stacktrace)
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class StacktraceInterface
5
+ # @return [<Array[Frame]>]
6
+ attr_reader :frames
7
+
8
+ # @param frames [<Array[Frame]>]
9
+ def initialize(frames:)
10
+ @frames = frames
11
+ end
12
+
13
+ # @return [Hash]
14
+ def to_hash
15
+ { frames: @frames.map(&:to_hash) }
16
+ end
17
+
18
+ # @return [String]
19
+ def inspect
20
+ @frames.map(&:to_s)
21
+ end
22
+
23
+ private
24
+
25
+ # Not actually an interface, but I want to use the same style
26
+ class Frame < Interface
27
+ attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
28
+ :lineno, :module, :pre_context, :post_context, :vars
29
+
30
+ def initialize(project_root, line)
31
+ @project_root = project_root
32
+
33
+ @abs_path = line.file
34
+ @function = line.method if line.method
35
+ @lineno = line.number
36
+ @in_app = line.in_app
37
+ @module = line.module_name if line.module_name
38
+ @filename = compute_filename
39
+ end
40
+
41
+ def to_s
42
+ "#{@filename}:#{@lineno}"
43
+ end
44
+
45
+ def compute_filename
46
+ return if abs_path.nil?
47
+
48
+ prefix =
49
+ if under_project_root? && in_app
50
+ @project_root
51
+ elsif under_project_root?
52
+ longest_load_path || @project_root
53
+ else
54
+ longest_load_path
55
+ end
56
+
57
+ prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
58
+ end
59
+
60
+ def set_context(linecache, context_lines)
61
+ return unless abs_path
62
+
63
+ @pre_context, @context_line, @post_context = \
64
+ linecache.get_file_context(abs_path, lineno, context_lines)
65
+ end
66
+
67
+ def to_hash(*args)
68
+ data = super(*args)
69
+ data.delete(:vars) unless vars && !vars.empty?
70
+ data.delete(:pre_context) unless pre_context && !pre_context.empty?
71
+ data.delete(:post_context) unless post_context && !post_context.empty?
72
+ data.delete(:context_line) unless context_line && !context_line.empty?
73
+ data
74
+ end
75
+
76
+ private
77
+
78
+ def under_project_root?
79
+ @project_root && abs_path.start_with?(@project_root)
80
+ end
81
+
82
+ def longest_load_path
83
+ $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class StacktraceBuilder
5
+ # @return [String]
6
+ attr_reader :project_root
7
+
8
+ # @return [Regexp, nil]
9
+ attr_reader :app_dirs_pattern
10
+
11
+ # @return [LineCache]
12
+ attr_reader :linecache
13
+
14
+ # @return [Integer, nil]
15
+ attr_reader :context_lines
16
+
17
+ # @return [Proc, nil]
18
+ attr_reader :backtrace_cleanup_callback
19
+
20
+ # @param project_root [String]
21
+ # @param app_dirs_pattern [Regexp, nil]
22
+ # @param linecache [LineCache]
23
+ # @param context_lines [Integer, nil]
24
+ # @param backtrace_cleanup_callback [Proc, nil]
25
+ # @see Configuration#project_root
26
+ # @see Configuration#app_dirs_pattern
27
+ # @see Configuration#linecache
28
+ # @see Configuration#context_lines
29
+ # @see Configuration#backtrace_cleanup_callback
30
+ def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
31
+ @project_root = project_root
32
+ @app_dirs_pattern = app_dirs_pattern
33
+ @linecache = linecache
34
+ @context_lines = context_lines
35
+ @backtrace_cleanup_callback = backtrace_cleanup_callback
36
+ end
37
+
38
+ # Generates a StacktraceInterface with the given backtrace.
39
+ # You can pass a block to customize/exclude frames:
40
+ #
41
+ # @example
42
+ # builder.build(backtrace) do |frame|
43
+ # if frame.module.match?(/a_gem/)
44
+ # nil
45
+ # else
46
+ # frame
47
+ # end
48
+ # end
49
+ # @param backtrace [Array<String>]
50
+ # @param frame_callback [Proc]
51
+ # @yieldparam frame [StacktraceInterface::Frame]
52
+ # @return [StacktraceInterface]
53
+ def build(backtrace:, &frame_callback)
54
+ parsed_lines = parse_backtrace_lines(backtrace).select(&:file)
55
+
56
+ frames = parsed_lines.reverse.map do |line|
57
+ frame = convert_parsed_line_into_frame(line)
58
+ frame = frame_callback.call(frame) if frame_callback
59
+ frame
60
+ end.compact
61
+
62
+ StacktraceInterface.new(frames: frames)
63
+ end
64
+
65
+ private
66
+
67
+ def convert_parsed_line_into_frame(line)
68
+ frame = StacktraceInterface::Frame.new(project_root, line)
69
+ frame.set_context(linecache, context_lines) if context_lines
70
+ frame
71
+ end
72
+
73
+ def parse_backtrace_lines(backtrace)
74
+ Backtrace.parse(
75
+ backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback
76
+ ).lines
77
+ end
78
+ end
79
+ end