sentry-ruby 5.3.1 → 5.16.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +11 -0
  3. data/.rspec +2 -0
  4. data/.yardopts +2 -0
  5. data/CHANGELOG.md +313 -0
  6. data/Gemfile +26 -0
  7. data/Makefile +4 -0
  8. data/README.md +11 -8
  9. data/Rakefile +20 -0
  10. data/bin/console +18 -0
  11. data/bin/setup +8 -0
  12. data/lib/sentry/background_worker.rb +79 -0
  13. data/lib/sentry/backpressure_monitor.rb +75 -0
  14. data/lib/sentry/backtrace.rb +124 -0
  15. data/lib/sentry/baggage.rb +70 -0
  16. data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
  17. data/lib/sentry/breadcrumb.rb +76 -0
  18. data/lib/sentry/breadcrumb_buffer.rb +64 -0
  19. data/lib/sentry/check_in_event.rb +60 -0
  20. data/lib/sentry/client.rb +248 -0
  21. data/lib/sentry/configuration.rb +650 -0
  22. data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
  23. data/lib/sentry/core_ext/object/duplicable.rb +155 -0
  24. data/lib/sentry/cron/configuration.rb +23 -0
  25. data/lib/sentry/cron/monitor_check_ins.rb +75 -0
  26. data/lib/sentry/cron/monitor_config.rb +53 -0
  27. data/lib/sentry/cron/monitor_schedule.rb +42 -0
  28. data/lib/sentry/dsn.rb +53 -0
  29. data/lib/sentry/envelope.rb +93 -0
  30. data/lib/sentry/error_event.rb +38 -0
  31. data/lib/sentry/event.rb +156 -0
  32. data/lib/sentry/exceptions.rb +9 -0
  33. data/lib/sentry/hub.rb +316 -0
  34. data/lib/sentry/integrable.rb +32 -0
  35. data/lib/sentry/interface.rb +16 -0
  36. data/lib/sentry/interfaces/exception.rb +43 -0
  37. data/lib/sentry/interfaces/request.rb +134 -0
  38. data/lib/sentry/interfaces/single_exception.rb +67 -0
  39. data/lib/sentry/interfaces/stacktrace.rb +87 -0
  40. data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
  41. data/lib/sentry/interfaces/threads.rb +42 -0
  42. data/lib/sentry/linecache.rb +47 -0
  43. data/lib/sentry/logger.rb +20 -0
  44. data/lib/sentry/net/http.rb +106 -0
  45. data/lib/sentry/profiler.rb +233 -0
  46. data/lib/sentry/propagation_context.rb +134 -0
  47. data/lib/sentry/puma.rb +32 -0
  48. data/lib/sentry/rack/capture_exceptions.rb +79 -0
  49. data/lib/sentry/rack.rb +5 -0
  50. data/lib/sentry/rake.rb +28 -0
  51. data/lib/sentry/redis.rb +108 -0
  52. data/lib/sentry/release_detector.rb +39 -0
  53. data/lib/sentry/scope.rb +360 -0
  54. data/lib/sentry/session.rb +33 -0
  55. data/lib/sentry/session_flusher.rb +90 -0
  56. data/lib/sentry/span.rb +273 -0
  57. data/lib/sentry/test_helper.rb +84 -0
  58. data/lib/sentry/transaction.rb +359 -0
  59. data/lib/sentry/transaction_event.rb +80 -0
  60. data/lib/sentry/transport/configuration.rb +98 -0
  61. data/lib/sentry/transport/dummy_transport.rb +21 -0
  62. data/lib/sentry/transport/http_transport.rb +206 -0
  63. data/lib/sentry/transport/spotlight_transport.rb +50 -0
  64. data/lib/sentry/transport.rb +225 -0
  65. data/lib/sentry/utils/argument_checking_helper.rb +19 -0
  66. data/lib/sentry/utils/custom_inspection.rb +14 -0
  67. data/lib/sentry/utils/encoding_helper.rb +22 -0
  68. data/lib/sentry/utils/exception_cause_chain.rb +20 -0
  69. data/lib/sentry/utils/logging_helper.rb +26 -0
  70. data/lib/sentry/utils/real_ip.rb +84 -0
  71. data/lib/sentry/utils/request_id.rb +18 -0
  72. data/lib/sentry/version.rb +5 -0
  73. data/lib/sentry-ruby.rb +580 -0
  74. data/sentry-ruby-core.gemspec +23 -0
  75. data/sentry-ruby.gemspec +24 -0
  76. metadata +75 -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|spec)/.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,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
5
+ module Sentry
6
+ # A {https://www.w3.org/TR/baggage W3C Baggage Header} implementation.
7
+ class Baggage
8
+ SENTRY_PREFIX = 'sentry-'
9
+ SENTRY_PREFIX_REGEX = /^sentry-/.freeze
10
+
11
+ # @return [Hash]
12
+ attr_reader :items
13
+
14
+ # @return [Boolean]
15
+ attr_reader :mutable
16
+
17
+ def initialize(items, mutable: true)
18
+ @items = items
19
+ @mutable = mutable
20
+ end
21
+
22
+ # Creates a Baggage object from an incoming W3C Baggage header string.
23
+ #
24
+ # Sentry items are identified with the 'sentry-' prefix and stored in a hash.
25
+ # The presence of a Sentry item makes the baggage object immutable.
26
+ #
27
+ # @param header [String] The incoming Baggage header string.
28
+ # @return [Baggage, nil]
29
+ def self.from_incoming_header(header)
30
+ items = {}
31
+ mutable = true
32
+
33
+ header.split(',').each do |item|
34
+ item = item.strip
35
+ key, val = item.split('=')
36
+
37
+ next unless key && val
38
+ next unless key =~ SENTRY_PREFIX_REGEX
39
+
40
+ baggage_key = key.split('-')[1]
41
+ next unless baggage_key
42
+
43
+ items[CGI.unescape(baggage_key)] = CGI.unescape(val)
44
+ mutable = false
45
+ end
46
+
47
+ new(items, mutable: mutable)
48
+ end
49
+
50
+ # Make the Baggage immutable.
51
+ # @return [void]
52
+ def freeze!
53
+ @mutable = false
54
+ end
55
+
56
+ # A {https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#envelope-header Dynamic Sampling Context}
57
+ # hash to be used in the trace envelope header.
58
+ # @return [Hash]
59
+ def dynamic_sampling_context
60
+ @items
61
+ end
62
+
63
+ # Serialize the Baggage object back to a string.
64
+ # @return [String]
65
+ def serialize
66
+ items = @items.map { |k, v| "#{SENTRY_PREFIX}#{CGI.escape(k)}=#{CGI.escape(v)}" }
67
+ items.join(',')
68
+ end
69
+ end
70
+ 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,76 @@
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_reader :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
+ @timestamp = timestamp || Sentry.utc_now.to_i
30
+ @type = type
31
+ self.message = message
32
+ self.level = level
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
+ # @param level [String]
54
+ # @return [void]
55
+ def level=(level) # needed to meet the Sentry spec
56
+ @level = level == "warn" ? "warning" : level
57
+ end
58
+
59
+ private
60
+
61
+ def serialized_data
62
+ begin
63
+ ::JSON.parse(::JSON.generate(@data))
64
+ rescue Exception => e
65
+ Sentry.logger.debug(LOGGER_PROGNAME) do
66
+ <<~MSG
67
+ can't serialize breadcrumb data because of error: #{e}
68
+ data: #{@data}
69
+ MSG
70
+ end
71
+
72
+ DATA_SERIALIZATION_ERROR_MESSAGE
73
+ end
74
+ end
75
+ end
76
+ 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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+ require 'sentry/cron/monitor_config'
5
+
6
+ module Sentry
7
+ class CheckInEvent < Event
8
+ TYPE = 'check_in'
9
+
10
+ # uuid to identify this check-in.
11
+ # @return [String]
12
+ attr_accessor :check_in_id
13
+
14
+ # Identifier of the monitor for this check-in.
15
+ # @return [String]
16
+ attr_accessor :monitor_slug
17
+
18
+ # Duration of this check since it has started in seconds.
19
+ # @return [Integer, nil]
20
+ attr_accessor :duration
21
+
22
+ # Monitor configuration to support upserts.
23
+ # @return [Cron::MonitorConfig, nil]
24
+ attr_accessor :monitor_config
25
+
26
+ # Status of this check-in.
27
+ # @return [Symbol]
28
+ attr_accessor :status
29
+
30
+ VALID_STATUSES = %i(ok in_progress error)
31
+
32
+ def initialize(
33
+ slug:,
34
+ status:,
35
+ duration: nil,
36
+ monitor_config: nil,
37
+ check_in_id: nil,
38
+ **options
39
+ )
40
+ super(**options)
41
+
42
+ self.monitor_slug = slug
43
+ self.status = status
44
+ self.duration = duration
45
+ self.monitor_config = monitor_config
46
+ self.check_in_id = check_in_id || SecureRandom.uuid.delete('-')
47
+ end
48
+
49
+ # @return [Hash]
50
+ def to_hash
51
+ data = super
52
+ data[:check_in_id] = check_in_id
53
+ data[:monitor_slug] = monitor_slug
54
+ data[:status] = status
55
+ data[:duration] = duration if duration
56
+ data[:monitor_config] = monitor_config.to_hash if monitor_config
57
+ data
58
+ end
59
+ end
60
+ end