sentry-ruby 5.3.1 → 5.4.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.
- checksums.yaml +4 -4
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.yardopts +2 -0
- data/CHANGELOG.md +313 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +27 -0
- data/Makefile +4 -0
- data/Rakefile +13 -0
- data/bin/console +18 -0
- data/bin/setup +8 -0
- data/lib/sentry/background_worker.rb +72 -0
- data/lib/sentry/backtrace.rb +124 -0
- data/lib/sentry/breadcrumb/sentry_logger.rb +90 -0
- data/lib/sentry/breadcrumb.rb +70 -0
- data/lib/sentry/breadcrumb_buffer.rb +64 -0
- data/lib/sentry/client.rb +190 -0
- data/lib/sentry/configuration.rb +502 -0
- data/lib/sentry/core_ext/object/deep_dup.rb +61 -0
- data/lib/sentry/core_ext/object/duplicable.rb +155 -0
- data/lib/sentry/dsn.rb +53 -0
- data/lib/sentry/envelope.rb +96 -0
- data/lib/sentry/error_event.rb +38 -0
- data/lib/sentry/event.rb +178 -0
- data/lib/sentry/exceptions.rb +9 -0
- data/lib/sentry/hub.rb +220 -0
- data/lib/sentry/integrable.rb +26 -0
- data/lib/sentry/interface.rb +16 -0
- data/lib/sentry/interfaces/exception.rb +43 -0
- data/lib/sentry/interfaces/request.rb +144 -0
- data/lib/sentry/interfaces/single_exception.rb +57 -0
- data/lib/sentry/interfaces/stacktrace.rb +87 -0
- data/lib/sentry/interfaces/stacktrace_builder.rb +79 -0
- data/lib/sentry/interfaces/threads.rb +42 -0
- data/lib/sentry/linecache.rb +47 -0
- data/lib/sentry/logger.rb +20 -0
- data/lib/sentry/net/http.rb +115 -0
- data/lib/sentry/rack/capture_exceptions.rb +80 -0
- data/lib/sentry/rack.rb +5 -0
- data/lib/sentry/rake.rb +41 -0
- data/lib/sentry/redis.rb +90 -0
- data/lib/sentry/release_detector.rb +39 -0
- data/lib/sentry/scope.rb +295 -0
- data/lib/sentry/session.rb +35 -0
- data/lib/sentry/session_flusher.rb +90 -0
- data/lib/sentry/span.rb +226 -0
- data/lib/sentry/test_helper.rb +76 -0
- data/lib/sentry/transaction.rb +206 -0
- data/lib/sentry/transaction_event.rb +29 -0
- data/lib/sentry/transport/configuration.rb +25 -0
- data/lib/sentry/transport/dummy_transport.rb +21 -0
- data/lib/sentry/transport/http_transport.rb +175 -0
- data/lib/sentry/transport.rb +210 -0
- data/lib/sentry/utils/argument_checking_helper.rb +13 -0
- data/lib/sentry/utils/custom_inspection.rb +14 -0
- data/lib/sentry/utils/exception_cause_chain.rb +20 -0
- data/lib/sentry/utils/logging_helper.rb +26 -0
- data/lib/sentry/utils/real_ip.rb +84 -0
- data/lib/sentry/utils/request_id.rb +18 -0
- data/lib/sentry/version.rb +5 -0
- data/lib/sentry-ruby.rb +505 -0
- data/sentry-ruby-core.gemspec +23 -0
- data/sentry-ruby.gemspec +24 -0
- 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.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
         |