sentry-ruby 4.0.0 → 4.1.3
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +78 -0
- data/Gemfile +2 -2
- data/README.md +49 -11
- data/lib/sentry-ruby.rb +40 -18
- data/lib/sentry/background_worker.rb +37 -0
- data/lib/sentry/backtrace.rb +3 -5
- data/lib/sentry/client.rb +26 -10
- data/lib/sentry/configuration.rb +14 -5
- data/lib/sentry/event.rb +27 -33
- data/lib/sentry/hub.rb +7 -7
- data/lib/sentry/integrable.rb +24 -0
- data/lib/sentry/interfaces/request.rb +50 -31
- data/lib/sentry/interfaces/stacktrace.rb +39 -6
- data/lib/sentry/rack.rb +2 -3
- data/lib/sentry/rack/capture_exceptions.rb +62 -0
- data/lib/sentry/rack/deprecations.rb +19 -0
- data/lib/sentry/rake.rb +17 -0
- data/lib/sentry/scope.rb +2 -2
- data/lib/sentry/span.rb +5 -28
- data/lib/sentry/transaction.rb +44 -0
- data/lib/sentry/transport.rb +12 -21
- data/lib/sentry/transport/http_transport.rb +3 -6
- data/lib/sentry/utils/request_id.rb +2 -2
- data/lib/sentry/version.rb +1 -1
- data/sentry-ruby.gemspec +1 -0
- metadata +27 -5
- data/lib/sentry/rack/capture_exception.rb +0 -45
- data/lib/sentry/rack/tracing.rb +0 -39
- data/lib/sentry/transport/state.rb +0 -40
data/lib/sentry/configuration.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "concurrent/utility/processor_counter"
|
2
|
+
|
1
3
|
require "sentry/utils/exception_cause_chain"
|
2
4
|
require "sentry/dsn"
|
3
5
|
require "sentry/transport/configuration"
|
@@ -13,7 +15,15 @@ module Sentry
|
|
13
15
|
# Provide an object that responds to `call` to send events asynchronously.
|
14
16
|
# E.g.: lambda { |event| Thread.new { Sentry.send_event(event) } }
|
15
17
|
attr_reader :async
|
16
|
-
|
18
|
+
|
19
|
+
# to send events in a non-blocking way, sentry-ruby has its own background worker
|
20
|
+
# by default, the worker holds a thread pool that has [the number of processors] threads
|
21
|
+
# but you can configure it with this configuration option
|
22
|
+
# E.g.: config.background_worker_threads = 5
|
23
|
+
#
|
24
|
+
# if you want to send events synchronously, set the value to 0
|
25
|
+
# E.g.: config.background_worker_threads = 0
|
26
|
+
attr_accessor :background_worker_threads
|
17
27
|
|
18
28
|
# a proc/lambda that takes an array of stack traces
|
19
29
|
# it'll be used to silence (reduce) backtrace of the exception
|
@@ -123,7 +133,6 @@ module Sentry
|
|
123
133
|
# Most of these errors generate 4XX responses. In general, Sentry clients
|
124
134
|
# only automatically report 5xx responses.
|
125
135
|
IGNORE_DEFAULT = [
|
126
|
-
'CGI::Session::CookieStore::TamperedWithCookie',
|
127
136
|
'Mongoid::Errors::DocumentNotFound',
|
128
137
|
'Rack::QueryParser::InvalidParameterError',
|
129
138
|
'Rack::QueryParser::ParameterTypeError',
|
@@ -145,7 +154,7 @@ module Sentry
|
|
145
154
|
AVAILABLE_BREADCRUMBS_LOGGERS = [:sentry_logger, :active_support_logger].freeze
|
146
155
|
|
147
156
|
def initialize
|
148
|
-
self.
|
157
|
+
self.background_worker_threads = Concurrent.processor_count
|
149
158
|
self.breadcrumbs_logger = []
|
150
159
|
self.context_lines = 3
|
151
160
|
self.environment = environment_from_env
|
@@ -182,8 +191,8 @@ module Sentry
|
|
182
191
|
|
183
192
|
|
184
193
|
def async=(value)
|
185
|
-
|
186
|
-
raise(ArgumentError, "async must be callable
|
194
|
+
if value && !value.respond_to?(:call)
|
195
|
+
raise(ArgumentError, "async must be callable")
|
187
196
|
end
|
188
197
|
|
189
198
|
@async = value
|
data/lib/sentry/event.rb
CHANGED
@@ -18,9 +18,9 @@ module Sentry
|
|
18
18
|
)
|
19
19
|
|
20
20
|
attr_accessor(*ATTRIBUTES)
|
21
|
-
attr_reader :configuration
|
21
|
+
attr_reader :configuration, :request, :exception, :stacktrace
|
22
22
|
|
23
|
-
def initialize(configuration:, message: nil)
|
23
|
+
def initialize(configuration:, integration_meta: nil, message: nil)
|
24
24
|
# this needs to go first because some setters rely on configuration
|
25
25
|
@configuration = configuration
|
26
26
|
|
@@ -28,7 +28,7 @@ module Sentry
|
|
28
28
|
@event_id = SecureRandom.uuid.delete("-")
|
29
29
|
@timestamp = Sentry.utc_now.iso8601
|
30
30
|
@platform = :ruby
|
31
|
-
@sdk = Sentry.sdk_meta
|
31
|
+
@sdk = integration_meta || Sentry.sdk_meta
|
32
32
|
|
33
33
|
@user = {}
|
34
34
|
@extra = {}
|
@@ -75,14 +75,15 @@ module Sentry
|
|
75
75
|
end
|
76
76
|
|
77
77
|
def rack_env=(env)
|
78
|
-
unless
|
79
|
-
|
80
|
-
|
81
|
-
|
78
|
+
unless request || env.empty?
|
79
|
+
env = env.dup
|
80
|
+
|
81
|
+
add_request_interface(env)
|
82
82
|
|
83
|
-
if configuration.send_default_pii
|
84
|
-
user[:ip_address] =
|
83
|
+
if configuration.send_default_pii
|
84
|
+
user[:ip_address] = calculate_real_ip_from_rack(env)
|
85
85
|
end
|
86
|
+
|
86
87
|
if request_id = Utils::RequestId.read_from(env)
|
87
88
|
tags[:request_id] = request_id
|
88
89
|
end
|
@@ -96,9 +97,9 @@ module Sentry
|
|
96
97
|
def to_hash
|
97
98
|
data = serialize_attributes
|
98
99
|
data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs
|
99
|
-
data[:stacktrace] =
|
100
|
-
data[:request] =
|
101
|
-
data[:exception] =
|
100
|
+
data[:stacktrace] = stacktrace.to_hash if stacktrace
|
101
|
+
data[:request] = request.to_hash if request
|
102
|
+
data[:exception] = exception.to_hash if exception
|
102
103
|
|
103
104
|
data
|
104
105
|
end
|
@@ -107,6 +108,10 @@ module Sentry
|
|
107
108
|
JSON.parse(JSON.generate(to_hash))
|
108
109
|
end
|
109
110
|
|
111
|
+
def add_request_interface(env)
|
112
|
+
@request = Sentry::RequestInterface.from_rack(env)
|
113
|
+
end
|
114
|
+
|
110
115
|
def add_exception_interface(exc)
|
111
116
|
if exc.respond_to?(:sentry_context)
|
112
117
|
@extra.merge!(exc.sentry_context)
|
@@ -124,33 +129,22 @@ module Sentry
|
|
124
129
|
int.stacktrace =
|
125
130
|
if e.backtrace && !backtraces.include?(e.backtrace.object_id)
|
126
131
|
backtraces << e.backtrace.object_id
|
127
|
-
|
128
|
-
stacktrace.frames = stacktrace_interface_from(e.backtrace)
|
129
|
-
end
|
132
|
+
initialize_stacktrace_interface(e.backtrace)
|
130
133
|
end
|
131
134
|
end
|
132
135
|
end
|
133
136
|
end
|
134
137
|
end
|
135
138
|
|
136
|
-
def
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
frame.module = line.module_name if line.module_name
|
146
|
-
|
147
|
-
if configuration.context_lines && frame.abs_path
|
148
|
-
frame.pre_context, frame.context_line, frame.post_context = \
|
149
|
-
configuration.linecache.get_file_context(frame.abs_path, frame.lineno, configuration.context_lines)
|
150
|
-
end
|
151
|
-
|
152
|
-
memo << frame if frame.filename
|
153
|
-
end
|
139
|
+
def initialize_stacktrace_interface(backtrace)
|
140
|
+
StacktraceInterface.new(
|
141
|
+
backtrace: backtrace,
|
142
|
+
project_root: configuration.project_root.to_s,
|
143
|
+
app_dirs_pattern: configuration.app_dirs_pattern,
|
144
|
+
linecache: configuration.linecache,
|
145
|
+
context_lines: configuration.context_lines,
|
146
|
+
backtrace_cleanup_callback: configuration.backtrace_cleanup_callback
|
147
|
+
)
|
154
148
|
end
|
155
149
|
|
156
150
|
private
|
data/lib/sentry/hub.rb
CHANGED
@@ -76,12 +76,12 @@ module Sentry
|
|
76
76
|
def capture_exception(exception, **options, &block)
|
77
77
|
return unless current_client
|
78
78
|
|
79
|
-
|
79
|
+
options[:hint] ||= {}
|
80
|
+
options[:hint][:exception] = exception
|
81
|
+
event = current_client.event_from_exception(exception, options[:hint])
|
80
82
|
|
81
83
|
return unless event
|
82
84
|
|
83
|
-
options[:hint] ||= {}
|
84
|
-
options[:hint] = options[:hint].merge(exception: exception)
|
85
85
|
capture_event(event, **options, &block)
|
86
86
|
end
|
87
87
|
|
@@ -89,15 +89,15 @@ module Sentry
|
|
89
89
|
return unless current_client
|
90
90
|
|
91
91
|
options[:hint] ||= {}
|
92
|
-
options[:hint]
|
93
|
-
event = current_client.event_from_message(message)
|
92
|
+
options[:hint][:message] = message
|
93
|
+
event = current_client.event_from_message(message, options[:hint])
|
94
94
|
capture_event(event, **options, &block)
|
95
95
|
end
|
96
96
|
|
97
97
|
def capture_event(event, **options, &block)
|
98
98
|
return unless current_client
|
99
99
|
|
100
|
-
hint = options.delete(:hint)
|
100
|
+
hint = options.delete(:hint) || {}
|
101
101
|
scope = current_scope.dup
|
102
102
|
|
103
103
|
if block
|
@@ -110,7 +110,7 @@ module Sentry
|
|
110
110
|
|
111
111
|
event = current_client.capture_event(event, scope, hint)
|
112
112
|
|
113
|
-
@last_event_id = event
|
113
|
+
@last_event_id = event&.event_id
|
114
114
|
event
|
115
115
|
end
|
116
116
|
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Sentry
|
2
|
+
module Integrable
|
3
|
+
def register_integration(name:, version:)
|
4
|
+
Sentry.register_integration(name, version)
|
5
|
+
@integration_name = name
|
6
|
+
end
|
7
|
+
|
8
|
+
def integration_name
|
9
|
+
@integration_name
|
10
|
+
end
|
11
|
+
|
12
|
+
def capture_exception(exception, **options, &block)
|
13
|
+
options[:hint] ||= {}
|
14
|
+
options[:hint][:integration] = integration_name
|
15
|
+
Sentry.capture_exception(exception, **options, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
def capture_message(message, **options, &block)
|
19
|
+
options[:hint] ||= {}
|
20
|
+
options[:hint][:integration] = integration_name
|
21
|
+
Sentry.capture_message(message, **options, &block)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -1,8 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Sentry
|
4
4
|
class RequestInterface < Interface
|
5
5
|
REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
|
6
|
+
CONTENT_HEADERS = %w(CONTENT_TYPE CONTENT_LENGTH).freeze
|
6
7
|
IP_HEADERS = [
|
7
8
|
"REMOTE_ADDR",
|
8
9
|
"HTTP_CLIENT_IP",
|
@@ -10,42 +11,52 @@ module Sentry
|
|
10
11
|
"HTTP_X_FORWARDED_FOR"
|
11
12
|
].freeze
|
12
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
|
+
|
13
18
|
attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env
|
14
19
|
|
15
|
-
def
|
16
|
-
|
17
|
-
|
18
|
-
self.
|
20
|
+
def self.from_rack(env)
|
21
|
+
env = clean_env(env)
|
22
|
+
req = ::Rack::Request.new(env)
|
23
|
+
self.new(req)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.clean_env(env)
|
27
|
+
unless Sentry.configuration.send_default_pii
|
28
|
+
# need to completely wipe out ip addresses
|
29
|
+
RequestInterface::IP_HEADERS.each do |header|
|
30
|
+
env.delete(header)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
env
|
19
35
|
end
|
20
36
|
|
21
|
-
def
|
22
|
-
|
37
|
+
def initialize(req)
|
38
|
+
env = req.env
|
23
39
|
|
24
40
|
if Sentry.configuration.send_default_pii
|
25
41
|
self.data = read_data_from(req)
|
26
42
|
self.cookies = req.cookies
|
27
|
-
else
|
28
|
-
# need to completely wipe out ip addresses
|
29
|
-
IP_HEADERS.each { |h| env_hash.delete(h) }
|
30
43
|
end
|
31
44
|
|
32
45
|
self.url = req.scheme && req.url.split('?').first
|
33
46
|
self.method = req.request_method
|
34
47
|
self.query_string = req.query_string
|
35
48
|
|
36
|
-
self.headers =
|
37
|
-
self.env =
|
49
|
+
self.headers = filter_and_format_headers(env)
|
50
|
+
self.env = filter_and_format_env(env)
|
38
51
|
end
|
39
52
|
|
40
53
|
private
|
41
54
|
|
42
|
-
# See Sentry server default limits at
|
43
|
-
# https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
|
44
55
|
def read_data_from(request)
|
45
56
|
if request.form_data?
|
46
57
|
request.POST
|
47
58
|
elsif request.body # JSON requests, etc
|
48
|
-
data = request.body.read(
|
59
|
+
data = request.body.read(MAX_BODY_LIMIT)
|
49
60
|
request.body.rewind
|
50
61
|
data
|
51
62
|
end
|
@@ -53,22 +64,14 @@ module Sentry
|
|
53
64
|
e.message
|
54
65
|
end
|
55
66
|
|
56
|
-
def
|
57
|
-
|
67
|
+
def filter_and_format_headers(env)
|
68
|
+
env.each_with_object({}) do |(key, value), memo|
|
58
69
|
begin
|
59
70
|
key = key.to_s # rack env can contain symbols
|
60
71
|
value = value.to_s
|
61
|
-
next memo['X-Request-Id'] ||= Utils::RequestId.read_from(
|
62
|
-
next
|
63
|
-
|
64
|
-
# Rack adds in an incorrect HTTP_VERSION key, which causes downstream
|
65
|
-
# to think this is a Version header. Instead, this is mapped to
|
66
|
-
# env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
|
67
|
-
# if the request has legitimately sent a Version header themselves.
|
68
|
-
# See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
|
69
|
-
next if key == 'HTTP_VERSION' && value == env_hash['SERVER_PROTOCOL']
|
70
|
-
next if key == 'HTTP_COOKIE' # Cookies don't go here, they go somewhere else
|
71
|
-
next unless key.start_with?('HTTP_') || %w(CONTENT_TYPE CONTENT_LENGTH).include?(key)
|
72
|
+
next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
|
73
|
+
next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
|
74
|
+
next if is_skippable_header?(key)
|
72
75
|
|
73
76
|
# Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
|
74
77
|
key = key.sub(/^HTTP_/, "")
|
@@ -84,10 +87,26 @@ module Sentry
|
|
84
87
|
end
|
85
88
|
end
|
86
89
|
|
87
|
-
def
|
88
|
-
|
90
|
+
def is_skippable_header?(key)
|
91
|
+
key.upcase != key || # lower-case envs aren't real http headers
|
92
|
+
key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
|
93
|
+
!(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
|
94
|
+
end
|
95
|
+
|
96
|
+
# Rack adds in an incorrect HTTP_VERSION key, which causes downstream
|
97
|
+
# to think this is a Version header. Instead, this is mapped to
|
98
|
+
# env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
|
99
|
+
# if the request has legitimately sent a Version header themselves.
|
100
|
+
# See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
|
101
|
+
# NOTE: This will be removed in version 3.0+
|
102
|
+
def is_server_protocol?(key, value, protocol_version)
|
103
|
+
key == 'HTTP_VERSION' && value == protocol_version
|
104
|
+
end
|
105
|
+
|
106
|
+
def filter_and_format_env(env)
|
107
|
+
return env if Sentry.configuration.rack_env_whitelist.empty?
|
89
108
|
|
90
|
-
|
109
|
+
env.select do |k, _v|
|
91
110
|
Sentry.configuration.rack_env_whitelist.include? k.to_s
|
92
111
|
end
|
93
112
|
end
|
@@ -1,11 +1,31 @@
|
|
1
1
|
module Sentry
|
2
|
-
class StacktraceInterface
|
3
|
-
|
2
|
+
class StacktraceInterface
|
3
|
+
attr_reader :frames
|
4
|
+
|
5
|
+
def initialize(backtrace:, project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil)
|
6
|
+
@project_root = project_root
|
7
|
+
@frames = []
|
8
|
+
|
9
|
+
parsed_backtrace_lines = Backtrace.parse(
|
10
|
+
backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback
|
11
|
+
).lines
|
12
|
+
|
13
|
+
parsed_backtrace_lines.reverse.each_with_object(@frames) do |line, frames|
|
14
|
+
frame = convert_parsed_line_into_frame(line, project_root, linecache, context_lines)
|
15
|
+
frames << frame if frame.filename
|
16
|
+
end
|
17
|
+
end
|
4
18
|
|
5
19
|
def to_hash
|
6
|
-
|
7
|
-
|
8
|
-
|
20
|
+
{ frames: @frames.map(&:to_hash) }
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def convert_parsed_line_into_frame(line, project_root, linecache, context_lines)
|
26
|
+
frame = StacktraceInterface::Frame.new(@project_root, line)
|
27
|
+
frame.set_context(linecache, context_lines) if context_lines
|
28
|
+
frame
|
9
29
|
end
|
10
30
|
|
11
31
|
# Not actually an interface, but I want to use the same style
|
@@ -13,8 +33,14 @@ module Sentry
|
|
13
33
|
attr_accessor :abs_path, :context_line, :function, :in_app,
|
14
34
|
:lineno, :module, :pre_context, :post_context, :vars
|
15
35
|
|
16
|
-
def initialize(project_root)
|
36
|
+
def initialize(project_root, line)
|
17
37
|
@project_root = project_root
|
38
|
+
|
39
|
+
@abs_path = line.file if line.file
|
40
|
+
@function = line.method if line.method
|
41
|
+
@lineno = line.number
|
42
|
+
@in_app = line.in_app
|
43
|
+
@module = line.module_name if line.module_name
|
18
44
|
end
|
19
45
|
|
20
46
|
def filename
|
@@ -33,6 +59,13 @@ module Sentry
|
|
33
59
|
@filename = prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
|
34
60
|
end
|
35
61
|
|
62
|
+
def set_context(linecache, context_lines)
|
63
|
+
return unless abs_path
|
64
|
+
|
65
|
+
self.pre_context, self.context_line, self.post_context = \
|
66
|
+
linecache.get_file_context(abs_path, lineno, context_lines)
|
67
|
+
end
|
68
|
+
|
36
69
|
def to_hash(*args)
|
37
70
|
data = super(*args)
|
38
71
|
data[:filename] = filename
|
data/lib/sentry/rack.rb
CHANGED
@@ -0,0 +1,62 @@
|
|
1
|
+
module Sentry
|
2
|
+
module Rack
|
3
|
+
class CaptureExceptions
|
4
|
+
def initialize(app)
|
5
|
+
@app = app
|
6
|
+
end
|
7
|
+
|
8
|
+
def call(env)
|
9
|
+
return @app.call(env) unless Sentry.initialized?
|
10
|
+
|
11
|
+
# make sure the current thread has a clean hub
|
12
|
+
Sentry.clone_hub_to_current_thread
|
13
|
+
|
14
|
+
Sentry.with_scope do |scope|
|
15
|
+
scope.clear_breadcrumbs
|
16
|
+
scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
|
17
|
+
scope.set_rack_env(env)
|
18
|
+
|
19
|
+
span = Sentry.start_transaction(name: scope.transaction_name, op: transaction_op)
|
20
|
+
scope.set_span(span)
|
21
|
+
|
22
|
+
begin
|
23
|
+
response = @app.call(env)
|
24
|
+
rescue Sentry::Error
|
25
|
+
finish_span(span, 500)
|
26
|
+
raise # Don't capture Sentry errors
|
27
|
+
rescue Exception => e
|
28
|
+
capture_exception(e)
|
29
|
+
finish_span(span, 500)
|
30
|
+
raise
|
31
|
+
end
|
32
|
+
|
33
|
+
exception = collect_exception(env)
|
34
|
+
capture_exception(exception) if exception
|
35
|
+
|
36
|
+
finish_span(span, response[0])
|
37
|
+
|
38
|
+
response
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def collect_exception(env)
|
45
|
+
env['rack.exception'] || env['sinatra.error']
|
46
|
+
end
|
47
|
+
|
48
|
+
def transaction_op
|
49
|
+
"rack.request".freeze
|
50
|
+
end
|
51
|
+
|
52
|
+
def capture_exception(exception)
|
53
|
+
Sentry.capture_exception(exception)
|
54
|
+
end
|
55
|
+
|
56
|
+
def finish_span(span, status_code)
|
57
|
+
span.set_http_status(status_code)
|
58
|
+
span.finish
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|