sentry-ruby 4.9.2 → 5.4.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/CODE_OF_CONDUCT.md +74 -0
  7. data/Gemfile +27 -0
  8. data/Makefile +4 -0
  9. data/README.md +8 -7
  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/breadcrumb/sentry_logger.rb +90 -0
  16. data/lib/sentry/breadcrumb.rb +70 -0
  17. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  18. data/lib/sentry/client.rb +190 -0
  19. data/lib/sentry/configuration.rb +502 -0
  20. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  21. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  22. data/lib/sentry/dsn.rb +53 -0
  23. data/lib/sentry/envelope.rb +96 -0
  24. data/lib/sentry/error_event.rb +38 -0
  25. data/lib/sentry/event.rb +178 -0
  26. data/lib/sentry/exceptions.rb +9 -0
  27. data/lib/sentry/hub.rb +220 -0
  28. data/lib/sentry/integrable.rb +26 -0
  29. data/lib/sentry/interface.rb +16 -0
  30. data/lib/sentry/interfaces/exception.rb +43 -0
  31. data/lib/sentry/interfaces/request.rb +144 -0
  32. data/lib/sentry/interfaces/single_exception.rb +57 -0
  33. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  34. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  35. data/lib/sentry/interfaces/threads.rb +42 -0
  36. data/lib/sentry/linecache.rb +47 -0
  37. data/lib/sentry/logger.rb +20 -0
  38. data/lib/sentry/net/http.rb +115 -0
  39. data/lib/sentry/rack/capture_exceptions.rb +80 -0
  40. data/lib/sentry/rack.rb +5 -0
  41. data/lib/sentry/rake.rb +41 -0
  42. data/lib/sentry/redis.rb +90 -0
  43. data/lib/sentry/release_detector.rb +39 -0
  44. data/lib/sentry/scope.rb +295 -0
  45. data/lib/sentry/session.rb +35 -0
  46. data/lib/sentry/session_flusher.rb +90 -0
  47. data/lib/sentry/span.rb +226 -0
  48. data/lib/sentry/test_helper.rb +76 -0
  49. data/lib/sentry/transaction.rb +206 -0
  50. data/lib/sentry/transaction_event.rb +29 -0
  51. data/lib/sentry/transport/configuration.rb +25 -0
  52. data/lib/sentry/transport/dummy_transport.rb +21 -0
  53. data/lib/sentry/transport/http_transport.rb +175 -0
  54. data/lib/sentry/transport.rb +210 -0
  55. data/lib/sentry/utils/argument_checking_helper.rb +13 -0
  56. data/lib/sentry/utils/custom_inspection.rb +14 -0
  57. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  58. data/lib/sentry/utils/logging_helper.rb +26 -0
  59. data/lib/sentry/utils/real_ip.rb +84 -0
  60. data/lib/sentry/utils/request_id.rb +18 -0
  61. data/lib/sentry/version.rb +5 -0
  62. data/lib/sentry-ruby.rb +505 -0
  63. data/sentry-ruby-core.gemspec +23 -0
  64. data/sentry-ruby.gemspec +24 -0
  65. metadata +64 -30
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ # @api private
5
+ class Backtrace
6
+ # Handles backtrace parsing line by line
7
+ class Line
8
+ RB_EXTENSION = ".rb"
9
+ # regexp (optional leading X: on windows, or JRuby9000 class-prefix)
10
+ RUBY_INPUT_FORMAT = /
11
+ ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>):
12
+ (\d+)
13
+ (?: :in \s `([^']+)')?$
14
+ /x.freeze
15
+
16
+ # org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170)
17
+ JAVA_INPUT_FORMAT = /^(.+)\.([^\.]+)\(([^\:]+)\:(\d+)\)$/.freeze
18
+
19
+ # The file portion of the line (such as app/models/user.rb)
20
+ attr_reader :file
21
+
22
+ # The line number portion of the line
23
+ attr_reader :number
24
+
25
+ # The method of the line (such as index)
26
+ attr_reader :method
27
+
28
+ # The module name (JRuby)
29
+ attr_reader :module_name
30
+
31
+ attr_reader :in_app_pattern
32
+
33
+ # Parses a single line of a given backtrace
34
+ # @param [String] unparsed_line The raw line from +caller+ or some backtrace
35
+ # @return [Line] The parsed backtrace line
36
+ def self.parse(unparsed_line, in_app_pattern)
37
+ ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT)
38
+ if ruby_match
39
+ _, file, number, method = ruby_match.to_a
40
+ file.sub!(/\.class$/, RB_EXTENSION)
41
+ module_name = nil
42
+ else
43
+ java_match = unparsed_line.match(JAVA_INPUT_FORMAT)
44
+ _, module_name, method, file, number = java_match.to_a
45
+ end
46
+ new(file, number, method, module_name, in_app_pattern)
47
+ end
48
+
49
+ def initialize(file, number, method, module_name, in_app_pattern)
50
+ @file = file
51
+ @module_name = module_name
52
+ @number = number.to_i
53
+ @method = method
54
+ @in_app_pattern = in_app_pattern
55
+ end
56
+
57
+ def in_app
58
+ if file =~ in_app_pattern
59
+ true
60
+ else
61
+ false
62
+ end
63
+ end
64
+
65
+ # Reconstructs the line in a readable fashion
66
+ def to_s
67
+ "#{file}:#{number}:in `#{method}'"
68
+ end
69
+
70
+ def ==(other)
71
+ to_s == other.to_s
72
+ end
73
+
74
+ def inspect
75
+ "<Line:#{self}>"
76
+ end
77
+ end
78
+
79
+ APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test)/.freeze
80
+
81
+ # holder for an Array of Backtrace::Line instances
82
+ attr_reader :lines
83
+
84
+ def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback)
85
+ ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/)
86
+
87
+ ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback
88
+
89
+ in_app_pattern ||= begin
90
+ Regexp.new("^(#{project_root}/)?#{app_dirs_pattern || APP_DIRS_PATTERN}")
91
+ end
92
+
93
+ lines = ruby_lines.to_a.map do |unparsed_line|
94
+ Line.parse(unparsed_line, in_app_pattern)
95
+ end
96
+
97
+ new(lines)
98
+ end
99
+
100
+ def initialize(lines)
101
+ @lines = lines
102
+ end
103
+
104
+ def inspect
105
+ "<Backtrace: " + lines.map(&:inspect).join(", ") + ">"
106
+ end
107
+
108
+ def to_s
109
+ content = []
110
+ lines.each do |line|
111
+ content << line
112
+ end
113
+ content.join("\n")
114
+ end
115
+
116
+ def ==(other)
117
+ if other.respond_to?(:lines)
118
+ lines == other.lines
119
+ else
120
+ false
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ module Sentry
6
+ class Breadcrumb
7
+ module SentryLogger
8
+ LEVELS = {
9
+ ::Logger::DEBUG => 'debug',
10
+ ::Logger::INFO => 'info',
11
+ ::Logger::WARN => 'warn',
12
+ ::Logger::ERROR => 'error',
13
+ ::Logger::FATAL => 'fatal'
14
+ }.freeze
15
+
16
+ def add(*args, &block)
17
+ super
18
+ add_breadcrumb(*args, &block)
19
+ nil
20
+ end
21
+
22
+ def add_breadcrumb(severity, message = nil, progname = nil)
23
+ # because the breadcrumbs now belongs to different Hub's Scope in different threads
24
+ # we need to make sure the current thread's Hub has been set before adding breadcrumbs
25
+ return unless Sentry.initialized? && Sentry.get_current_hub
26
+
27
+ category = "logger"
28
+
29
+ # this is because the nature of Ruby Logger class:
30
+ #
31
+ # when given 1 argument, the argument will become both message and progname
32
+ #
33
+ # ```
34
+ # logger.info("foo")
35
+ # # message == progname == "foo"
36
+ # ```
37
+ #
38
+ # and to specify progname with a different message,
39
+ # we need to pass the progname as the argument and pass the message as a proc
40
+ #
41
+ # ```
42
+ # logger.info("progname") { "the message" }
43
+ # ```
44
+ #
45
+ # so the condition below is to replicate the similar behavior
46
+ if message.nil?
47
+ if block_given?
48
+ message = yield
49
+ category = progname
50
+ else
51
+ message = progname
52
+ end
53
+ end
54
+
55
+ return if ignored_logger?(progname) || message == ""
56
+
57
+ # some loggers will add leading/trailing space as they (incorrectly, mind you)
58
+ # think of logging as a shortcut to std{out,err}
59
+ message = message.to_s.strip
60
+
61
+ last_crumb = current_breadcrumbs.peek
62
+ # try to avoid dupes from logger broadcasts
63
+ if last_crumb.nil? || last_crumb.message != message
64
+ level = Sentry::Breadcrumb::SentryLogger::LEVELS.fetch(severity, nil)
65
+ crumb = Sentry::Breadcrumb.new(
66
+ level: level,
67
+ category: category,
68
+ message: message,
69
+ type: severity >= 3 ? "error" : level
70
+ )
71
+
72
+ Sentry.add_breadcrumb(crumb, hint: { severity: severity })
73
+ end
74
+ end
75
+
76
+ private
77
+
78
+ def ignored_logger?(progname)
79
+ progname == LOGGER_PROGNAME ||
80
+ Sentry.configuration.exclude_loggers.include?(progname)
81
+ end
82
+
83
+ def current_breadcrumbs
84
+ Sentry.get_current_scope.breadcrumbs
85
+ end
86
+ end
87
+ end
88
+ end
89
+
90
+ ::Logger.send(:prepend, Sentry::Breadcrumb::SentryLogger)
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Sentry
4
+ class Breadcrumb
5
+ DATA_SERIALIZATION_ERROR_MESSAGE = "[data were removed due to serialization issues]"
6
+
7
+ # @return [String, nil]
8
+ attr_accessor :category
9
+ # @return [Hash, nil]
10
+ attr_accessor :data
11
+ # @return [String, nil]
12
+ attr_accessor :level
13
+ # @return [Time, Integer, nil]
14
+ attr_accessor :timestamp
15
+ # @return [String, nil]
16
+ attr_accessor :type
17
+ # @return [String, nil]
18
+ attr_reader :message
19
+
20
+ # @param category [String, nil]
21
+ # @param data [Hash, nil]
22
+ # @param message [String, nil]
23
+ # @param timestamp [Time, Integer, nil]
24
+ # @param level [String, nil]
25
+ # @param type [String, nil]
26
+ def initialize(category: nil, data: nil, message: nil, timestamp: nil, level: nil, type: nil)
27
+ @category = category
28
+ @data = data || {}
29
+ @level = level
30
+ @timestamp = timestamp || Sentry.utc_now.to_i
31
+ @type = type
32
+ self.message = message
33
+ end
34
+
35
+ # @return [Hash]
36
+ def to_hash
37
+ {
38
+ category: @category,
39
+ data: serialized_data,
40
+ level: @level,
41
+ message: @message,
42
+ timestamp: @timestamp,
43
+ type: @type
44
+ }
45
+ end
46
+
47
+ # @param message [String]
48
+ # @return [void]
49
+ def message=(message)
50
+ @message = (message || "").byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)
51
+ end
52
+
53
+ private
54
+
55
+ def serialized_data
56
+ begin
57
+ ::JSON.parse(::JSON.generate(@data))
58
+ rescue Exception => e
59
+ Sentry.logger.debug(LOGGER_PROGNAME) do
60
+ <<~MSG
61
+ can't serialize breadcrumb data because of error: #{e}
62
+ data: #{@data}
63
+ MSG
64
+ end
65
+
66
+ DATA_SERIALIZATION_ERROR_MESSAGE
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/breadcrumb"
4
+
5
+ module Sentry
6
+ class BreadcrumbBuffer
7
+ DEFAULT_SIZE = 100
8
+ include Enumerable
9
+
10
+ # @return [Array]
11
+ attr_accessor :buffer
12
+
13
+ # @param size [Integer, nil] If it's not provided, it'll fallback to DEFAULT_SIZE
14
+ def initialize(size = nil)
15
+ @buffer = Array.new(size || DEFAULT_SIZE)
16
+ end
17
+
18
+ # @param crumb [Breadcrumb]
19
+ # @return [void]
20
+ def record(crumb)
21
+ yield(crumb) if block_given?
22
+ @buffer.slice!(0)
23
+ @buffer << crumb
24
+ end
25
+
26
+ # @return [Array]
27
+ def members
28
+ @buffer.compact
29
+ end
30
+
31
+ # Returns the last breadcrumb stored in the buffer. If the buffer it's empty, it returns nil.
32
+ # @return [Breadcrumb, nil]
33
+ def peek
34
+ members.last
35
+ end
36
+
37
+ # Iterates through all breadcrumbs.
38
+ # @param block [Proc]
39
+ # @yieldparam crumb [Breadcrumb]
40
+ # @return [Array]
41
+ def each(&block)
42
+ members.each(&block)
43
+ end
44
+
45
+ # @return [Boolean]
46
+ def empty?
47
+ members.none?
48
+ end
49
+
50
+ # @return [Hash]
51
+ def to_hash
52
+ {
53
+ values: members.map(&:to_hash)
54
+ }
55
+ end
56
+
57
+ # @return [BreadcrumbBuffer]
58
+ def dup
59
+ copy = super
60
+ copy.buffer = buffer.deep_dup
61
+ copy
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,190 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "sentry/transport"
4
+
5
+ module Sentry
6
+ class Client
7
+ include LoggingHelper
8
+
9
+ # The Transport object that'll send events for the client.
10
+ # @return [Transport]
11
+ attr_reader :transport
12
+
13
+ # @!macro configuration
14
+ attr_reader :configuration
15
+
16
+ # @deprecated Use Sentry.logger to retrieve the current logger instead.
17
+ attr_reader :logger
18
+
19
+ # @param configuration [Configuration]
20
+ def initialize(configuration)
21
+ @configuration = configuration
22
+ @logger = configuration.logger
23
+
24
+ if transport_class = configuration.transport.transport_class
25
+ @transport = transport_class.new(configuration)
26
+ else
27
+ @transport =
28
+ case configuration.dsn&.scheme
29
+ when 'http', 'https'
30
+ HTTPTransport.new(configuration)
31
+ else
32
+ DummyTransport.new(configuration)
33
+ end
34
+ end
35
+ end
36
+
37
+ # Applies the given scope's data to the event and sends it to Sentry.
38
+ # @param event [Event] the event to be sent.
39
+ # @param scope [Scope] the scope with contextual data that'll be applied to the event before it's sent.
40
+ # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
41
+ # @return [Event, nil]
42
+ def capture_event(event, scope, hint = {})
43
+ return unless configuration.sending_allowed?
44
+
45
+ unless event.is_a?(TransactionEvent) || configuration.sample_allowed?
46
+ transport.record_lost_event(:sample_rate, 'event')
47
+ return
48
+ end
49
+
50
+ event_type = event.is_a?(Event) ? event.type : event["type"]
51
+ event = scope.apply_to_event(event, hint)
52
+
53
+ if event.nil?
54
+ log_info("Discarded event because one of the event processors returned nil")
55
+ transport.record_lost_event(:event_processor, event_type)
56
+ return
57
+ end
58
+
59
+ if async_block = configuration.async
60
+ dispatch_async_event(async_block, event, hint)
61
+ elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true)
62
+ queued = dispatch_background_event(event, hint)
63
+ transport.record_lost_event(:queue_overflow, event_type) unless queued
64
+ else
65
+ send_event(event, hint)
66
+ end
67
+
68
+ event
69
+ rescue => e
70
+ log_error("Event capturing failed", e, debug: configuration.debug)
71
+ nil
72
+ end
73
+
74
+ # Initializes an Event object with the given exception. Returns `nil` if the exception's class is excluded from reporting.
75
+ # @param exception [Exception] the exception to be reported.
76
+ # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
77
+ # @return [Event, nil]
78
+ def event_from_exception(exception, hint = {})
79
+ return unless @configuration.sending_allowed? && @configuration.exception_class_allowed?(exception)
80
+
81
+ integration_meta = Sentry.integrations[hint[:integration]]
82
+
83
+ ErrorEvent.new(configuration: configuration, integration_meta: integration_meta).tap do |event|
84
+ event.add_exception_interface(exception)
85
+ event.add_threads_interface(crashed: true)
86
+ event.level = :error
87
+ end
88
+ end
89
+
90
+ # Initializes an Event object with the given message.
91
+ # @param message [String] the message to be reported.
92
+ # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors.
93
+ # @return [Event]
94
+ def event_from_message(message, hint = {}, backtrace: nil)
95
+ return unless @configuration.sending_allowed?
96
+
97
+ integration_meta = Sentry.integrations[hint[:integration]]
98
+ event = ErrorEvent.new(configuration: configuration, integration_meta: integration_meta, message: message)
99
+ event.add_threads_interface(backtrace: backtrace || caller)
100
+ event.level = :error
101
+ event
102
+ end
103
+
104
+ # Initializes an Event object with the given Transaction object.
105
+ # @param transaction [Transaction] the transaction to be recorded.
106
+ # @return [TransactionEvent]
107
+ def event_from_transaction(transaction)
108
+ TransactionEvent.new(configuration: configuration).tap do |event|
109
+ event.transaction = transaction.name
110
+ event.contexts.merge!(trace: transaction.get_trace_context)
111
+ event.timestamp = transaction.timestamp
112
+ event.start_timestamp = transaction.start_timestamp
113
+ event.tags = transaction.tags
114
+
115
+ finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction }
116
+ event.spans = finished_spans.map(&:to_hash)
117
+ end
118
+ end
119
+
120
+ # @!macro send_event
121
+ def send_event(event, hint = nil)
122
+ event_type = event.is_a?(Event) ? event.type : event["type"]
123
+
124
+ if event_type != TransactionEvent::TYPE && configuration.before_send
125
+ event = configuration.before_send.call(event, hint)
126
+
127
+ if event.nil?
128
+ log_info("Discarded event because before_send returned nil")
129
+ transport.record_lost_event(:before_send, 'event')
130
+ return
131
+ end
132
+ end
133
+
134
+ transport.send_event(event)
135
+
136
+ event
137
+ rescue => e
138
+ loggable_event_type = event_type.capitalize
139
+ log_error("#{loggable_event_type} sending failed", e, debug: configuration.debug)
140
+
141
+ event_info = Event.get_log_message(event.to_hash)
142
+ log_info("Unreported #{loggable_event_type}: #{event_info}")
143
+ transport.record_lost_event(:network_error, event_type)
144
+ raise
145
+ end
146
+
147
+ # Generates a Sentry trace for distribted tracing from the given Span.
148
+ # Returns `nil` if `config.propagate_traces` is `false`.
149
+ # @param span [Span] the span to generate trace from.
150
+ # @return [String, nil]
151
+ def generate_sentry_trace(span)
152
+ return unless configuration.propagate_traces
153
+
154
+ trace = span.to_sentry_trace
155
+ log_debug("[Tracing] Adding #{SENTRY_TRACE_HEADER_NAME} header to outgoing request: #{trace}")
156
+ trace
157
+ end
158
+
159
+ private
160
+
161
+ def dispatch_background_event(event, hint)
162
+ Sentry.background_worker.perform do
163
+ send_event(event, hint)
164
+ end
165
+ end
166
+
167
+ def dispatch_async_event(async_block, event, hint)
168
+ # We have to convert to a JSON-like hash, because background job
169
+ # processors (esp ActiveJob) may not like weird types in the event hash
170
+
171
+ event_hash =
172
+ begin
173
+ event.to_json_compatible
174
+ rescue => e
175
+ log_error("Converting #{event.type} (#{event.event_id}) to JSON compatible hash failed", e, debug: configuration.debug)
176
+ return
177
+ end
178
+
179
+ if async_block.arity == 2
180
+ hint = JSON.parse(JSON.generate(hint))
181
+ async_block.call(event_hash, hint)
182
+ else
183
+ async_block.call(event_hash)
184
+ end
185
+ rescue => e
186
+ log_error("Async #{event_hash["type"]} sending failed", e, debug: configuration.debug)
187
+ send_event(event, hint)
188
+ end
189
+ end
190
+ end