sentry-ruby 5.1.0 → 5.4.2

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +27 -0
  8. data/Makefile +4 -0
  9. data/README.md +8 -7
  10. data/Rakefile +13 -0
  11. data/bin/console +18 -0
  12. data/bin/setup +8 -0
  13. data/lib/sentry/background_worker.rb +72 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  16. data/lib/sentry/breadcrumb.rb +70 -0
  17. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  18. data/lib/sentry/client.rb +190 -0
  19. data/lib/sentry/configuration.rb +502 -0
  20. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  21. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  22. data/lib/sentry/dsn.rb +53 -0
  23. data/lib/sentry/envelope.rb +96 -0
  24. data/lib/sentry/error_event.rb +38 -0
  25. data/lib/sentry/event.rb +178 -0
  26. data/lib/sentry/exceptions.rb +9 -0
  27. data/lib/sentry/hub.rb +220 -0
  28. data/lib/sentry/integrable.rb +26 -0
  29. data/lib/sentry/interface.rb +16 -0
  30. data/lib/sentry/interfaces/exception.rb +43 -0
  31. data/lib/sentry/interfaces/request.rb +144 -0
  32. data/lib/sentry/interfaces/single_exception.rb +57 -0
  33. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  34. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  35. data/lib/sentry/interfaces/threads.rb +42 -0
  36. data/lib/sentry/linecache.rb +47 -0
  37. data/lib/sentry/logger.rb +20 -0
  38. data/lib/sentry/net/http.rb +115 -0
  39. data/lib/sentry/rack/capture_exceptions.rb +80 -0
  40. data/lib/sentry/rack.rb +5 -0
  41. data/lib/sentry/rake.rb +41 -0
  42. data/lib/sentry/redis.rb +90 -0
  43. data/lib/sentry/release_detector.rb +39 -0
  44. data/lib/sentry/scope.rb +295 -0
  45. data/lib/sentry/session.rb +35 -0
  46. data/lib/sentry/session_flusher.rb +90 -0
  47. data/lib/sentry/span.rb +226 -0
  48. data/lib/sentry/test_helper.rb +76 -0
  49. data/lib/sentry/transaction.rb +206 -0
  50. data/lib/sentry/transaction_event.rb +29 -0
  51. data/lib/sentry/transport/configuration.rb +25 -0
  52. data/lib/sentry/transport/dummy_transport.rb +21 -0
  53. data/lib/sentry/transport/http_transport.rb +175 -0
  54. data/lib/sentry/transport.rb +210 -0
  55. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  56. data/lib/sentry/utils/custom_inspection.rb +14 -0
  57. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  58. data/lib/sentry/utils/logging_helper.rb +26 -0
  59. data/lib/sentry/utils/real_ip.rb +84 -0
  60. data/lib/sentry/utils/request_id.rb +18 -0
  61. data/lib/sentry/version.rb +5 -0
  62. data/lib/sentry-ruby.rb +505 -0
  63. data/sentry-ruby-core.gemspec +23 -0
  64. data/sentry-ruby.gemspec +24 -0
  65. metadata +64 -16
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
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
7
+ IP_HEADERS = [
8
+ "REMOTE_ADDR",
9
+ "HTTP_CLIENT_IP",
10
+ "HTTP_X_REAL_IP",
11
+ "HTTP_X_FORWARDED_FOR"
12
+ ].freeze
13
+
14
+ # See Sentry server default limits at
15
+ # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
16
+ MAX_BODY_LIMIT = 4096 * 4
17
+
18
+ # @return [String]
19
+ attr_accessor :url
20
+
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
32
+
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
48
+ # need to completely wipe out ip addresses
49
+ RequestInterface::IP_HEADERS.each do |header|
50
+ env.delete(header)
51
+ end
52
+ end
53
+
54
+ request = ::Rack::Request.new(env)
55
+
56
+ if send_default_pii
57
+ self.data = read_data_from(request)
58
+ self.cookies = request.cookies
59
+ self.query_string = request.query_string
60
+ end
61
+
62
+ self.url = request.scheme && request.url.split('?').first
63
+ self.method = request.request_method
64
+
65
+ self.headers = filter_and_format_headers(env, send_default_pii)
66
+ self.env = filter_and_format_env(env, rack_env_whitelist)
67
+ end
68
+
69
+ private
70
+
71
+ def read_data_from(request)
72
+ if request.form_data?
73
+ request.POST
74
+ elsif request.body # JSON requests, etc
75
+ data = request.body.read(MAX_BODY_LIMIT)
76
+ data = encode_to_utf_8(data.to_s)
77
+ request.body.rewind
78
+ data
79
+ end
80
+ rescue IOError => e
81
+ e.message
82
+ end
83
+
84
+ def filter_and_format_headers(env, send_default_pii)
85
+ env.each_with_object({}) do |(key, value), memo|
86
+ begin
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)
89
+ next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
90
+ next if is_skippable_header?(key)
91
+ next if key == "HTTP_AUTHORIZATION" && !send_default_pii
92
+
93
+ # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
94
+ key = key.sub(/^HTTP_/, "")
95
+ key = key.split('_').map(&:capitalize).join('-')
96
+
97
+ memo[key] = encode_to_utf_8(value.to_s)
98
+ rescue StandardError => e
99
+ # Rails adds objects to the Rack env that can sometimes raise exceptions
100
+ # when `to_s` is called.
101
+ # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134
102
+ Sentry.logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
103
+ next
104
+ end
105
+ end
106
+ end
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
+
120
+ def is_skippable_header?(key)
121
+ key.upcase != key || # lower-case envs aren't real http headers
122
+ key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
123
+ !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
124
+ end
125
+
126
+ # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
127
+ # to think this is a Version header. Instead, this is mapped to
128
+ # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
129
+ # if the request has legitimately sent a Version header themselves.
130
+ # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
131
+ # NOTE: This will be removed in version 3.0+
132
+ def is_server_protocol?(key, value, protocol_version)
133
+ key == 'HTTP_VERSION' && value == protocol_version
134
+ end
135
+
136
+ def filter_and_format_env(env, rack_env_whitelist)
137
+ return env if rack_env_whitelist.empty?
138
+
139
+ env.select do |k, _v|
140
+ rack_env_whitelist.include? k.to_s
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/utils/exception_cause_chain"
4
+
5
+ module Sentry
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
+
14
+ attr_reader :type, :value, :module, :thread_id, :stacktrace
15
+
16
+ def initialize(exception:, stacktrace: nil)
17
+ @type = exception.class.to_s
18
+ @value = (exception.message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
19
+ @module = exception.class.to_s.split('::')[0...-1].join('::')
20
+ @thread_id = Thread.current.object_id
21
+ @stacktrace = stacktrace
22
+ end
23
+
24
+ def to_hash
25
+ data = super
26
+ data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
27
+ data
28
+ end
29
+
30
+ # patch this method if you want to change an exception's stacktrace frames
31
+ # also see `StacktraceBuilder.build`.
32
+ def self.build_with_stacktrace(exception:, stacktrace_builder:)
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
+
54
+ new(exception: exception, stacktrace: stacktrace)
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class StacktraceInterface
5
+ # @return [<Array[Frame]>]
6
+ attr_reader :frames
7
+
8
+ # @param frames [<Array[Frame]>]
9
+ def initialize(frames:)
10
+ @frames = frames
11
+ end
12
+
13
+ # @return [Hash]
14
+ def to_hash
15
+ { frames: @frames.map(&:to_hash) }
16
+ end
17
+
18
+ # @return [String]
19
+ def inspect
20
+ @frames.map(&:to_s)
21
+ end
22
+
23
+ private
24
+
25
+ # Not actually an interface, but I want to use the same style
26
+ class Frame < Interface
27
+ attr_accessor :abs_path, :context_line, :function, :in_app, :filename,
28
+ :lineno, :module, :pre_context, :post_context, :vars
29
+
30
+ def initialize(project_root, line)
31
+ @project_root = project_root
32
+
33
+ @abs_path = line.file
34
+ @function = line.method if line.method
35
+ @lineno = line.number
36
+ @in_app = line.in_app
37
+ @module = line.module_name if line.module_name
38
+ @filename = compute_filename
39
+ end
40
+
41
+ def to_s
42
+ "#{@filename}:#{@lineno}"
43
+ end
44
+
45
+ def compute_filename
46
+ return if abs_path.nil?
47
+
48
+ prefix =
49
+ if under_project_root? && in_app
50
+ @project_root
51
+ elsif under_project_root?
52
+ longest_load_path || @project_root
53
+ else
54
+ longest_load_path
55
+ end
56
+
57
+ prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
58
+ end
59
+
60
+ def set_context(linecache, context_lines)
61
+ return unless abs_path
62
+
63
+ @pre_context, @context_line, @post_context = \
64
+ linecache.get_file_context(abs_path, lineno, context_lines)
65
+ end
66
+
67
+ def to_hash(*args)
68
+ data = super(*args)
69
+ data.delete(:vars) unless vars && !vars.empty?
70
+ data.delete(:pre_context) unless pre_context && !pre_context.empty?
71
+ data.delete(:post_context) unless post_context && !post_context.empty?
72
+ data.delete(:context_line) unless context_line && !context_line.empty?
73
+ data
74
+ end
75
+
76
+ private
77
+
78
+ def under_project_root?
79
+ @project_root && abs_path.start_with?(@project_root)
80
+ end
81
+
82
+ def longest_load_path
83
+ $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class StacktraceBuilder
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
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
30
+ def initialize(project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
31
+ @project_root = project_root
32
+ @app_dirs_pattern = app_dirs_pattern
33
+ @linecache = linecache
34
+ @context_lines = context_lines
35
+ @backtrace_cleanup_callback = backtrace_cleanup_callback
36
+ end
37
+
38
+ # Generates a StacktraceInterface with the given backtrace.
39
+ # You can pass a block to customize/exclude frames:
40
+ #
41
+ # @example
42
+ # builder.build(backtrace) do |frame|
43
+ # if frame.module.match?(/a_gem/)
44
+ # nil
45
+ # else
46
+ # frame
47
+ # end
48
+ # end
49
+ # @param backtrace [Array<String>]
50
+ # @param frame_callback [Proc]
51
+ # @yieldparam frame [StacktraceInterface::Frame]
52
+ # @return [StacktraceInterface]
53
+ def build(backtrace:, &frame_callback)
54
+ parsed_lines = parse_backtrace_lines(backtrace).select(&:file)
55
+
56
+ frames = parsed_lines.reverse.map do |line|
57
+ frame = convert_parsed_line_into_frame(line)
58
+ frame = frame_callback.call(frame) if frame_callback
59
+ frame
60
+ end.compact
61
+
62
+ StacktraceInterface.new(frames: frames)
63
+ end
64
+
65
+ private
66
+
67
+ def convert_parsed_line_into_frame(line)
68
+ frame = StacktraceInterface::Frame.new(project_root, line)
69
+ frame.set_context(linecache, context_lines) if context_lines
70
+ frame
71
+ end
72
+
73
+ def parse_backtrace_lines(backtrace)
74
+ Backtrace.parse(
75
+ backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback
76
+ ).lines
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class ThreadsInterface
5
+ # @param crashed [Boolean]
6
+ # @param stacktrace [Array]
7
+ def initialize(crashed: false, stacktrace: nil)
8
+ @id = Thread.current.object_id
9
+ @name = Thread.current.name
10
+ @current = true
11
+ @crashed = crashed
12
+ @stacktrace = stacktrace
13
+ end
14
+
15
+ # @return [Hash]
16
+ def to_hash
17
+ {
18
+ values: [
19
+ {
20
+ id: @id,
21
+ name: @name,
22
+ crashed: @crashed,
23
+ current: @current,
24
+ stacktrace: @stacktrace&.to_hash
25
+ }
26
+ ]
27
+ }
28
+ end
29
+
30
+ # Builds the ThreadsInterface with given backtrace and stacktrace_builder.
31
+ # Patch this method if you want to change a threads interface's stacktrace frames.
32
+ # @see StacktraceBuilder.build
33
+ # @param backtrace [Array]
34
+ # @param stacktrace_builder [StacktraceBuilder]
35
+ # @param crashed [Hash]
36
+ # @return [ThreadsInterface]
37
+ def self.build(backtrace:, stacktrace_builder:, **options)
38
+ stacktrace = stacktrace_builder.build(backtrace: backtrace) if backtrace
39
+ new(**options, stacktrace: stacktrace)
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class LineCache
6
+ def initialize
7
+ @cache = {}
8
+ end
9
+
10
+ # Any linecache you provide to Sentry must implement this method.
11
+ # Returns an Array of Strings representing the lines in the source
12
+ # file. The number of lines retrieved is (2 * context) + 1, the middle
13
+ # line should be the line requested by lineno. See specs for more information.
14
+ def get_file_context(filename, lineno, context)
15
+ return nil, nil, nil unless valid_path?(filename)
16
+
17
+ lines = Array.new(2 * context + 1) do |i|
18
+ getline(filename, lineno - context + i)
19
+ end
20
+ [lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]]
21
+ end
22
+
23
+ private
24
+
25
+ def valid_path?(path)
26
+ lines = getlines(path)
27
+ !lines.nil?
28
+ end
29
+
30
+ def getlines(path)
31
+ @cache[path] ||= begin
32
+ IO.readlines(path)
33
+ rescue
34
+ nil
35
+ end
36
+ end
37
+
38
+ def getline(path, n)
39
+ return nil if n < 1
40
+
41
+ lines = getlines(path)
42
+ return nil if lines.nil?
43
+
44
+ lines[n - 1]
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Sentry
6
+ class Logger < ::Logger
7
+ LOG_PREFIX = "** [Sentry] "
8
+ PROGNAME = "sentry"
9
+
10
+ def initialize(*)
11
+ super
12
+ @level = ::Logger::INFO
13
+ original_formatter = ::Logger::Formatter.new
14
+ @default_formatter = proc do |severity, datetime, _progname, msg|
15
+ msg = "#{LOG_PREFIX}#{msg}"
16
+ original_formatter.call(severity, datetime, PROGNAME, msg)
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+
5
+ module Sentry
6
+ # @api private
7
+ module Net
8
+ module HTTP
9
+ OP_NAME = "http.client"
10
+ BREADCRUMB_CATEGORY = "net.http"
11
+
12
+ # To explain how the entire thing works, we need to know how the original Net::HTTP#request works
13
+ # Here's part of its definition. As you can see, it usually calls itself inside a #start block
14
+ #
15
+ # ```
16
+ # def request(req, body = nil, &block)
17
+ # unless started?
18
+ # start {
19
+ # req['connection'] ||= 'close'
20
+ # return request(req, body, &block) # <- request will be called for the second time from the first call
21
+ # }
22
+ # end
23
+ # # .....
24
+ # end
25
+ # ```
26
+ #
27
+ # So we're only instrumenting request when `Net::HTTP` is already started
28
+ def request(req, body = nil, &block)
29
+ return super unless started?
30
+
31
+ sentry_span = start_sentry_span
32
+ set_sentry_trace_header(req, sentry_span)
33
+
34
+ super.tap do |res|
35
+ record_sentry_breadcrumb(req, res)
36
+ record_sentry_span(req, res, sentry_span)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ def set_sentry_trace_header(req, sentry_span)
43
+ return unless sentry_span
44
+
45
+ trace = Sentry.get_current_client.generate_sentry_trace(sentry_span)
46
+ req[SENTRY_TRACE_HEADER_NAME] = trace if trace
47
+ end
48
+
49
+ def record_sentry_breadcrumb(req, res)
50
+ return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
51
+ return if from_sentry_sdk?
52
+
53
+ request_info = extract_request_info(req)
54
+
55
+ crumb = Sentry::Breadcrumb.new(
56
+ level: :info,
57
+ category: BREADCRUMB_CATEGORY,
58
+ type: :info,
59
+ data: {
60
+ status: res.code.to_i,
61
+ **request_info
62
+ }
63
+ )
64
+ Sentry.add_breadcrumb(crumb)
65
+ end
66
+
67
+ def record_sentry_span(req, res, sentry_span)
68
+ return unless Sentry.initialized? && sentry_span
69
+
70
+ request_info = extract_request_info(req)
71
+ sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
72
+ sentry_span.set_data(:status, res.code.to_i)
73
+ finish_sentry_span(sentry_span)
74
+ end
75
+
76
+ def start_sentry_span
77
+ return unless Sentry.initialized? && span = Sentry.get_current_scope.get_span
78
+ return if from_sentry_sdk?
79
+ return if span.sampled == false
80
+
81
+ span.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f)
82
+ end
83
+
84
+ def finish_sentry_span(sentry_span)
85
+ return unless Sentry.initialized? && sentry_span
86
+
87
+ sentry_span.set_timestamp(Sentry.utc_now.to_f)
88
+ end
89
+
90
+ def from_sentry_sdk?
91
+ dsn = Sentry.configuration.dsn
92
+ dsn && dsn.host == self.address
93
+ end
94
+
95
+ def extract_request_info(req)
96
+ uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}#{req.path}")
97
+ url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
98
+
99
+ result = { method: req.method, url: url }
100
+
101
+ if Sentry.configuration.send_default_pii
102
+ result[:url] = result[:url] + "?#{uri.query}"
103
+ result[:body] = req.body
104
+ end
105
+
106
+ result
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ Sentry.register_patch do
113
+ patch = Sentry::Net::HTTP
114
+ Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch)
115
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ module Rack
5
+ class CaptureExceptions
6
+ ERROR_EVENT_ID_KEY = "sentry.error_event_id"
7
+
8
+ def initialize(app)
9
+ @app = app
10
+ end
11
+
12
+ def call(env)
13
+ return @app.call(env) unless Sentry.initialized?
14
+
15
+ # make sure the current thread has a clean hub
16
+ Sentry.clone_hub_to_current_thread
17
+
18
+ Sentry.with_scope do |scope|
19
+ Sentry.with_session_tracking do
20
+ scope.clear_breadcrumbs
21
+ scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
22
+ scope.set_rack_env(env)
23
+
24
+ transaction = start_transaction(env, scope)
25
+ scope.set_span(transaction) if transaction
26
+
27
+ begin
28
+ response = @app.call(env)
29
+ rescue Sentry::Error
30
+ finish_transaction(transaction, 500)
31
+ raise # Don't capture Sentry errors
32
+ rescue Exception => e
33
+ capture_exception(e, env)
34
+ finish_transaction(transaction, 500)
35
+ raise
36
+ end
37
+
38
+ exception = collect_exception(env)
39
+ capture_exception(exception, env) if exception
40
+
41
+ finish_transaction(transaction, response[0])
42
+
43
+ response
44
+ end
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def collect_exception(env)
51
+ env['rack.exception'] || env['sinatra.error']
52
+ end
53
+
54
+ def transaction_op
55
+ "rack.request".freeze
56
+ end
57
+
58
+ def capture_exception(exception, env)
59
+ Sentry.capture_exception(exception).tap do |event|
60
+ env[ERROR_EVENT_ID_KEY] = event.event_id if event
61
+ end
62
+ end
63
+
64
+ def start_transaction(env, scope)
65
+ sentry_trace = env["HTTP_SENTRY_TRACE"]
66
+ options = { name: scope.transaction_name, op: transaction_op }
67
+ transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, **options) if sentry_trace
68
+ Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
69
+ end
70
+
71
+
72
+ def finish_transaction(transaction, status_code)
73
+ return unless transaction
74
+
75
+ transaction.set_http_status(status_code)
76
+ transaction.finish
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ require 'sentry/rack/capture_exceptions'