sentry-ruby 5.16.1 → 5.20.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/README.md +20 -10
  4. data/Rakefile +1 -1
  5. data/bin/console +1 -0
  6. data/lib/sentry/attachment.rb +40 -0
  7. data/lib/sentry/background_worker.rb +1 -1
  8. data/lib/sentry/backpressure_monitor.rb +2 -32
  9. data/lib/sentry/backtrace.rb +8 -6
  10. data/lib/sentry/baggage.rb +6 -6
  11. data/lib/sentry/breadcrumb/sentry_logger.rb +6 -6
  12. data/lib/sentry/check_in_event.rb +5 -5
  13. data/lib/sentry/client.rb +61 -11
  14. data/lib/sentry/configuration.rb +53 -25
  15. data/lib/sentry/core_ext/object/deep_dup.rb +1 -1
  16. data/lib/sentry/cron/monitor_check_ins.rb +1 -1
  17. data/lib/sentry/cron/monitor_config.rb +1 -1
  18. data/lib/sentry/cron/monitor_schedule.rb +1 -1
  19. data/lib/sentry/dsn.rb +4 -4
  20. data/lib/sentry/envelope.rb +19 -2
  21. data/lib/sentry/error_event.rb +2 -2
  22. data/lib/sentry/event.rb +20 -18
  23. data/lib/sentry/faraday.rb +77 -0
  24. data/lib/sentry/graphql.rb +9 -0
  25. data/lib/sentry/hub.rb +23 -3
  26. data/lib/sentry/integrable.rb +4 -0
  27. data/lib/sentry/interface.rb +1 -0
  28. data/lib/sentry/interfaces/exception.rb +5 -3
  29. data/lib/sentry/interfaces/mechanism.rb +20 -0
  30. data/lib/sentry/interfaces/request.rb +7 -7
  31. data/lib/sentry/interfaces/single_exception.rb +7 -5
  32. data/lib/sentry/interfaces/stacktrace.rb +3 -1
  33. data/lib/sentry/interfaces/stacktrace_builder.rb +23 -2
  34. data/lib/sentry/logger.rb +1 -1
  35. data/lib/sentry/metrics/aggregator.rb +248 -0
  36. data/lib/sentry/metrics/configuration.rb +47 -0
  37. data/lib/sentry/metrics/counter_metric.rb +25 -0
  38. data/lib/sentry/metrics/distribution_metric.rb +25 -0
  39. data/lib/sentry/metrics/gauge_metric.rb +35 -0
  40. data/lib/sentry/metrics/local_aggregator.rb +53 -0
  41. data/lib/sentry/metrics/metric.rb +19 -0
  42. data/lib/sentry/metrics/set_metric.rb +28 -0
  43. data/lib/sentry/metrics/timing.rb +43 -0
  44. data/lib/sentry/metrics.rb +56 -0
  45. data/lib/sentry/net/http.rb +18 -39
  46. data/lib/sentry/profiler.rb +19 -20
  47. data/lib/sentry/propagation_context.rb +10 -9
  48. data/lib/sentry/puma.rb +1 -1
  49. data/lib/sentry/rack/capture_exceptions.rb +15 -3
  50. data/lib/sentry/rack.rb +2 -2
  51. data/lib/sentry/rake.rb +4 -2
  52. data/lib/sentry/redis.rb +2 -1
  53. data/lib/sentry/release_detector.rb +4 -4
  54. data/lib/sentry/scope.rb +36 -26
  55. data/lib/sentry/session.rb +2 -2
  56. data/lib/sentry/session_flusher.rb +7 -39
  57. data/lib/sentry/span.rb +46 -5
  58. data/lib/sentry/test_helper.rb +3 -2
  59. data/lib/sentry/threaded_periodic_worker.rb +39 -0
  60. data/lib/sentry/transaction.rb +17 -15
  61. data/lib/sentry/transaction_event.rb +6 -1
  62. data/lib/sentry/transport/configuration.rb +0 -1
  63. data/lib/sentry/transport/http_transport.rb +12 -12
  64. data/lib/sentry/transport.rb +18 -26
  65. data/lib/sentry/utils/argument_checking_helper.rb +6 -0
  66. data/lib/sentry/utils/env_helper.rb +21 -0
  67. data/lib/sentry/utils/http_tracing.rb +41 -0
  68. data/lib/sentry/utils/logging_helper.rb +0 -4
  69. data/lib/sentry/utils/real_ip.rb +2 -2
  70. data/lib/sentry/utils/request_id.rb +1 -1
  71. data/lib/sentry/version.rb +1 -1
  72. data/lib/sentry-ruby.rb +34 -3
  73. data/sentry-ruby-core.gemspec +1 -1
  74. data/sentry-ruby.gemspec +13 -6
  75. metadata +40 -7
@@ -4,10 +4,12 @@ require "concurrent/utility/processor_counter"
4
4
 
5
5
  require "sentry/utils/exception_cause_chain"
6
6
  require 'sentry/utils/custom_inspection'
7
+ require 'sentry/utils/env_helper'
7
8
  require "sentry/dsn"
8
9
  require "sentry/release_detector"
9
10
  require "sentry/transport/configuration"
10
11
  require "sentry/cron/configuration"
12
+ require "sentry/metrics/configuration"
11
13
  require "sentry/linecache"
12
14
  require "sentry/interfaces/stacktrace_builder"
13
15
 
@@ -21,6 +23,8 @@ module Sentry
21
23
  # have an `engines` dir at the root of your project, you may want
22
24
  # to set this to something like /(app|config|engines|lib)/
23
25
  #
26
+ # The default is value is /(bin|exe|app|config|lib|test|spec)/
27
+ #
24
28
  # @return [Regexp, nil]
25
29
  attr_accessor :app_dirs_pattern
26
30
 
@@ -186,6 +190,11 @@ module Sentry
186
190
  # @return [String]
187
191
  attr_accessor :project_root
188
192
 
193
+ # Whether to strip the load path while constructing the backtrace frame filename.
194
+ # Defaults to true.
195
+ # @return [Boolean]
196
+ attr_accessor :strip_backtrace_load_path
197
+
189
198
  # Insert sentry-trace to outgoing requests' headers
190
199
  # @return [Boolean]
191
200
  attr_accessor :propagate_traces
@@ -235,6 +244,10 @@ module Sentry
235
244
  # @return [Cron::Configuration]
236
245
  attr_reader :cron
237
246
 
247
+ # Metrics related configuration.
248
+ # @return [Metrics::Configuration]
249
+ attr_reader :metrics
250
+
238
251
  # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions).
239
252
  # @return [Float, nil]
240
253
  attr_reader :traces_sample_rate
@@ -297,25 +310,25 @@ module Sentry
297
310
  # But they are mostly considered as noise and should be ignored by default
298
311
  # Please see https://github.com/getsentry/sentry-ruby/pull/2026 for more information
299
312
  PUMA_IGNORE_DEFAULT = [
300
- 'Puma::MiniSSL::SSLError',
301
- 'Puma::HttpParserError',
302
- 'Puma::HttpParserError501'
313
+ "Puma::MiniSSL::SSLError",
314
+ "Puma::HttpParserError",
315
+ "Puma::HttpParserError501"
303
316
  ].freeze
304
317
 
305
318
  # Most of these errors generate 4XX responses. In general, Sentry clients
306
319
  # only automatically report 5xx responses.
307
320
  IGNORE_DEFAULT = [
308
- 'Mongoid::Errors::DocumentNotFound',
309
- 'Rack::QueryParser::InvalidParameterError',
310
- 'Rack::QueryParser::ParameterTypeError',
311
- 'Sinatra::NotFound'
321
+ "Mongoid::Errors::DocumentNotFound",
322
+ "Rack::QueryParser::InvalidParameterError",
323
+ "Rack::QueryParser::ParameterTypeError",
324
+ "Sinatra::NotFound"
312
325
  ].freeze
313
326
 
314
- RACK_ENV_WHITELIST_DEFAULT = %w(
327
+ RACK_ENV_WHITELIST_DEFAULT = %w[
315
328
  REMOTE_ADDR
316
329
  SERVER_NAME
317
330
  SERVER_PORT
318
- ).freeze
331
+ ].freeze
319
332
 
320
333
  HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\
321
334
  "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`".freeze
@@ -328,7 +341,9 @@ module Sentry
328
341
 
329
342
  PROPAGATION_TARGETS_MATCH_ALL = /.*/.freeze
330
343
 
331
- DEFAULT_PATCHES = %i(redis puma http).freeze
344
+ DEFAULT_PATCHES = %i[redis puma http].freeze
345
+
346
+ APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/.freeze
332
347
 
333
348
  class << self
334
349
  # Post initialization callbacks are called at the end of initialization process
@@ -337,18 +352,19 @@ module Sentry
337
352
  @post_initialization_callbacks ||= []
338
353
  end
339
354
 
340
- # allow extensions to add their hooks to the Configuration class
355
+ # allow extensions to add their hooks to the Configuration class
341
356
  def add_post_initialization_callback(&block)
342
357
  post_initialization_callbacks << block
343
358
  end
344
359
  end
345
360
 
346
361
  def initialize
347
- self.app_dirs_pattern = nil
348
- self.debug = false
349
- self.background_worker_threads = Concurrent.processor_count
362
+ self.app_dirs_pattern = APP_DIRS_PATTERN
363
+ self.debug = Sentry::Utils::EnvHelper.env_to_bool(ENV["SENTRY_DEBUG"])
364
+ self.background_worker_threads = (processor_count / 2.0).ceil
350
365
  self.background_worker_max_queue = BackgroundWorker::DEFAULT_MAX_QUEUE
351
366
  self.backtrace_cleanup_callback = nil
367
+ self.strip_backtrace_load_path = true
352
368
  self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE
353
369
  self.breadcrumbs_logger = []
354
370
  self.context_lines = 3
@@ -372,7 +388,10 @@ module Sentry
372
388
  self.enable_backpressure_handling = false
373
389
  self.trusted_proxies = []
374
390
  self.dsn = ENV['SENTRY_DSN']
375
- self.spotlight = false
391
+
392
+ spotlight_env = ENV['SENTRY_SPOTLIGHT']
393
+ spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true)
394
+ self.spotlight = spotlight_bool.nil? ? (spotlight_env || false) : spotlight_bool
376
395
  self.server_name = server_name_from_env
377
396
  self.instrumenter = :sentry
378
397
  self.trace_propagation_targets = [PROPAGATION_TARGETS_MATCH_ALL]
@@ -386,6 +405,7 @@ module Sentry
386
405
 
387
406
  @transport = Transport::Configuration.new
388
407
  @cron = Cron::Configuration.new
408
+ @metrics = Metrics::Configuration.new
389
409
  @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map)
390
410
 
391
411
  run_post_initialization_callbacks
@@ -479,9 +499,13 @@ module Sentry
479
499
  end
480
500
 
481
501
  def sending_allowed?
502
+ spotlight || sending_to_dsn_allowed?
503
+ end
504
+
505
+ def sending_to_dsn_allowed?
482
506
  @errors = []
483
507
 
484
- spotlight || (valid? && capture_in_environment?)
508
+ valid? && capture_in_environment?
485
509
  end
486
510
 
487
511
  def sample_allowed?
@@ -490,6 +514,10 @@ module Sentry
490
514
  Random.rand < sample_rate
491
515
  end
492
516
 
517
+ def session_tracking?
518
+ auto_session_tracking && enabled_in_current_env?
519
+ end
520
+
493
521
  def exception_class_allowed?(exc)
494
522
  if exc.is_a?(Sentry::Error)
495
523
  # Try to prevent error reporting loops
@@ -541,7 +569,8 @@ module Sentry
541
569
  app_dirs_pattern: @app_dirs_pattern,
542
570
  linecache: @linecache,
543
571
  context_lines: @context_lines,
544
- backtrace_cleanup_callback: @backtrace_cleanup_callback
572
+ backtrace_cleanup_callback: @backtrace_cleanup_callback,
573
+ strip_backtrace_load_path: @strip_backtrace_load_path
545
574
  )
546
575
  end
547
576
 
@@ -566,12 +595,6 @@ module Sentry
566
595
 
567
596
  private
568
597
 
569
- def check_callable!(name, value)
570
- unless value == nil || value.respond_to?(:call)
571
- raise ArgumentError, "#{name} must be callable (or nil to disable)"
572
- end
573
- end
574
-
575
598
  def init_dsn(dsn_string)
576
599
  return if dsn_string.nil? || dsn_string.empty?
577
600
 
@@ -624,12 +647,12 @@ module Sentry
624
647
  end
625
648
 
626
649
  def environment_from_env
627
- ENV['SENTRY_CURRENT_ENV'] || ENV['SENTRY_ENVIRONMENT'] || ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
650
+ ENV["SENTRY_CURRENT_ENV"] || ENV["SENTRY_ENVIRONMENT"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development"
628
651
  end
629
652
 
630
653
  def server_name_from_env
631
654
  if running_on_heroku?
632
- ENV['DYNO']
655
+ ENV["DYNO"]
633
656
  else
634
657
  # Try to resolve the hostname to an FQDN, but fall back to whatever
635
658
  # the load name is.
@@ -646,5 +669,10 @@ module Sentry
646
669
  instance_eval(&hook)
647
670
  end
648
671
  end
672
+
673
+ def processor_count
674
+ available_processor_count = Concurrent.available_processor_count if Concurrent.respond_to?(:available_processor_count)
675
+ available_processor_count || Concurrent.processor_count
676
+ end
649
677
  end
650
678
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  return if Object.method_defined?(:deep_dup)
4
4
 
5
- require 'sentry/core_ext/object/duplicable'
5
+ require "sentry/core_ext/object/duplicable"
6
6
 
7
7
  #########################################
8
8
  # This file was copied from Rails 5.2 #
@@ -57,7 +57,7 @@ module Sentry
57
57
 
58
58
  def sentry_monitor_slug(name: self.name)
59
59
  @sentry_monitor_slug ||= begin
60
- slug = name.gsub('::', '-').downcase
60
+ slug = name.gsub("::", "-").downcase
61
61
  slug[-MAX_SLUG_LENGTH..-1] || slug
62
62
  end
63
63
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'sentry/cron/monitor_schedule'
3
+ require "sentry/cron/monitor_schedule"
4
4
 
5
5
  module Sentry
6
6
  module Cron
@@ -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
@@ -4,8 +4,8 @@ require "uri"
4
4
 
5
5
  module Sentry
6
6
  class DSN
7
- PORT_MAP = { 'http' => 80, 'https' => 443 }.freeze
8
- REQUIRED_ATTRIBUTES = %w(host path public_key project_id).freeze
7
+ PORT_MAP = { "http" => 80, "https" => 443 }.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
 
@@ -13,7 +13,7 @@ module Sentry
13
13
  @raw_value = dsn_string
14
14
 
15
15
  uri = URI.parse(dsn_string)
16
- uri_path = uri.path.split('/')
16
+ uri_path = uri.path.split("/")
17
17
 
18
18
  if uri.user
19
19
  # DSN-style string
@@ -25,7 +25,7 @@ module Sentry
25
25
  @scheme = uri.scheme
26
26
  @host = uri.host
27
27
  @port = uri.port if uri.port
28
- @path = uri_path.join('/')
28
+ @path = uri_path.join("/")
29
29
  end
30
30
 
31
31
  def valid?
@@ -15,11 +15,28 @@ module Sentry
15
15
  end
16
16
 
17
17
  def type
18
- @headers[:type] || 'event'
18
+ @headers[:type] || "event"
19
+ end
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)
19
36
  end
20
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
@@ -1,12 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'socket'
4
- require 'securerandom'
5
- require 'sentry/interface'
6
- require 'sentry/backtrace'
7
- require 'sentry/utils/real_ip'
8
- require 'sentry/utils/request_id'
9
- require 'sentry/utils/custom_inspection'
3
+ require "socket"
4
+ require "securerandom"
5
+ require "sentry/interface"
6
+ require "sentry/backtrace"
7
+ require "sentry/utils/real_ip"
8
+ require "sentry/utils/request_id"
9
+ require "sentry/utils/custom_inspection"
10
10
 
11
11
  module Sentry
12
12
  # This is an abstract class that defines the shared attributes of an event.
@@ -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
@@ -104,9 +108,7 @@ module Sentry
104
108
  unless request || env.empty?
105
109
  add_request_interface(env)
106
110
 
107
- if @send_default_pii
108
- user[:ip_address] = calculate_real_ip_from_rack(env)
109
- end
111
+ user[:ip_address] ||= calculate_real_ip_from_rack(env) if @send_default_pii
110
112
 
111
113
  if request_id = Utils::RequestId.read_from(env)
112
114
  tags[:request_id] = request_id
@@ -145,11 +147,11 @@ module Sentry
145
147
  # REMOTE_ADDR to determine the Event IP, and must use other headers instead.
146
148
  def calculate_real_ip_from_rack(env)
147
149
  Utils::RealIp.new(
148
- :remote_addr => env["REMOTE_ADDR"],
149
- :client_ip => env["HTTP_CLIENT_IP"],
150
- :real_ip => env["HTTP_X_REAL_IP"],
151
- :forwarded_for => env["HTTP_X_FORWARDED_FOR"],
152
- :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
153
155
  ).calculate_ip
154
156
  end
155
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
@@ -73,7 +73,13 @@ module Sentry
73
73
  end
74
74
 
75
75
  def pop_scope
76
- @stack.pop
76
+ if @stack.size > 1
77
+ @stack.pop
78
+ else
79
+ # We never want to enter a situation where we have no scope and no client
80
+ client = current_client
81
+ @stack = [Layer.new(client, Scope.new)]
82
+ end
77
83
  end
78
84
 
79
85
  def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options)
@@ -193,7 +199,14 @@ module Sentry
193
199
  elsif custom_scope = options[:scope]
194
200
  scope.update_from_scope(custom_scope)
195
201
  elsif !options.empty?
196
- scope.update_from_options(**options)
202
+ unsupported_option_keys = scope.update_from_options(**options)
203
+
204
+ unless unsupported_option_keys.empty?
205
+ configuration.log_debug <<~MSG
206
+ Options #{unsupported_option_keys} are not supported and will not be applied to the event.
207
+ You may want to set them under the `extra` option.
208
+ MSG
209
+ end
197
210
  end
198
211
 
199
212
  event = current_client.capture_event(event, scope, hint)
@@ -207,6 +220,7 @@ module Sentry
207
220
  end
208
221
 
209
222
  def add_breadcrumb(breadcrumb, hint: {})
223
+ return unless current_client
210
224
  return unless configuration.enabled_in_current_env?
211
225
 
212
226
  if before_breadcrumb = current_client.configuration.before_breadcrumb
@@ -245,7 +259,7 @@ module Sentry
245
259
  end
246
260
 
247
261
  def with_session_tracking(&block)
248
- return yield unless configuration.auto_session_tracking
262
+ return yield unless configuration.session_tracking?
249
263
 
250
264
  start_session
251
265
  yield
@@ -279,6 +293,12 @@ module Sentry
279
293
  headers
280
294
  end
281
295
 
296
+ def get_trace_propagation_meta
297
+ get_trace_propagation_headers.map do |k, v|
298
+ "<meta name=\"#{k}\" content=\"#{v}\">"
299
+ end.join("\n")
300
+ end
301
+
282
302
  def continue_trace(env, **options)
283
303
  configure_scope { |s| s.generate_propagation_context(env) }
284
304
 
@@ -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",
@@ -59,7 +59,7 @@ module Sentry
59
59
  self.query_string = request.query_string
60
60
  end
61
61
 
62
- self.url = request.scheme && request.url.split('?').first
62
+ self.url = request.scheme && request.url.split("?").first
63
63
  self.method = request.request_method
64
64
 
65
65
  self.headers = filter_and_format_headers(env, send_default_pii)
@@ -85,14 +85,14 @@ module Sentry
85
85
  env.each_with_object({}) do |(key, value), memo|
86
86
  begin
87
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)
88
+ next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
89
89
  next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
90
90
  next if is_skippable_header?(key)
91
91
  next if key == "HTTP_AUTHORIZATION" && !send_default_pii
92
92
 
93
93
  # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
94
94
  key = key.sub(/^HTTP_/, "")
95
- key = key.split('_').map(&:capitalize).join('-')
95
+ key = key.split("_").map(&:capitalize).join("-")
96
96
 
97
97
  memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s)
98
98
  rescue StandardError => e
@@ -108,7 +108,7 @@ module Sentry
108
108
  def is_skippable_header?(key)
109
109
  key.upcase != key || # lower-case envs aren't real http headers
110
110
  key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
111
- !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
111
+ !(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key))
112
112
  end
113
113
 
114
114
  # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream
@@ -120,7 +120,7 @@ module Sentry
120
120
  rack_version = Gem::Version.new(::Rack.release)
121
121
  return false if rack_version >= Gem::Version.new("3.0")
122
122
 
123
- key == 'HTTP_VERSION' && value == protocol_version
123
+ key == "HTTP_VERSION" && value == protocol_version
124
124
  end
125
125
 
126
126
  def filter_and_format_env(env, rack_env_whitelist)