sentry-ruby 5.1.0 → 5.4.2

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