sentry-ruby 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/.craft.yml +18 -0
  3. data/.gitignore +11 -0
  4. data/.rspec +3 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +10 -0
  7. data/CODE_OF_CONDUCT.md +74 -0
  8. data/Gemfile +11 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +44 -0
  11. data/Rakefile +6 -0
  12. data/bin/console +14 -0
  13. data/bin/setup +8 -0
  14. data/lib/sentry.rb +97 -0
  15. data/lib/sentry/backtrace.rb +128 -0
  16. data/lib/sentry/breadcrumb.rb +25 -0
  17. data/lib/sentry/breadcrumb/sentry_logger.rb +103 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +50 -0
  19. data/lib/sentry/client.rb +85 -0
  20. data/lib/sentry/configuration.rb +401 -0
  21. data/lib/sentry/core_ext/object/deep_dup.rb +57 -0
  22. data/lib/sentry/core_ext/object/duplicable.rb +153 -0
  23. data/lib/sentry/dsn.rb +45 -0
  24. data/lib/sentry/event.rb +175 -0
  25. data/lib/sentry/event/options.rb +31 -0
  26. data/lib/sentry/hub.rb +126 -0
  27. data/lib/sentry/interface.rb +22 -0
  28. data/lib/sentry/interfaces/exception.rb +11 -0
  29. data/lib/sentry/interfaces/request.rb +104 -0
  30. data/lib/sentry/interfaces/single_exception.rb +14 -0
  31. data/lib/sentry/interfaces/stacktrace.rb +57 -0
  32. data/lib/sentry/linecache.rb +44 -0
  33. data/lib/sentry/logger.rb +20 -0
  34. data/lib/sentry/rack.rb +4 -0
  35. data/lib/sentry/rack/capture_exception.rb +45 -0
  36. data/lib/sentry/ruby.rb +1 -0
  37. data/lib/sentry/scope.rb +192 -0
  38. data/lib/sentry/transport.rb +110 -0
  39. data/lib/sentry/transport/configuration.rb +28 -0
  40. data/lib/sentry/transport/dummy_transport.rb +14 -0
  41. data/lib/sentry/transport/http_transport.rb +62 -0
  42. data/lib/sentry/transport/state.rb +40 -0
  43. data/lib/sentry/utils/deep_merge.rb +22 -0
  44. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  45. data/lib/sentry/utils/real_ip.rb +70 -0
  46. data/lib/sentry/version.rb +3 -0
  47. data/sentry-ruby.gemspec +26 -0
  48. metadata +107 -0
@@ -0,0 +1,126 @@
1
+ require "sentry/scope"
2
+ require "sentry/client"
3
+
4
+ module Sentry
5
+ class Hub
6
+ attr_reader :last_event_id
7
+
8
+ def initialize(client, scope)
9
+ first_layer = Layer.new(client, scope)
10
+ @stack = [first_layer]
11
+ @last_event_id = nil
12
+ end
13
+
14
+ def new_from_top
15
+ Hub.new(current_client, current_scope)
16
+ end
17
+
18
+ def current_client
19
+ current_layer&.client
20
+ end
21
+
22
+ def current_scope
23
+ current_layer&.scope
24
+ end
25
+
26
+ def clone
27
+ layer = current_layer
28
+
29
+ if layer
30
+ scope = layer.scope&.dup
31
+
32
+ Hub.new(layer.client, scope)
33
+ end
34
+ end
35
+
36
+ def bind_client(client)
37
+ layer = current_layer
38
+
39
+ if layer
40
+ layer.client = client
41
+ end
42
+ end
43
+
44
+ def configure_scope(&block)
45
+ block.call(current_scope)
46
+ end
47
+
48
+ def with_scope(&block)
49
+ push_scope
50
+ yield(current_scope)
51
+ ensure
52
+ pop_scope
53
+ end
54
+
55
+ def push_scope
56
+ new_scope =
57
+ if current_scope
58
+ current_scope.dup
59
+ else
60
+ Scope.new
61
+ end
62
+
63
+ @stack << Layer.new(current_client, new_scope)
64
+ end
65
+
66
+ def pop_scope
67
+ @stack.pop
68
+ end
69
+
70
+ def capture_exception(exception, **options, &block)
71
+ return unless current_client
72
+
73
+ event = current_client.event_from_exception(exception)
74
+
75
+ return unless event
76
+
77
+ capture_event(event, **options, &block)
78
+ end
79
+
80
+ def capture_message(message, **options, &block)
81
+ return unless current_client
82
+
83
+ event = current_client.event_from_message(message)
84
+ capture_event(event, **options, &block)
85
+ end
86
+
87
+ def capture_event(event, **options, &block)
88
+ return unless current_client
89
+
90
+ scope = current_scope.dup
91
+
92
+ if block
93
+ block.call(scope)
94
+ elsif custom_scope = options[:scope]
95
+ scope.update_from_scope(custom_scope)
96
+ elsif !options.empty?
97
+ scope.update_from_options(**options)
98
+ end
99
+
100
+ event = current_client.capture_event(event, scope)
101
+
102
+ @last_event_id = event.id
103
+ event
104
+ end
105
+
106
+ def add_breadcrumb(breadcrumb)
107
+ current_scope.add_breadcrumb(breadcrumb)
108
+ end
109
+
110
+ private
111
+
112
+ def current_layer
113
+ @stack.last
114
+ end
115
+
116
+ class Layer
117
+ attr_accessor :client
118
+ attr_reader :scope
119
+
120
+ def initialize(client, scope)
121
+ @client = client
122
+ @scope = scope
123
+ end
124
+ end
125
+ end
126
+ 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,104 @@
1
+ require 'rack'
2
+
3
+ module Sentry
4
+ class RequestInterface < Interface
5
+ REQUEST_ID_HEADERS = %w(action_dispatch.request_id HTTP_X_REQUEST_ID).freeze
6
+ IP_HEADERS = [
7
+ "REMOTE_ADDR",
8
+ "HTTP_CLIENT_IP",
9
+ "HTTP_X_REAL_IP",
10
+ "HTTP_X_FORWARDED_FOR"
11
+ ].freeze
12
+
13
+ attr_accessor :url, :method, :data, :query_string, :cookies, :headers, :env
14
+
15
+ def initialize
16
+ self.headers = {}
17
+ self.env = {}
18
+ self.cookies = nil
19
+ end
20
+
21
+ def from_rack(env_hash)
22
+ req = ::Rack::Request.new(env_hash)
23
+
24
+ if Sentry.configuration.send_default_pii
25
+ self.data = read_data_from(req)
26
+ self.cookies = req.cookies
27
+ else
28
+ # need to completely wipe out ip addresses
29
+ IP_HEADERS.each { |h| env_hash.delete(h) }
30
+ end
31
+
32
+ self.url = req.scheme && req.url.split('?').first
33
+ self.method = req.request_method
34
+ self.query_string = req.query_string
35
+
36
+ self.headers = format_headers_for_sentry(env_hash)
37
+ self.env = format_env_for_sentry(env_hash)
38
+ end
39
+
40
+ private
41
+
42
+ # Request ID based on ActionDispatch::RequestId
43
+ def read_request_id_from(env_hash)
44
+ REQUEST_ID_HEADERS.each do |key|
45
+ request_id = env_hash[key]
46
+ return request_id if request_id
47
+ end
48
+ nil
49
+ end
50
+
51
+ # See Sentry server default limits at
52
+ # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py
53
+ def read_data_from(request)
54
+ if request.form_data?
55
+ request.POST
56
+ elsif request.body # JSON requests, etc
57
+ data = request.body.read(4096 * 4) # Sentry server limit
58
+ request.body.rewind
59
+ data
60
+ end
61
+ rescue IOError => e
62
+ e.message
63
+ end
64
+
65
+ def format_headers_for_sentry(env_hash)
66
+ env_hash.each_with_object({}) do |(key, value), memo|
67
+ begin
68
+ key = key.to_s # rack env can contain symbols
69
+ value = value.to_s
70
+ next memo['X-Request-Id'] ||= read_request_id_from(env_hash) if REQUEST_ID_HEADERS.include?(key)
71
+ next unless key.upcase == key # Non-upper case stuff isn't either
72
+
73
+ # Rack adds in an incorrect HTTP_VERSION key, which causes downstream
74
+ # to think this is a Version header. Instead, this is mapped to
75
+ # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header
76
+ # if the request has legitimately sent a Version header themselves.
77
+ # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29
78
+ next if key == 'HTTP_VERSION' && value == env_hash['SERVER_PROTOCOL']
79
+ next if key == 'HTTP_COOKIE' # Cookies don't go here, they go somewhere else
80
+ next unless key.start_with?('HTTP_') || %w(CONTENT_TYPE CONTENT_LENGTH).include?(key)
81
+
82
+ # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever
83
+ key = key.sub(/^HTTP_/, "")
84
+ key = key.split('_').map(&:capitalize).join('-')
85
+ memo[key] = value
86
+ rescue StandardError => e
87
+ # Rails adds objects to the Rack env that can sometimes raise exceptions
88
+ # when `to_s` is called.
89
+ # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134
90
+ Sentry.logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" }
91
+ next
92
+ end
93
+ end
94
+ end
95
+
96
+ def format_env_for_sentry(env_hash)
97
+ return env_hash if Sentry.configuration.rack_env_whitelist.empty?
98
+
99
+ env_hash.select do |k, _v|
100
+ Sentry.configuration.rack_env_whitelist.include? k.to_s
101
+ end
102
+ end
103
+ end
104
+ 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,57 @@
1
+ module Sentry
2
+ class StacktraceInterface < Interface
3
+ attr_accessor :frames
4
+
5
+ def to_hash
6
+ data = super
7
+ data[:frames] = data[:frames].map(&:to_hash)
8
+ data
9
+ end
10
+
11
+ # Not actually an interface, but I want to use the same style
12
+ class Frame < Interface
13
+ attr_accessor :abs_path, :context_line, :function, :in_app,
14
+ :lineno, :module, :pre_context, :post_context, :vars
15
+
16
+ def initialize(project_root)
17
+ @project_root = project_root
18
+ end
19
+
20
+ def filename
21
+ return if abs_path.nil?
22
+ return @filename if instance_variable_defined?(:@filename)
23
+
24
+ prefix =
25
+ if under_project_root? && in_app
26
+ @project_root
27
+ elsif under_project_root?
28
+ longest_load_path || @project_root
29
+ else
30
+ longest_load_path
31
+ end
32
+
33
+ @filename = prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path
34
+ end
35
+
36
+ def to_hash(*args)
37
+ data = super(*args)
38
+ data[:filename] = filename
39
+ data.delete(:vars) unless vars && !vars.empty?
40
+ data.delete(:pre_context) unless pre_context && !pre_context.empty?
41
+ data.delete(:post_context) unless post_context && !post_context.empty?
42
+ data.delete(:context_line) unless context_line && !context_line.empty?
43
+ data
44
+ end
45
+
46
+ private
47
+
48
+ def under_project_root?
49
+ @project_root && abs_path.start_with?(@project_root)
50
+ end
51
+
52
+ def longest_load_path
53
+ $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size)
54
+ end
55
+ end
56
+ end
57
+ 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 'time'
2
+ require 'rack'
3
+
4
+ require 'sentry/rack/capture_exception'
@@ -0,0 +1,45 @@
1
+ module Sentry
2
+ module Rack
3
+ class CaptureException
4
+ def initialize(app)
5
+ @app = app
6
+ end
7
+
8
+ def call(env)
9
+ # this call clones the main (global) hub
10
+ # and assigns it to the current thread's Sentry#get_current_hub
11
+ # it's essential for multi-thread servers (e.g. puma)
12
+ Sentry.clone_hub_to_current_thread unless Sentry.get_current_hub
13
+ # this call creates an isolated scope for every request
14
+ # it's essential for multi-process servers (e.g. unicorn)
15
+ Sentry.with_scope do |scope|
16
+ # there could be some breadcrumbs already stored in the top-level scope
17
+ # and for request information, we don't need those breadcrumbs
18
+ scope.clear_breadcrumbs
19
+ env['sentry.client'] = Sentry.get_current_client
20
+
21
+ scope.set_transaction_name(env["PATH_INFO"]) if env["PATH_INFO"]
22
+ scope.set_rack_env(env)
23
+
24
+ begin
25
+ response = @app.call(env)
26
+ rescue Sentry::Error
27
+ raise # Don't capture Sentry errors
28
+ rescue Exception => e
29
+ Sentry.capture_exception(e)
30
+ raise
31
+ end
32
+
33
+ exception = collect_exception(env)
34
+ Sentry.capture_exception(exception) if exception
35
+
36
+ response
37
+ end
38
+ end
39
+
40
+ def collect_exception(env)
41
+ env['rack.exception'] || env['sinatra.error']
42
+ end
43
+ end
44
+ end
45
+ end