sentry-ruby 5.13.0 → 5.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -18
  3. data/README.md +20 -10
  4. data/Rakefile +1 -1
  5. data/bin/console +1 -0
  6. data/lib/sentry/attachment.rb +42 -0
  7. data/lib/sentry/background_worker.rb +9 -2
  8. data/lib/sentry/backpressure_monitor.rb +45 -0
  9. data/lib/sentry/backtrace.rb +7 -3
  10. data/lib/sentry/check_in_event.rb +1 -1
  11. data/lib/sentry/client.rb +69 -16
  12. data/lib/sentry/configuration.rb +57 -14
  13. data/lib/sentry/cron/configuration.rb +23 -0
  14. data/lib/sentry/cron/monitor_check_ins.rb +40 -26
  15. data/lib/sentry/cron/monitor_schedule.rb +1 -1
  16. data/lib/sentry/dsn.rb +1 -1
  17. data/lib/sentry/envelope.rb +18 -1
  18. data/lib/sentry/error_event.rb +2 -2
  19. data/lib/sentry/event.rb +13 -39
  20. data/lib/sentry/faraday.rb +77 -0
  21. data/lib/sentry/graphql.rb +9 -0
  22. data/lib/sentry/hub.rb +17 -4
  23. data/lib/sentry/integrable.rb +4 -0
  24. data/lib/sentry/interface.rb +1 -0
  25. data/lib/sentry/interfaces/exception.rb +5 -3
  26. data/lib/sentry/interfaces/mechanism.rb +20 -0
  27. data/lib/sentry/interfaces/request.rb +2 -2
  28. data/lib/sentry/interfaces/single_exception.rb +7 -4
  29. data/lib/sentry/interfaces/stacktrace_builder.rb +8 -0
  30. data/lib/sentry/metrics/aggregator.rb +248 -0
  31. data/lib/sentry/metrics/configuration.rb +47 -0
  32. data/lib/sentry/metrics/counter_metric.rb +25 -0
  33. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  34. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  35. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  36. data/lib/sentry/metrics/metric.rb +19 -0
  37. data/lib/sentry/metrics/set_metric.rb +28 -0
  38. data/lib/sentry/metrics/timing.rb +43 -0
  39. data/lib/sentry/metrics.rb +56 -0
  40. data/lib/sentry/net/http.rb +22 -39
  41. data/lib/sentry/propagation_context.rb +9 -8
  42. data/lib/sentry/puma.rb +1 -1
  43. data/lib/sentry/rack/capture_exceptions.rb +14 -2
  44. data/lib/sentry/rake.rb +3 -14
  45. data/lib/sentry/redis.rb +2 -1
  46. data/lib/sentry/release_detector.rb +1 -1
  47. data/lib/sentry/scope.rb +47 -37
  48. data/lib/sentry/session.rb +2 -2
  49. data/lib/sentry/session_flusher.rb +6 -38
  50. data/lib/sentry/span.rb +40 -5
  51. data/lib/sentry/test_helper.rb +2 -1
  52. data/lib/sentry/threaded_periodic_worker.rb +39 -0
  53. data/lib/sentry/transaction.rb +25 -16
  54. data/lib/sentry/transaction_event.rb +5 -0
  55. data/lib/sentry/transport/configuration.rb +73 -1
  56. data/lib/sentry/transport/http_transport.rb +68 -37
  57. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  58. data/lib/sentry/transport.rb +32 -37
  59. data/lib/sentry/utils/argument_checking_helper.rb +6 -0
  60. data/lib/sentry/utils/http_tracing.rb +41 -0
  61. data/lib/sentry/utils/logging_helper.rb +0 -4
  62. data/lib/sentry/utils/real_ip.rb +1 -1
  63. data/lib/sentry/utils/request_id.rb +1 -1
  64. data/lib/sentry/version.rb +1 -1
  65. data/lib/sentry-ruby.rb +57 -24
  66. data/sentry-ruby.gemspec +12 -5
  67. metadata +42 -7
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Cron
5
+ class Configuration
6
+ # Defaults set here will apply to all {Cron::MonitorConfig} objects unless overwritten.
7
+
8
+ # How long (in minutes) after the expected checkin time will we wait
9
+ # until we consider the checkin to have been missed.
10
+ # @return [Integer, nil]
11
+ attr_accessor :default_checkin_margin
12
+
13
+ # How long (in minutes) is the checkin allowed to run for in in_progress
14
+ # before it is considered failed.
15
+ # @return [Integer, nil]
16
+ attr_accessor :default_max_runtime
17
+
18
+ # tz database style timezone string
19
+ # @return [String, nil]
20
+ attr_accessor :default_timezone
21
+ end
22
+ end
23
+ end
@@ -1,9 +1,11 @@
1
1
  module Sentry
2
2
  module Cron
3
3
  module MonitorCheckIns
4
+ MAX_SLUG_LENGTH = 50
5
+
4
6
  module Patch
5
- def perform(*args)
6
- slug = self.class.sentry_monitor_slug || self.class.name
7
+ def perform(*args, **opts)
8
+ slug = self.class.sentry_monitor_slug
7
9
  monitor_config = self.class.sentry_monitor_config
8
10
 
9
11
  check_in_id = Sentry.capture_check_in(slug,
@@ -11,39 +13,53 @@ module Sentry
11
13
  monitor_config: monitor_config)
12
14
 
13
15
  start = Sentry.utc_now.to_i
14
- ret = super
15
- duration = Sentry.utc_now.to_i - start
16
-
17
- Sentry.capture_check_in(slug,
18
- :ok,
19
- check_in_id: check_in_id,
20
- duration: duration,
21
- monitor_config: monitor_config)
22
-
23
- ret
24
- rescue Exception
25
- duration = Sentry.utc_now.to_i - start
26
-
27
- Sentry.capture_check_in(slug,
28
- :error,
29
- check_in_id: check_in_id,
30
- duration: duration,
31
- monitor_config: monitor_config)
32
-
33
- raise
16
+
17
+ begin
18
+ # need to do this on ruby <= 2.6 sadly
19
+ ret = method(:perform).super_method.arity == 0 ? super() : super
20
+ duration = Sentry.utc_now.to_i - start
21
+
22
+ Sentry.capture_check_in(slug,
23
+ :ok,
24
+ check_in_id: check_in_id,
25
+ duration: duration,
26
+ monitor_config: monitor_config)
27
+
28
+ ret
29
+ rescue Exception
30
+ duration = Sentry.utc_now.to_i - start
31
+
32
+ Sentry.capture_check_in(slug,
33
+ :error,
34
+ check_in_id: check_in_id,
35
+ duration: duration,
36
+ monitor_config: monitor_config)
37
+
38
+ raise
39
+ end
34
40
  end
35
41
  end
36
42
 
37
43
  module ClassMethods
38
44
  def sentry_monitor_check_ins(slug: nil, monitor_config: nil)
45
+ if monitor_config && Sentry.configuration
46
+ cron_config = Sentry.configuration.cron
47
+ monitor_config.checkin_margin ||= cron_config.default_checkin_margin
48
+ monitor_config.max_runtime ||= cron_config.default_max_runtime
49
+ monitor_config.timezone ||= cron_config.default_timezone
50
+ end
51
+
39
52
  @sentry_monitor_slug = slug
40
53
  @sentry_monitor_config = monitor_config
41
54
 
42
55
  prepend Patch
43
56
  end
44
57
 
45
- def sentry_monitor_slug
46
- @sentry_monitor_slug
58
+ def sentry_monitor_slug(name: self.name)
59
+ @sentry_monitor_slug ||= begin
60
+ slug = name.gsub('::', '-').downcase
61
+ slug[-MAX_SLUG_LENGTH..-1] || slug
62
+ end
47
63
  end
48
64
 
49
65
  def sentry_monitor_config
@@ -51,8 +67,6 @@ module Sentry
51
67
  end
52
68
  end
53
69
 
54
- extend ClassMethods
55
-
56
70
  def self.included(base)
57
71
  base.extend(ClassMethods)
58
72
  end
@@ -26,7 +26,7 @@ module Sentry
26
26
  # @return [Symbol]
27
27
  attr_accessor :unit
28
28
 
29
- VALID_UNITS = %i(year month week day hour minute)
29
+ VALID_UNITS = %i[year month week day hour minute]
30
30
 
31
31
  def initialize(value, unit)
32
32
  @value = value
data/lib/sentry/dsn.rb CHANGED
@@ -5,7 +5,7 @@ require "uri"
5
5
  module Sentry
6
6
  class DSN
7
7
  PORT_MAP = { 'http' => 80, 'https' => 443 }.freeze
8
- REQUIRED_ATTRIBUTES = %w(host path public_key project_id).freeze
8
+ REQUIRED_ATTRIBUTES = %w[host path public_key project_id].freeze
9
9
 
10
10
  attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES
11
11
 
@@ -18,8 +18,25 @@ module Sentry
18
18
  @headers[:type] || 'event'
19
19
  end
20
20
 
21
+ # rate limits and client reports use the data_category rather than envelope item type
22
+ def self.data_category(type)
23
+ case type
24
+ when 'session', 'attachment', 'transaction', 'profile', 'span' then type
25
+ when 'sessions' then 'session'
26
+ when 'check_in' then 'monitor'
27
+ when 'statsd', 'metric_meta' then 'metric_bucket'
28
+ when 'event' then 'error'
29
+ when 'client_report' then 'internal'
30
+ else 'default'
31
+ end
32
+ end
33
+
34
+ def data_category
35
+ self.class.data_category(type)
36
+ end
37
+
21
38
  def to_s
22
- [JSON.generate(@headers), JSON.generate(@payload)].join("\n")
39
+ [JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n")
23
40
  end
24
41
 
25
42
  def serialize
@@ -27,12 +27,12 @@ module Sentry
27
27
  end
28
28
 
29
29
  # @!visibility private
30
- def add_exception_interface(exception)
30
+ def add_exception_interface(exception, mechanism:)
31
31
  if exception.respond_to?(:sentry_context)
32
32
  @extra.merge!(exception.sentry_context)
33
33
  end
34
34
 
35
- @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder)
35
+ @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder, mechanism: mechanism)
36
36
  end
37
37
  end
38
38
  end
data/lib/sentry/event.rb CHANGED
@@ -14,16 +14,16 @@ module Sentry
14
14
  class Event
15
15
  TYPE = "event"
16
16
  # These are readable attributes.
17
- SERIALIZEABLE_ATTRIBUTES = %i(
17
+ SERIALIZEABLE_ATTRIBUTES = %i[
18
18
  event_id level timestamp
19
19
  release environment server_name modules
20
20
  message user tags contexts extra
21
21
  fingerprint breadcrumbs transaction transaction_info
22
22
  platform sdk type
23
- )
23
+ ]
24
24
 
25
25
  # These are writable attributes.
26
- WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i(type timestamp level)
26
+ WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i[type timestamp level]
27
27
 
28
28
  MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8
29
29
 
@@ -42,6 +42,9 @@ module Sentry
42
42
  # @return [Hash, nil]
43
43
  attr_accessor :dynamic_sampling_context
44
44
 
45
+ # @return [Array<Attachment>]
46
+ attr_accessor :attachments
47
+
45
48
  # @param configuration [Configuration]
46
49
  # @param integration_meta [Hash, nil]
47
50
  # @param message [String, nil]
@@ -57,6 +60,7 @@ module Sentry
57
60
  @extra = {}
58
61
  @contexts = {}
59
62
  @tags = {}
63
+ @attachments = []
60
64
 
61
65
  @fingerprint = []
62
66
  @dynamic_sampling_context = nil
@@ -76,34 +80,6 @@ module Sentry
76
80
  @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES)
77
81
  end
78
82
 
79
- class << self
80
- # @!visibility private
81
- def get_log_message(event_hash)
82
- message = event_hash[:message] || event_hash['message']
83
-
84
- return message unless message.nil? || message.empty?
85
-
86
- message = get_message_from_exception(event_hash)
87
-
88
- return message unless message.nil? || message.empty?
89
-
90
- message = event_hash[:transaction] || event_hash["transaction"]
91
-
92
- return message unless message.nil? || message.empty?
93
-
94
- '<no message value>'
95
- end
96
-
97
- # @!visibility private
98
- def get_message_from_exception(event_hash)
99
- if exception = event_hash.dig(:exception, :values, 0)
100
- "#{exception[:type]}: #{exception[:value]}"
101
- elsif exception = event_hash.dig("exception", "values", 0)
102
- "#{exception["type"]}: #{exception["value"]}"
103
- end
104
- end
105
- end
106
-
107
83
  # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration
108
84
  # @return [Configuration]
109
85
  def configuration
@@ -132,9 +108,7 @@ module Sentry
132
108
  unless request || env.empty?
133
109
  add_request_interface(env)
134
110
 
135
- if @send_default_pii
136
- user[:ip_address] = calculate_real_ip_from_rack(env)
137
- end
111
+ user[:ip_address] ||= calculate_real_ip_from_rack(env) if @send_default_pii
138
112
 
139
113
  if request_id = Utils::RequestId.read_from(env)
140
114
  tags[:request_id] = request_id
@@ -173,11 +147,11 @@ module Sentry
173
147
  # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
174
148
  def calculate_real_ip_from_rack(env)
175
149
  Utils::RealIp.new(
176
- :remote_addr => env["REMOTE_ADDR"],
177
- :client_ip => env["HTTP_CLIENT_IP"],
178
- :real_ip => env["HTTP_X_REAL_IP"],
179
- :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
180
- :trusted_proxies => @trusted_proxies
150
+ remote_addr: env["REMOTE_ADDR"],
151
+ client_ip: env["HTTP_CLIENT_IP"],
152
+ real_ip: env["HTTP_X_REAL_IP"],
153
+ forwarded_for: env["HTTP_X_FORWARDED_FOR"],
154
+ trusted_proxies: @trusted_proxies
181
155
  ).calculate_ip
182
156
  end
183
157
  end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Faraday
5
+ OP_NAME = "http.client"
6
+
7
+ module Connection
8
+ # Since there's no way to preconfigure Faraday connections and add our instrumentation
9
+ # by default, we need to extend the connection constructor and do it there
10
+ #
11
+ # @see https://lostisland.github.io/faraday/#/customization/index?id=configuration
12
+ def initialize(url = nil, options = nil)
13
+ super
14
+
15
+ # Ensure that we attach instrumentation only if the adapter is not net/http
16
+ # because if is is, then the net/http instrumentation will take care of it
17
+ if builder.adapter.name != "Faraday::Adapter::NetHttp"
18
+ # Make sure that it's going to be the first middleware so that it can capture
19
+ # the entire request processing involving other middlewares
20
+ builder.insert(0, ::Faraday::Request::Instrumentation, name: OP_NAME, instrumenter: Instrumenter.new)
21
+ end
22
+ end
23
+ end
24
+
25
+ class Instrumenter
26
+ SPAN_ORIGIN = "auto.http.faraday"
27
+ BREADCRUMB_CATEGORY = "http"
28
+
29
+ include Utils::HttpTracing
30
+
31
+ def instrument(op_name, env, &block)
32
+ return block.call unless Sentry.initialized?
33
+
34
+ Sentry.with_child_span(op: op_name, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span|
35
+ request_info = extract_request_info(env)
36
+
37
+ if propagate_trace?(request_info[:url])
38
+ set_propagation_headers(env[:request_headers])
39
+ end
40
+
41
+ res = block.call
42
+ response_status = res.status
43
+
44
+ if record_sentry_breadcrumb?
45
+ record_sentry_breadcrumb(request_info, response_status)
46
+ end
47
+
48
+ if sentry_span
49
+ set_span_info(sentry_span, request_info, response_status)
50
+ end
51
+
52
+ res
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def extract_request_info(env)
59
+ url = env[:url].scheme + "://" + env[:url].host + env[:url].path
60
+ result = { method: env[:method].to_s.upcase, url: url }
61
+
62
+ if Sentry.configuration.send_default_pii
63
+ result[:query] = env[:url].query
64
+ result[:body] = env[:body]
65
+ end
66
+
67
+ result
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ Sentry.register_patch(:faraday) do
74
+ if defined?(::Faraday)
75
+ ::Faraday::Connection.prepend(Sentry::Faraday::Connection)
76
+ end
77
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ Sentry.register_patch(:graphql) do |config|
4
+ if defined?(::GraphQL::Schema) && defined?(::GraphQL::Tracing::SentryTrace) && ::GraphQL::Schema.respond_to?(:trace_with)
5
+ ::GraphQL::Schema.trace_with(::GraphQL::Tracing::SentryTrace, set_transaction_name: true)
6
+ else
7
+ config.logger.warn(Sentry::LOGGER_PROGNAME) { 'You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile.' }
8
+ end
9
+ end
data/lib/sentry/hub.rb CHANGED
@@ -156,7 +156,7 @@ module Sentry
156
156
  capture_event(event, **options, &block)
157
157
  end
158
158
 
159
- def capture_check_in(slug, status, **options, &block)
159
+ def capture_check_in(slug, status, **options)
160
160
  check_argument_type!(slug, ::String)
161
161
  check_argument_includes!(status, Sentry::CheckInEvent::VALID_STATUSES)
162
162
 
@@ -176,7 +176,7 @@ module Sentry
176
176
 
177
177
  return unless event
178
178
 
179
- capture_event(event, **options, &block)
179
+ capture_event(event, **options)
180
180
  event.check_in_id
181
181
  end
182
182
 
@@ -193,7 +193,14 @@ module Sentry
193
193
  elsif custom_scope = options[:scope]
194
194
  scope.update_from_scope(custom_scope)
195
195
  elsif !options.empty?
196
- scope.update_from_options(**options)
196
+ unsupported_option_keys = scope.update_from_options(**options)
197
+
198
+ unless unsupported_option_keys.empty?
199
+ configuration.log_debug <<~MSG
200
+ Options #{unsupported_option_keys} are not supported and will not be applied to the event.
201
+ You may want to set them under the `extra` option.
202
+ MSG
203
+ end
197
204
  end
198
205
 
199
206
  event = current_client.capture_event(event, scope, hint)
@@ -245,7 +252,7 @@ module Sentry
245
252
  end
246
253
 
247
254
  def with_session_tracking(&block)
248
- return yield unless configuration.auto_session_tracking
255
+ return yield unless configuration.session_tracking?
249
256
 
250
257
  start_session
251
258
  yield
@@ -279,6 +286,12 @@ module Sentry
279
286
  headers
280
287
  end
281
288
 
289
+ def get_trace_propagation_meta
290
+ get_trace_propagation_headers.map do |k, v|
291
+ "<meta name=\"#{k}\" content=\"#{v}\">"
292
+ end.join("\n")
293
+ end
294
+
282
295
  def continue_trace(env, **options)
283
296
  configure_scope { |s| s.generate_propagation_context(env) }
284
297
 
@@ -14,6 +14,10 @@ module Sentry
14
14
  def capture_exception(exception, **options, &block)
15
15
  options[:hint] ||= {}
16
16
  options[:hint][:integration] = integration_name
17
+
18
+ # within an integration, we usually intercept uncaught exceptions so we set handled to false.
19
+ options[:hint][:mechanism] ||= Sentry::Mechanism.new(type: integration_name, handled: false)
20
+
17
21
  Sentry.capture_exception(exception, **options, &block)
18
22
  end
19
23
 
@@ -14,3 +14,4 @@ require "sentry/interfaces/request"
14
14
  require "sentry/interfaces/single_exception"
15
15
  require "sentry/interfaces/stacktrace"
16
16
  require "sentry/interfaces/threads"
17
+ require "sentry/interfaces/mechanism"
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "set"
3
4
 
4
5
  module Sentry
@@ -23,17 +24,18 @@ module Sentry
23
24
  # @param stacktrace_builder [StacktraceBuilder]
24
25
  # @see SingleExceptionInterface#build_with_stacktrace
25
26
  # @see SingleExceptionInterface#initialize
27
+ # @param mechanism [Mechanism]
26
28
  # @return [ExceptionInterface]
27
- def self.build(exception:, stacktrace_builder:)
29
+ def self.build(exception:, stacktrace_builder:, mechanism:)
28
30
  exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse
29
31
  processed_backtrace_ids = Set.new
30
32
 
31
33
  exceptions = exceptions.map do |e|
32
34
  if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id)
33
35
  processed_backtrace_ids << e.backtrace.object_id
34
- SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder)
36
+ SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder, mechanism: mechanism)
35
37
  else
36
- SingleExceptionInterface.new(exception: exception)
38
+ SingleExceptionInterface.new(exception: exception, mechanism: mechanism)
37
39
  end
38
40
  end
39
41
 
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Mechanism < Interface
5
+ # Generic identifier, mostly the source integration for this exception.
6
+ # @return [String]
7
+ attr_accessor :type
8
+
9
+ # A manually captured exception has handled set to true,
10
+ # false if coming from an integration where we intercept an uncaught exception.
11
+ # Defaults to true here and will be set to false explicitly in integrations.
12
+ # @return [Boolean]
13
+ attr_accessor :handled
14
+
15
+ def initialize(type: 'generic', handled: true)
16
+ @type = type
17
+ @handled = handled
18
+ end
19
+ end
20
+ end
@@ -2,8 +2,8 @@
2
2
 
3
3
  module Sentry
4
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
5
+ REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze
6
+ CONTENT_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze
7
7
  IP_HEADERS = [
8
8
  "REMOTE_ADDR",
9
9
  "HTTP_CLIENT_IP",
@@ -11,10 +11,10 @@ module Sentry
11
11
  OMISSION_MARK = "...".freeze
12
12
  MAX_LOCAL_BYTES = 1024
13
13
 
14
- attr_reader :type, :module, :thread_id, :stacktrace
14
+ attr_reader :type, :module, :thread_id, :stacktrace, :mechanism
15
15
  attr_accessor :value
16
16
 
17
- def initialize(exception:, stacktrace: nil)
17
+ def initialize(exception:, mechanism:, stacktrace: nil)
18
18
  @type = exception.class.to_s
19
19
  exception_message =
20
20
  if exception.respond_to?(:detailed_message)
@@ -22,23 +22,26 @@ module Sentry
22
22
  else
23
23
  exception.message || ""
24
24
  end
25
+ exception_message = exception_message.inspect unless exception_message.is_a?(String)
25
26
 
26
27
  @value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES))
27
28
 
28
29
  @module = exception.class.to_s.split('::')[0...-1].join('::')
29
30
  @thread_id = Thread.current.object_id
30
31
  @stacktrace = stacktrace
32
+ @mechanism = mechanism
31
33
  end
32
34
 
33
35
  def to_hash
34
36
  data = super
35
37
  data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
38
+ data[:mechanism] = data[:mechanism].to_hash
36
39
  data
37
40
  end
38
41
 
39
42
  # patch this method if you want to change an exception's stacktrace frames
40
43
  # also see `StacktraceBuilder.build`.
41
- def self.build_with_stacktrace(exception:, stacktrace_builder:)
44
+ def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism:)
42
45
  stacktrace = stacktrace_builder.build(backtrace: exception.backtrace)
43
46
 
44
47
  if locals = exception.instance_variable_get(:@sentry_locals)
@@ -60,7 +63,7 @@ module Sentry
60
63
  stacktrace.frames.last.vars = locals
61
64
  end
62
65
 
63
- new(exception: exception, stacktrace: stacktrace)
66
+ new(exception: exception, stacktrace: stacktrace, mechanism: mechanism)
64
67
  end
65
68
  end
66
69
  end
@@ -62,6 +62,14 @@ module Sentry
62
62
  StacktraceInterface.new(frames: frames)
63
63
  end
64
64
 
65
+ # Get the code location hash for a single line for where metrics where added.
66
+ # @return [Hash]
67
+ def metrics_code_location(unparsed_line)
68
+ parsed_line = Backtrace::Line.parse(unparsed_line)
69
+ frame = convert_parsed_line_into_frame(parsed_line)
70
+ frame.to_hash.reject { |k, _| %i[project_root in_app].include?(k) }
71
+ end
72
+
65
73
  private
66
74
 
67
75
  def convert_parsed_line_into_frame(line)