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.
Files changed (67) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +31 -0
  8. data/Makefile +4 -0
  9. data/README.md +10 -6
  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/baggage.rb +81 -0
  16. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  17. data/lib/sentry/breadcrumb.rb +70 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  19. data/lib/sentry/client.rb +207 -0
  20. data/lib/sentry/configuration.rb +543 -0
  21. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  22. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  23. data/lib/sentry/dsn.rb +53 -0
  24. data/lib/sentry/envelope.rb +96 -0
  25. data/lib/sentry/error_event.rb +38 -0
  26. data/lib/sentry/event.rb +178 -0
  27. data/lib/sentry/exceptions.rb +9 -0
  28. data/lib/sentry/hub.rb +241 -0
  29. data/lib/sentry/integrable.rb +26 -0
  30. data/lib/sentry/interface.rb +16 -0
  31. data/lib/sentry/interfaces/exception.rb +43 -0
  32. data/lib/sentry/interfaces/request.rb +134 -0
  33. data/lib/sentry/interfaces/single_exception.rb +65 -0
  34. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  35. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  36. data/lib/sentry/interfaces/threads.rb +42 -0
  37. data/lib/sentry/linecache.rb +47 -0
  38. data/lib/sentry/logger.rb +20 -0
  39. data/lib/sentry/net/http.rb +103 -0
  40. data/lib/sentry/rack/capture_exceptions.rb +82 -0
  41. data/lib/sentry/rack.rb +5 -0
  42. data/lib/sentry/rake.rb +41 -0
  43. data/lib/sentry/redis.rb +107 -0
  44. data/lib/sentry/release_detector.rb +39 -0
  45. data/lib/sentry/scope.rb +339 -0
  46. data/lib/sentry/session.rb +33 -0
  47. data/lib/sentry/session_flusher.rb +90 -0
  48. data/lib/sentry/span.rb +236 -0
  49. data/lib/sentry/test_helper.rb +78 -0
  50. data/lib/sentry/transaction.rb +345 -0
  51. data/lib/sentry/transaction_event.rb +53 -0
  52. data/lib/sentry/transport/configuration.rb +25 -0
  53. data/lib/sentry/transport/dummy_transport.rb +21 -0
  54. data/lib/sentry/transport/http_transport.rb +175 -0
  55. data/lib/sentry/transport.rb +214 -0
  56. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  57. data/lib/sentry/utils/custom_inspection.rb +14 -0
  58. data/lib/sentry/utils/encoding_helper.rb +22 -0
  59. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  60. data/lib/sentry/utils/logging_helper.rb +26 -0
  61. data/lib/sentry/utils/real_ip.rb +84 -0
  62. data/lib/sentry/utils/request_id.rb +18 -0
  63. data/lib/sentry/version.rb +5 -0
  64. data/lib/sentry-ruby.rb +511 -0
  65. data/sentry-ruby-core.gemspec +23 -0
  66. data/sentry-ruby.gemspec +24 -0
  67. 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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rack'
4
+
5
+ require 'sentry/rack/capture_exceptions'
@@ -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
@@ -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