sentry-ruby 5.13.0 → 5.19.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/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)