sentry-ruby-core 4.4.0 → 5.1.1

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