sentry-ruby 5.3.1 → 5.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (64) 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/Rakefile +13 -0
  10. data/bin/console +18 -0
  11. data/bin/setup +8 -0
  12. data/lib/sentry/background_worker.rb +72 -0
  13. data/lib/sentry/backtrace.rb +124 -0
  14. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  15. data/lib/sentry/breadcrumb.rb +70 -0
  16. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  17. data/lib/sentry/client.rb +190 -0
  18. data/lib/sentry/configuration.rb +502 -0
  19. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  20. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  21. data/lib/sentry/dsn.rb +53 -0
  22. data/lib/sentry/envelope.rb +96 -0
  23. data/lib/sentry/error_event.rb +38 -0
  24. data/lib/sentry/event.rb +178 -0
  25. data/lib/sentry/exceptions.rb +9 -0
  26. data/lib/sentry/hub.rb +220 -0
  27. data/lib/sentry/integrable.rb +26 -0
  28. data/lib/sentry/interface.rb +16 -0
  29. data/lib/sentry/interfaces/exception.rb +43 -0
  30. data/lib/sentry/interfaces/request.rb +144 -0
  31. data/lib/sentry/interfaces/single_exception.rb +57 -0
  32. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  33. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  34. data/lib/sentry/interfaces/threads.rb +42 -0
  35. data/lib/sentry/linecache.rb +47 -0
  36. data/lib/sentry/logger.rb +20 -0
  37. data/lib/sentry/net/http.rb +115 -0
  38. data/lib/sentry/rack/capture_exceptions.rb +80 -0
  39. data/lib/sentry/rack.rb +5 -0
  40. data/lib/sentry/rake.rb +41 -0
  41. data/lib/sentry/redis.rb +90 -0
  42. data/lib/sentry/release_detector.rb +39 -0
  43. data/lib/sentry/scope.rb +295 -0
  44. data/lib/sentry/session.rb +35 -0
  45. data/lib/sentry/session_flusher.rb +90 -0
  46. data/lib/sentry/span.rb +226 -0
  47. data/lib/sentry/test_helper.rb +76 -0
  48. data/lib/sentry/transaction.rb +206 -0
  49. data/lib/sentry/transaction_event.rb +29 -0
  50. data/lib/sentry/transport/configuration.rb +25 -0
  51. data/lib/sentry/transport/dummy_transport.rb +21 -0
  52. data/lib/sentry/transport/http_transport.rb +175 -0
  53. data/lib/sentry/transport.rb +210 -0
  54. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  55. data/lib/sentry/utils/custom_inspection.rb +14 -0
  56. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  57. data/lib/sentry/utils/logging_helper.rb +26 -0
  58. data/lib/sentry/utils/real_ip.rb +84 -0
  59. data/lib/sentry/utils/request_id.rb +18 -0
  60. data/lib/sentry/version.rb +5 -0
  61. data/lib/sentry-ruby.rb +505 -0
  62. data/sentry-ruby-core.gemspec +23 -0
  63. data/sentry-ruby.gemspec +24 -0
  64. 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'