sentry-ruby 0.1.1

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 (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