sentry-ruby 5.3.0 → 5.8.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +11 -0
- data/.rspec +2 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +31 -0
- data/Makefile +4 -0
- data/README.md +10 -6
- data/Rakefile +13 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/sentry/background_worker.rb +72 -0
- data/lib/sentry/backtrace.rb +124 -0
- data/lib/sentry/baggage.rb +81 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
- data/lib/sentry/breadcrumb.rb +70 -0
- data/lib/sentry/breadcrumb_buffer.rb +64 -0
- data/lib/sentry/client.rb +207 -0
- data/lib/sentry/configuration.rb +543 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
- data/lib/sentry/core_ext/object/duplicable.rb +155 -0
- data/lib/sentry/dsn.rb +53 -0
- data/lib/sentry/envelope.rb +96 -0
- data/lib/sentry/error_event.rb +38 -0
- data/lib/sentry/event.rb +178 -0
- data/lib/sentry/exceptions.rb +9 -0
- data/lib/sentry/hub.rb +241 -0
- data/lib/sentry/integrable.rb +26 -0
- data/lib/sentry/interface.rb +16 -0
- data/lib/sentry/interfaces/exception.rb +43 -0
- data/lib/sentry/interfaces/request.rb +134 -0
- data/lib/sentry/interfaces/single_exception.rb +65 -0
- data/lib/sentry/interfaces/stacktrace.rb +87 -0
- data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
- data/lib/sentry/interfaces/threads.rb +42 -0
- data/lib/sentry/linecache.rb +47 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/net/http.rb +103 -0
- data/lib/sentry/rack/capture_exceptions.rb +82 -0
- data/lib/sentry/rack.rb +5 -0
- data/lib/sentry/rake.rb +41 -0
- data/lib/sentry/redis.rb +107 -0
- data/lib/sentry/release_detector.rb +39 -0
- data/lib/sentry/scope.rb +339 -0
- data/lib/sentry/session.rb +33 -0
- data/lib/sentry/session_flusher.rb +90 -0
- data/lib/sentry/span.rb +236 -0
- data/lib/sentry/test_helper.rb +78 -0
- data/lib/sentry/transaction.rb +345 -0
- data/lib/sentry/transaction_event.rb +53 -0
- data/lib/sentry/transport/configuration.rb +25 -0
- data/lib/sentry/transport/dummy_transport.rb +21 -0
- data/lib/sentry/transport/http_transport.rb +175 -0
- data/lib/sentry/transport.rb +214 -0
- data/lib/sentry/utils/argument_checking_helper.rb +13 -0
- data/lib/sentry/utils/custom_inspection.rb +14 -0
- data/lib/sentry/utils/encoding_helper.rb +22 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/logging_helper.rb +26 -0
- data/lib/sentry/utils/real_ip.rb +84 -0
- data/lib/sentry/utils/request_id.rb +18 -0
- data/lib/sentry/version.rb +5 -0
- data/lib/sentry-ruby.rb +511 -0
- data/sentry-ruby-core.gemspec +23 -0
- data/sentry-ruby.gemspec +24 -0
- metadata +66 -16
@@ -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,103 @@
|
|
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? && Sentry.initialized?
|
30
|
+
return super if from_sentry_sdk?
|
31
|
+
|
32
|
+
Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |sentry_span|
|
33
|
+
set_sentry_trace_header(req, sentry_span)
|
34
|
+
|
35
|
+
super.tap do |res|
|
36
|
+
record_sentry_breadcrumb(req, res)
|
37
|
+
|
38
|
+
if sentry_span
|
39
|
+
request_info = extract_request_info(req)
|
40
|
+
sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}")
|
41
|
+
sentry_span.set_data(:status, res.code.to_i)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def set_sentry_trace_header(req, sentry_span)
|
50
|
+
return unless sentry_span
|
51
|
+
|
52
|
+
client = Sentry.get_current_client
|
53
|
+
|
54
|
+
trace = client.generate_sentry_trace(sentry_span)
|
55
|
+
req[SENTRY_TRACE_HEADER_NAME] = trace if trace
|
56
|
+
|
57
|
+
baggage = client.generate_baggage(sentry_span)
|
58
|
+
req[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty?
|
59
|
+
end
|
60
|
+
|
61
|
+
def record_sentry_breadcrumb(req, res)
|
62
|
+
return unless Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger)
|
63
|
+
|
64
|
+
request_info = extract_request_info(req)
|
65
|
+
|
66
|
+
crumb = Sentry::Breadcrumb.new(
|
67
|
+
level: :info,
|
68
|
+
category: BREADCRUMB_CATEGORY,
|
69
|
+
type: :info,
|
70
|
+
data: {
|
71
|
+
status: res.code.to_i,
|
72
|
+
**request_info
|
73
|
+
}
|
74
|
+
)
|
75
|
+
Sentry.add_breadcrumb(crumb)
|
76
|
+
end
|
77
|
+
|
78
|
+
def from_sentry_sdk?
|
79
|
+
dsn = Sentry.configuration.dsn
|
80
|
+
dsn && dsn.host == self.address
|
81
|
+
end
|
82
|
+
|
83
|
+
def extract_request_info(req)
|
84
|
+
uri = req.uri || URI.parse("#{use_ssl? ? 'https' : 'http'}://#{address}#{req.path}")
|
85
|
+
url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s
|
86
|
+
|
87
|
+
result = { method: req.method, url: url }
|
88
|
+
|
89
|
+
if Sentry.configuration.send_default_pii
|
90
|
+
result[:url] = result[:url] + "?#{uri.query}"
|
91
|
+
result[:body] = req.body
|
92
|
+
end
|
93
|
+
|
94
|
+
result
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
Sentry.register_patch do
|
101
|
+
patch = Sentry::Net::HTTP
|
102
|
+
Net::HTTP.send(:prepend, patch) unless Net::HTTP.ancestors.include?(patch)
|
103
|
+
end
|
@@ -0,0 +1,82 @@
|
|
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"], source: :url) 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
|
+
"http.server".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
|
+
baggage = env["HTTP_BAGGAGE"]
|
67
|
+
|
68
|
+
options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op }
|
69
|
+
transaction = Sentry::Transaction.from_sentry_trace(sentry_trace, baggage: baggage, **options) if sentry_trace
|
70
|
+
Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options)
|
71
|
+
end
|
72
|
+
|
73
|
+
|
74
|
+
def finish_transaction(transaction, status_code)
|
75
|
+
return unless transaction
|
76
|
+
|
77
|
+
transaction.set_http_status(status_code)
|
78
|
+
transaction.finish
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
data/lib/sentry/rack.rb
ADDED
data/lib/sentry/rake.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "rake"
|
4
|
+
require "rake/task"
|
5
|
+
|
6
|
+
module Sentry
|
7
|
+
module Rake
|
8
|
+
module Application
|
9
|
+
# @api private
|
10
|
+
def display_error_message(ex)
|
11
|
+
Sentry.capture_exception(ex) do |scope|
|
12
|
+
task_name = top_level_tasks.join(' ')
|
13
|
+
scope.set_transaction_name(task_name, source: :task)
|
14
|
+
scope.set_tag("rake_task", task_name)
|
15
|
+
end if Sentry.initialized? && !Sentry.configuration.skip_rake_integration
|
16
|
+
|
17
|
+
super
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
module Task
|
22
|
+
# @api private
|
23
|
+
def execute(args=nil)
|
24
|
+
return super unless Sentry.initialized? && Sentry.get_current_hub
|
25
|
+
|
26
|
+
super
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
# @api private
|
33
|
+
module Rake
|
34
|
+
class Application
|
35
|
+
prepend(Sentry::Rake::Application)
|
36
|
+
end
|
37
|
+
|
38
|
+
class Task
|
39
|
+
prepend(Sentry::Rake::Task)
|
40
|
+
end
|
41
|
+
end
|
data/lib/sentry/redis.rb
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
# @api private
|
5
|
+
class Redis
|
6
|
+
OP_NAME = "db.redis"
|
7
|
+
LOGGER_NAME = :redis_logger
|
8
|
+
|
9
|
+
def initialize(commands, host, port, db)
|
10
|
+
@commands, @host, @port, @db = commands, host, port, db
|
11
|
+
end
|
12
|
+
|
13
|
+
def instrument
|
14
|
+
return yield unless Sentry.initialized?
|
15
|
+
|
16
|
+
Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f) do |span|
|
17
|
+
yield.tap do
|
18
|
+
record_breadcrumb
|
19
|
+
|
20
|
+
if span
|
21
|
+
span.set_description(commands_description)
|
22
|
+
span.set_data(:server, server_description)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :commands, :host, :port, :db
|
31
|
+
|
32
|
+
def record_breadcrumb
|
33
|
+
return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME)
|
34
|
+
|
35
|
+
Sentry.add_breadcrumb(
|
36
|
+
Sentry::Breadcrumb.new(
|
37
|
+
level: :info,
|
38
|
+
category: OP_NAME,
|
39
|
+
type: :info,
|
40
|
+
data: {
|
41
|
+
commands: parsed_commands,
|
42
|
+
server: server_description
|
43
|
+
}
|
44
|
+
)
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
def commands_description
|
49
|
+
parsed_commands.map do |statement|
|
50
|
+
statement.values.join(" ").strip
|
51
|
+
end.join(", ")
|
52
|
+
end
|
53
|
+
|
54
|
+
def parsed_commands
|
55
|
+
commands.map do |statement|
|
56
|
+
command, key, *arguments = statement
|
57
|
+
command_set = { command: command.to_s.upcase }
|
58
|
+
command_set[:key] = key if Utils::EncodingHelper.valid_utf_8?(key)
|
59
|
+
|
60
|
+
if Sentry.configuration.send_default_pii
|
61
|
+
command_set[:arguments] = arguments
|
62
|
+
.select { |a| Utils::EncodingHelper.valid_utf_8?(a) }
|
63
|
+
.join(" ")
|
64
|
+
end
|
65
|
+
|
66
|
+
command_set
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def server_description
|
71
|
+
"#{host}:#{port}/#{db}"
|
72
|
+
end
|
73
|
+
|
74
|
+
module OldClientPatch
|
75
|
+
def logging(commands, &block)
|
76
|
+
Sentry::Redis.new(commands, host, port, db).instrument { super }
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
module GlobalRedisInstrumentation
|
81
|
+
def call(command, redis_config)
|
82
|
+
Sentry::Redis
|
83
|
+
.new([command], redis_config.host, redis_config.port, redis_config.db)
|
84
|
+
.instrument { super }
|
85
|
+
end
|
86
|
+
|
87
|
+
def call_pipelined(commands, redis_config)
|
88
|
+
Sentry::Redis
|
89
|
+
.new(commands, redis_config.host, redis_config.port, redis_config.db)
|
90
|
+
.instrument { super }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
if defined?(::Redis::Client)
|
97
|
+
if Gem::Version.new(::Redis::VERSION) < Gem::Version.new("5.0")
|
98
|
+
Sentry.register_patch do
|
99
|
+
patch = Sentry::Redis::OldClientPatch
|
100
|
+
unless Redis::Client.ancestors.include?(patch)
|
101
|
+
Redis::Client.prepend(patch)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
elsif defined?(RedisClient)
|
105
|
+
RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation)
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sentry
|
4
|
+
# @api private
|
5
|
+
class ReleaseDetector
|
6
|
+
class << self
|
7
|
+
def detect_release(project_root:, running_on_heroku:)
|
8
|
+
detect_release_from_env ||
|
9
|
+
detect_release_from_git ||
|
10
|
+
detect_release_from_capistrano(project_root) ||
|
11
|
+
detect_release_from_heroku(running_on_heroku)
|
12
|
+
end
|
13
|
+
|
14
|
+
def detect_release_from_heroku(running_on_heroku)
|
15
|
+
return unless running_on_heroku
|
16
|
+
ENV['HEROKU_SLUG_COMMIT']
|
17
|
+
end
|
18
|
+
|
19
|
+
def detect_release_from_capistrano(project_root)
|
20
|
+
revision_file = File.join(project_root, 'REVISION')
|
21
|
+
revision_log = File.join(project_root, '..', 'revisions.log')
|
22
|
+
|
23
|
+
if File.exist?(revision_file)
|
24
|
+
File.read(revision_file).strip
|
25
|
+
elsif File.exist?(revision_log)
|
26
|
+
File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def detect_release_from_git
|
31
|
+
Sentry.sys_command("git rev-parse --short HEAD") if File.directory?(".git")
|
32
|
+
end
|
33
|
+
|
34
|
+
def detect_release_from_env
|
35
|
+
ENV['SENTRY_RELEASE']
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|