sentry-ruby-core 4.4.0 → 5.1.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +2 -0
  3. data/CHANGELOG.md +12 -0
  4. data/Gemfile +9 -5
  5. data/LICENSE.txt +1 -1
  6. data/README.md +29 -175
  7. data/bin/console +5 -1
  8. data/lib/sentry/background_worker.rb +33 -3
  9. data/lib/sentry/backtrace.rb +1 -3
  10. data/lib/sentry/breadcrumb/sentry_logger.rb +3 -1
  11. data/lib/sentry/breadcrumb.rb +28 -2
  12. data/lib/sentry/breadcrumb_buffer.rb +16 -0
  13. data/lib/sentry/client.rb +66 -7
  14. data/lib/sentry/configuration.rb +156 -112
  15. data/lib/sentry/core_ext/object/deep_dup.rb +4 -0
  16. data/lib/sentry/core_ext/object/duplicable.rb +2 -0
  17. data/lib/sentry/dsn.rb +6 -1
  18. data/lib/sentry/envelope.rb +49 -0
  19. data/lib/sentry/event.rb +65 -23
  20. data/lib/sentry/exceptions.rb +2 -0
  21. data/lib/sentry/hub.rb +37 -6
  22. data/lib/sentry/integrable.rb +2 -0
  23. data/lib/sentry/interface.rb +3 -10
  24. data/lib/sentry/interfaces/exception.rb +13 -3
  25. data/lib/sentry/interfaces/request.rb +52 -21
  26. data/lib/sentry/interfaces/single_exception.rb +31 -0
  27. data/lib/sentry/interfaces/stacktrace.rb +14 -0
  28. data/lib/sentry/interfaces/stacktrace_builder.rb +39 -10
  29. data/lib/sentry/interfaces/threads.rb +12 -2
  30. data/lib/sentry/linecache.rb +3 -0
  31. data/lib/sentry/net/http.rb +79 -51
  32. data/lib/sentry/rack/capture_exceptions.rb +2 -0
  33. data/lib/sentry/rack.rb +2 -1
  34. data/lib/sentry/rake.rb +33 -9
  35. data/lib/sentry/redis.rb +88 -0
  36. data/lib/sentry/release_detector.rb +39 -0
  37. data/lib/sentry/scope.rb +76 -6
  38. data/lib/sentry/span.rb +84 -8
  39. data/lib/sentry/transaction.rb +50 -13
  40. data/lib/sentry/transaction_event.rb +19 -6
  41. data/lib/sentry/transport/configuration.rb +4 -2
  42. data/lib/sentry/transport/dummy_transport.rb +2 -0
  43. data/lib/sentry/transport/http_transport.rb +55 -42
  44. data/lib/sentry/transport.rb +101 -32
  45. data/lib/sentry/utils/argument_checking_helper.rb +2 -0
  46. data/lib/sentry/utils/custom_inspection.rb +14 -0
  47. data/lib/sentry/utils/exception_cause_chain.rb +10 -10
  48. data/lib/sentry/utils/logging_helper.rb +6 -4
  49. data/lib/sentry/utils/real_ip.rb +9 -1
  50. data/lib/sentry/utils/request_id.rb +2 -0
  51. data/lib/sentry/version.rb +3 -1
  52. data/lib/sentry-ruby.rb +247 -47
  53. data/sentry-ruby-core.gemspec +2 -3
  54. data/sentry-ruby.gemspec +2 -3
  55. metadata +10 -22
  56. data/.craft.yml +0 -29
  57. data/lib/sentry/benchmarks/benchmark_transport.rb +0 -14
  58. data/lib/sentry/rack/deprecations.rb +0 -19
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class Envelope
6
+ class Item
7
+ attr_accessor :headers, :payload
8
+
9
+ def initialize(headers, payload)
10
+ @headers = headers
11
+ @payload = payload
12
+ end
13
+
14
+ def type
15
+ @headers[:type] || 'event'
16
+ end
17
+
18
+ def to_s
19
+ <<~ITEM
20
+ #{JSON.generate(@headers)}
21
+ #{JSON.generate(@payload)}
22
+ ITEM
23
+ end
24
+ end
25
+
26
+ attr_accessor :headers, :items
27
+
28
+ def initialize(headers = {})
29
+ @headers = headers
30
+ @items = []
31
+ end
32
+
33
+ def add_item(headers, payload)
34
+ @items << Item.new(headers, payload)
35
+ end
36
+
37
+ def to_s
38
+ [JSON.generate(@headers), *@items.map(&:to_s)].join("\n")
39
+ end
40
+
41
+ def item_types
42
+ @items.map(&:type)
43
+ end
44
+
45
+ def event_id
46
+ @headers[:event_id]
47
+ end
48
+ end
49
+ end
data/lib/sentry/event.rb CHANGED
@@ -6,26 +6,44 @@ require 'sentry/interface'
6
6
  require 'sentry/backtrace'
7
7
  require 'sentry/utils/real_ip'
8
8
  require 'sentry/utils/request_id'
9
+ require 'sentry/utils/custom_inspection'
9
10
 
10
11
  module Sentry
11
12
  class Event
12
- ATTRIBUTES = %i(
13
+ # These are readable attributes.
14
+ SERIALIZEABLE_ATTRIBUTES = %i(
13
15
  event_id level timestamp
14
16
  release environment server_name modules
15
17
  message user tags contexts extra
16
- fingerprint breadcrumbs backtrace transaction
18
+ fingerprint breadcrumbs transaction
17
19
  platform sdk type
18
20
  )
19
21
 
22
+ # These are writable attributes.
23
+ WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
24
+
20
25
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
21
26
 
22
- attr_accessor(*ATTRIBUTES)
23
- attr_reader :configuration, :request, :exception, :threads
27
+ SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist]
24
28
 
25
- def initialize(configuration:, integration_meta: nil, message: nil)
26
- # this needs to go first because some setters rely on configuration
27
- @configuration = configuration
29
+ include CustomInspection
30
+
31
+ attr_writer(*WRITER_ATTRIBUTES)
32
+ attr_reader(*SERIALIZEABLE_ATTRIBUTES)
33
+
34
+ # @return [RequestInterface]
35
+ attr_reader :request
36
+
37
+ # @return [ExceptionInterface]
38
+ attr_reader :exception
28
39
 
40
+ # @return [ThreadsInterface]
41
+ attr_reader :threads
42
+
43
+ # @param configuration [Configuration]
44
+ # @param integration_meta [Hash, nil]
45
+ # @param message [String, nil]
46
+ def initialize(configuration:, integration_meta: nil, message: nil)
29
47
  # Set some simple default values
30
48
  @event_id = SecureRandom.uuid.delete("-")
31
49
  @timestamp = Sentry.utc_now.iso8601
@@ -39,17 +57,25 @@ module Sentry
39
57
 
40
58
  @fingerprint = []
41
59
 
60
+ # configuration data that's directly used by events
42
61
  @server_name = configuration.server_name
43
62
  @environment = configuration.environment
44
63
  @release = configuration.release
45
64
  @modules = configuration.gem_specs if configuration.send_modules
46
65
 
66
+ # configuration options to help events process data
67
+ @send_default_pii = configuration.send_default_pii
68
+ @trusted_proxies = configuration.trusted_proxies
69
+ @stacktrace_builder = configuration.stacktrace_builder
70
+ @rack_env_whitelist = configuration.rack_env_whitelist
71
+
47
72
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
48
73
 
49
74
  self.level = :error
50
75
  end
51
76
 
52
77
  class << self
78
+ # @!visibility private
53
79
  def get_log_message(event_hash)
54
80
  message = event_hash[:message] || event_hash['message']
55
81
 
@@ -66,6 +92,7 @@ module Sentry
66
92
  '<no message value>'
67
93
  end
68
94
 
95
+ # @!visibility private
69
96
  def get_message_from_exception(event_hash)
70
97
  if exception = event_hash.dig(:exception, :values, 0)
71
98
  "#{exception[:type]}: #{exception[:value]}"
@@ -75,21 +102,35 @@ module Sentry
75
102
  end
76
103
  end
77
104
 
105
+ # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
106
+ # @return [Configuration]
107
+ def configuration
108
+ Sentry.configuration
109
+ end
110
+
111
+ # Sets the event's timestamp.
112
+ # @param time [Time, Float]
113
+ # @return [void]
78
114
  def timestamp=(time)
79
115
  @timestamp = time.is_a?(Time) ? time.to_f : time
80
116
  end
81
117
 
82
- def level=(new_level) # needed to meet the Sentry spec
83
- @level = new_level.to_s == "warn" ? :warning : new_level
118
+ # Sets the event's level.
119
+ # @param level [String, Symbol]
120
+ # @return [void]
121
+ def level=(level) # needed to meet the Sentry spec
122
+ @level = level.to_s == "warn" ? :warning : level
84
123
  end
85
124
 
125
+ # Sets the event's request environment data with RequestInterface.
126
+ # @see RequestInterface
127
+ # @param env [Hash]
128
+ # @return [void]
86
129
  def rack_env=(env)
87
130
  unless request || env.empty?
88
- env = env.dup
89
-
90
131
  add_request_interface(env)
91
132
 
92
- if configuration.send_default_pii
133
+ if @send_default_pii
93
134
  user[:ip_address] = calculate_real_ip_from_rack(env)
94
135
  end
95
136
 
@@ -99,9 +140,7 @@ module Sentry
99
140
  end
100
141
  end
101
142
 
102
- def type
103
- end
104
-
143
+ # @return [Hash]
105
144
  def to_hash
106
145
  data = serialize_attributes
107
146
  data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
@@ -112,34 +151,37 @@ module Sentry
112
151
  data
113
152
  end
114
153
 
154
+ # @return [Hash]
115
155
  def to_json_compatible
116
156
  JSON.parse(JSON.generate(to_hash))
117
157
  end
118
158
 
119
- def add_request_interface(env)
120
- @request = Sentry::RequestInterface.build(env: env)
121
- end
122
-
159
+ # @!visibility private
123
160
  def add_threads_interface(backtrace: nil, **options)
124
161
  @threads = ThreadsInterface.build(
125
162
  backtrace: backtrace,
126
- stacktrace_builder: configuration.stacktrace_builder,
163
+ stacktrace_builder: @stacktrace_builder,
127
164
  **options
128
165
  )
129
166
  end
130
167
 
168
+ # @!visibility private
131
169
  def add_exception_interface(exception)
132
170
  if exception.respond_to?(:sentry_context)
133
171
  @extra.merge!(exception.sentry_context)
134
172
  end
135
173
 
136
- @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: configuration.stacktrace_builder)
174
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
137
175
  end
138
176
 
139
177
  private
140
178
 
179
+ def add_request_interface(env)
180
+ @request = Sentry::RequestInterface.new(env: env, send_default_pii: @send_default_pii, rack_env_whitelist: @rack_env_whitelist)
181
+ end
182
+
141
183
  def serialize_attributes
142
- self.class::ATTRIBUTES.each_with_object({}) do |att, memo|
184
+ self.class::SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |att, memo|
143
185
  if value = public_send(att)
144
186
  memo[att] = value
145
187
  end
@@ -154,7 +196,7 @@ module Sentry
154
196
  :client_ip => env["HTTP_CLIENT_IP"],
155
197
  :real_ip => env["HTTP_X_REAL_IP"],
156
198
  :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
157
- :trusted_proxies => configuration.trusted_proxies
199
+ :trusted_proxies => @trusted_proxies
158
200
  ).calculate_ip
159
201
  end
160
202
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class Error < StandardError
3
5
  end
data/lib/sentry/hub.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "sentry/scope"
2
4
  require "sentry/client"
3
5
 
@@ -90,33 +92,44 @@ module Sentry
90
92
  end
91
93
 
92
94
  def capture_exception(exception, **options, &block)
93
- return unless current_client
94
-
95
95
  check_argument_type!(exception, ::Exception)
96
96
 
97
+ return if Sentry.exception_captured?(exception)
98
+
99
+ return unless current_client
100
+
97
101
  options[:hint] ||= {}
98
102
  options[:hint][:exception] = exception
99
103
  event = current_client.event_from_exception(exception, options[:hint])
100
104
 
101
105
  return unless event
102
106
 
103
- capture_event(event, **options, &block)
107
+ capture_event(event, **options, &block).tap do
108
+ # mark the exception as captured so we can use this information to avoid duplicated capturing
109
+ exception.instance_variable_set(Sentry::CAPTURED_SIGNATURE, true)
110
+ end
104
111
  end
105
112
 
106
113
  def capture_message(message, **options, &block)
114
+ check_argument_type!(message, ::String)
115
+
107
116
  return unless current_client
108
117
 
109
118
  options[:hint] ||= {}
110
119
  options[:hint][:message] = message
111
- event = current_client.event_from_message(message, options[:hint])
120
+ backtrace = options.delete(:backtrace)
121
+ event = current_client.event_from_message(message, options[:hint], backtrace: backtrace)
122
+
123
+ return unless event
124
+
112
125
  capture_event(event, **options, &block)
113
126
  end
114
127
 
115
128
  def capture_event(event, **options, &block)
116
- return unless current_client
117
-
118
129
  check_argument_type!(event, Sentry::Event)
119
130
 
131
+ return unless current_client
132
+
120
133
  hint = options.delete(:hint) || {}
121
134
  scope = current_scope.dup
122
135
 
@@ -130,11 +143,18 @@ module Sentry
130
143
 
131
144
  event = current_client.capture_event(event, scope, hint)
132
145
 
146
+
147
+ if event && configuration.debug
148
+ configuration.log_debug(event.to_json_compatible)
149
+ end
150
+
133
151
  @last_event_id = event&.event_id
134
152
  event
135
153
  end
136
154
 
137
155
  def add_breadcrumb(breadcrumb, hint: {})
156
+ return unless configuration.enabled_in_current_env?
157
+
138
158
  if before_breadcrumb = current_client.configuration.before_breadcrumb
139
159
  breadcrumb = before_breadcrumb.call(breadcrumb, hint)
140
160
  end
@@ -144,6 +164,17 @@ module Sentry
144
164
  current_scope.add_breadcrumb(breadcrumb)
145
165
  end
146
166
 
167
+ # this doesn't do anything to the already initialized background worker
168
+ # but it temporarily disables dispatching events to it
169
+ def with_background_worker_disabled(&block)
170
+ original_background_worker_threads = configuration.background_worker_threads
171
+ configuration.background_worker_threads = 0
172
+
173
+ block.call
174
+ ensure
175
+ configuration.background_worker_threads = original_background_worker_threads
176
+ end
177
+
147
178
  private
148
179
 
149
180
  def current_layer
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  module Integrable
3
5
  def register_integration(name:, version:)
@@ -1,15 +1,8 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class Interface
3
- def self.inherited(klass)
4
- name = klass.name.split("::").last.downcase.gsub("interface", "")
5
- registered[name.to_sym] = klass
6
- super
7
- end
8
-
9
- def self.registered
10
- @@registered ||= {} # rubocop:disable Style/ClassVars
11
- end
12
-
5
+ # @return [Hash]
13
6
  def to_hash
14
7
  Hash[instance_variables.map { |name| [name[1..-1].to_sym, instance_variable_get(name)] }]
15
8
  end
@@ -1,15 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class ExceptionInterface < Interface
3
- def initialize(values:)
4
- @values = values
5
+ # @param exceptions [Array<SingleExceptionInterface>]
6
+ def initialize(exceptions:)
7
+ @values = exceptions
5
8
  end
6
9
 
10
+ # @return [Hash]
7
11
  def to_hash
8
12
  data = super
9
13
  data[:values] = data[:values].map(&:to_hash) if data[:values]
10
14
  data
11
15
  end
12
16
 
17
+ # Builds ExceptionInterface with given exception and stacktrace_builder.
18
+ # @param exception [Exception]
19
+ # @param stacktrace_builder [StacktraceBuilder]
20
+ # @see SingleExceptionInterface#build_with_stacktrace
21
+ # @see SingleExceptionInterface#initialize
22
+ # @return [ExceptionInterface]
13
23
  def self.build(exception:, stacktrace_builder:)
14
24
  exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
15
25
  processed_backtrace_ids = Set.new
@@ -23,7 +33,7 @@ module Sentry
23
33
  end
24
34
  end
25
35
 
26
- new(values: exceptions)
36
+ new(exceptions: exceptions)
27
37
  end
28
38
  end
29
39
  end
@@ -15,29 +15,45 @@ module Sentry
15
15
  # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
16
16
  MAX_BODY_LIMIT = 4096 * 4
17
17
 
18
- attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env
18
+ # @return [String]
19
+ attr_accessor :url
19
20
 
20
- def self.build(env:)
21
- env = clean_env(env)
22
- request = ::Rack::Request.new(env)
23
- self.new(request: request)
24
- end
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
25
32
 
26
- def self.clean_env(env)
27
- unless Sentry.configuration.send_default_pii
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
28
48
  # need to completely wipe out ip addresses
29
49
  RequestInterface::IP_HEADERS.each do |header|
30
50
  env.delete(header)
31
51
  end
32
52
  end
33
53
 
34
- env
35
- end
36
-
37
- def initialize(request:)
38
- env = request.env
54
+ request = ::Rack::Request.new(env)
39
55
 
40
- if Sentry.configuration.send_default_pii
56
+ if send_default_pii
41
57
  self.data = read_data_from(request)
42
58
  self.cookies = request.cookies
43
59
  self.query_string = request.query_string
@@ -46,8 +62,8 @@ module Sentry
46
62
  self.url = request.scheme && request.url.split('?').first
47
63
  self.method = request.request_method
48
64
 
49
- self.headers = filter_and_format_headers(env)
50
- self.env = filter_and_format_env(env)
65
+ self.headers = filter_and_format_headers(env, send_default_pii)
66
+ self.env = filter_and_format_env(env, rack_env_whitelist)
51
67
  end
52
68
 
53
69
  private
@@ -57,6 +73,7 @@ module Sentry
57
73
  request.POST
58
74
  elsif request.body # JSON requests, etc
59
75
  data = request.body.read(MAX_BODY_LIMIT)
76
+ data = encode_to_utf_8(data.to_s)
60
77
  request.body.rewind
61
78
  data
62
79
  end
@@ -64,18 +81,20 @@ module Sentry
64
81
  e.message
65
82
  end
66
83
 
67
- def filter_and_format_headers(env)
84
+ def filter_and_format_headers(env, send_default_pii)
68
85
  env.each_with_object({}) do |(key, value), memo|
69
86
  begin
70
87
  key = key.to_s # rack env can contain symbols
71
88
  next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
72
89
  next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
73
90
  next if is_skippable_header?(key)
91
+ next if key == "HTTP_AUTHORIZATION" && !send_default_pii
74
92
 
75
93
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
76
94
  key = key.sub(/^HTTP_/, "")
77
95
  key = key.split('_').map(&:capitalize).join('-')
78
- memo[key] = value.to_s
96
+
97
+ memo[key] = encode_to_utf_8(value.to_s)
79
98
  rescue StandardError => e
80
99
  # Rails adds objects to the Rack env that can sometimes raise exceptions
81
100
  # when `to_s` is called.
@@ -86,6 +105,18 @@ module Sentry
86
105
  end
87
106
  end
88
107
 
108
+ def encode_to_utf_8(value)
109
+ if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding)
110
+ value = value.dup.force_encoding(Encoding::UTF_8)
111
+ end
112
+
113
+ if !value.valid_encoding?
114
+ value = value.scrub
115
+ end
116
+
117
+ value
118
+ end
119
+
89
120
  def is_skippable_header?(key)
90
121
  key.upcase != key || # lower-case envs aren't real http headers
91
122
  key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
@@ -102,11 +133,11 @@ module Sentry
102
133
  key == 'HTTP_VERSION' && value == protocol_version
103
134
  end
104
135
 
105
- def filter_and_format_env(env)
106
- return env if Sentry.configuration.rack_env_whitelist.empty?
136
+ def filter_and_format_env(env, rack_env_whitelist)
137
+ return env if rack_env_whitelist.empty?
107
138
 
108
139
  env.select do |k, _v|
109
- Sentry.configuration.rack_env_whitelist.include? k.to_s
140
+ rack_env_whitelist.include? k.to_s
110
141
  end
111
142
  end
112
143
  end
@@ -1,5 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/utils/exception_cause_chain"
4
+
1
5
  module Sentry
2
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
+
3
14
  attr_reader :type, :value, :module, :thread_id, :stacktrace
4
15
 
5
16
  def initialize(exception:, stacktrace: nil)
@@ -20,6 +31,26 @@ module Sentry
20
31
  # also see `StacktraceBuilder.build`.
21
32
  def self.build_with_stacktrace(exception:, stacktrace_builder:)
22
33
  stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
34
+
35
+ if locals = exception.instance_variable_get(:@sentry_locals)
36
+ locals.each do |k, v|
37
+ locals[k] =
38
+ begin
39
+ v = v.inspect unless v.is_a?(String)
40
+
41
+ if v.length >= MAX_LOCAL_BYTES
42
+ v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK
43
+ end
44
+
45
+ v
46
+ rescue StandardError
47
+ PROBLEMATIC_LOCAL_VALUE_REPLACEMENT
48
+ end
49
+ end
50
+
51
+ stacktrace.frames.last.vars = locals
52
+ end
53
+
23
54
  new(exception: exception, stacktrace: stacktrace)
24
55
  end
25
56
  end
@@ -1,15 +1,25 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class StacktraceInterface
5
+ # @return [<Array[Frame]>]
3
6
  attr_reader :frames
4
7
 
8
+ # @param frames [<Array[Frame]>]
5
9
  def initialize(frames:)
6
10
  @frames = frames
7
11
  end
8
12
 
13
+ # @return [Hash]
9
14
  def to_hash
10
15
  { frames: @frames.map(&:to_hash) }
11
16
  end
12
17
 
18
+ # @return [String]
19
+ def inspect
20
+ @frames.map(&:to_s)
21
+ end
22
+
13
23
  private
14
24
 
15
25
  # Not actually an interface, but I want to use the same style
@@ -28,6 +38,10 @@ module Sentry
28
38
  @filename = compute_filename
29
39
  end
30
40
 
41
+ def to_s
42
+ "#{@filename}:#{@lineno}"
43
+ end
44
+
31
45
  def compute_filename
32
46
  return if abs_path.nil?
33
47
 
@@ -1,7 +1,32 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sentry
2
4
  class StacktraceBuilder
3
- attr_reader :project_root, :app_dirs_pattern, :linecache, :context_lines, :backtrace_cleanup_callback
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
4
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
5
30
  def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
6
31
  @project_root = project_root
7
32
  @app_dirs_pattern = app_dirs_pattern
@@ -10,17 +35,21 @@ module Sentry
10
35
  @backtrace_cleanup_callback = backtrace_cleanup_callback
11
36
  end
12
37
 
13
- # you can pass a block to customize/exclude frames:
38
+ # Generates a StacktraceInterface with the given backtrace.
39
+ # You can pass a block to customize/exclude frames:
14
40
  #
15
- # ```ruby
16
- # builder.build(backtrace) do |frame|
17
- # if frame.module.match?(/a_gem/)
18
- # nil
19
- # else
20
- # frame
41
+ # @example
42
+ # builder.build(backtrace) do |frame|
43
+ # if frame.module.match?(/a_gem/)
44
+ # nil
45
+ # else
46
+ # frame
47
+ # end
21
48
  # end
22
- # end
23
- # ```
49
+ # @param backtrace [Array<String>]
50
+ # @param frame_callback [Proc]
51
+ # @yieldparam frame [StacktraceInterface::Frame]
52
+ # @return [StacktraceInterface]
24
53
  def build(backtrace:, &frame_callback)
25
54
  parsed_lines = parse_backtrace_lines(backtrace).select(&:file)
26
55