sentry-ruby-core 4.1.5.pre.beta.0

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 (55) hide show
  1. checksums.yaml +7 -0
  2. data/.craft.yml +33 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +125 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +16 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +258 -0
  11. data/Rakefile +13 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/lib/sentry-ruby.rb +191 -0
  15. data/lib/sentry/background_worker.rb +37 -0
  16. data/lib/sentry/backtrace.rb +126 -0
  17. data/lib/sentry/benchmarks/benchmark_transport.rb +14 -0
  18. data/lib/sentry/breadcrumb.rb +25 -0
  19. data/lib/sentry/breadcrumb/sentry_logger.rb +87 -0
  20. data/lib/sentry/breadcrumb_buffer.rb +47 -0
  21. data/lib/sentry/client.rb +96 -0
  22. data/lib/sentry/configuration.rb +396 -0
  23. data/lib/sentry/core_ext/object/deep_dup.rb +57 -0
  24. data/lib/sentry/core_ext/object/duplicable.rb +153 -0
  25. data/lib/sentry/dsn.rb +48 -0
  26. data/lib/sentry/event.rb +173 -0
  27. data/lib/sentry/hub.rb +143 -0
  28. data/lib/sentry/integrable.rb +24 -0
  29. data/lib/sentry/interface.rb +22 -0
  30. data/lib/sentry/interfaces/exception.rb +11 -0
  31. data/lib/sentry/interfaces/request.rb +113 -0
  32. data/lib/sentry/interfaces/single_exception.rb +14 -0
  33. data/lib/sentry/interfaces/stacktrace.rb +90 -0
  34. data/lib/sentry/linecache.rb +44 -0
  35. data/lib/sentry/logger.rb +20 -0
  36. data/lib/sentry/rack.rb +4 -0
  37. data/lib/sentry/rack/capture_exceptions.rb +68 -0
  38. data/lib/sentry/rack/deprecations.rb +19 -0
  39. data/lib/sentry/rake.rb +17 -0
  40. data/lib/sentry/scope.rb +210 -0
  41. data/lib/sentry/span.rb +133 -0
  42. data/lib/sentry/transaction.rb +157 -0
  43. data/lib/sentry/transaction_event.rb +29 -0
  44. data/lib/sentry/transport.rb +88 -0
  45. data/lib/sentry/transport/configuration.rb +21 -0
  46. data/lib/sentry/transport/dummy_transport.rb +14 -0
  47. data/lib/sentry/transport/http_transport.rb +62 -0
  48. data/lib/sentry/utils/argument_checking_helper.rb +11 -0
  49. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  50. data/lib/sentry/utils/real_ip.rb +70 -0
  51. data/lib/sentry/utils/request_id.rb +16 -0
  52. data/lib/sentry/version.rb +3 -0
  53. data/sentry-ruby-core.gemspec +27 -0
  54. data/sentry-ruby.gemspec +23 -0
  55. metadata +128 -0
@@ -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
@@ -0,0 +1,22 @@
1
+ module Sentry
2
+ class Interface
3
+ def self.inherited(klass)
4
+ name = klass.name.split("::").last.downcase.gsub("interface", "")
5
+ registered[name.to_sym] = klass
6
+ super
7
+ end
8
+
9
+ def self.registered
10
+ @@registered ||= {} # rubocop:disable Style/ClassVars
11
+ end
12
+
13
+ def to_hash
14
+ Hash[instance_variables.map { |name| [name[1..-1].to_sym, instance_variable_get(name)] }]
15
+ end
16
+ end
17
+ end
18
+
19
+ require "sentry/interfaces/exception"
20
+ require "sentry/interfaces/request"
21
+ require "sentry/interfaces/single_exception"
22
+ require "sentry/interfaces/stacktrace"
@@ -0,0 +1,11 @@
1
+ module Sentry
2
+ class ExceptionInterface < Interface
3
+ attr_accessor :values
4
+
5
+ def to_hash
6
+ data = super
7
+ data[:values] = data[:values].map(&:to_hash) if data[:values]
8
+ data
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,113 @@
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
+ attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env
19
+
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
35
+ end
36
+
37
+ def initialize(req)
38
+ env = req.env
39
+
40
+ if Sentry.configuration.send_default_pii
41
+ self.data = read_data_from(req)
42
+ self.cookies = req.cookies
43
+ end
44
+
45
+ self.url = req.scheme && req.url.split('?').first
46
+ self.method = req.request_method
47
+ self.query_string = req.query_string
48
+
49
+ self.headers = filter_and_format_headers(env)
50
+ self.env = filter_and_format_env(env)
51
+ end
52
+
53
+ private
54
+
55
+ def read_data_from(request)
56
+ if request.form_data?
57
+ request.POST
58
+ elsif request.body # JSON requests, etc
59
+ data = request.body.read(MAX_BODY_LIMIT)
60
+ request.body.rewind
61
+ data
62
+ end
63
+ rescue IOError => e
64
+ e.message
65
+ end
66
+
67
+ def filter_and_format_headers(env)
68
+ env.each_with_object({}) do |(key, value), memo|
69
+ begin
70
+ key = key.to_s # rack env can contain symbols
71
+ next memo['X-Request-Id'] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key)
72
+ next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"])
73
+ next if is_skippable_header?(key)
74
+
75
+ # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
76
+ key = key.sub(/^HTTP_/, "")
77
+ key = key.split('_').map(&:capitalize).join('-')
78
+ memo[key] = value.to_s
79
+ rescue StandardError => e
80
+ # Rails adds objects to the Rack env that can sometimes raise exceptions
81
+ # when `to_s` is called.
82
+ # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134
83
+ Sentry.logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
84
+ next
85
+ end
86
+ end
87
+ end
88
+
89
+ def is_skippable_header?(key)
90
+ key.upcase != key || # lower-case envs aren't real http headers
91
+ key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else
92
+ !(key.start_with?('HTTP_') || CONTENT_HEADERS.include?(key))
93
+ end
94
+
95
+ # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
96
+ # to think this is a Version header. Instead, this is mapped to
97
+ # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
98
+ # if the request has legitimately sent a Version header themselves.
99
+ # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
100
+ # NOTE: This will be removed in version 3.0+
101
+ def is_server_protocol?(key, value, protocol_version)
102
+ key == 'HTTP_VERSION' && value == protocol_version
103
+ end
104
+
105
+ def filter_and_format_env(env)
106
+ return env if Sentry.configuration.rack_env_whitelist.empty?
107
+
108
+ env.select do |k, _v|
109
+ Sentry.configuration.rack_env_whitelist.include? k.to_s
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,14 @@
1
+ module Sentry
2
+ class SingleExceptionInterface < Interface
3
+ attr_accessor :type
4
+ attr_accessor :value
5
+ attr_accessor :module
6
+ attr_accessor :stacktrace
7
+
8
+ def to_hash
9
+ data = super
10
+ data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace]
11
+ data
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,90 @@
1
+ module Sentry
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
18
+
19
+ def to_hash
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
29
+ end
30
+
31
+ # Not actually an interface, but I want to use the same style
32
+ class Frame < Interface
33
+ attr_accessor :abs_path, :context_line, :function, :in_app,
34
+ :lineno, :module, :pre_context, :post_context, :vars
35
+
36
+ def initialize(project_root, line)
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
44
+ end
45
+
46
+ def filename
47
+ return if abs_path.nil?
48
+ return @filename if instance_variable_defined?(:@filename)
49
+
50
+ prefix =
51
+ if under_project_root? && in_app
52
+ @project_root
53
+ elsif under_project_root?
54
+ longest_load_path || @project_root
55
+ else
56
+ longest_load_path
57
+ end
58
+
59
+ @filename = prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
60
+ end
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
+
69
+ def to_hash(*args)
70
+ data = super(*args)
71
+ data[:filename] = filename
72
+ data.delete(:vars) unless vars && !vars.empty?
73
+ data.delete(:pre_context) unless pre_context && !pre_context.empty?
74
+ data.delete(:post_context) unless post_context && !post_context.empty?
75
+ data.delete(:context_line) unless context_line && !context_line.empty?
76
+ data
77
+ end
78
+
79
+ private
80
+
81
+ def under_project_root?
82
+ @project_root && abs_path.start_with?(@project_root)
83
+ end
84
+
85
+ def longest_load_path
86
+ $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,44 @@
1
+ module Sentry
2
+ class LineCache
3
+ def initialize
4
+ @cache = {}
5
+ end
6
+
7
+ # Any linecache you provide to Sentry must implement this method.
8
+ # Returns an Array of Strings representing the lines in the source
9
+ # file. The number of lines retrieved is (2 * context) + 1, the middle
10
+ # line should be the line requested by lineno. See specs for more information.
11
+ def get_file_context(filename, lineno, context)
12
+ return nil, nil, nil unless valid_path?(filename)
13
+
14
+ lines = Array.new(2 * context + 1) do |i|
15
+ getline(filename, lineno - context + i)
16
+ end
17
+ [lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]]
18
+ end
19
+
20
+ private
21
+
22
+ def valid_path?(path)
23
+ lines = getlines(path)
24
+ !lines.nil?
25
+ end
26
+
27
+ def getlines(path)
28
+ @cache[path] ||= begin
29
+ IO.readlines(path)
30
+ rescue
31
+ nil
32
+ end
33
+ end
34
+
35
+ def getline(path, n)
36
+ return nil if n < 1
37
+
38
+ lines = getlines(path)
39
+ return nil if lines.nil?
40
+
41
+ lines[n - 1]
42
+ end
43
+ end
44
+ 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,4 @@
1
+ require 'rack'
2
+
3
+ require 'sentry/rack/capture_exceptions'
4
+ require 'sentry/rack/deprecations'
@@ -0,0 +1,68 @@
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 =
20
+ if sentry_trace = env["sentry-trace"]
21
+ Sentry::Transaction.from_sentry_trace(sentry_trace, name: scope.transaction_name, op: transaction_op)
22
+ else
23
+ Sentry.start_transaction(name: scope.transaction_name, op: transaction_op)
24
+ end
25
+
26
+ scope.set_span(span)
27
+
28
+ begin
29
+ response = @app.call(env)
30
+ rescue Sentry::Error
31
+ finish_span(span, 500)
32
+ raise # Don't capture Sentry errors
33
+ rescue Exception => e
34
+ capture_exception(e)
35
+ finish_span(span, 500)
36
+ raise
37
+ end
38
+
39
+ exception = collect_exception(env)
40
+ capture_exception(exception) if exception
41
+
42
+ finish_span(span, response[0])
43
+
44
+ response
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)
59
+ Sentry.capture_exception(exception)
60
+ end
61
+
62
+ def finish_span(span, status_code)
63
+ span.set_http_status(status_code)
64
+ span.finish
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,19 @@
1
+ module Sentry
2
+ module Rack
3
+ class DeprecatedMiddleware
4
+ def initialize(_)
5
+ raise Sentry::Error.new <<~MSG
6
+
7
+ You're seeing this message because #{self.class} has been replaced by Sentry::Rack::CaptureExceptions.
8
+ Removing this middleware from your app and upgrading sentry-rails to 4.1.0+ should solve the issue.
9
+ MSG
10
+ end
11
+ end
12
+
13
+ class Tracing < DeprecatedMiddleware
14
+ end
15
+
16
+ class CaptureException < DeprecatedMiddleware
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ require "rake"
2
+ require "rake/task"
3
+
4
+ module Rake
5
+ class Application
6
+ alias orig_display_error_messsage display_error_message
7
+ def display_error_message(ex)
8
+ Sentry.capture_exception(ex, hint: { background: false }) do |scope|
9
+ task_name = top_level_tasks.join(' ')
10
+ scope.set_transaction_name(task_name)
11
+ scope.set_tag("rake_task", task_name)
12
+ end if Sentry.initialized?
13
+
14
+ orig_display_error_messsage(ex)
15
+ end
16
+ end
17
+ end