sentry-ruby-core-5.28.0/0000755000004100000410000000000015067721773015106 5ustar www-datawww-datasentry-ruby-core-5.28.0/sentry-ruby.gemspec0000644000004100000410000000226115067721773020757 0ustar www-datawww-data# frozen_string_literal: true require_relative "lib/sentry/version" Gem::Specification.new do |spec| spec.name = "sentry-ruby" spec.version = Sentry::VERSION spec.authors = ["Sentry Team"] spec.description = spec.summary = "A gem that provides a client interface for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") github_root_uri = 'https://github.com/getsentry/sentry-ruby' spec.homepage = "#{github_root_uri}/tree/#{spec.version}/#{spec.name}" spec.metadata = { "homepage_uri" => spec.homepage, "source_code_uri" => spec.homepage, "changelog_uri" => "#{github_root_uri}/blob/#{spec.version}/CHANGELOG.md", "bug_tracker_uri" => "#{github_root_uri}/issues", "documentation_uri" => "http://www.rubydoc.info/gems/#{spec.name}/#{spec.version}" } spec.require_paths = ["lib"] spec.add_dependency "concurrent-ruby", "~> 1.0", ">= 1.0.2" spec.add_dependency "bigdecimal" end sentry-ruby-core-5.28.0/bin/0000755000004100000410000000000015067721773015656 5ustar www-datawww-datasentry-ruby-core-5.28.0/bin/setup0000755000004100000410000000020315067721773016737 0ustar www-datawww-data#!/usr/bin/env bash set -euo pipefail IFS=$'\n\t' set -vx bundle install # Do any other automated setup that you need to do here sentry-ruby-core-5.28.0/bin/console0000755000004100000410000000100615067721773017243 0ustar www-datawww-data#!/usr/bin/env ruby # frozen_string_literal: true require "bundler/setup" require "debug" require "sentry-ruby" # You can add fixtures and/or initialization code here to make experimenting # with your gem easier. You can also use a different console, if you like. # (If you use this, don't forget to add pry to your Gemfile!) # require "pry" # Pry.start # Sentry.init do |config| # config.dsn = 'https://2fb45f003d054a7ea47feb45898f7649@o447951.ingest.sentry.io/5434472' # end require "irb" IRB.start(__FILE__) sentry-ruby-core-5.28.0/.gitignore0000644000004100000410000000016115067721773017074 0ustar www-datawww-data/.bundle/ /.yardoc /_yardoc/ /coverage/ /doc/ /pkg/ /spec/reports/ /tmp/ # rspec failure tracking .rspec_status sentry-ruby-core-5.28.0/lib/0000755000004100000410000000000015067721773015654 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/0000755000004100000410000000000015067721773017200 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/interfaces/0000755000004100000410000000000015067721773021323 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/interfaces/request.rb0000644000004100000410000001043615067721773023344 0ustar www-datawww-data# frozen_string_literal: true module Sentry class RequestInterface < Interface REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze CONTENT_HEADERS = %w[CONTENT_TYPE CONTENT_LENGTH].freeze IP_HEADERS = [ "REMOTE_ADDR", "HTTP_CLIENT_IP", "HTTP_X_REAL_IP", "HTTP_X_FORWARDED_FOR" ].freeze # See Sentry server default limits at # https://github.com/getsentry/sentry/blob/master/src/sentry/conf/server.py MAX_BODY_LIMIT = 4096 * 4 # @return [String] attr_accessor :url # @return [String] attr_accessor :method # @return [Hash] attr_accessor :data # @return [String] attr_accessor :query_string # @return [String] attr_accessor :cookies # @return [Hash] attr_accessor :headers # @return [Hash] attr_accessor :env # @param env [Hash] # @param send_default_pii [Boolean] # @param rack_env_whitelist [Array] # @see Configuration#send_default_pii # @see Configuration#rack_env_whitelist def initialize(env:, send_default_pii:, rack_env_whitelist:) env = env.dup unless send_default_pii # need to completely wipe out ip addresses RequestInterface::IP_HEADERS.each do |header| env.delete(header) end end request = ::Rack::Request.new(env) if send_default_pii self.data = read_data_from(request) self.cookies = request.cookies self.query_string = request.query_string end self.url = request.scheme && request.url.split("?").first self.method = request.request_method self.headers = filter_and_format_headers(env, send_default_pii) self.env = filter_and_format_env(env, rack_env_whitelist) end private def read_data_from(request) if request.form_data? request.POST elsif request.body # JSON requests, etc data = request.body.read(MAX_BODY_LIMIT) data = Utils::EncodingHelper.encode_to_utf_8(data.to_s) request.body.rewind data end rescue IOError => e e.message end def filter_and_format_headers(env, send_default_pii) env.each_with_object({}) do |(key, value), memo| begin key = key.to_s # rack env can contain symbols next memo["X-Request-Id"] ||= Utils::RequestId.read_from(env) if Utils::RequestId::REQUEST_ID_HEADERS.include?(key) next if is_server_protocol?(key, value, env["SERVER_PROTOCOL"]) next if is_skippable_header?(key) next if key == "HTTP_AUTHORIZATION" && !send_default_pii # Rack stores headers as HTTP_WHAT_EVER, we need What-Ever key = key.sub(/^HTTP_/, "") key = key.split("_").map(&:capitalize).join("-") memo[key] = Utils::EncodingHelper.encode_to_utf_8(value.to_s) rescue StandardError => e # Rails adds objects to the Rack env that can sometimes raise exceptions # when `to_s` is called. # See: https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/remote_ip.rb#L134 Sentry.sdk_logger.warn(LOGGER_PROGNAME) { "Error raised while formatting headers: #{e.message}" } next end end end def is_skippable_header?(key) key.upcase != key || # lower-case envs aren't real http headers key == "HTTP_COOKIE" || # Cookies don't go here, they go somewhere else !(key.start_with?("HTTP_") || CONTENT_HEADERS.include?(key)) end # In versions < 3, Rack adds in an incorrect HTTP_VERSION key, which causes downstream # to think this is a Version header. Instead, this is mapped to # env['SERVER_PROTOCOL']. But we don't want to ignore a valid header # if the request has legitimately sent a Version header themselves. # See: https://github.com/rack/rack/blob/028438f/lib/rack/handler/cgi.rb#L29 def is_server_protocol?(key, value, protocol_version) rack_version = Gem::Version.new(::Rack.release) return false if rack_version >= Gem::Version.new("3.0") key == "HTTP_VERSION" && value == protocol_version end def filter_and_format_env(env, rack_env_whitelist) return env if rack_env_whitelist.empty? env.select do |k, _v| rack_env_whitelist.include? k.to_s end end end end sentry-ruby-core-5.28.0/lib/sentry/interfaces/mechanism.rb0000644000004100000410000000111615067721773023613 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Mechanism < Interface # Generic identifier, mostly the source integration for this exception. # @return [String] attr_accessor :type # A manually captured exception has handled set to true, # false if coming from an integration where we intercept an uncaught exception. # Defaults to true here and will be set to false explicitly in integrations. # @return [Boolean] attr_accessor :handled def initialize(type: "generic", handled: true) @type = type @handled = handled end end end sentry-ruby-core-5.28.0/lib/sentry/interfaces/threads.rb0000644000004100000410000000221015067721773023275 0ustar www-datawww-data# frozen_string_literal: true module Sentry class ThreadsInterface # @param crashed [Boolean] # @param stacktrace [Array] def initialize(crashed: false, stacktrace: nil) @id = Thread.current.object_id @name = Thread.current.name @current = true @crashed = crashed @stacktrace = stacktrace end # @return [Hash] def to_hash { values: [ { id: @id, name: @name, crashed: @crashed, current: @current, stacktrace: @stacktrace&.to_hash } ] } end # Builds the ThreadsInterface with given backtrace and stacktrace_builder. # Patch this method if you want to change a threads interface's stacktrace frames. # @see StacktraceBuilder.build # @param backtrace [Array] # @param stacktrace_builder [StacktraceBuilder] # @param crashed [Hash] # @return [ThreadsInterface] def self.build(backtrace:, stacktrace_builder:, **options) stacktrace = stacktrace_builder.build(backtrace: backtrace) if backtrace new(**options, stacktrace: stacktrace) end end end sentry-ruby-core-5.28.0/lib/sentry/interfaces/exception.rb0000644000004100000410000000267315067721773023656 0ustar www-datawww-data# frozen_string_literal: true require "set" module Sentry class ExceptionInterface < Interface # @return [] attr_reader :values # @param exceptions [Array] def initialize(exceptions:) @values = exceptions end # @return [Hash] def to_hash data = super data[:values] = data[:values].map(&:to_hash) if data[:values] data end # Builds ExceptionInterface with given exception and stacktrace_builder. # @param exception [Exception] # @param stacktrace_builder [StacktraceBuilder] # @see SingleExceptionInterface#build_with_stacktrace # @see SingleExceptionInterface#initialize # @param mechanism [Mechanism] # @return [ExceptionInterface] def self.build(exception:, stacktrace_builder:, mechanism:) exceptions = Sentry::Utils::ExceptionCauseChain.exception_to_array(exception).reverse processed_backtrace_ids = Set.new exceptions = exceptions.map do |e| if e.backtrace && !processed_backtrace_ids.include?(e.backtrace.object_id) processed_backtrace_ids << e.backtrace.object_id SingleExceptionInterface.build_with_stacktrace(exception: e, stacktrace_builder: stacktrace_builder, mechanism: mechanism) else SingleExceptionInterface.new(exception: exception, mechanism: mechanism) end end new(exceptions: exceptions) end end end sentry-ruby-core-5.28.0/lib/sentry/interfaces/single_exception.rb0000644000004100000410000000424115067721773025210 0ustar www-datawww-data# frozen_string_literal: true require "sentry/utils/exception_cause_chain" module Sentry class SingleExceptionInterface < Interface include CustomInspection SKIP_INSPECTION_ATTRIBUTES = [:@stacktrace] PROBLEMATIC_LOCAL_VALUE_REPLACEMENT = "[ignored due to error]" OMISSION_MARK = "..." MAX_LOCAL_BYTES = 1024 attr_reader :type, :module, :thread_id, :stacktrace, :mechanism attr_accessor :value def initialize(exception:, mechanism:, stacktrace: nil) @type = exception.class.to_s exception_message = if exception.respond_to?(:detailed_message) exception.detailed_message(highlight: false) else exception.message || "" end exception_message = exception_message.inspect unless exception_message.is_a?(String) @value = Utils::EncodingHelper.encode_to_utf_8(exception_message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES)) @module = exception.class.to_s.split("::")[0...-1].join("::") @thread_id = Thread.current.object_id @stacktrace = stacktrace @mechanism = mechanism end def to_hash data = super data[:stacktrace] = data[:stacktrace].to_hash if data[:stacktrace] data[:mechanism] = data[:mechanism].to_hash data end # patch this method if you want to change an exception's stacktrace frames # also see `StacktraceBuilder.build`. def self.build_with_stacktrace(exception:, stacktrace_builder:, mechanism:) stacktrace = stacktrace_builder.build(backtrace: exception.backtrace) if locals = exception.instance_variable_get(:@sentry_locals) locals.each do |k, v| locals[k] = begin v = v.inspect unless v.is_a?(String) if v.length >= MAX_LOCAL_BYTES v = v.byteslice(0..MAX_LOCAL_BYTES - 1) + OMISSION_MARK end Utils::EncodingHelper.encode_to_utf_8(v) rescue StandardError PROBLEMATIC_LOCAL_VALUE_REPLACEMENT end end stacktrace.frames.last.vars = locals end new(exception: exception, stacktrace: stacktrace, mechanism: mechanism) end end end sentry-ruby-core-5.28.0/lib/sentry/interfaces/stacktrace.rb0000644000004100000410000000454615067721773024005 0ustar www-datawww-data# frozen_string_literal: true module Sentry class StacktraceInterface # @return [] attr_reader :frames # @param frames [] def initialize(frames:) @frames = frames end # @return [Hash] def to_hash { frames: @frames.map(&:to_hash) } end # @return [String] def inspect @frames.map(&:to_s) end private # Not actually an interface, but I want to use the same style class Frame < Interface attr_accessor :abs_path, :context_line, :function, :in_app, :filename, :lineno, :module, :pre_context, :post_context, :vars def initialize(project_root, line, strip_backtrace_load_path = true) @project_root = project_root @strip_backtrace_load_path = strip_backtrace_load_path @abs_path = line.file @function = line.method if line.method @lineno = line.number @in_app = line.in_app @module = line.module_name if line.module_name @filename = compute_filename end def to_s "#{@filename}:#{@lineno}" end def compute_filename return if abs_path.nil? return abs_path unless @strip_backtrace_load_path prefix = if under_project_root? && in_app @project_root elsif under_project_root? longest_load_path || @project_root else longest_load_path end prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path end def set_context(linecache, context_lines) return unless abs_path @pre_context, @context_line, @post_context = \ linecache.get_file_context(abs_path, lineno, context_lines) end def to_hash(*args) data = super(*args) data.delete(:vars) unless vars && !vars.empty? data.delete(:pre_context) unless pre_context && !pre_context.empty? data.delete(:post_context) unless post_context && !post_context.empty? data.delete(:context_line) unless context_line && !context_line.empty? data end private def under_project_root? @project_root && abs_path.start_with?(@project_root) end def longest_load_path $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) end end end end sentry-ruby-core-5.28.0/lib/sentry/interfaces/stacktrace_builder.rb0000644000004100000410000000570315067721773025507 0ustar www-datawww-data# frozen_string_literal: true module Sentry class StacktraceBuilder # @return [String] attr_reader :project_root # @return [Regexp, nil] attr_reader :app_dirs_pattern # @return [LineCache] attr_reader :linecache # @return [Integer, nil] attr_reader :context_lines # @return [Proc, nil] attr_reader :backtrace_cleanup_callback # @return [Boolean] attr_reader :strip_backtrace_load_path # @param project_root [String] # @param app_dirs_pattern [Regexp, nil] # @param linecache [LineCache] # @param context_lines [Integer, nil] # @param backtrace_cleanup_callback [Proc, nil] # @param strip_backtrace_load_path [Boolean] # @see Configuration#project_root # @see Configuration#app_dirs_pattern # @see Configuration#linecache # @see Configuration#context_lines # @see Configuration#backtrace_cleanup_callback # @see Configuration#strip_backtrace_load_path def initialize( project_root:, app_dirs_pattern:, linecache:, context_lines:, backtrace_cleanup_callback: nil, strip_backtrace_load_path: true ) @project_root = project_root @app_dirs_pattern = app_dirs_pattern @linecache = linecache @context_lines = context_lines @backtrace_cleanup_callback = backtrace_cleanup_callback @strip_backtrace_load_path = strip_backtrace_load_path end # Generates a StacktraceInterface with the given backtrace. # You can pass a block to customize/exclude frames: # # @example # builder.build(backtrace) do |frame| # if frame.module.match?(/a_gem/) # nil # else # frame # end # end # @param backtrace [Array] # @param frame_callback [Proc] # @yieldparam frame [StacktraceInterface::Frame] # @return [StacktraceInterface] def build(backtrace:, &frame_callback) parsed_lines = parse_backtrace_lines(backtrace).select(&:file) frames = parsed_lines.reverse.map do |line| frame = convert_parsed_line_into_frame(line) frame = frame_callback.call(frame) if frame_callback frame end.compact StacktraceInterface.new(frames: frames) end # Get the code location hash for a single line for where metrics where added. # @return [Hash] def metrics_code_location(unparsed_line) parsed_line = Backtrace::Line.parse(unparsed_line) frame = convert_parsed_line_into_frame(parsed_line) frame.to_hash.reject { |k, _| %i[project_root in_app].include?(k) } end private def convert_parsed_line_into_frame(line) frame = StacktraceInterface::Frame.new(project_root, line, strip_backtrace_load_path) frame.set_context(linecache, context_lines) if context_lines frame end def parse_backtrace_lines(backtrace) Backtrace.parse( backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback ).lines end end end sentry-ruby-core-5.28.0/lib/sentry/structured_logger.rb0000644000004100000410000001237415067721773023277 0ustar www-datawww-data# frozen_string_literal: true module Sentry # The StructuredLogger class implements Sentry's SDK telemetry logs protocol. # It provides methods for logging messages at different severity levels and # sending them to Sentry with structured data. # # This class follows the Sentry Logs Protocol as defined in: # https://develop.sentry.dev/sdk/telemetry/logs/ # # @example Basic usage # Sentry.logger.info("User logged in", user_id: 123) # # @example With structured data # Sentry.logger.warn("API request failed", # status_code: 404, # endpoint: "/api/users", # request_id: "abc-123" # ) # # @example With a message template # # Using positional parameters # Sentry.logger.info("User %s logged in", ["Jane Doe"]) # # # Using hash parameters # Sentry.logger.info("User %{name} logged in", name: "Jane Doe") # # # Using hash parameters and extra attributes # Sentry.logger.info("User %{name} logged in", name: "Jane Doe", user_id: 312) # # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol class StructuredLogger # Severity number mapping for log levels according to the Sentry Logs Protocol # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-severity-number LEVELS = { trace: 1, debug: 5, info: 9, warn: 13, error: 17, fatal: 21 }.freeze # @return [Configuration] The Sentry configuration # @!visibility private attr_reader :config # Initializes a new StructuredLogger instance # # @param config [Configuration] The Sentry configuration def initialize(config) @config = config end # Logs a message at TRACE level # # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # # @return [LogEvent, nil] The created log event or nil if logging is disabled def trace(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) end # Logs a message at DEBUG level # # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # # @return [LogEvent, nil] The created log event or nil if logging is disabled def debug(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) end # Logs a message at INFO level # # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # # @return [LogEvent, nil] The created log event or nil if logging is disabled def info(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) end # Logs a message at WARN level # # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # # @return [LogEvent, nil] The created log event or nil if logging is disabled def warn(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) end # Logs a message at ERROR level # # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # # @return [LogEvent, nil] The created log event or nil if logging is disabled def error(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) end # Logs a message at FATAL level # # @param message [String] The log message # @param parameters [Array] Array of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # # @return [LogEvent, nil] The created log event or nil if logging is disabled def fatal(message, parameters = [], **attributes) log(__method__, message, parameters: parameters, **attributes) end # Logs a message at the specified level # # @param level [Symbol] The log level (:trace, :debug, :info, :warn, :error, :fatal) # @param message [String] The log message # @param parameters [Array, Hash] Array or Hash of values to replace template parameters in the message # @param attributes [Hash] Additional attributes to include with the log # # @return [LogEvent, nil] The created log event or nil if logging is disabled def log(level, message, parameters:, **attributes) case parameters when Array then Sentry.capture_log(message, level: level, severity: LEVELS[level], parameters: parameters, **attributes) else Sentry.capture_log(message, level: level, severity: LEVELS[level], **parameters) end end end end sentry-ruby-core-5.28.0/lib/sentry/logger.rb0000644000004100000410000000072215067721773021005 0ustar www-datawww-data# frozen_string_literal: true require "logger" module Sentry class Logger < ::Logger LOG_PREFIX = "** [Sentry] " PROGNAME = "sentry" def initialize(*) super @level = ::Logger::INFO original_formatter = ::Logger::Formatter.new @default_formatter = proc do |severity, datetime, _progname, msg| msg = "#{LOG_PREFIX}#{msg}" original_formatter.call(severity, datetime, PROGNAME, msg) end end end end sentry-ruby-core-5.28.0/lib/sentry/transaction_event.rb0000644000004100000410000000500615067721773023254 0ustar www-datawww-data# frozen_string_literal: true module Sentry # TransactionEvent represents events that carry transaction data (type: "transaction"). class TransactionEvent < Event TYPE = "transaction" # @return [] attr_accessor :spans # @return [Hash] attr_accessor :measurements # @return [Float, nil] attr_reader :start_timestamp # @return [Hash, nil] attr_accessor :profile # @return [Hash, nil] attr_accessor :metrics_summary def initialize(transaction:, **options) super(**options) self.transaction = transaction.name self.transaction_info = { source: transaction.source } self.contexts.merge!(transaction.contexts) self.contexts.merge!(trace: transaction.get_trace_context) self.timestamp = transaction.timestamp self.start_timestamp = transaction.start_timestamp self.tags = transaction.tags self.dynamic_sampling_context = transaction.get_baggage.dynamic_sampling_context self.measurements = transaction.measurements self.metrics_summary = transaction.metrics_summary finished_spans = transaction.span_recorder.spans.select { |span| span.timestamp && span != transaction } self.spans = finished_spans.map(&:to_hash) populate_profile(transaction) end # Sets the event's start_timestamp. # @param time [Time, Float] # @return [void] def start_timestamp=(time) @start_timestamp = time.is_a?(Time) ? time.to_f : time end # @return [Hash] def to_hash data = super data[:spans] = @spans.map(&:to_hash) if @spans data[:start_timestamp] = @start_timestamp data[:measurements] = @measurements data[:_metrics_summary] = @metrics_summary if @metrics_summary data end private EMPTY_PROFILE = {}.freeze def populate_profile(transaction) profile_hash = transaction.profiler&.to_hash || EMPTY_PROFILE return if profile_hash.empty? profile_hash.merge!( environment: environment, release: release, timestamp: Time.at(start_timestamp).iso8601, device: { architecture: Scope.os_context[:machine] }, os: { name: Scope.os_context[:name], version: Scope.os_context[:version] }, runtime: Scope.runtime_context, transaction: { id: event_id, name: transaction.name, trace_id: transaction.trace_id, active_thread_id: transaction.profiler.active_thread_id.to_s } ) self.profile = profile_hash end end end sentry-ruby-core-5.28.0/lib/sentry/propagation_context.rb0000644000004100000410000001231715067721773023620 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" require "sentry/baggage" require "sentry/utils/uuid" require "sentry/utils/sample_rand" module Sentry class PropagationContext SENTRY_TRACE_REGEXP = Regexp.new( "\\A([0-9a-f]{32})?" + # trace_id "-?([0-9a-f]{16})?" + # span_id "-?([01])?\\z" # sampled ) # An uuid that can be used to identify a trace. # @return [String] attr_reader :trace_id # An uuid that can be used to identify the span. # @return [String] attr_reader :span_id # Span parent's span_id. # @return [String, nil] attr_reader :parent_span_id # The sampling decision of the parent transaction. # @return [Boolean, nil] attr_reader :parent_sampled # Is there an incoming trace or not? # @return [Boolean] attr_reader :incoming_trace # This is only for accessing the current baggage variable. # Please use the #get_baggage method for interfacing outside this class. # @return [Baggage, nil] attr_reader :baggage # The propagated random value used for sampling decisions. # @return [Float, nil] attr_reader :sample_rand # Extract the trace_id, parent_span_id and parent_sampled values from a sentry-trace header. # # @param sentry_trace [String] the sentry-trace header value from the previous transaction. # @return [Array, nil] def self.extract_sentry_trace(sentry_trace) value = sentry_trace.to_s.strip return if value.empty? match = SENTRY_TRACE_REGEXP.match(value) return if match.nil? trace_id, parent_span_id, sampled_flag = match[1..3] parent_sampled = sampled_flag.nil? ? nil : sampled_flag != "0" [trace_id, parent_span_id, parent_sampled] end def self.extract_sample_rand_from_baggage(baggage, trace_id = nil) return unless baggage&.items sample_rand_str = baggage.items["sample_rand"] return unless sample_rand_str generator = Utils::SampleRand.new(trace_id: trace_id) generator.generate_from_value(sample_rand_str) end def self.generate_sample_rand(baggage, trace_id, parent_sampled) generator = Utils::SampleRand.new(trace_id: trace_id) if baggage&.items && !parent_sampled.nil? sample_rate_str = baggage.items["sample_rate"] sample_rate = sample_rate_str&.to_f if sample_rate && !parent_sampled.nil? generator.generate_from_sampling_decision(parent_sampled, sample_rate) else generator.generate_from_trace_id end else generator.generate_from_trace_id end end def initialize(scope, env = nil) @scope = scope @parent_span_id = nil @parent_sampled = nil @baggage = nil @incoming_trace = false @sample_rand = nil if env sentry_trace_header = env["HTTP_SENTRY_TRACE"] || env[SENTRY_TRACE_HEADER_NAME] baggage_header = env["HTTP_BAGGAGE"] || env[BAGGAGE_HEADER_NAME] if sentry_trace_header sentry_trace_data = self.class.extract_sentry_trace(sentry_trace_header) if sentry_trace_data @trace_id, @parent_span_id, @parent_sampled = sentry_trace_data @baggage = if baggage_header && !baggage_header.empty? Baggage.from_incoming_header(baggage_header) else # If there's an incoming sentry-trace but no incoming baggage header, # for instance in traces coming from older SDKs, # baggage will be empty and frozen and won't be populated as head SDK. Baggage.new({}) end @sample_rand = self.class.extract_sample_rand_from_baggage(@baggage, @trace_id) @baggage.freeze! @incoming_trace = true end end end @trace_id ||= Utils.uuid @span_id = Utils.uuid.slice(0, 16) @sample_rand ||= self.class.generate_sample_rand(@baggage, @trace_id, @parent_sampled) end # Returns the trace context that can be used to embed in an Event. # @return [Hash] def get_trace_context { trace_id: trace_id, span_id: span_id, parent_span_id: parent_span_id } end # Returns the sentry-trace header from the propagation context. # @return [String] def get_traceparent "#{trace_id}-#{span_id}" end # Returns the Baggage from the propagation context or populates as head SDK if empty. # @return [Baggage, nil] def get_baggage populate_head_baggage if @baggage.nil? || @baggage.mutable @baggage end # Returns the Dynamic Sampling Context from the baggage. # @return [Hash, nil] def get_dynamic_sampling_context get_baggage&.dynamic_sampling_context end private def populate_head_baggage return unless Sentry.initialized? configuration = Sentry.configuration items = { "trace_id" => trace_id, "sample_rand" => Utils::SampleRand.format(@sample_rand), "environment" => configuration.environment, "release" => configuration.release, "public_key" => configuration.dsn&.public_key } items.compact! @baggage = Baggage.new(items, mutable: false) end end end sentry-ruby-core-5.28.0/lib/sentry/profiler.rb0000644000004100000410000001253015067721773021350 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" require_relative "profiler/helpers" require "sentry/utils/uuid" module Sentry class Profiler include Profiler::Helpers VERSION = "1" PLATFORM = "ruby" # 101 Hz in microseconds DEFAULT_INTERVAL = 1e6 / 101 MICRO_TO_NANO_SECONDS = 1e3 MIN_SAMPLES_REQUIRED = 2 attr_reader :sampled, :started, :event_id def initialize(configuration) @event_id = Utils.uuid @started = false @sampled = nil @profiling_enabled = defined?(StackProf) && configuration.profiling_enabled? @profiles_sample_rate = configuration.profiles_sample_rate @project_root = configuration.project_root @app_dirs_pattern = configuration.app_dirs_pattern @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}") end def start return unless @sampled @started = StackProf.start(interval: DEFAULT_INTERVAL, mode: :wall, raw: true, aggregate: false) @started ? log("Started") : log("Not started since running elsewhere") end def stop return unless @sampled return unless @started StackProf.stop log("Stopped") end def active_thread_id "0" end # Sets initial sampling decision of the profile. # @return [void] def set_initial_sample_decision(transaction_sampled) unless @profiling_enabled @sampled = false return end unless transaction_sampled @sampled = false log("Discarding profile because transaction not sampled") return end case @profiles_sample_rate when 0.0 @sampled = false log("Discarding profile because sample_rate is 0") return when 1.0 @sampled = true return else @sampled = Random.rand < @profiles_sample_rate end log("Discarding profile due to sampling decision") unless @sampled end def to_hash unless @sampled record_lost_event(:sample_rate) return {} end return {} unless @started results = StackProf.results if !results || results.empty? || results[:samples] == 0 || !results[:raw] record_lost_event(:insufficient_data) return {} end frame_map = {} frames = results[:frames].map.with_index do |(frame_id, frame_data), idx| # need to map over stackprof frame ids to ours frame_map[frame_id] = idx file_path = frame_data[:file] lineno = frame_data[:line] in_app = in_app?(file_path) filename = compute_filename(file_path, in_app) function, mod = split_module(frame_data[:name]) frame_hash = { abs_path: file_path, function: function, filename: filename, in_app: in_app } frame_hash[:module] = mod if mod frame_hash[:lineno] = lineno if lineno && lineno >= 0 frame_hash end idx = 0 stacks = [] num_seen = [] # extract stacks from raw # raw is a single array of [.., len_stack, *stack_frames(len_stack), num_stack_seen , ..] while (len = results[:raw][idx]) idx += 1 # our call graph is reversed stack = results[:raw].slice(idx, len).map { |id| frame_map[id] }.compact.reverse stacks << stack num_seen << results[:raw][idx + len] idx += len + 1 log("Unknown frame in stack") if stack.size != len end idx = 0 elapsed_since_start_ns = 0 samples = [] num_seen.each_with_index do |n, i| n.times do # stackprof deltas are in microseconds delta = results[:raw_timestamp_deltas][idx] elapsed_since_start_ns += (delta * MICRO_TO_NANO_SECONDS).to_i idx += 1 # Not sure why but some deltas are very small like 0/1 values, # they pollute our flamegraph so just ignore them for now. # Open issue at https://github.com/tmm1/stackprof/issues/201 next if delta < 10 samples << { stack_id: i, # TODO-neel-profiler we need to patch rb_profile_frames and write our own C extension to enable threading info. # Till then, on multi-threaded servers like puma, we will get frames from other active threads when the one # we're profiling is idle/sleeping/waiting for IO etc. # https://bugs.ruby-lang.org/issues/10602 thread_id: "0", elapsed_since_start_ns: elapsed_since_start_ns.to_s } end end log("Some samples thrown away") if samples.size != results[:samples] if samples.size <= MIN_SAMPLES_REQUIRED log("Not enough samples, discarding profiler") record_lost_event(:insufficient_data) return {} end profile = { frames: frames, stacks: stacks, samples: samples } { event_id: @event_id, platform: PLATFORM, version: VERSION, profile: profile } end private def log(message) Sentry.sdk_logger.debug(LOGGER_PROGNAME) { "[Profiler] #{message}" } end def record_lost_event(reason) Sentry.get_current_client&.transport&.record_lost_event(reason, "profile") end end end sentry-ruby-core-5.28.0/lib/sentry/transport.rb0000644000004100000410000001403015067721773021557 0ustar www-datawww-data# frozen_string_literal: true require "json" require "sentry/envelope" module Sentry class Transport PROTOCOL_VERSION = "7" USER_AGENT = "sentry-ruby/#{Sentry::VERSION}" CLIENT_REPORT_INTERVAL = 30 # https://develop.sentry.dev/sdk/client-reports/#envelope-item-payload CLIENT_REPORT_REASONS = [ :ratelimit_backoff, :queue_overflow, :cache_overflow, # NA :network_error, :sample_rate, :before_send, :event_processor, :insufficient_data, :backpressure ] include LoggingHelper attr_reader :rate_limits, :discarded_events, :last_client_report_sent def initialize(configuration) @sdk_logger = configuration.sdk_logger @transport_configuration = configuration.transport @dsn = configuration.dsn @rate_limits = {} @send_client_reports = configuration.send_client_reports if @send_client_reports @discarded_events = Hash.new(0) @last_client_report_sent = Time.now end end def send_data(data, options = {}) raise NotImplementedError end def send_event(event) envelope = envelope_from_event(event) send_envelope(envelope) event end def send_envelope(envelope) reject_rate_limited_items(envelope) return if envelope.items.empty? data, serialized_items = serialize_envelope(envelope) if data log_debug("[Transport] Sending envelope with items [#{serialized_items.map(&:type).join(', ')}] #{envelope.event_id} to Sentry") send_data(data) end end def serialize_envelope(envelope) serialized_items = [] serialized_results = [] envelope.items.each do |item| result, oversized = item.serialize if oversized log_debug("Envelope item [#{item.type}] is still oversized after size reduction: {#{item.size_breakdown}}") next end serialized_results << result serialized_items << item end data = [JSON.generate(envelope.headers), *serialized_results].join("\n") unless serialized_results.empty? [data, serialized_items] end def is_rate_limited?(data_category) # check category-specific limit category_delay = @rate_limits[data_category] # check universal limit if not category limit universal_delay = @rate_limits[nil] delay = if category_delay && universal_delay if category_delay > universal_delay category_delay else universal_delay end elsif category_delay category_delay else universal_delay end !!delay && delay > Time.now end def any_rate_limited? @rate_limits.values.any? { |t| t && t > Time.now } end def envelope_from_event(event) # Convert to hash event_payload = event.to_hash event_id = event_payload[:event_id] || event_payload["event_id"] item_type = event_payload[:type] || event_payload["type"] envelope_headers = { event_id: event_id, dsn: @dsn.to_s, sdk: Sentry.sdk_meta, sent_at: Sentry.utc_now.iso8601 } if event.is_a?(Event) && event.dynamic_sampling_context envelope_headers[:trace] = event.dynamic_sampling_context end envelope = Envelope.new(envelope_headers) if event.is_a?(LogEvent) envelope.add_item( { type: "log", item_count: 1, content_type: "application/vnd.sentry.items.log+json" }, { items: [event_payload] } ) else envelope.add_item( { type: item_type, content_type: "application/json" }, event_payload ) end if event.is_a?(TransactionEvent) && event.profile envelope.add_item( { type: "profile", content_type: "application/json" }, event.profile ) end if event.is_a?(Event) && event.attachments.any? event.attachments.each do |attachment| envelope.add_item(attachment.to_envelope_headers, attachment.payload) end end client_report_headers, client_report_payload = fetch_pending_client_report envelope.add_item(client_report_headers, client_report_payload) if client_report_headers envelope end def record_lost_event(reason, data_category, num: 1) return unless @send_client_reports return unless CLIENT_REPORT_REASONS.include?(reason) @discarded_events[[reason, data_category]] += num end def flush client_report_headers, client_report_payload = fetch_pending_client_report(force: true) return unless client_report_headers envelope = Envelope.new envelope.add_item(client_report_headers, client_report_payload) send_envelope(envelope) end private def fetch_pending_client_report(force: false) return nil unless @send_client_reports return nil if !force && @last_client_report_sent > Time.now - CLIENT_REPORT_INTERVAL return nil if @discarded_events.empty? discarded_events_hash = @discarded_events.map do |key, val| reason, category = key { reason: reason, category: category, quantity: val } end item_header = { type: "client_report" } item_payload = { timestamp: Sentry.utc_now.iso8601, discarded_events: discarded_events_hash } @discarded_events = Hash.new(0) @last_client_report_sent = Time.now [item_header, item_payload] end def reject_rate_limited_items(envelope) envelope.items.reject! do |item| if is_rate_limited?(item.data_category) log_debug("[Transport] Envelope item [#{item.type}] not sent: rate limiting") record_lost_event(:ratelimit_backoff, item.data_category) true else false end end end end end require "sentry/transport/dummy_transport" require "sentry/transport/http_transport" require "sentry/transport/spotlight_transport" require "sentry/transport/debug_transport" sentry-ruby-core-5.28.0/lib/sentry/release_detector.rb0000644000004100000410000000214415067721773023037 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class ReleaseDetector class << self def detect_release(project_root:, running_on_heroku:) detect_release_from_env || detect_release_from_git || detect_release_from_capistrano(project_root) || detect_release_from_heroku(running_on_heroku) end def detect_release_from_heroku(running_on_heroku) return unless running_on_heroku ENV["HEROKU_SLUG_COMMIT"] end def detect_release_from_capistrano(project_root) revision_file = File.join(project_root, "REVISION") revision_log = File.join(project_root, "..", "revisions.log") if File.exist?(revision_file) File.read(revision_file).strip elsif File.exist?(revision_log) File.open(revision_log).to_a.last.strip.sub(/.*as release ([0-9]+).*/, '\1') end end def detect_release_from_git Sentry.sys_command("git rev-parse HEAD") if File.directory?(".git") end def detect_release_from_env ENV["SENTRY_RELEASE"] end end end end sentry-ruby-core-5.28.0/lib/sentry/configuration.rb0000644000004100000410000006363415067721773022410 0ustar www-datawww-data# frozen_string_literal: true require "concurrent/utility/processor_counter" require "sentry/utils/exception_cause_chain" require "sentry/utils/custom_inspection" require "sentry/utils/env_helper" require "sentry/dsn" require "sentry/release_detector" require "sentry/transport/configuration" require "sentry/cron/configuration" require "sentry/metrics/configuration" require "sentry/linecache" require "sentry/interfaces/stacktrace_builder" require "sentry/logger" require "sentry/structured_logger" require "sentry/log_event_buffer" module Sentry class Configuration include CustomInspection include LoggingHelper include ArgumentCheckingHelper # Directories to be recognized as part of your app. e.g. if you # have an `engines` dir at the root of your project, you may want # to set this to something like /(app|config|engines|lib)/ # # The default is value is /(bin|exe|app|config|lib|test|spec)/ # # @return [Regexp, nil] attr_accessor :app_dirs_pattern # Provide an object that responds to `call` to send events asynchronously. # E.g.: lambda { |event| Thread.new { Sentry.send_event(event) } } # # @deprecated It will be removed in the next major release. Please read https://github.com/getsentry/sentry-ruby/issues/1522 for more information # @return [Proc, nil] attr_reader :async # to send events in a non-blocking way, sentry-ruby has its own background worker # by default, the worker holds a thread pool that has [the number of processors] threads # but you can configure it with this configuration option # E.g.: config.background_worker_threads = 5 # # if you want to send events synchronously, set the value to 0 # E.g.: config.background_worker_threads = 0 # @return [Integer] attr_accessor :background_worker_threads # The maximum queue size for the background worker. # Jobs will be rejected above this limit. # # Default is {BackgroundWorker::DEFAULT_MAX_QUEUE}. # @return [Integer] attr_accessor :background_worker_max_queue # a proc/lambda that takes an array of stack traces # it'll be used to silence (reduce) backtrace of the exception # # @example # config.backtrace_cleanup_callback = lambda do |backtrace| # Rails.backtrace_cleaner.clean(backtrace) # end # # @return [Proc, nil] attr_accessor :backtrace_cleanup_callback # Optional Proc, called before adding the breadcrumb to the current scope # @example # config.before = lambda do |breadcrumb, hint| # breadcrumb.message = 'a' # breadcrumb # end # @return [Proc] attr_reader :before_breadcrumb # Optional Proc, called before sending an event to the server # @example # config.before_send = lambda do |event, hint| # # skip ZeroDivisionError exceptions # # note: hint[:exception] would be a String if you use async callback # if hint[:exception].is_a?(ZeroDivisionError) # nil # else # event # end # end # @return [Proc] attr_reader :before_send # Optional Proc, called before sending an event to the server # @example # config.before_send_transaction = lambda do |event, hint| # # skip unimportant transactions or strip sensitive data # if event.transaction == "/healthcheck/route" # nil # else # event # end # end # @return [Proc] attr_reader :before_send_transaction # Optional Proc, called before sending an event to the server # @example # config.before_send_log = lambda do |log| # log.attributes["sentry"] = true # log # end # @return [Proc] attr_accessor :before_send_log # An array of breadcrumbs loggers to be used. Available options are: # - :sentry_logger # - :http_logger # - :redis_logger # # And if you also use sentry-rails: # - :active_support_logger # - :monotonic_active_support_logger # # @return [Array] attr_reader :breadcrumbs_logger # Max number of breadcrumbs a breadcrumb buffer can hold # @return [Integer] attr_accessor :max_breadcrumbs # Number of lines of code context to capture, or nil for none # @return [Integer, nil] attr_accessor :context_lines # RACK_ENV by default. # @return [String] attr_reader :environment # Whether the SDK should run in the debugging mode. Default is false. # If set to true, SDK errors will be logged with backtrace # @return [Boolean] attr_accessor :debug # the dsn value, whether it's set via `config.dsn=` or `ENV["SENTRY_DSN"]` # @return [String] attr_reader :dsn # Whitelist of enabled_environments that will send notifications to Sentry. Array of Strings. # @return [Array] attr_accessor :enabled_environments # Logger 'progname's to exclude from breadcrumbs # @return [Array] attr_accessor :exclude_loggers # Array of exception classes that should never be sent. See IGNORE_DEFAULT. # You should probably append to this rather than overwrite it. # @return [Array] attr_accessor :excluded_exceptions # Boolean to check nested exceptions when deciding if to exclude. Defaults to true # @return [Boolean] attr_accessor :inspect_exception_causes_for_exclusion alias inspect_exception_causes_for_exclusion? inspect_exception_causes_for_exclusion # Whether to capture local variables from the raised exception's frame. Default is false. # @return [Boolean] attr_accessor :include_local_variables # Whether to capture events and traces into Spotlight. Default is false. # If you set this to true, Sentry will send events and traces to the local # Sidecar proxy at http://localhost:8969/stream. # If you want to use a different Sidecar proxy address, set this to String # with the proxy URL. # @return [Boolean, String] attr_accessor :spotlight # @deprecated Use {#include_local_variables} instead. alias_method :capture_exception_frame_locals, :include_local_variables # @deprecated Use {#include_local_variables=} instead. def capture_exception_frame_locals=(value) log_warn <<~MSG `capture_exception_frame_locals` is now deprecated in favor of `include_local_variables`. MSG self.include_local_variables = value end # You may provide your own LineCache for matching paths with source files. # This may be useful if you need to get source code from places other than the disk. # @see LineCache # @return [LineCache] attr_accessor :linecache # Logger used by Sentry. In Rails, this is the Rails logger, otherwise # Sentry provides its own Sentry::Logger. # @return [Logger] attr_accessor :sdk_logger # File path for DebugTransport to log events to. If not set, defaults to a temporary file. # This is useful for debugging and testing purposes. # @return [String, nil] attr_accessor :sdk_debug_transport_log_file # @deprecated Use {#sdk_logger=} instead. def logger=(logger) warn "[sentry] `config.logger=` is deprecated. Please use `config.sdk_logger=` instead." self.sdk_logger = logger end # @deprecated Use {#sdk_logger} instead. def logger warn "[sentry] `config.logger` is deprecated. Please use `config.sdk_logger` instead." self.sdk_logger end # Project directory root for in_app detection. Could be Rails root, etc. # Set automatically for Rails. # @return [String] attr_accessor :project_root # Whether to strip the load path while constructing the backtrace frame filename. # Defaults to true. # @return [Boolean] attr_accessor :strip_backtrace_load_path # Insert sentry-trace to outgoing requests' headers # @return [Boolean] attr_accessor :propagate_traces # Array of rack env parameters to be included in the event sent to sentry. # @return [Array] attr_accessor :rack_env_whitelist # Release tag to be passed with every event sent to Sentry. # We automatically try to set this to a git SHA or Capistrano release. # @return [String] attr_reader :release # The sampling factor to apply to events. A value of 0.0 will not send # any events, and a value of 1.0 will send 100% of events. # @return [Float] attr_accessor :sample_rate # Include module versions in reports - boolean. # @return [Boolean] attr_accessor :send_modules # When send_default_pii's value is false (default), sensitive information like # - user ip # - user cookie # - request body # - query string # will not be sent to Sentry. # @return [Boolean] attr_accessor :send_default_pii # Allow to skip Sentry emails within rake tasks # @return [Boolean] attr_accessor :skip_rake_integration # IP ranges for trusted proxies that will be skipped when calculating IP address. attr_accessor :trusted_proxies # @return [String] attr_accessor :server_name # Transport related configuration. # @return [Transport::Configuration] attr_reader :transport # Cron related configuration. # @return [Cron::Configuration] attr_reader :cron # Metrics related configuration. # @return [Metrics::Configuration] attr_reader :metrics # Take a float between 0.0 and 1.0 as the sample rate for tracing events (transactions). # @return [Float, nil] attr_reader :traces_sample_rate # Take a Proc that controls the sample rate for every tracing event, e.g. # @example # config.traces_sampler = lambda do |tracing_context| # # tracing_context[:transaction_context] contains the information about the transaction # # tracing_context[:parent_sampled] contains the transaction's parent's sample decision # true # return value can be a boolean or a float between 0.0 and 1.0 # end # @return [Proc] attr_accessor :traces_sampler # Enable Structured Logging # @return [Boolean] attr_accessor :enable_logs # Structured logging configuration. # @return [StructuredLoggingConfiguration] attr_reader :structured_logging # Easier way to use performance tracing # If set to true, will set traces_sample_rate to 1.0 # @deprecated It will be removed in the next major release. # @return [Boolean, nil] attr_reader :enable_tracing # Send diagnostic client reports about dropped events, true by default # tries to attach to an existing envelope max once every 30s # @return [Boolean] attr_accessor :send_client_reports # Track sessions in request/response cycles automatically # @return [Boolean] attr_accessor :auto_session_tracking # Whether to downsample transactions automatically because of backpressure. # Starts a new monitor thread to check health of the SDK every 10 seconds. # Default is false # @return [Boolean] attr_accessor :enable_backpressure_handling # Allowlist of outgoing request targets to which sentry-trace and baggage headers are attached. # Default is all (/.*/) # @return [Array] attr_accessor :trace_propagation_targets # The instrumenter to use, :sentry or :otel # @return [Symbol] attr_reader :instrumenter # The profiler class # @return [Class] attr_reader :profiler_class # Take a float between 0.0 and 1.0 as the sample rate for capturing profiles. # Note that this rate is relative to traces_sample_rate / traces_sampler, # i.e. the profile is sampled by this rate after the transaction is sampled. # @return [Float, nil] attr_reader :profiles_sample_rate # Array of patches to apply. # Default is {DEFAULT_PATCHES} # @return [Array] attr_accessor :enabled_patches # Maximum number of log events to buffer before sending # @return [Integer] attr_accessor :max_log_events # these are not config options # @!visibility private attr_reader :errors, :gem_specs # These exceptions could enter Puma's `lowlevel_error_handler` callback and the SDK's Puma integration # But they are mostly considered as noise and should be ignored by default # Please see https://github.com/getsentry/sentry-ruby/pull/2026 for more information PUMA_IGNORE_DEFAULT = [ "Puma::MiniSSL::SSLError", "Puma::HttpParserError", "Puma::HttpParserError501" ].freeze # Most of these errors generate 4XX responses. In general, Sentry clients # only automatically report 5xx responses. IGNORE_DEFAULT = [ "Mongoid::Errors::DocumentNotFound", "Rack::QueryParser::InvalidParameterError", "Rack::QueryParser::ParameterTypeError", "Sinatra::NotFound" ].freeze RACK_ENV_WHITELIST_DEFAULT = %w[ REMOTE_ADDR SERVER_NAME SERVER_PORT ].freeze HEROKU_DYNO_METADATA_MESSAGE = "You are running on Heroku but haven't enabled Dyno Metadata. For Sentry's "\ "release detection to work correctly, please run `heroku labs:enable runtime-dyno-metadata`" LOG_PREFIX = "** [Sentry] " MODULE_SEPARATOR = "::" SKIP_INSPECTION_ATTRIBUTES = [:@linecache, :@stacktrace_builder] INSTRUMENTERS = [:sentry, :otel] PROPAGATION_TARGETS_MATCH_ALL = /.*/ DEFAULT_PATCHES = %i[redis puma http].freeze APP_DIRS_PATTERN = /(bin|exe|app|config|lib|test|spec)/ class << self # Post initialization callbacks are called at the end of initialization process # allowing extending the configuration of sentry-ruby by multiple extensions def post_initialization_callbacks @post_initialization_callbacks ||= [] end # allow extensions to add their hooks to the Configuration class def add_post_initialization_callback(&block) callbacks[:initialize][:after] << block end def before(event, &block) callbacks[event.to_sym][:before] << block end def after(event, &block) callbacks[event.to_sym][:after] << block end # @!visibility private def callbacks @callbacks ||= { initialize: { before: [], after: [] }, configured: { before: [], after: [] } } end def validations @validations ||= {} end def validate(attribute, optional: false, type: nil) validations[attribute] = { optional: optional, type: type, proc: build_validation_proc(optional, type) } end private def build_validation_proc(optional, type) case type when :numeric ->(value) do if optional && value.nil? true else unless value.is_a?(Numeric) message = "must be a Numeric" message += " or nil" if optional { error: message, value: value } else true end end end else ->(value) { true } end end end validate :traces_sample_rate, optional: true, type: :numeric validate :profiles_sample_rate, optional: true, type: :numeric def initialize run_callbacks(:before, :initialize) self.app_dirs_pattern = APP_DIRS_PATTERN self.debug = Sentry::Utils::EnvHelper.env_to_bool(ENV["SENTRY_DEBUG"]) self.background_worker_threads = (processor_count / 2.0).ceil self.background_worker_max_queue = BackgroundWorker::DEFAULT_MAX_QUEUE self.backtrace_cleanup_callback = nil self.strip_backtrace_load_path = true self.max_breadcrumbs = BreadcrumbBuffer::DEFAULT_SIZE self.breadcrumbs_logger = [] self.context_lines = 3 self.include_local_variables = false self.environment = environment_from_env self.enabled_environments = [] self.exclude_loggers = [] self.excluded_exceptions = IGNORE_DEFAULT + PUMA_IGNORE_DEFAULT self.inspect_exception_causes_for_exclusion = true self.linecache = ::Sentry::LineCache.new self.sdk_logger = ::Sentry::Logger.new(STDOUT) self.project_root = Dir.pwd self.propagate_traces = true self.sample_rate = 1.0 self.send_modules = true self.send_default_pii = false self.skip_rake_integration = false self.send_client_reports = true self.auto_session_tracking = true self.enable_backpressure_handling = false self.trusted_proxies = [] self.dsn = ENV["SENTRY_DSN"] spotlight_env = ENV["SENTRY_SPOTLIGHT"] spotlight_bool = Sentry::Utils::EnvHelper.env_to_bool(spotlight_env, strict: true) self.spotlight = spotlight_bool.nil? ? (spotlight_env || false) : spotlight_bool self.server_name = server_name_from_env self.instrumenter = :sentry self.trace_propagation_targets = [PROPAGATION_TARGETS_MATCH_ALL] self.enabled_patches = DEFAULT_PATCHES.dup self.before_send = nil self.before_send_transaction = nil self.before_send_log = nil self.rack_env_whitelist = RACK_ENV_WHITELIST_DEFAULT self.traces_sampler = nil self.enable_tracing = nil self.enable_logs = false self.profiler_class = Sentry::Profiler @transport = Transport::Configuration.new @cron = Cron::Configuration.new @metrics = Metrics::Configuration.new(self.sdk_logger) @structured_logging = StructuredLoggingConfiguration.new @gem_specs = Hash[Gem::Specification.map { |spec| [spec.name, spec.version.to_s] }] if Gem::Specification.respond_to?(:map) self.max_log_events = LogEventBuffer::DEFAULT_MAX_EVENTS run_callbacks(:after, :initialize) yield(self) if block_given? run_callbacks(:after, :configured) end def validate if profiler_class == Sentry::Profiler && profiles_sample_rate && !Sentry.dependency_installed?(:StackProf) log_warn("Please add the 'stackprof' gem to your Gemfile to use the StackProf profiler with Sentry.") end if profiler_class == Sentry::Vernier::Profiler && profiles_sample_rate && !Sentry.dependency_installed?(:Vernier) log_warn("Please add the 'vernier' gem to your Gemfile to use the Vernier profiler with Sentry.") end self.class.validations.each do |attribute, validation| value = public_send(attribute) next if (result = validation[:proc].call(value)) === true raise ArgumentError, result[:error] end end def dsn=(value) @dsn = init_dsn(value) end alias server= dsn= def release=(value) check_argument_type!(value, String, NilClass) @release = value end def async=(value) check_callable!("async", value) log_warn <<~MSG sentry-ruby now sends events asynchronously by default with its background worker (supported since 4.1.0). The `config.async` callback has become redundant while continuing to cause issues. (The problems of `async` are detailed in https://github.com/getsentry/sentry-ruby/issues/1522) Therefore, we encourage you to remove it and let the background worker take care of async job sending. It's deprecation is planned in the next major release (6.0), which is scheduled around the 3rd quarter of 2022. MSG @async = value end def breadcrumbs_logger=(logger) loggers = if logger.is_a?(Array) logger else Array(logger) end require "sentry/breadcrumb/sentry_logger" if loggers.include?(:sentry_logger) @breadcrumbs_logger = logger end def before_send=(value) check_callable!("before_send", value) @before_send = value end def before_send_transaction=(value) check_callable!("before_send_transaction", value) @before_send_transaction = value end def before_breadcrumb=(value) check_callable!("before_breadcrumb", value) @before_breadcrumb = value end def environment=(environment) @environment = environment.to_s end def instrumenter=(instrumenter) @instrumenter = INSTRUMENTERS.include?(instrumenter) ? instrumenter : :sentry end def enable_tracing=(enable_tracing) unless enable_tracing.nil? log_warn <<~MSG `enable_tracing` is now deprecated in favor of `traces_sample_rate = 1.0`. MSG end @enable_tracing = enable_tracing @traces_sample_rate ||= 1.0 if enable_tracing end def traces_sample_rate=(traces_sample_rate) @traces_sample_rate = traces_sample_rate end def profiles_sample_rate=(profiles_sample_rate) @profiles_sample_rate = profiles_sample_rate end def profiler_class=(profiler_class) if profiler_class == Sentry::Vernier::Profiler begin require "vernier" rescue LoadError end end @profiler_class = profiler_class end def sending_allowed? spotlight || sending_to_dsn_allowed? end def sending_to_dsn_allowed? @errors = [] valid? && capture_in_environment? end def sample_allowed? return true if sample_rate == 1.0 Random.rand < sample_rate end def session_tracking? auto_session_tracking && enabled_in_current_env? end def exception_class_allowed?(exc) if exc.is_a?(Sentry::Error) # Try to prevent error reporting loops log_debug("Refusing to capture Sentry error: #{exc.inspect}") false elsif excluded_exception?(exc) log_debug("User excluded error: #{exc.inspect}") false else true end end def enabled_in_current_env? enabled_environments.empty? || enabled_environments.include?(environment) end def valid_sample_rate?(sample_rate) return false unless sample_rate.is_a?(Numeric) sample_rate >= 0.0 && sample_rate <= 1.0 end def tracing_enabled? valid_sampler = !!((valid_sample_rate?(@traces_sample_rate)) || @traces_sampler) (@enable_tracing != false) && valid_sampler && sending_allowed? end def profiling_enabled? valid_sampler = !!(valid_sample_rate?(@profiles_sample_rate)) tracing_enabled? && valid_sampler && sending_allowed? end # @return [String, nil] def csp_report_uri if dsn && dsn.valid? uri = dsn.csp_report_uri uri += "&sentry_release=#{CGI.escape(release)}" if release && !release.empty? uri += "&sentry_environment=#{CGI.escape(environment)}" if environment && !environment.empty? uri end end # @api private def stacktrace_builder @stacktrace_builder ||= StacktraceBuilder.new( project_root: @project_root.to_s, app_dirs_pattern: @app_dirs_pattern, linecache: @linecache, context_lines: @context_lines, backtrace_cleanup_callback: @backtrace_cleanup_callback, strip_backtrace_load_path: @strip_backtrace_load_path ) end # @api private def detect_release return unless sending_allowed? @release ||= ReleaseDetector.detect_release(project_root: project_root, running_on_heroku: running_on_heroku?) if running_on_heroku? && release.nil? log_warn(HEROKU_DYNO_METADATA_MESSAGE) end rescue => e log_error("Error detecting release", e, debug: debug) end # @api private def error_messages @errors = [@errors[0]] + @errors[1..-1].map(&:downcase) # fix case of all but first @errors.join(", ") end private def init_dsn(dsn_string) return if dsn_string.nil? || dsn_string.empty? DSN.new(dsn_string) end def excluded_exception?(incoming_exception) excluded_exception_classes.any? do |excluded_exception| matches_exception?(excluded_exception, incoming_exception) end end def excluded_exception_classes @excluded_exception_classes ||= excluded_exceptions.map { |e| get_exception_class(e) } end def get_exception_class(x) x.is_a?(Module) ? x : safe_const_get(x) end def matches_exception?(excluded_exception_class, incoming_exception) if inspect_exception_causes_for_exclusion? Sentry::Utils::ExceptionCauseChain.exception_to_array(incoming_exception).any? { |cause| excluded_exception_class === cause } else excluded_exception_class === incoming_exception end end def safe_const_get(x) x = x.to_s unless x.is_a?(String) Object.const_get(x) rescue NameError # There's no way to safely ask if a constant exist for an unknown string nil end def capture_in_environment? return true if enabled_in_current_env? @errors << "Not configured to send/capture in environment '#{environment}'" false end def valid? if @dsn&.valid? true else @errors << "DSN not set or not valid" false end end def environment_from_env ENV["SENTRY_CURRENT_ENV"] || ENV["SENTRY_ENVIRONMENT"] || ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "development" end def server_name_from_env if running_on_heroku? ENV["DYNO"] else # Try to resolve the hostname to an FQDN, but fall back to whatever # the load name is. Socket.gethostname || Socket.gethostbyname(hostname).first rescue server_name end end def running_on_heroku? File.directory?("/etc/heroku") && !ENV["CI"] end def run_callbacks(hook, event) self.class.callbacks[event][hook].each do |hook| instance_eval(&hook) end end def processor_count available_processor_count = Concurrent.available_processor_count if Concurrent.respond_to?(:available_processor_count) available_processor_count || Concurrent.processor_count end end class StructuredLoggingConfiguration # File path for DebugStructuredLogger to log events to # @return [String, Pathname, nil] attr_accessor :file_path # The class to use as a structured logger. # @return [Class] attr_accessor :logger_class def initialize @file_path = nil @logger_class = Sentry::StructuredLogger end end end sentry-ruby-core-5.28.0/lib/sentry/test_helper.rb0000644000004100000410000001071715067721773022051 0ustar www-datawww-data# frozen_string_literal: true module Sentry module TestHelper module_function DUMMY_DSN = "http://12345:67890@sentry.localdomain/sentry/42" # Not really real, but it will be resolved as a non-local for testing needs REAL_DSN = "https://user:pass@getsentry.io/project/42" # Alters the existing SDK configuration with test-suitable options. Mainly: # - Sets a dummy DSN instead of `nil` or an actual DSN. # - Sets the transport to DummyTransport, which allows easy access to the captured events. # - Disables background worker. # - Makes sure the SDK is enabled under the current environment ("test" in most cases). # # It should be called **before** every test case. # # @yieldparam config [Configuration] # @return [void] def setup_sentry_test(&block) raise "please make sure the SDK is initialized for testing" unless Sentry.initialized? dummy_config = Sentry.configuration.dup # configure dummy DSN, so the events will not be sent to the actual service dummy_config.dsn = DUMMY_DSN # set transport to DummyTransport, so we can easily intercept the captured events dummy_config.transport.transport_class = Sentry::DummyTransport # make sure SDK allows sending under the current environment dummy_config.enabled_environments += [dummy_config.environment] unless dummy_config.enabled_environments.include?(dummy_config.environment) # disble async event sending dummy_config.background_worker_threads = 0 # user can overwrite some of the configs, with a few exceptions like: # - include_local_variables # - auto_session_tracking block&.call(dummy_config) # the base layer's client should already use the dummy config so nothing will be sent by accident base_client = Sentry::Client.new(dummy_config) Sentry.get_current_hub.bind_client(base_client) # create a new layer so mutations made to the testing scope or configuration could be simply popped later Sentry.get_current_hub.push_scope test_client = Sentry::Client.new(dummy_config.dup) Sentry.get_current_hub.bind_client(test_client) end # Clears all stored events and envelopes. # It should be called **after** every test case. # @return [void] def teardown_sentry_test return unless Sentry.initialized? clear_sentry_events # pop testing layer created by `setup_sentry_test` # but keep the base layer to avoid nil-pointer errors # TODO: find a way to notify users if they somehow popped the test layer before calling this method if Sentry.get_current_hub.instance_variable_get(:@stack).size > 1 Sentry.get_current_hub.pop_scope end Sentry::Scope.global_event_processors.clear end def clear_sentry_events return unless Sentry.initialized? sentry_transport.clear if sentry_transport.respond_to?(:clear) if Sentry.configuration.enable_logs && sentry_logger.respond_to?(:clear) sentry_logger.clear end end # @return [Sentry::StructuredLogger, Sentry::DebugStructuredLogger] def sentry_logger Sentry.logger end # @return [Transport] def sentry_transport Sentry.get_current_client.transport end # Returns the captured event objects. # @return [Array] def sentry_events sentry_transport.events end # Returns the captured envelope objects. # @return [Array] def sentry_envelopes sentry_transport.envelopes end def sentry_logs sentry_envelopes .flat_map(&:items) .select { |item| item.headers[:type] == "log" } .flat_map { |item| item.payload[:items] } end # Returns the last captured event object. # @return [Event, nil] def last_sentry_event sentry_events.last end # Extracts SDK's internal exception container (not actual exception objects) from an given event. # @return [Array] def extract_sentry_exceptions(event) event&.exception&.values || [] end def reset_sentry_globals! Sentry::MUTEX.synchronize do # Don't check initialized? because sometimes we stub it in tests if Sentry.instance_variable_defined?(:@main_hub) Sentry::GLOBALS.each do |var| Sentry.instance_variable_set(:"@#{var}", nil) end Thread.current.thread_variable_set(Sentry::THREAD_LOCAL, nil) end end end end end sentry-ruby-core-5.28.0/lib/sentry/envelope.rb0000644000004100000410000000066315067721773021347 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class Envelope attr_accessor :headers, :items def initialize(headers = {}) @headers = headers @items = [] end def add_item(headers, payload) @items << Item.new(headers, payload) end def item_types @items.map(&:type) end def event_id @headers[:event_id] end end end require_relative "envelope/item" sentry-ruby-core-5.28.0/lib/sentry/cron/0000755000004100000410000000000015067721773020141 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/cron/configuration.rb0000644000004100000410000000130415067721773023333 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Cron class Configuration # Defaults set here will apply to all {Cron::MonitorConfig} objects unless overwritten. # How long (in minutes) after the expected checkin time will we wait # until we consider the checkin to have been missed. # @return [Integer, nil] attr_accessor :default_checkin_margin # How long (in minutes) is the checkin allowed to run for in in_progress # before it is considered failed. # @return [Integer, nil] attr_accessor :default_max_runtime # tz database style timezone string # @return [String, nil] attr_accessor :default_timezone end end end sentry-ruby-core-5.28.0/lib/sentry/cron/monitor_config.rb0000644000004100000410000000277615067721773023516 0ustar www-datawww-data# frozen_string_literal: true require "sentry/cron/monitor_schedule" module Sentry module Cron class MonitorConfig # The monitor schedule configuration # @return [MonitorSchedule::Crontab, MonitorSchedule::Interval] attr_accessor :schedule # How long (in minutes) after the expected checkin time will we wait # until we consider the checkin to have been missed. # @return [Integer, nil] attr_accessor :checkin_margin # How long (in minutes) is the checkin allowed to run for in in_progress # before it is considered failed. # @return [Integer, nil] attr_accessor :max_runtime # tz database style timezone string # @return [String, nil] attr_accessor :timezone def initialize(schedule, checkin_margin: nil, max_runtime: nil, timezone: nil) @schedule = schedule @checkin_margin = checkin_margin @max_runtime = max_runtime @timezone = timezone end def self.from_crontab(crontab, **options) new(MonitorSchedule::Crontab.new(crontab), **options) end def self.from_interval(num, unit, **options) return nil unless MonitorSchedule::Interval::VALID_UNITS.include?(unit) new(MonitorSchedule::Interval.new(num, unit), **options) end def to_hash { schedule: schedule.to_hash, checkin_margin: checkin_margin, max_runtime: max_runtime, timezone: timezone }.compact end end end end sentry-ruby-core-5.28.0/lib/sentry/cron/monitor_schedule.rb0000644000004100000410000000160415067721773024032 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Cron module MonitorSchedule class Crontab # A crontab formatted string such as "0 * * * *". # @return [String] attr_accessor :value def initialize(value) @value = value end def to_hash { type: :crontab, value: value } end end class Interval # The number representing duration of the interval. # @return [Integer] attr_accessor :value # The unit representing duration of the interval. # @return [Symbol] attr_accessor :unit VALID_UNITS = %i[year month week day hour minute] def initialize(value, unit) @value = value @unit = unit end def to_hash { type: :interval, value: value, unit: unit } end end end end end sentry-ruby-core-5.28.0/lib/sentry/cron/monitor_check_ins.rb0000644000004100000410000000452515067721773024171 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Cron module MonitorCheckIns MAX_SLUG_LENGTH = 50 module Patch def perform(*args, **opts) slug = self.class.sentry_monitor_slug monitor_config = self.class.sentry_monitor_config check_in_id = Sentry.capture_check_in(slug, :in_progress, monitor_config: monitor_config) start = Metrics::Timing.duration_start begin # need to do this on ruby <= 2.6 sadly ret = method(:perform).super_method.arity == 0 ? super() : super duration = Metrics::Timing.duration_end(start) Sentry.capture_check_in(slug, :ok, check_in_id: check_in_id, duration: duration, monitor_config: monitor_config) ret rescue Exception duration = Metrics::Timing.duration_end(start) Sentry.capture_check_in(slug, :error, check_in_id: check_in_id, duration: duration, monitor_config: monitor_config) raise end end end module ClassMethods def sentry_monitor_check_ins(slug: nil, monitor_config: nil) if monitor_config && Sentry.configuration cron_config = Sentry.configuration.cron monitor_config.checkin_margin ||= cron_config.default_checkin_margin monitor_config.max_runtime ||= cron_config.default_max_runtime monitor_config.timezone ||= cron_config.default_timezone end @sentry_monitor_slug = slug @sentry_monitor_config = monitor_config prepend Patch end def sentry_monitor_slug(name: self.name) @sentry_monitor_slug ||= begin slug = name.gsub("::", "-").downcase slug[-MAX_SLUG_LENGTH..-1] || slug end end def sentry_monitor_config @sentry_monitor_config end end def self.included(base) base.extend(ClassMethods) end end end end sentry-ruby-core-5.28.0/lib/sentry/interface.rb0000644000004100000410000000067715067721773021477 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Interface # @return [Hash] def to_hash Hash[instance_variables.map { |name| [name[1..-1].to_sym, instance_variable_get(name)] }] end end end require "sentry/interfaces/exception" require "sentry/interfaces/request" require "sentry/interfaces/single_exception" require "sentry/interfaces/stacktrace" require "sentry/interfaces/threads" require "sentry/interfaces/mechanism" sentry-ruby-core-5.28.0/lib/sentry/utils/0000755000004100000410000000000015067721773020340 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/utils/sample_rand.rb0000644000004100000410000000425215067721773023155 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils class SampleRand PRECISION = 1_000_000.0 FORMAT_PRECISION = 6 attr_reader :trace_id def self.valid?(value) return false unless value value >= 0.0 && value < 1.0 end def self.format(value) return unless value truncated = (value * PRECISION).floor / PRECISION "%.#{FORMAT_PRECISION}f" % truncated end def initialize(trace_id: nil) @trace_id = trace_id end def generate_from_trace_id (random_from_trace_id * PRECISION).floor / PRECISION end def generate_from_sampling_decision(sampled, sample_rate) if invalid_sample_rate?(sample_rate) fallback_generation else generate_based_on_sampling(sampled, sample_rate) end end def generate_from_value(sample_rand_value) parsed_value = parse_value(sample_rand_value) if self.class.valid?(parsed_value) parsed_value else fallback_generation end end private def random_from_trace_id if @trace_id Random.new(@trace_id[0, 16].to_i(16)) else Random.new end.rand(1.0) end def invalid_sample_rate?(sample_rate) sample_rate.nil? || sample_rate <= 0.0 || sample_rate > 1.0 end def fallback_generation if @trace_id (random_from_trace_id * PRECISION).floor / PRECISION else format_random(Random.rand(1.0)) end end def generate_based_on_sampling(sampled, sample_rate) random = random_from_trace_id result = if sampled random * sample_rate elsif sample_rate == 1.0 random else sample_rate + random * (1.0 - sample_rate) end format_random(result) end def format_random(value) truncated = (value * PRECISION).floor / PRECISION ("%.#{FORMAT_PRECISION}f" % truncated).to_f end def parse_value(sample_rand_value) Float(sample_rand_value) rescue ArgumentError nil end end end end sentry-ruby-core-5.28.0/lib/sentry/utils/logging_helper.rb0000644000004100000410000000120715067721773023652 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @private module LoggingHelper # @!visibility private attr_reader :sdk_logger # @!visibility private def log_error(message, exception, debug: false) message = "#{message}: #{exception.message}" message += "\n#{exception.backtrace.join("\n")}" if debug sdk_logger.error(LOGGER_PROGNAME) do message end end # @!visibility private def log_debug(message) sdk_logger.debug(LOGGER_PROGNAME) { message } end # @!visibility private def log_warn(message) sdk_logger.warn(LOGGER_PROGNAME) { message } end end end sentry-ruby-core-5.28.0/lib/sentry/utils/custom_inspection.rb0000644000004100000410000000061115067721773024430 0ustar www-datawww-data# frozen_string_literal: true module Sentry module CustomInspection def inspect attr_strings = (instance_variables - self.class::SKIP_INSPECTION_ATTRIBUTES).each_with_object([]) do |attr, result| value = instance_variable_get(attr) result << "#{attr}=#{value.inspect}" if value end "#<#{self.class.name} #{attr_strings.join(", ")}>" end end end sentry-ruby-core-5.28.0/lib/sentry/utils/encoding_helper.rb0000644000004100000410000000105215067721773024010 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils module EncodingHelper def self.encode_to_utf_8(value) if value.encoding != Encoding::UTF_8 && value.respond_to?(:force_encoding) value = value.dup.force_encoding(Encoding::UTF_8) end value = value.scrub unless value.valid_encoding? value end def self.valid_utf_8?(value) return true unless value.respond_to?(:force_encoding) value.dup.force_encoding(Encoding::UTF_8).valid_encoding? end end end end sentry-ruby-core-5.28.0/lib/sentry/utils/argument_checking_helper.rb0000644000004100000410000000142615067721773025704 0ustar www-datawww-data# frozen_string_literal: true module Sentry module ArgumentCheckingHelper private def check_argument_type!(argument, *expected_types) unless expected_types.any? { |t| argument.is_a?(t) } raise ArgumentError, "expect the argument to be a #{expected_types.join(' or ')}, got #{argument.class} (#{argument.inspect})" end end def check_argument_includes!(argument, values) unless values.include?(argument) raise ArgumentError, "expect the argument to be one of #{values.map(&:inspect).join(' or ')}, got #{argument.inspect}" end end def check_callable!(name, value) unless value == nil || value.respond_to?(:call) raise ArgumentError, "#{name} must be callable (or nil to disable)" end end end end sentry-ruby-core-5.28.0/lib/sentry/utils/uuid.rb0000644000004100000410000000026715067721773021640 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" module Sentry module Utils DELIMITER = "-" def self.uuid SecureRandom.uuid.delete(DELIMITER) end end end sentry-ruby-core-5.28.0/lib/sentry/utils/env_helper.rb0000644000004100000410000000100015067721773023003 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils module EnvHelper TRUTHY_ENV_VALUES = %w[t true yes y 1 on].freeze FALSY_ENV_VALUES = %w[f false no n 0 off].freeze def self.env_to_bool(value, strict: false) value = value.to_s normalized = value.downcase return false if FALSY_ENV_VALUES.include?(normalized) return true if TRUTHY_ENV_VALUES.include?(normalized) strict ? nil : !(value.nil? || value.empty?) end end end end sentry-ruby-core-5.28.0/lib/sentry/utils/exception_cause_chain.rb0000644000004100000410000000063315067721773025207 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils module ExceptionCauseChain def self.exception_to_array(exception) exceptions = [exception] while exception.cause exception = exception.cause break if exceptions.any? { |e| e.object_id == exception.object_id } exceptions << exception end exceptions end end end end sentry-ruby-core-5.28.0/lib/sentry/utils/real_ip.rb0000644000004100000410000000533615067721773022307 0ustar www-datawww-data# frozen_string_literal: true require "ipaddr" # Based on ActionDispatch::RemoteIp. All security-related precautions from that # middleware have been removed, because the Event IP just needs to be accurate, # and spoofing an IP here only makes data inaccurate, not insecure. Don't re-use # this module if you have to *trust* the IP address. module Sentry module Utils class RealIp LOCAL_ADDRESSES = [ "127.0.0.1", # localhost IPv4 "::1", # localhost IPv6 "fc00::/7", # private IPv6 range fc00::/7 "10.0.0.0/8", # private IPv4 range 10.x.x.x "172.16.0.0/12", # private IPv4 range 172.16.0.0 .. 172.31.255.255 "192.168.0.0/16" # private IPv4 range 192.168.x.x ] attr_reader :ip def initialize( remote_addr: nil, client_ip: nil, real_ip: nil, forwarded_for: nil, trusted_proxies: [] ) @remote_addr = remote_addr @client_ip = client_ip @real_ip = real_ip @forwarded_for = forwarded_for @trusted_proxies = (LOCAL_ADDRESSES + Array(trusted_proxies)).map do |proxy| if proxy.is_a?(IPAddr) proxy else IPAddr.new(proxy.to_s) end end.uniq end def calculate_ip # CGI environment variable set by Rack remote_addr = ips_from(@remote_addr).last # Could be a CSV list and/or repeated headers that were concatenated. client_ips = ips_from(@client_ip) real_ips = ips_from(@real_ip) # The first address in this list is the original client, followed by # the IPs of successive proxies. We want to search starting from the end # until we find the first proxy that we do not trust. forwarded_ips = ips_from(@forwarded_for).reverse ips = [client_ips, real_ips, forwarded_ips, remote_addr].flatten.compact # If every single IP option is in the trusted list, just return REMOTE_ADDR @ip = filter_trusted_proxy_addresses(ips).first || remote_addr end protected def ips_from(header) # Split the comma-separated list into an array of strings ips = header ? header.strip.split(/[,\s]+/) : [] ips.select do |ip| begin # Only return IPs that are valid according to the IPAddr#new method range = IPAddr.new(ip).to_range # we want to make sure nobody is sneaking a netmask in range.begin == range.end rescue ArgumentError nil end end end def filter_trusted_proxy_addresses(ips) ips.reject { |ip| @trusted_proxies.any? { |proxy| proxy === ip } } end end end end sentry-ruby-core-5.28.0/lib/sentry/utils/request_id.rb0000644000004100000410000000064015067721773023031 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils module RequestId REQUEST_ID_HEADERS = %w[action_dispatch.request_id HTTP_X_REQUEST_ID].freeze # Request ID based on ActionDispatch::RequestId def self.read_from(env) REQUEST_ID_HEADERS.each do |key| request_id = env[key] return request_id if request_id end nil end end end end sentry-ruby-core-5.28.0/lib/sentry/utils/http_tracing.rb0000644000004100000410000000440315067721773023354 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Utils module HttpTracing def set_span_info(sentry_span, request_info, response_status) sentry_span.set_description("#{request_info[:method]} #{request_info[:url]}") sentry_span.set_data(Span::DataConventions::URL, request_info[:url]) sentry_span.set_data(Span::DataConventions::HTTP_METHOD, request_info[:method]) sentry_span.set_data(Span::DataConventions::HTTP_QUERY, request_info[:query]) if request_info[:query] sentry_span.set_data(Span::DataConventions::HTTP_STATUS_CODE, response_status) end def set_propagation_headers(req) Sentry.get_trace_propagation_headers&.each { |k, v| req[k] = v } end def record_sentry_breadcrumb(request_info, response_status) crumb = Sentry::Breadcrumb.new( level: get_level(response_status), category: self.class::BREADCRUMB_CATEGORY, type: "info", data: { status: response_status, **request_info } ) Sentry.add_breadcrumb(crumb) end def record_sentry_breadcrumb? Sentry.initialized? && Sentry.configuration.breadcrumbs_logger.include?(:http_logger) end def propagate_trace?(url) url && Sentry.initialized? && Sentry.configuration.propagate_traces && Sentry.configuration.trace_propagation_targets.any? { |target| url.match?(target) } end # Kindly borrowed from Rack::Utils def build_nested_query(value, prefix = nil) case value when Array value.map { |v| build_nested_query(v, "#{prefix}[]") }.join("&") when Hash value.map { |k, v| build_nested_query(v, prefix ? "#{prefix}[#{k}]" : k) }.delete_if(&:empty?).join("&") when nil URI.encode_www_form_component(prefix) else raise ArgumentError, "value must be a Hash" if prefix.nil? "#{URI.encode_www_form_component(prefix)}=#{URI.encode_www_form_component(value)}" end end private def get_level(status) return :info unless status && status.is_a?(Integer) if status >= 500 :error elsif status >= 400 :warning else :info end end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics.rb0000644000004100000410000000454115067721773021177 0ustar www-datawww-data# frozen_string_literal: true require "sentry/metrics/metric" require "sentry/metrics/counter_metric" require "sentry/metrics/distribution_metric" require "sentry/metrics/gauge_metric" require "sentry/metrics/set_metric" require "sentry/metrics/timing" require "sentry/metrics/aggregator" module Sentry module Metrics DURATION_UNITS = %w[nanosecond microsecond millisecond second minute hour day week] INFORMATION_UNITS = %w[bit byte kilobyte kibibyte megabyte mebibyte gigabyte gibibyte terabyte tebibyte petabyte pebibyte exabyte exbibyte] FRACTIONAL_UNITS = %w[ratio percent] OP_NAME = "metric.timing" SPAN_ORIGIN = "auto.metric.timing" class << self def increment(key, value = 1.0, unit: "none", tags: {}, timestamp: nil) log_deprecation Sentry.metrics_aggregator&.add(:c, key, value, unit: unit, tags: tags, timestamp: timestamp) end def distribution(key, value, unit: "none", tags: {}, timestamp: nil) log_deprecation Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp) end def set(key, value, unit: "none", tags: {}, timestamp: nil) log_deprecation Sentry.metrics_aggregator&.add(:s, key, value, unit: unit, tags: tags, timestamp: timestamp) end def gauge(key, value, unit: "none", tags: {}, timestamp: nil) log_deprecation Sentry.metrics_aggregator&.add(:g, key, value, unit: unit, tags: tags, timestamp: timestamp) end def timing(key, unit: "second", tags: {}, timestamp: nil, &block) log_deprecation return unless block_given? return yield unless DURATION_UNITS.include?(unit) result, value = Sentry.with_child_span(op: OP_NAME, description: key, origin: SPAN_ORIGIN) do |span| tags.each { |k, v| span.set_tag(k, v.is_a?(Array) ? v.join(", ") : v.to_s) } if span start = Timing.send(unit.to_sym) result = yield value = Timing.send(unit.to_sym) - start [result, value] end Sentry.metrics_aggregator&.add(:d, key, value, unit: unit, tags: tags, timestamp: timestamp) result end def log_deprecation Sentry.sdk_logger.warn(LOGGER_PROGNAME) do "`Sentry::Metrics` is now deprecated and will be removed in the next major." end end end end end sentry-ruby-core-5.28.0/lib/sentry/graphql.rb0000644000004100000410000000077615067721773021175 0ustar www-datawww-data# frozen_string_literal: true Sentry.register_patch(:graphql) do |config| if defined?(::GraphQL::Schema) && defined?(::GraphQL::Tracing::SentryTrace) && ::GraphQL::Schema.respond_to?(:trace_with) ::GraphQL::Schema.trace_with(::GraphQL::Tracing::SentryTrace, set_transaction_name: true) else config.sdk_logger.warn(Sentry::LOGGER_PROGNAME) { "You tried to enable the GraphQL integration but no GraphQL gem was detected. Make sure you have the `graphql` gem (>= 2.2.6) in your Gemfile." } end end sentry-ruby-core-5.28.0/lib/sentry/rspec.rb0000644000004100000410000000477015067721773020651 0ustar www-datawww-data# frozen_string_literal: true RSpec::Matchers.define :include_sentry_event do |event_message = "", **opts| match do |sentry_events| @expected_exception = expected_exception(**opts) @context = context(**opts) @tags = tags(**opts) @expected_event = expected_event(event_message) @matched_event = find_matched_event(event_message, sentry_events) return false unless @matched_event [verify_context(), verify_tags()].all? end chain :with_context do |context| @context = context end chain :with_tags do |tags| @tags = tags end failure_message do |sentry_events| info = ["Failed to find event matching:\n"] info << " message: #{@expected_event.message.inspect}" info << " exception: #{@expected_exception.inspect}" info << " context: #{@context.inspect}" info << " tags: #{@tags.inspect}" info << "\n" info << "Captured events:\n" info << dump_events(sentry_events) info.join("\n") end def expected_event(event_message) if @expected_exception Sentry.get_current_client.event_from_exception(@expected_exception) else Sentry.get_current_client.event_from_message(event_message) end end def expected_exception(**opts) opts[:exception].new(opts[:message]) if opts[:exception] end def context(**opts) opts.fetch(:context, @context || {}) end def tags(**opts) opts.fetch(:tags, @tags || {}) end def find_matched_event(event_message, sentry_events) @matched_event ||= sentry_events .find { |event| if @expected_exception # Is it OK that we only compare the first exception? event_exception = event.exception.values.first expected_event_exception = @expected_event.exception.values.first event_exception.type == expected_event_exception.type && event_exception.value == expected_event_exception.value else event.message == @expected_event.message end } end def dump_events(sentry_events) sentry_events.map(&Kernel.method(:Hash)).map do |hash| hash.select { |k, _| [:message, :contexts, :tags, :exception].include?(k) } end.map do |hash| JSON.pretty_generate(hash) end.join("\n\n") end def verify_context return true if @context.empty? @matched_event.contexts.any? { |key, value| value == @context[key] } end def verify_tags return true if @tags.empty? @tags.all? { |key, value| @matched_event.tags.include?(key) && @matched_event.tags[key] == value } end end sentry-ruby-core-5.28.0/lib/sentry/hub.rb0000644000004100000410000002304215067721773020304 0ustar www-datawww-data# frozen_string_literal: true require "sentry/scope" require "sentry/client" require "sentry/session" module Sentry class Hub include ArgumentCheckingHelper MUTEX = Mutex.new attr_reader :last_event_id attr_reader :current_profiler def initialize(client, scope) first_layer = Layer.new(client, scope) @stack = [first_layer] @last_event_id = nil @current_profiler = {} end # This is an internal private method # @api private def start_profiler!(transaction) MUTEX.synchronize do transaction.start_profiler! @current_profiler[transaction.__id__] = transaction.profiler end end # This is an internal private method # @api private def stop_profiler!(transaction) MUTEX.synchronize do @current_profiler.delete(transaction.__id__)&.stop end end # This is an internal private method # @api private def profiler_running? MUTEX.synchronize do !@current_profiler.empty? end end def new_from_top Hub.new(current_client, current_scope) end def current_client current_layer&.client end def configuration current_client.configuration end def current_scope current_layer&.scope end def clone layer = current_layer if layer scope = layer.scope&.dup Hub.new(layer.client, scope) end end def bind_client(client) layer = current_layer if layer layer.client = client end end def configure_scope(&block) block.call(current_scope) end def with_scope(&block) push_scope yield(current_scope) ensure pop_scope end def push_scope new_scope = if current_scope current_scope.dup else Scope.new end @stack << Layer.new(current_client, new_scope) end def pop_scope if @stack.size > 1 @stack.pop else # We never want to enter a situation where we have no scope and no client client = current_client @stack = [Layer.new(client, Scope.new)] end end def start_transaction(transaction: nil, custom_sampling_context: {}, instrumenter: :sentry, **options) return unless configuration.tracing_enabled? return unless instrumenter == configuration.instrumenter transaction ||= Transaction.new(**options.merge(hub: self)) sampling_context = { transaction_context: transaction.to_hash, parent_sampled: transaction.parent_sampled, parent_sample_rate: transaction.parent_sample_rate } sampling_context.merge!(custom_sampling_context) transaction.set_initial_sample_decision(sampling_context: sampling_context) start_profiler!(transaction) transaction end def with_child_span(instrumenter: :sentry, **attributes, &block) return yield(nil) unless instrumenter == configuration.instrumenter current_span = current_scope.get_span return yield(nil) unless current_span result = nil begin current_span.with_child_span(**attributes) do |child_span| current_scope.set_span(child_span) result = yield(child_span) end ensure current_scope.set_span(current_span) end result end def capture_exception(exception, **options, &block) if RUBY_PLATFORM == "java" check_argument_type!(exception, ::Exception, ::Java::JavaLang::Throwable) else check_argument_type!(exception, ::Exception) end return if Sentry.exception_captured?(exception) return unless current_client options[:hint] ||= {} options[:hint][:exception] = exception event = current_client.event_from_exception(exception, options[:hint]) return unless event current_scope.session&.update_from_exception(event.exception) capture_event(event, **options, &block).tap do # mark the exception as captured so we can use this information to avoid duplicated capturing exception.instance_variable_set(Sentry::CAPTURED_SIGNATURE, true) end end def capture_message(message, **options, &block) check_argument_type!(message, ::String) return unless current_client options[:hint] ||= {} options[:hint][:message] = message backtrace = options.delete(:backtrace) event = current_client.event_from_message(message, options[:hint], backtrace: backtrace) return unless event capture_event(event, **options, &block) end def capture_check_in(slug, status, **options) check_argument_type!(slug, ::String) check_argument_includes!(status, Sentry::CheckInEvent::VALID_STATUSES) return unless current_client options[:hint] ||= {} options[:hint][:slug] = slug event = current_client.event_from_check_in( slug, status, options[:hint], duration: options.delete(:duration), monitor_config: options.delete(:monitor_config), check_in_id: options.delete(:check_in_id) ) return unless event capture_event(event, **options) event.check_in_id end def capture_log_event(message, **options) return unless current_client event = current_client.event_from_log(message, **options) return unless event current_client.buffer_log_event(event, current_scope) end def capture_event(event, **options, &block) check_argument_type!(event, Sentry::Event) return unless current_client hint = options.delete(:hint) || {} scope = current_scope.dup if block block.call(scope) elsif custom_scope = options[:scope] scope.update_from_scope(custom_scope) elsif !options.empty? unsupported_option_keys = scope.update_from_options(**options) unless unsupported_option_keys.empty? configuration.log_debug <<~MSG Options #{unsupported_option_keys} are not supported and will not be applied to the event. You may want to set them under the `extra` option. MSG end end event = current_client.capture_event(event, scope, hint) if event && configuration.debug configuration.log_debug(event.to_json_compatible) end @last_event_id = event&.event_id if event.is_a?(Sentry::ErrorEvent) event end def add_breadcrumb(breadcrumb, hint: {}) return unless current_client return unless configuration.enabled_in_current_env? if before_breadcrumb = current_client.configuration.before_breadcrumb breadcrumb = before_breadcrumb.call(breadcrumb, hint) end return unless breadcrumb current_scope.add_breadcrumb(breadcrumb) end # this doesn't do anything to the already initialized background worker # but it temporarily disables dispatching events to it def with_background_worker_disabled(&block) original_background_worker_threads = configuration.background_worker_threads configuration.background_worker_threads = 0 block.call ensure configuration.background_worker_threads = original_background_worker_threads end def start_session return unless current_scope current_scope.set_session(Session.new) end def end_session return unless current_scope session = current_scope.session current_scope.set_session(nil) return unless session session.close # NOTE: Under some circumstances, session_flusher nilified out of sync # See: https://github.com/getsentry/sentry-ruby/issues/2378 # See: https://github.com/getsentry/sentry-ruby/pull/2396 Sentry.session_flusher&.add_session(session) end def with_session_tracking(&block) return yield unless configuration.session_tracking? start_session yield ensure end_session end def get_traceparent return nil unless current_scope current_scope.get_span&.to_sentry_trace || current_scope.propagation_context.get_traceparent end def get_baggage return nil unless current_scope current_scope.get_span&.to_baggage || current_scope.propagation_context.get_baggage&.serialize end def get_trace_propagation_headers headers = {} traceparent = get_traceparent headers[SENTRY_TRACE_HEADER_NAME] = traceparent if traceparent baggage = get_baggage headers[BAGGAGE_HEADER_NAME] = baggage if baggage && !baggage.empty? headers end def get_trace_propagation_meta get_trace_propagation_headers.map do |k, v| "" end.join("\n") end def continue_trace(env, **options) configure_scope { |s| s.generate_propagation_context(env) } return nil unless configuration.tracing_enabled? propagation_context = current_scope.propagation_context return nil unless propagation_context.incoming_trace Transaction.new( hub: self, trace_id: propagation_context.trace_id, parent_span_id: propagation_context.parent_span_id, parent_sampled: propagation_context.parent_sampled, baggage: propagation_context.baggage, sample_rand: propagation_context.sample_rand, **options ) end private def current_layer @stack.last end class Layer attr_accessor :client attr_reader :scope def initialize(client, scope) @client = client @scope = scope end end end end sentry-ruby-core-5.28.0/lib/sentry/exceptions.rb0000644000004100000410000000017215067721773021706 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Error < StandardError end class ExternalError < Error end end sentry-ruby-core-5.28.0/lib/sentry/debug_structured_logger.rb0000644000004100000410000000516715067721773024447 0ustar www-datawww-data# frozen_string_literal: true require "json" require "fileutils" require "pathname" require "delegate" module Sentry # DebugStructuredLogger is a logger that captures structured log events to a file for debugging purposes. # # It can optionally also send log events to Sentry via the normal structured logger if logging # is enabled. class DebugStructuredLogger < SimpleDelegator DEFAULT_LOG_FILE_PATH = File.join("log", "sentry_debug_logs.log") attr_reader :log_file, :backend def initialize(configuration) @log_file = initialize_log_file( configuration.structured_logging.file_path || DEFAULT_LOG_FILE_PATH ) @backend = initialize_backend(configuration) super(@backend) end # Override all log level methods to capture events %i[trace debug info warn error fatal].each do |level| define_method(level) do |message, parameters = [], **attributes| log_event = capture_log_event(level, message, parameters, **attributes) backend.public_send(level, message, parameters, **attributes) log_event end end def log(level, message, parameters:, **attributes) log_event = capture_log_event(level, message, parameters, **attributes) backend.log(level, message, parameters: parameters, **attributes) log_event end def capture_log_event(level, message, parameters, **attributes) log_event_json = { timestamp: Time.now.utc.iso8601, level: level.to_s, message: message, parameters: parameters, attributes: attributes } File.open(log_file, "a") { |file| file << JSON.dump(log_event_json) << "\n" } log_event_json end def logged_events File.readlines(log_file).map do |line| JSON.parse(line) end end def clear File.write(log_file, "") if backend.respond_to?(:config) backend.config.sdk_logger.debug("DebugStructuredLogger: Cleared events from #{log_file}") end end private def initialize_backend(configuration) if configuration.enable_logs StructuredLogger.new(configuration) else # Create a no-op logger if logging is disabled NoOpLogger.new end end def initialize_log_file(log_file_path) log_file = Pathname(log_file_path) FileUtils.mkdir_p(log_file.dirname) unless log_file.dirname.exist? log_file end # No-op logger for when structured logging is disabled class NoOpLogger %i[trace debug info warn error fatal log].each do |method| define_method(method) { |*args, **kwargs| nil } end end end end sentry-ruby-core-5.28.0/lib/sentry/dsn.rb0000644000004100000410000000367315067721773020322 0ustar www-datawww-data# frozen_string_literal: true require "uri" require "ipaddr" require "resolv" module Sentry class DSN PORT_MAP = { "http" => 80, "https" => 443 }.freeze REQUIRED_ATTRIBUTES = %w[host path public_key project_id].freeze LOCALHOST_NAMES = %w[localhost 127.0.0.1 ::1 [::1]].freeze LOCALHOST_PATTERN = /\.local(host|domain)?$/i attr_reader :scheme, :secret_key, :port, *REQUIRED_ATTRIBUTES def initialize(dsn_string) @raw_value = dsn_string uri = URI.parse(dsn_string) uri_path = uri.path.split("/") if uri.user # DSN-style string @project_id = uri_path.pop @public_key = uri.user @secret_key = !(uri.password.nil? || uri.password.empty?) ? uri.password : nil end @scheme = uri.scheme @host = uri.host @port = uri.port if uri.port @path = uri_path.join("/") end def valid? REQUIRED_ATTRIBUTES.all? { |k| public_send(k) } end def to_s @raw_value end def server server = "#{scheme}://#{host}" server += ":#{port}" unless port == PORT_MAP[scheme] server end def csp_report_uri "#{server}/api/#{project_id}/security/?sentry_key=#{public_key}" end def envelope_endpoint "#{path}/api/#{project_id}/envelope/" end def local? @local ||= (localhost? || private_ip? || resolved_ips_private?) end def localhost? LOCALHOST_NAMES.include?(host.downcase) || LOCALHOST_PATTERN.match?(host) end def private_ip? @private_ip ||= begin begin IPAddr.new(host).private? rescue IPAddr::InvalidAddressError false end end end def resolved_ips_private? @resolved_ips_private ||= begin begin Resolv.getaddresses(host).any? { |ip| IPAddr.new(ip).private? } rescue Resolv::ResolvError, IPAddr::InvalidAddressError false end end end end end sentry-ruby-core-5.28.0/lib/sentry/envelope/0000755000004100000410000000000015067721773021015 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/envelope/item.rb0000644000004100000410000000500615067721773022301 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class Envelope::Item STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD = 500 MAX_SERIALIZED_PAYLOAD_SIZE = 1024 * 1000 SIZE_LIMITS = Hash.new(MAX_SERIALIZED_PAYLOAD_SIZE).update( "profile" => 1024 * 1000 * 50 ) attr_reader :size_limit, :headers, :payload, :type, :data_category # rate limits and client reports use the data_category rather than envelope item type def self.data_category(type) case type when "session", "attachment", "transaction", "profile", "span", "log" then type when "sessions" then "session" when "check_in" then "monitor" when "statsd", "metric_meta" then "metric_bucket" when "event" then "error" when "client_report" then "internal" else "default" end end def initialize(headers, payload) @headers = headers @payload = payload @type = headers[:type] || "event" @data_category = self.class.data_category(type) @size_limit = SIZE_LIMITS[type] end def to_s [JSON.generate(@headers), @payload.is_a?(String) ? @payload : JSON.generate(@payload)].join("\n") end def serialize result = to_s if result.bytesize > size_limit remove_breadcrumbs! result = to_s end if result.bytesize > size_limit reduce_stacktrace! result = to_s end [result, result.bytesize > size_limit] end def size_breakdown payload.map do |key, value| "#{key}: #{JSON.generate(value).bytesize}" end.join(", ") end private def remove_breadcrumbs! if payload.key?(:breadcrumbs) payload.delete(:breadcrumbs) elsif payload.key?("breadcrumbs") payload.delete("breadcrumbs") end end def reduce_stacktrace! if exceptions = payload.dig(:exception, :values) || payload.dig("exception", "values") exceptions.each do |exception| # in most cases there is only one exception (2 or 3 when have multiple causes), so we won't loop through this double condition much traces = exception.dig(:stacktrace, :frames) || exception.dig("stacktrace", "frames") if traces && traces.size > STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD size_on_both_ends = STACKTRACE_FRAME_LIMIT_ON_OVERSIZED_PAYLOAD / 2 traces.replace( traces[0..(size_on_both_ends - 1)] + traces[-size_on_both_ends..-1], ) end end end end end end sentry-ruby-core-5.28.0/lib/sentry/vernier/0000755000004100000410000000000015067721773020652 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/vernier/output.rb0000644000004100000410000000437615067721773022551 0ustar www-datawww-data# frozen_string_literal: true require "json" require "rbconfig" module Sentry module Vernier class Output include Profiler::Helpers attr_reader :profile def initialize(profile, project_root:, in_app_pattern:, app_dirs_pattern:) @profile = profile @project_root = project_root @in_app_pattern = in_app_pattern @app_dirs_pattern = app_dirs_pattern end def to_h @to_h ||= { frames: frames, stacks: stacks, samples: samples, thread_metadata: thread_metadata } end private def thread_metadata profile.threads.map { |thread_id, thread_info| [thread_id, { name: thread_info[:name] }] }.to_h end def samples profile.threads.flat_map { |thread_id, thread_info| started_at = thread_info[:started_at] samples, timestamps = thread_info.values_at(:samples, :timestamps) samples.zip(timestamps).map { |stack_id, timestamp| elapsed_since_start_ns = timestamp - started_at next if elapsed_since_start_ns < 0 { thread_id: thread_id.to_s, stack_id: stack_id, elapsed_since_start_ns: elapsed_since_start_ns.to_s } }.compact } end def frames funcs = stack_table_hash[:frame_table].fetch(:func) lines = stack_table_hash[:func_table].fetch(:first_line) funcs.map do |idx| function, mod = split_module(stack_table_hash[:func_table][:name][idx]) abs_path = stack_table_hash[:func_table][:filename][idx] in_app = in_app?(abs_path) filename = compute_filename(abs_path, in_app) { function: function, module: mod, filename: filename, abs_path: abs_path, lineno: (lineno = lines[idx]) > 0 ? lineno : nil, in_app: in_app }.compact end end def stacks profile._stack_table.stack_count.times.map do |stack_id| profile.stack(stack_id).frames.map(&:idx) end end def stack_table_hash @stack_table_hash ||= profile._stack_table.to_h end end end end sentry-ruby-core-5.28.0/lib/sentry/vernier/profiler.rb0000644000004100000410000000625315067721773023027 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" require_relative "../profiler/helpers" require_relative "output" require "sentry/utils/uuid" module Sentry module Vernier class Profiler EMPTY_RESULT = {}.freeze attr_reader :started, :event_id, :result def initialize(configuration) @event_id = Utils.uuid @started = false @sampled = nil @profiling_enabled = defined?(Vernier) && configuration.profiling_enabled? @profiles_sample_rate = configuration.profiles_sample_rate @project_root = configuration.project_root @app_dirs_pattern = configuration.app_dirs_pattern @in_app_pattern = Regexp.new("^(#{@project_root}/)?#{@app_dirs_pattern}") end def set_initial_sample_decision(transaction_sampled) unless @profiling_enabled @sampled = false return end unless transaction_sampled @sampled = false log("Discarding profile because transaction not sampled") return end case @profiles_sample_rate when 0.0 @sampled = false log("Discarding profile because sample_rate is 0") return when 1.0 @sampled = true return else @sampled = Random.rand < @profiles_sample_rate end log("Discarding profile due to sampling decision") unless @sampled end def start return unless @sampled return if @started @started = ::Vernier.start_profile log("Started") @started rescue RuntimeError => e # TODO: once Vernier raises something more dedicated, we should catch that instead if e.message.include?("Profile already started") log("Not started since running elsewhere") else log("Failed to start: #{e.message}") end end def stop return unless @sampled return unless @started @result = ::Vernier.stop_profile @started = false log("Stopped") rescue RuntimeError => e if e.message.include?("Profile not started") log("Not stopped since not started") else log("Failed to stop Vernier: #{e.message}") end end def active_thread_id Thread.current.object_id end def to_hash unless @sampled record_lost_event(:sample_rate) return EMPTY_RESULT end return EMPTY_RESULT unless result { **profile_meta, profile: output.to_h } end private def log(message) Sentry.sdk_logger.debug(LOGGER_PROGNAME) { "[Profiler::Vernier] #{message}" } end def record_lost_event(reason) Sentry.get_current_client&.transport&.record_lost_event(reason, "profile") end def profile_meta { event_id: @event_id, version: "1", platform: "ruby" } end def output @output ||= Output.new( result, project_root: @project_root, app_dirs_pattern: @app_dirs_pattern, in_app_pattern: @in_app_pattern ) end end end end sentry-ruby-core-5.28.0/lib/sentry/baggage.rb0000644000004100000410000000337615067721773021113 0ustar www-datawww-data# frozen_string_literal: true require "cgi" module Sentry # A {https://www.w3.org/TR/baggage W3C Baggage Header} implementation. class Baggage SENTRY_PREFIX = "sentry-" SENTRY_PREFIX_REGEX = /^sentry-/ # @return [Hash] attr_reader :items # @return [Boolean] attr_reader :mutable def initialize(items, mutable: true) @items = items @mutable = mutable end # Creates a Baggage object from an incoming W3C Baggage header string. # # Sentry items are identified with the 'sentry-' prefix and stored in a hash. # The presence of a Sentry item makes the baggage object immutable. # # @param header [String] The incoming Baggage header string. # @return [Baggage, nil] def self.from_incoming_header(header) items = {} mutable = true header.split(",").each do |item| item = item.strip key, val = item.split("=") next unless key && val next unless key =~ SENTRY_PREFIX_REGEX baggage_key = key.split("-")[1] next unless baggage_key items[CGI.unescape(baggage_key)] = CGI.unescape(val) mutable = false end new(items, mutable: mutable) end # Make the Baggage immutable. # @return [void] def freeze! @mutable = false end # A {https://develop.sentry.dev/sdk/performance/dynamic-sampling-context/#envelope-header Dynamic Sampling Context} # hash to be used in the trace envelope header. # @return [Hash] def dynamic_sampling_context @items end # Serialize the Baggage object back to a string. # @return [String] def serialize items = @items.map { |k, v| "#{SENTRY_PREFIX}#{CGI.escape(k)}=#{CGI.escape(v)}" } items.join(",") end end end sentry-ruby-core-5.28.0/lib/sentry/session.rb0000644000004100000410000000145715067721773021217 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Session attr_reader :started, :status, :aggregation_key # TODO-neel add :crashed after adding handled mechanism STATUSES = %i[ok errored exited] AGGREGATE_STATUSES = %i[errored exited] def initialize @started = Sentry.utc_now @status = :ok # truncate seconds from the timestamp since we only care about # minute level granularity for aggregation @aggregation_key = Time.utc(@started.year, @started.month, @started.day, @started.hour, @started.min) end # TODO-neel add :crashed after adding handled mechanism def update_from_exception(_exception = nil) @status = :errored end def close @status = :exited if @status == :ok end def deep_dup dup end end end sentry-ruby-core-5.28.0/lib/sentry/faraday.rb0000644000004100000410000000445315067721773021142 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Faraday OP_NAME = "http.client" module Connection # Since there's no way to preconfigure Faraday connections and add our instrumentation # by default, we need to extend the connection constructor and do it there # # @see https://lostisland.github.io/faraday/#/customization/index?id=configuration def initialize(url = nil, options = nil) super # Ensure that we attach instrumentation only if the adapter is not net/http # because if is is, then the net/http instrumentation will take care of it if builder.adapter.name != "Faraday::Adapter::NetHttp" # Make sure that it's going to be the first middleware so that it can capture # the entire request processing involving other middlewares builder.insert(0, ::Faraday::Request::Instrumentation, name: OP_NAME, instrumenter: Instrumenter.new) end end end class Instrumenter SPAN_ORIGIN = "auto.http.faraday" BREADCRUMB_CATEGORY = "http" include Utils::HttpTracing def instrument(op_name, env, &block) return block.call unless Sentry.initialized? Sentry.with_child_span(op: op_name, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span| request_info = extract_request_info(env) if propagate_trace?(request_info[:url]) set_propagation_headers(env[:request_headers]) end res = block.call response_status = res.status if record_sentry_breadcrumb? record_sentry_breadcrumb(request_info, response_status) end if sentry_span set_span_info(sentry_span, request_info, response_status) end res end end private def extract_request_info(env) url = env[:url].scheme + "://" + env[:url].host + env[:url].path result = { method: env[:method].to_s.upcase, url: url } if Sentry.configuration.send_default_pii result[:query] = env[:url].query result[:body] = env[:body] end result end end end end Sentry.register_patch(:faraday) do if defined?(::Faraday) ::Faraday::Connection.prepend(Sentry::Faraday::Connection) end end sentry-ruby-core-5.28.0/lib/sentry/error_event.rb0000644000004100000410000000177115067721773022065 0ustar www-datawww-data# frozen_string_literal: true module Sentry # ErrorEvent represents error or normal message events. class ErrorEvent < Event # @return [ExceptionInterface] attr_reader :exception # @return [ThreadsInterface] attr_reader :threads # @return [Hash] def to_hash data = super data[:threads] = threads.to_hash if threads data[:exception] = exception.to_hash if exception data end # @!visibility private def add_threads_interface(backtrace: nil, **options) @threads = ThreadsInterface.build( backtrace: backtrace, stacktrace_builder: @stacktrace_builder, **options ) end # @!visibility private def add_exception_interface(exception, mechanism:) if exception.respond_to?(:sentry_context) @extra.merge!(exception.sentry_context) end @exception = Sentry::ExceptionInterface.build(exception: exception, stacktrace_builder: @stacktrace_builder, mechanism: mechanism) end end end sentry-ruby-core-5.28.0/lib/sentry/backpressure_monitor.rb0000644000004100000410000000215115067721773023764 0ustar www-datawww-data# frozen_string_literal: true module Sentry class BackpressureMonitor < ThreadedPeriodicWorker DEFAULT_INTERVAL = 10 MAX_DOWNSAMPLE_FACTOR = 10 def initialize(configuration, client, interval: DEFAULT_INTERVAL) super(configuration.sdk_logger, interval) @client = client @healthy = true @downsample_factor = 0 end def healthy? ensure_thread @healthy end def downsample_factor ensure_thread @downsample_factor end def run check_health set_downsample_factor end def check_health @healthy = !(@client.transport.any_rate_limited? || Sentry.background_worker&.full?) end def set_downsample_factor if @healthy log_debug("[BackpressureMonitor] health check positive, reverting to normal sampling") if @downsample_factor.positive? @downsample_factor = 0 else @downsample_factor += 1 if @downsample_factor < MAX_DOWNSAMPLE_FACTOR log_debug("[BackpressureMonitor] health check negative, downsampling with a factor of #{@downsample_factor}") end end end end sentry-ruby-core-5.28.0/lib/sentry/rake.rb0000644000004100000410000000130515067721773020446 0ustar www-datawww-data# frozen_string_literal: true require "rake" require "rake/task" module Sentry module Rake module Application # @api private def display_error_message(ex) mechanism = Sentry::Mechanism.new(type: "rake", handled: false) Sentry.capture_exception(ex, hint: { mechanism: mechanism }) do |scope| task_name = top_level_tasks.join(" ") scope.set_transaction_name(task_name, source: :task) scope.set_tag("rake_task", task_name) end if Sentry.initialized? && !Sentry.configuration.skip_rake_integration super end end end end # @api private module Rake class Application prepend(Sentry::Rake::Application) end end sentry-ruby-core-5.28.0/lib/sentry/span.rb0000644000004100000410000002137615067721773020477 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" require "sentry/metrics/local_aggregator" require "sentry/utils/uuid" module Sentry class Span # We will try to be consistent with OpenTelemetry on this front going forward. # https://develop.sentry.dev/sdk/performance/span-data-conventions/ module DataConventions URL = "url" HTTP_STATUS_CODE = "http.response.status_code" HTTP_QUERY = "http.query" HTTP_METHOD = "http.request.method" # An identifier for the database management system (DBMS) product being used. # Example: postgresql DB_SYSTEM = "db.system" # The name of the database being accessed. # For commands that switch the database, this should be set to the target database # (even if the command fails). # Example: myDatabase DB_NAME = "db.name" # Name of the database host. # Example: example.com SERVER_ADDRESS = "server.address" # Logical server port number # Example: 80; 8080; 443 SERVER_PORT = "server.port" # Physical server IP address or Unix socket address. # Example: 10.5.3.2 SERVER_SOCKET_ADDRESS = "server.socket.address" # Physical server port. # Recommended: If different than server.port. # Example: 16456 SERVER_SOCKET_PORT = "server.socket.port" FILEPATH = "code.filepath" LINENO = "code.lineno" FUNCTION = "code.function" NAMESPACE = "code.namespace" MESSAGING_MESSAGE_ID = "messaging.message.id" MESSAGING_DESTINATION_NAME = "messaging.destination.name" MESSAGING_MESSAGE_RECEIVE_LATENCY = "messaging.message.receive.latency" MESSAGING_MESSAGE_RETRY_COUNT = "messaging.message.retry.count" end STATUS_MAP = { 400 => "invalid_argument", 401 => "unauthenticated", 403 => "permission_denied", 404 => "not_found", 409 => "already_exists", 429 => "resource_exhausted", 499 => "cancelled", 500 => "internal_error", 501 => "unimplemented", 503 => "unavailable", 504 => "deadline_exceeded" } DEFAULT_SPAN_ORIGIN = "manual" # An uuid that can be used to identify a trace. # @return [String] attr_reader :trace_id # An uuid that can be used to identify the span. # @return [String] attr_reader :span_id # Span parent's span_id. # @return [String] attr_reader :parent_span_id # Sampling result of the span. # @return [Boolean, nil] attr_reader :sampled # Starting timestamp of the span. # @return [Float] attr_reader :start_timestamp # Finishing timestamp of the span. # @return [Float] attr_reader :timestamp # Span description # @return [String] attr_reader :description # Span operation # @return [String] attr_reader :op # Span status # @return [String] attr_reader :status # Span tags # @return [Hash] attr_reader :tags # Span data # @return [Hash] attr_reader :data # Span origin that tracks what kind of instrumentation created a span # @return [String] attr_reader :origin # The SpanRecorder the current span belongs to. # SpanRecorder holds all spans under the same Transaction object (including the Transaction itself). # @return [SpanRecorder] attr_accessor :span_recorder # The Transaction object the Span belongs to. # Every span needs to be attached to a Transaction and their child spans will also inherit the same transaction. # @return [Transaction] attr_reader :transaction def initialize( transaction:, description: nil, op: nil, status: nil, trace_id: nil, span_id: nil, parent_span_id: nil, sampled: nil, start_timestamp: nil, timestamp: nil, origin: nil ) @trace_id = trace_id || Utils.uuid @span_id = span_id || Utils.uuid.slice(0, 16) @parent_span_id = parent_span_id @sampled = sampled @start_timestamp = start_timestamp || Sentry.utc_now.to_f @timestamp = timestamp @description = description @transaction = transaction @op = op @status = status @data = {} @tags = {} @origin = origin || DEFAULT_SPAN_ORIGIN end # Finishes the span by adding a timestamp. # @return [self] def finish(end_timestamp: nil) @timestamp = end_timestamp || @timestamp || Sentry.utc_now.to_f self end # Generates a trace string that can be used to connect other transactions. # @return [String] def to_sentry_trace sampled_flag = "" sampled_flag = @sampled ? 1 : 0 unless @sampled.nil? "#{@trace_id}-#{@span_id}-#{sampled_flag}" end # Generates a W3C Baggage header string for distributed tracing # from the incoming baggage stored on the transaction. # @return [String, nil] def to_baggage transaction.get_baggage&.serialize end # Returns the Dynamic Sampling Context from the transaction baggage. # @return [Hash, nil] def get_dynamic_sampling_context transaction.get_baggage&.dynamic_sampling_context end # @return [Hash] def to_hash hash = { trace_id: @trace_id, span_id: @span_id, parent_span_id: @parent_span_id, start_timestamp: @start_timestamp, timestamp: @timestamp, description: @description, op: @op, status: @status, tags: @tags, data: @data, origin: @origin } summary = metrics_summary hash[:_metrics_summary] = summary if summary hash end # Returns the span's context that can be used to embed in an Event. # @return [Hash] def get_trace_context { trace_id: @trace_id, span_id: @span_id, parent_span_id: @parent_span_id, description: @description, op: @op, status: @status, origin: @origin, data: @data } end # Starts a child span with given attributes. # @param attributes [Hash] the attributes for the child span. def start_child(**attributes) attributes = attributes.dup.merge(transaction: @transaction, trace_id: @trace_id, parent_span_id: @span_id, sampled: @sampled) new_span = Span.new(**attributes) new_span.span_recorder = span_recorder if span_recorder span_recorder.add(new_span) end new_span end # Starts a child span, yield it to the given block, and then finish the span after the block is executed. # @example # span.with_child_span do |child_span| # # things happen here will be recorded in a child span # end # # @param attributes [Hash] the attributes for the child span. # @param block [Proc] the action to be recorded in the child span. # @yieldparam child_span [Span] def with_child_span(**attributes, &block) child_span = start_child(**attributes) yield(child_span) child_span.finish rescue child_span.set_http_status(500) child_span.finish raise end def deep_dup dup end # Sets the span's operation. # @param op [String] operation of the span. def set_op(op) @op = op end # Sets the span's description. # @param description [String] description of the span. def set_description(description) @description = description end # Sets the span's status. # @param status [String] status of the span. def set_status(status) @status = status end # Sets the span's finish timestamp. # @param timestamp [Float] finished time in float format (most precise). def set_timestamp(timestamp) @timestamp = timestamp end # Sets the span's status with given http status code. # @param status_code [String] example: "500". def set_http_status(status_code) status_code = status_code.to_i set_data(DataConventions::HTTP_STATUS_CODE, status_code) status = if status_code >= 200 && status_code < 299 "ok" else STATUS_MAP[status_code] end set_status(status) end # Inserts a key-value pair to the span's data payload. # @param key [String, Symbol] # @param value [Object] def set_data(key, value) @data[key] = value end # Sets a tag to the span. # @param key [String, Symbol] # @param value [String] def set_tag(key, value) @tags[key] = value end # Sets the origin of the span. # @param origin [String] def set_origin(origin) @origin = origin end # Collects gauge metrics on the span for metric summaries. def metrics_local_aggregator @metrics_local_aggregator ||= Sentry::Metrics::LocalAggregator.new end def metrics_summary @metrics_local_aggregator&.to_hash end end end sentry-ruby-core-5.28.0/lib/sentry/breadcrumb_buffer.rb0000644000004100000410000000234715067721773023172 0ustar www-datawww-data# frozen_string_literal: true require "sentry/breadcrumb" module Sentry class BreadcrumbBuffer DEFAULT_SIZE = 100 include Enumerable # @return [Array] attr_accessor :buffer # @param size [Integer, nil] If it's not provided, it'll fallback to DEFAULT_SIZE def initialize(size = nil) @buffer = Array.new(size || DEFAULT_SIZE) end # @param crumb [Breadcrumb] # @return [void] def record(crumb) yield(crumb) if block_given? @buffer.slice!(0) @buffer << crumb end # @return [Array] def members @buffer.compact end # Returns the last breadcrumb stored in the buffer. If the buffer it's empty, it returns nil. # @return [Breadcrumb, nil] def peek members.last end # Iterates through all breadcrumbs. # @param block [Proc] # @yieldparam crumb [Breadcrumb] # @return [Array] def each(&block) members.each(&block) end # @return [Boolean] def empty? members.none? end # @return [Hash] def to_hash { values: members.map(&:to_hash) } end # @return [BreadcrumbBuffer] def dup copy = super copy.buffer = buffer.deep_dup copy end end end sentry-ruby-core-5.28.0/lib/sentry/background_worker.rb0000644000004100000410000000412615067721773023240 0ustar www-datawww-data# frozen_string_literal: true require "concurrent/executor/thread_pool_executor" require "concurrent/executor/immediate_executor" require "concurrent/configuration" module Sentry class BackgroundWorker include LoggingHelper attr_reader :max_queue, :number_of_threads attr_accessor :shutdown_timeout DEFAULT_MAX_QUEUE = 30 def initialize(configuration) @shutdown_timeout = 1 @number_of_threads = configuration.background_worker_threads @max_queue = configuration.background_worker_max_queue @sdk_logger = configuration.sdk_logger @debug = configuration.debug @shutdown_callback = nil @executor = if configuration.async log_debug("config.async is set, BackgroundWorker is disabled") Concurrent::ImmediateExecutor.new elsif @number_of_threads == 0 log_debug("config.background_worker_threads is set to 0, all events will be sent synchronously") Concurrent::ImmediateExecutor.new else log_debug("Initializing the Sentry background worker with #{@number_of_threads} threads") executor = Concurrent::ThreadPoolExecutor.new( min_threads: 0, max_threads: @number_of_threads, max_queue: @max_queue, fallback_policy: :discard ) @shutdown_callback = proc do executor.shutdown executor.wait_for_termination(@shutdown_timeout) end executor end end # if you want to monkey-patch this method, please override `_perform` instead def perform(&block) @executor.post do begin _perform(&block) rescue Exception => e log_error("exception happened in background worker", e, debug: @debug) end end end def shutdown log_debug("Shutting down background worker") @shutdown_callback&.call end def full? @executor.is_a?(Concurrent::ThreadPoolExecutor) && @executor.remaining_capacity == 0 end private def _perform(&block) block.call end end end sentry-ruby-core-5.28.0/lib/sentry/breadcrumb/0000755000004100000410000000000015067721773021306 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/breadcrumb/sentry_logger.rb0000644000004100000410000000511315067721773024516 0ustar www-datawww-data# frozen_string_literal: true require "logger" module Sentry class Breadcrumb module SentryLogger LEVELS = { ::Logger::DEBUG => "debug", ::Logger::INFO => "info", ::Logger::WARN => "warn", ::Logger::ERROR => "error", ::Logger::FATAL => "fatal" }.freeze def add(*args, &block) super add_breadcrumb(*args, &block) nil end def add_breadcrumb(severity, message = nil, progname = nil) # because the breadcrumbs now belongs to different Hub's Scope in different threads # we need to make sure the current thread's Hub has been set before adding breadcrumbs return unless Sentry.initialized? && Sentry.get_current_hub category = "logger" # this is because the nature of Ruby Logger class: # # when given 1 argument, the argument will become both message and progname # # ``` # logger.info("foo") # # message == progname == "foo" # ``` # # and to specify progname with a different message, # we need to pass the progname as the argument and pass the message as a proc # # ``` # logger.info("progname") { "the message" } # ``` # # so the condition below is to replicate the similar behavior if message.nil? if block_given? message = yield category = progname else message = progname end end return if ignored_logger?(progname) || message == "" # some loggers will add leading/trailing space as they (incorrectly, mind you) # think of logging as a shortcut to std{out,err} message = message.to_s.strip last_crumb = current_breadcrumbs.peek # try to avoid dupes from logger broadcasts if last_crumb.nil? || last_crumb.message != message level = Sentry::Breadcrumb::SentryLogger::LEVELS.fetch(severity, nil) crumb = Sentry::Breadcrumb.new( level: level, category: category, message: message, type: severity >= 3 ? "error" : level ) Sentry.add_breadcrumb(crumb, hint: { severity: severity }) end end private def ignored_logger?(progname) progname == LOGGER_PROGNAME || Sentry.configuration.exclude_loggers.include?(progname) end def current_breadcrumbs Sentry.get_current_scope.breadcrumbs end end end end ::Logger.send(:prepend, Sentry::Breadcrumb::SentryLogger) sentry-ruby-core-5.28.0/lib/sentry/excon/0000755000004100000410000000000015067721773020314 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/excon/middleware.rb0000644000004100000410000000361115067721773022757 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Excon OP_NAME = "http.client" class Middleware < ::Excon::Middleware::Base def initialize(stack) super @instrumenter = Instrumenter.new end def request_call(datum) @instrumenter.start_transaction(datum) @stack.request_call(datum) end def response_call(datum) @instrumenter.finish_transaction(datum) @stack.response_call(datum) end end class Instrumenter SPAN_ORIGIN = "auto.http.excon" BREADCRUMB_CATEGORY = "http" include Utils::HttpTracing def start_transaction(env) return unless Sentry.initialized? current_span = Sentry.get_current_scope&.span @span = current_span&.start_child(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) request_info = extract_request_info(env) if propagate_trace?(request_info[:url]) set_propagation_headers(env[:headers]) end end def finish_transaction(response) return unless @span response_status = response[:response][:status] request_info = extract_request_info(response) if record_sentry_breadcrumb? record_sentry_breadcrumb(request_info, response_status) end set_span_info(@span, request_info, response_status) ensure @span&.finish end private def extract_request_info(env) url = env[:scheme] + "://" + env[:hostname] + env[:path] result = { method: env[:method].to_s.upcase, url: url } if Sentry.configuration.send_default_pii result[:query] = env[:query] # Handle excon 1.0.0+ result[:query] = build_nested_query(result[:query]) unless result[:query].is_a?(String) result[:body] = env[:body] end result end end end end sentry-ruby-core-5.28.0/lib/sentry/redis.rb0000644000004100000410000000572015067721773020637 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class Redis OP_NAME = "db.redis" SPAN_ORIGIN = "auto.db.redis" LOGGER_NAME = :redis_logger def initialize(commands, host, port, db) @commands, @host, @port, @db = commands, host, port, db end def instrument return yield unless Sentry.initialized? Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |span| yield.tap do record_breadcrumb if span span.set_description(commands_description) span.set_data(Span::DataConventions::DB_SYSTEM, "redis") span.set_data(Span::DataConventions::DB_NAME, db) span.set_data(Span::DataConventions::SERVER_ADDRESS, host) span.set_data(Span::DataConventions::SERVER_PORT, port) end end end end private attr_reader :commands, :host, :port, :db def record_breadcrumb return unless Sentry.initialized? return unless Sentry.configuration.breadcrumbs_logger.include?(LOGGER_NAME) Sentry.add_breadcrumb( Sentry::Breadcrumb.new( level: :info, category: OP_NAME, type: :info, data: { commands: parsed_commands, server: server_description } ) ) end def commands_description parsed_commands.map do |statement| statement.values.join(" ").strip end.join(", ") end def parsed_commands commands.map do |statement| command, key, *arguments = statement command_set = { command: command.to_s.upcase } command_set[:key] = key if Utils::EncodingHelper.valid_utf_8?(key) if Sentry.configuration.send_default_pii command_set[:arguments] = arguments .select { |a| Utils::EncodingHelper.valid_utf_8?(a) } .join(" ") end command_set end end def server_description "#{host}:#{port}/#{db}" end module OldClientPatch def logging(commands, &block) Sentry::Redis.new(commands, host, port, db).instrument { super } end end module GlobalRedisInstrumentation def call(command, redis_config) Sentry::Redis .new([command], redis_config.host, redis_config.port, redis_config.db) .instrument { super } end def call_pipelined(commands, redis_config) Sentry::Redis .new(commands, redis_config.host, redis_config.port, redis_config.db) .instrument { super } end end end end if defined?(::Redis::Client) if Gem::Version.new(::Redis::VERSION) < Gem::Version.new("5.0") Sentry.register_patch(:redis, Sentry::Redis::OldClientPatch, ::Redis::Client) elsif defined?(RedisClient) Sentry.register_patch(:redis) do RedisClient.register(Sentry::Redis::GlobalRedisInstrumentation) end end end sentry-ruby-core-5.28.0/lib/sentry/log_event.rb0000644000004100000410000001144515067721773021514 0ustar www-datawww-data# frozen_string_literal: true module Sentry # Event type that represents a log entry with its attributes # # @see https://develop.sentry.dev/sdk/telemetry/logs/#log-envelope-item-payload class LogEvent TYPE = "log" DEFAULT_PARAMETERS = [].freeze DEFAULT_ATTRIBUTES = {}.freeze SERIALIZEABLE_ATTRIBUTES = %i[ level body timestamp environment release server_name trace_id attributes contexts ] SENTRY_ATTRIBUTES = { "sentry.trace.parent_span_id" => :parent_span_id, "sentry.environment" => :environment, "sentry.release" => :release, "sentry.address" => :server_name, "sentry.sdk.name" => :sdk_name, "sentry.sdk.version" => :sdk_version, "sentry.message.template" => :template, "sentry.origin" => :origin } PARAMETER_PREFIX = "sentry.message.parameter" USER_ATTRIBUTES = { "user.id" => :user_id, "user.name" => :user_username, "user.email" => :user_email } LEVELS = %i[trace debug info warn error fatal].freeze attr_accessor :level, :body, :template, :attributes, :user, :origin attr_reader :configuration, *(SERIALIZEABLE_ATTRIBUTES - %i[level body attributes]) SERIALIZERS = %i[ attributes body level parent_span_id sdk_name sdk_version template timestamp trace_id user_id user_username user_email ].map { |name| [name, :"serialize_#{name}"] }.to_h VALUE_TYPES = Hash.new("string").merge!({ TrueClass => "boolean", FalseClass => "boolean", Integer => "integer", Float => "double" }).freeze TOKEN_REGEXP = /%\{(\w+)\}/ def initialize(configuration: Sentry.configuration, **options) @configuration = configuration @type = TYPE @server_name = configuration.server_name @environment = configuration.environment @release = configuration.release @timestamp = Sentry.utc_now @level = options.fetch(:level) @body = options[:body] @template = @body if is_template? @attributes = options[:attributes] || DEFAULT_ATTRIBUTES @user = options[:user] || {} @origin = options[:origin] @contexts = {} end def to_hash SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |name, memo| memo[name] = serialize(name) end end private def serialize(name) serializer = SERIALIZERS[name] if serializer __send__(serializer) else public_send(name) end end def serialize_level level.to_s end def serialize_sdk_name Sentry.sdk_meta["name"] end def serialize_sdk_version Sentry.sdk_meta["version"] end def serialize_timestamp timestamp.to_f end def serialize_trace_id contexts.dig(:trace, :trace_id) end def serialize_parent_span_id contexts.dig(:trace, :parent_span_id) end def serialize_body if parameters.empty? body elsif parameters.is_a?(Hash) body % parameters else sprintf(body, *parameters) end end def serialize_user_id user[:id] end def serialize_user_username user[:username] end def serialize_user_email user[:email] end def serialize_template template if has_parameters? end def serialize_attributes hash = {} attributes.each do |key, value| hash[key] = attribute_hash(value) end SENTRY_ATTRIBUTES.each do |key, name| if (value = serialize(name)) hash[key] = attribute_hash(value) end end USER_ATTRIBUTES.each do |key, name| if (value = serialize(name)) hash[key] = value end end hash end def attribute_hash(value) { value: value, type: value_type(value) } end def value_type(value) VALUE_TYPES[value.class] end def parameters @parameters ||= begin return DEFAULT_PARAMETERS unless template parameters = template_tokens.empty? ? attributes.fetch(:parameters, DEFAULT_PARAMETERS) : attributes.slice(*template_tokens) if parameters.is_a?(Hash) parameters.each do |key, value| attributes["#{PARAMETER_PREFIX}.#{key}"] = value end else parameters.each_with_index do |param, index| attributes["#{PARAMETER_PREFIX}.#{index}"] = param end end end end def template_tokens @template_tokens ||= body.scan(TOKEN_REGEXP).flatten.map(&:to_sym) end def is_template? body.include?("%s") || TOKEN_REGEXP.match?(body) end def has_parameters? attributes.keys.any? { |key| key.start_with?(PARAMETER_PREFIX) } end end end sentry-ruby-core-5.28.0/lib/sentry/attachment.rb0000644000004100000410000000164615067721773021664 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Attachment PathNotFoundError = Class.new(StandardError) attr_reader :bytes, :filename, :path, :content_type def initialize(bytes: nil, filename: nil, content_type: nil, path: nil) @bytes = bytes @filename = filename || infer_filename(path) @path = path @content_type = content_type end def to_envelope_headers { type: "attachment", filename: filename, content_type: content_type, length: payload.bytesize } end def payload @payload ||= if bytes bytes else File.binread(path) end rescue Errno::ENOENT raise PathNotFoundError, "Failed to read attachment file, file not found: #{path}" end private def infer_filename(path) if path File.basename(path) else raise ArgumentError, "filename or path is required" end end end end sentry-ruby-core-5.28.0/lib/sentry/net/0000755000004100000410000000000015067721773017766 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/net/http.rb0000644000004100000410000000510215067721773021270 0ustar www-datawww-data# frozen_string_literal: true require "net/http" require "resolv" require "sentry/utils/http_tracing" module Sentry # @api private module Net module HTTP include Utils::HttpTracing OP_NAME = "http.client" SPAN_ORIGIN = "auto.http.net_http" BREADCRUMB_CATEGORY = "net.http" URI_PARSER = URI.const_defined?("RFC2396_PARSER") ? URI::RFC2396_PARSER : URI::DEFAULT_PARSER # To explain how the entire thing works, we need to know how the original Net::HTTP#request works # Here's part of its definition. As you can see, it usually calls itself inside a #start block # # ``` # def request(req, body = nil, &block) # unless started? # start { # req['connection'] ||= 'close' # return request(req, body, &block) # <- request will be called for the second time from the first call # } # end # ..... # end # ``` # # So we're only instrumenting request when `Net::HTTP` is already started def request(req, body = nil, &block) return super unless started? && Sentry.initialized? return super if from_sentry_sdk? Sentry.with_child_span(op: OP_NAME, start_timestamp: Sentry.utc_now.to_f, origin: SPAN_ORIGIN) do |sentry_span| request_info = extract_request_info(req) if propagate_trace?(request_info[:url]) set_propagation_headers(req) end res = super response_status = res.code.to_i if record_sentry_breadcrumb? record_sentry_breadcrumb(request_info, response_status) end if sentry_span set_span_info(sentry_span, request_info, response_status) end res end end private def from_sentry_sdk? dsn = Sentry.configuration.dsn dsn && dsn.host == self.address end def extract_request_info(req) # IPv6 url could look like '::1/path', and that won't parse without # wrapping it in square brackets. hostname = address =~ Resolv::IPv6::Regex ? "[#{address}]" : address uri = req.uri || URI.parse(URI_PARSER.escape("#{use_ssl? ? 'https' : 'http'}://#{hostname}#{req.path}")) url = "#{uri.scheme}://#{uri.host}#{uri.path}" rescue uri.to_s result = { method: req.method, url: url } if Sentry.configuration.send_default_pii result[:query] = uri.query result[:body] = req.body end result end end end end Sentry.register_patch(:http, Sentry::Net::HTTP, Net::HTTP) sentry-ruby-core-5.28.0/lib/sentry/metrics/0000755000004100000410000000000015067721773020646 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/metrics/set_metric.rb0000644000004100000410000000066015067721773023333 0ustar www-datawww-data# frozen_string_literal: true require "set" require "zlib" module Sentry module Metrics class SetMetric < Metric attr_reader :value def initialize(value) @value = Set[value] end def add(value) @value << value end def serialize value.map { |x| x.is_a?(String) ? Zlib.crc32(x) : x.to_i } end def weight value.size end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/timing.rb0000644000004100000410000000175115067721773022466 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics module Timing class << self def nanosecond time = Sentry.utc_now time.to_i * (10 ** 9) + time.nsec end def microsecond time = Sentry.utc_now time.to_i * (10 ** 6) + time.usec end def millisecond Sentry.utc_now.to_i * (10 ** 3) end def second Sentry.utc_now.to_i end def minute Sentry.utc_now.to_i / 60.0 end def hour Sentry.utc_now.to_i / 3600.0 end def day Sentry.utc_now.to_i / (3600.0 * 24.0) end def week Sentry.utc_now.to_i / (3600.0 * 24.0 * 7.0) end def duration_start Process.clock_gettime(Process::CLOCK_MONOTONIC) end def duration_end(start) Process.clock_gettime(Process::CLOCK_MONOTONIC) - start end end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/configuration.rb0000644000004100000410000000270315067721773024044 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics class Configuration include ArgumentCheckingHelper include LoggingHelper # Enable metrics usage. # Starts a new {Sentry::Metrics::Aggregator} instance to aggregate metrics # and a thread to aggregate flush every 5 seconds. # @return [Boolean] attr_reader :enabled # Enable code location reporting. # Will be sent once per day. # True by default. # @return [Boolean] attr_accessor :enable_code_locations # Optional Proc, called before emitting a metric to the aggregator. # Use it to filter keys (return false/nil) or update tags. # Make sure to return true at the end. # # @example # config.metrics.before_emit = lambda do |key, tags| # return nil if key == 'foo' # tags[:bar] = 42 # tags.delete(:baz) # true # end # # @return [Proc, nil] attr_reader :before_emit def initialize(sdk_logger) @sdk_logger = sdk_logger @enabled = false @enable_code_locations = true end def enabled=(value) log_warn <<~MSG `config.metrics` is now deprecated and will be removed in the next major. MSG @enabled = value end def before_emit=(value) check_callable!("metrics.before_emit", value) @before_emit = value end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/counter_metric.rb0000644000004100000410000000053715067721773024222 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics class CounterMetric < Metric attr_reader :value def initialize(value) @value = value.to_f end def add(value) @value += value.to_f end def serialize [value] end def weight 1 end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/gauge_metric.rb0000644000004100000410000000116215067721773023626 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics class GaugeMetric < Metric attr_reader :last, :min, :max, :sum, :count def initialize(value) value = value.to_f @last = value @min = value @max = value @sum = value @count = 1 end def add(value) value = value.to_f @last = value @min = [@min, value].min @max = [@max, value].max @sum += value @count += 1 end def serialize [last, min, max, sum, count] end def weight 5 end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/distribution_metric.rb0000644000004100000410000000055515067721773025262 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics class DistributionMetric < Metric attr_reader :value def initialize(value) @value = [value.to_f] end def add(value) @value << value.to_f end def serialize value end def weight value.size end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/aggregator.rb0000644000004100000410000001701515067721773023321 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics class Aggregator < ThreadedPeriodicWorker FLUSH_INTERVAL = 5 ROLLUP_IN_SECONDS = 10 # this is how far removed from user code in the backtrace we are # when we record code locations DEFAULT_STACKLEVEL = 4 KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.]+/ UNIT_SANITIZATION_REGEX = /[^a-zA-Z0-9_]+/ TAG_KEY_SANITIZATION_REGEX = /[^a-zA-Z0-9_\-.\/]+/ TAG_VALUE_SANITIZATION_MAP = { "\n" => "\\n", "\r" => "\\r", "\t" => "\\t", "\\" => "\\\\", "|" => "\\u{7c}", "," => "\\u{2c}" } METRIC_TYPES = { c: CounterMetric, d: DistributionMetric, g: GaugeMetric, s: SetMetric } # exposed only for testing attr_reader :client, :thread, :buckets, :flush_shift, :code_locations def initialize(configuration, client) super(configuration.sdk_logger, FLUSH_INTERVAL) @client = client @before_emit = configuration.metrics.before_emit @enable_code_locations = configuration.metrics.enable_code_locations @stacktrace_builder = configuration.stacktrace_builder @default_tags = {} @default_tags["release"] = configuration.release if configuration.release @default_tags["environment"] = configuration.environment if configuration.environment @mutex = Mutex.new # a nested hash of timestamp -> bucket keys -> Metric instance @buckets = {} # the flush interval needs to be shifted once per startup to create jittering @flush_shift = Random.rand * ROLLUP_IN_SECONDS # a nested hash of timestamp (start of day) -> meta keys -> frame @code_locations = {} end def add(type, key, value, unit: "none", tags: {}, timestamp: nil, stacklevel: nil) return unless ensure_thread return unless METRIC_TYPES.keys.include?(type) updated_tags = get_updated_tags(tags) return if @before_emit && !@before_emit.call(key, updated_tags) timestamp ||= Sentry.utc_now # this is integer division and thus takes the floor of the division # and buckets into 10 second intervals bucket_timestamp = (timestamp.to_i / ROLLUP_IN_SECONDS) * ROLLUP_IN_SECONDS serialized_tags = serialize_tags(updated_tags) bucket_key = [type, key, unit, serialized_tags] added = @mutex.synchronize do record_code_location(type, key, unit, timestamp, stacklevel: stacklevel) if @enable_code_locations process_bucket(bucket_timestamp, bucket_key, type, value) end # for sets, we pass on if there was a new entry to the local gauge local_value = type == :s ? added : value process_span_aggregator(bucket_key, local_value) end def flush(force: false) flushable_buckets = get_flushable_buckets!(force) code_locations = get_code_locations! return if flushable_buckets.empty? && code_locations.empty? envelope = Envelope.new unless flushable_buckets.empty? payload = serialize_buckets(flushable_buckets) envelope.add_item( { type: "statsd", length: payload.bytesize }, payload ) end unless code_locations.empty? code_locations.each do |timestamp, locations| payload = serialize_locations(timestamp, locations) envelope.add_item( { type: "metric_meta", content_type: "application/json" }, payload ) end end @client.capture_envelope(envelope) end alias_method :run, :flush private # important to sort for key consistency def serialize_tags(tags) tags.flat_map do |k, v| if v.is_a?(Array) v.map { |x| [k.to_s, x.to_s] } else [[k.to_s, v.to_s]] end end.sort end def get_flushable_buckets!(force) @mutex.synchronize do flushable_buckets = {} if force flushable_buckets = @buckets @buckets = {} else cutoff = Sentry.utc_now.to_i - ROLLUP_IN_SECONDS - @flush_shift flushable_buckets = @buckets.select { |k, _| k <= cutoff } @buckets.reject! { |k, _| k <= cutoff } end flushable_buckets end end def get_code_locations! @mutex.synchronize do code_locations = @code_locations @code_locations = {} code_locations end end # serialize buckets to statsd format def serialize_buckets(buckets) buckets.map do |timestamp, timestamp_buckets| timestamp_buckets.map do |metric_key, metric| type, key, unit, tags = metric_key values = metric.serialize.join(":") sanitized_tags = tags.map { |k, v| "#{sanitize_tag_key(k)}:#{sanitize_tag_value(v)}" }.join(",") "#{sanitize_key(key)}@#{sanitize_unit(unit)}:#{values}|#{type}|\##{sanitized_tags}|T#{timestamp}" end end.flatten.join("\n") end def serialize_locations(timestamp, locations) mapping = locations.map do |meta_key, location| type, key, unit = meta_key mri = "#{type}:#{sanitize_key(key)}@#{sanitize_unit(unit)}" # note this needs to be an array but it really doesn't serve a purpose right now [mri, [location.merge(type: "location")]] end.to_h { timestamp: timestamp, mapping: mapping } end def sanitize_key(key) key.gsub(KEY_SANITIZATION_REGEX, "_") end def sanitize_unit(unit) unit.gsub(UNIT_SANITIZATION_REGEX, "") end def sanitize_tag_key(key) key.gsub(TAG_KEY_SANITIZATION_REGEX, "") end def sanitize_tag_value(value) value.chars.map { |c| TAG_VALUE_SANITIZATION_MAP[c] || c }.join end def get_transaction_name scope = Sentry.get_current_scope return nil unless scope && scope.transaction_name return nil if scope.transaction_source_low_quality? scope.transaction_name end def get_updated_tags(tags) updated_tags = @default_tags.merge(tags) transaction_name = get_transaction_name updated_tags["transaction"] = transaction_name if transaction_name updated_tags end def process_span_aggregator(key, value) scope = Sentry.get_current_scope return nil unless scope && scope.span return nil if scope.transaction_source_low_quality? scope.span.metrics_local_aggregator.add(key, value) end def process_bucket(timestamp, key, type, value) @buckets[timestamp] ||= {} if (metric = @buckets[timestamp][key]) old_weight = metric.weight metric.add(value) metric.weight - old_weight else metric = METRIC_TYPES[type].new(value) @buckets[timestamp][key] = metric metric.weight end end def record_code_location(type, key, unit, timestamp, stacklevel: nil) meta_key = [type, key, unit] start_of_day = Time.utc(timestamp.year, timestamp.month, timestamp.day).to_i @code_locations[start_of_day] ||= {} @code_locations[start_of_day][meta_key] ||= @stacktrace_builder.metrics_code_location(caller[stacklevel || DEFAULT_STACKLEVEL]) end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/local_aggregator.rb0000644000004100000410000000215215067721773024467 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics class LocalAggregator # exposed only for testing attr_reader :buckets def initialize @buckets = {} end def add(key, value) if @buckets[key] @buckets[key].add(value) else @buckets[key] = GaugeMetric.new(value) end end def to_hash return nil if @buckets.empty? @buckets.map do |bucket_key, metric| type, key, unit, tags = bucket_key payload_key = "#{type}:#{key}@#{unit}" payload_value = { tags: deserialize_tags(tags), min: metric.min, max: metric.max, count: metric.count, sum: metric.sum } [payload_key, payload_value] end.to_h end private def deserialize_tags(tags) tags.inject({}) do |h, tag| k, v = tag old = h[k] # make it an array if key repeats h[k] = old ? (old.is_a?(Array) ? old << v : [old, v]) : v h end end end end end sentry-ruby-core-5.28.0/lib/sentry/metrics/metric.rb0000644000004100000410000000044115067721773022455 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Metrics class Metric def add(value) raise NotImplementedError end def serialize raise NotImplementedError end def weight raise NotImplementedError end end end end sentry-ruby-core-5.28.0/lib/sentry/event.rb0000644000004100000410000001144715067721773020655 0ustar www-datawww-data# frozen_string_literal: true require "socket" require "securerandom" require "sentry/interface" require "sentry/backtrace" require "sentry/utils/real_ip" require "sentry/utils/request_id" require "sentry/utils/custom_inspection" require "sentry/utils/uuid" module Sentry # This is an abstract class that defines the shared attributes of an event. # Please don't use it directly. The user-facing classes are its child classes. class Event TYPE = "event" # These are readable attributes. SERIALIZEABLE_ATTRIBUTES = %i[ event_id level timestamp release environment server_name modules message user tags contexts extra fingerprint breadcrumbs transaction transaction_info platform sdk type ] # These are writable attributes. WRITER_ATTRIBUTES = SERIALIZEABLE_ATTRIBUTES - %i[type timestamp level] MAX_MESSAGE_SIZE_IN_BYTES = 1024 * 8 SKIP_INSPECTION_ATTRIBUTES = [:@modules, :@stacktrace_builder, :@send_default_pii, :@trusted_proxies, :@rack_env_whitelist] include CustomInspection attr_writer(*WRITER_ATTRIBUTES) attr_reader(*SERIALIZEABLE_ATTRIBUTES) # @return [RequestInterface] attr_reader :request # Dynamic Sampling Context (DSC) that gets attached # as the trace envelope header in the transport. # @return [Hash, nil] attr_accessor :dynamic_sampling_context # @return [Array] attr_accessor :attachments # @param configuration [Configuration] # @param integration_meta [Hash, nil] # @param message [String, nil] def initialize(configuration:, integration_meta: nil, message: nil) # Set some simple default values @event_id = Utils.uuid @timestamp = Sentry.utc_now.iso8601 @platform = :ruby @type = self.class::TYPE @sdk = integration_meta || Sentry.sdk_meta @user = {} @extra = {} @contexts = {} @tags = {} @attachments = [] @fingerprint = [] @dynamic_sampling_context = nil # configuration data that's directly used by events @server_name = configuration.server_name @environment = configuration.environment @release = configuration.release @modules = configuration.gem_specs if configuration.send_modules # configuration options to help events process data @send_default_pii = configuration.send_default_pii @trusted_proxies = configuration.trusted_proxies @stacktrace_builder = configuration.stacktrace_builder @rack_env_whitelist = configuration.rack_env_whitelist @message = (message || "").byteslice(0..MAX_MESSAGE_SIZE_IN_BYTES) end # @deprecated This method will be removed in v5.0.0. Please just use Sentry.configuration # @return [Configuration] def configuration Sentry.configuration end # Sets the event's timestamp. # @param time [Time, Float] # @return [void] def timestamp=(time) @timestamp = time.is_a?(Time) ? time.to_f : time end # Sets the event's level. # @param level [String, Symbol] # @return [void] def level=(level) # needed to meet the Sentry spec @level = level.to_s == "warn" ? :warning : level end # Sets the event's request environment data with RequestInterface. # @see RequestInterface # @param env [Hash] # @return [void] def rack_env=(env) unless request || env.empty? add_request_interface(env) user[:ip_address] ||= calculate_real_ip_from_rack(env) if @send_default_pii if request_id = Utils::RequestId.read_from(env) tags[:request_id] = request_id end end end # @return [Hash] def to_hash data = serialize_attributes data[:breadcrumbs] = breadcrumbs.to_hash if breadcrumbs data[:request] = request.to_hash if request data end # @return [Hash] def to_json_compatible JSON.parse(JSON.generate(to_hash)) end private def add_request_interface(env) @request = Sentry::RequestInterface.new(env: env, send_default_pii: @send_default_pii, rack_env_whitelist: @rack_env_whitelist) end def serialize_attributes self.class::SERIALIZEABLE_ATTRIBUTES.each_with_object({}) do |att, memo| if value = public_send(att) memo[att] = value end end end # When behind a proxy (or if the user is using a proxy), we can't use # REMOTE_ADDR to determine the Event IP, and must use other headers instead. def calculate_real_ip_from_rack(env) Utils::RealIp.new( remote_addr: env["REMOTE_ADDR"], client_ip: env["HTTP_CLIENT_IP"], real_ip: env["HTTP_X_REAL_IP"], forwarded_for: env["HTTP_X_FORWARDED_FOR"], trusted_proxies: @trusted_proxies ).calculate_ip end end end sentry-ruby-core-5.28.0/lib/sentry/core_ext/0000755000004100000410000000000015067721773021010 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/core_ext/object/0000755000004100000410000000000015067721773022256 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/core_ext/object/duplicable.rb0000644000004100000410000000640615067721773024715 0ustar www-datawww-data# frozen_string_literal: true return if Object.method_defined?(:duplicable?) ######################################### # This file was copied from Rails 5.2 # ######################################### #-- # Most objects are cloneable, but not all. For example you can't dup methods: # # method(:puts).dup # => TypeError: allocator undefined for Method # # Classes may signal their instances are not duplicable removing +dup+/+clone+ # or raising exceptions from them. So, to dup an arbitrary object you normally # use an optimistic approach and are ready to catch an exception, say: # # arbitrary_object.dup rescue object # # Rails dups objects in a few critical spots where they are not that arbitrary. # That rescue is very expensive (like 40 times slower than a predicate), and it # is often triggered. # # That's why we hardcode the following cases and check duplicable? instead of # using that rescue idiom. #++ class Object # Can you safely dup this object? # # False for method objects; # true otherwise. def duplicable? true end end class NilClass begin nil.dup rescue TypeError # +nil+ is not duplicable: # # nil.duplicable? # => false # nil.dup # => TypeError: can't dup NilClass def duplicable? false end end end class FalseClass begin false.dup rescue TypeError # +false+ is not duplicable: # # false.duplicable? # => false # false.dup # => TypeError: can't dup FalseClass def duplicable? false end end end class TrueClass begin true.dup rescue TypeError # +true+ is not duplicable: # # true.duplicable? # => false # true.dup # => TypeError: can't dup TrueClass def duplicable? false end end end class Symbol begin :symbol.dup # Ruby 2.4.x. "symbol_from_string".to_sym.dup # Some symbols can't `dup` in Ruby 2.4.0. rescue TypeError # Symbols are not duplicable: # # :my_symbol.duplicable? # => false # :my_symbol.dup # => TypeError: can't dup Symbol def duplicable? false end end end class Numeric begin 1.dup rescue TypeError # Numbers are not duplicable: # # 3.duplicable? # => false # 3.dup # => TypeError: can't dup Integer def duplicable? false end end end require "bigdecimal" class BigDecimal # BigDecimals are duplicable: # # BigDecimal("1.2").duplicable? # => true # BigDecimal("1.2").dup # => # def duplicable? true end end class Method # Methods are not duplicable: # # method(:puts).duplicable? # => false # method(:puts).dup # => TypeError: allocator undefined for Method def duplicable? false end end class Complex begin Complex(1).dup rescue TypeError # Complexes are not duplicable: # # Complex(1).duplicable? # => false # Complex(1).dup # => TypeError: can't copy Complex def duplicable? false end end end class Rational begin Rational(1).dup rescue TypeError # Rationals are not duplicable: # # Rational(1).duplicable? # => false # Rational(1).dup # => TypeError: can't copy Rational def duplicable? false end end end sentry-ruby-core-5.28.0/lib/sentry/core_ext/object/deep_dup.rb0000644000004100000410000000242415067721773024372 0ustar www-datawww-data# frozen_string_literal: true return if Object.method_defined?(:deep_dup) require "sentry/core_ext/object/duplicable" ######################################### # This file was copied from Rails 5.2 # ######################################### class Object # Returns a deep copy of object if it's duplicable. If it's # not duplicable, returns +self+. # # object = Object.new # dup = object.deep_dup # dup.instance_variable_set(:@a, 1) # # object.instance_variable_defined?(:@a) # => false # dup.instance_variable_defined?(:@a) # => true def deep_dup duplicable? ? dup : self end end class Array # Returns a deep copy of array. # # array = [1, [2, 3]] # dup = array.deep_dup # dup[1][2] = 4 # # array[1][2] # => nil # dup[1][2] # => 4 def deep_dup map(&:deep_dup) end end class Hash # Returns a deep copy of hash. # # hash = { a: { b: 'b' } } # dup = hash.deep_dup # dup[:a][:c] = 'c' # # hash[:a][:c] # => nil # dup[:a][:c] # => "c" def deep_dup hash = dup each_pair do |key, value| if key.frozen? && ::String === key hash[key] = value.deep_dup else hash.delete(key) hash[key.deep_dup] = value.deep_dup end end hash end end sentry-ruby-core-5.28.0/lib/sentry/profiler/0000755000004100000410000000000015067721773021022 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/profiler/helpers.rb0000644000004100000410000000233515067721773023014 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" module Sentry class Profiler module Helpers def in_app?(abs_path) abs_path.match?(@in_app_pattern) end # copied from stacktrace.rb since I don't want to touch existing code # TODO-neel-profiler try to fetch this from stackprof once we patch # the native extension def compute_filename(abs_path, in_app) return nil if abs_path.nil? under_project_root = @project_root && abs_path.start_with?(@project_root) prefix = if under_project_root && in_app @project_root else longest_load_path = $LOAD_PATH.select { |path| abs_path.start_with?(path.to_s) }.max_by(&:size) if under_project_root longest_load_path || @project_root else longest_load_path end end prefix ? abs_path[prefix.to_s.chomp(File::SEPARATOR).length + 1..-1] : abs_path end def split_module(name) # last module plus class/instance method i = name.rindex("::") function = i ? name[(i + 2)..-1] : name mod = i ? name[0...i] : nil [function, mod] end end end end sentry-ruby-core-5.28.0/lib/sentry/puma.rb0000644000004100000410000000127115067721773020470 0ustar www-datawww-data# frozen_string_literal: true return unless defined?(Puma::Server) module Sentry module Puma module Server PUMA_4_AND_PRIOR = Gem::Version.new(::Puma::Const::PUMA_VERSION) < Gem::Version.new("5.0.0") def lowlevel_error(e, env, status = 500) result = if PUMA_4_AND_PRIOR super(e, env) else super end begin Sentry.capture_exception(e) do |scope| scope.set_rack_env(env) end rescue # if anything happens, we don't want to break the app end result end end end end Sentry.register_patch(:puma, Sentry::Puma::Server, Puma::Server) sentry-ruby-core-5.28.0/lib/sentry/version.rb0000644000004100000410000000010615067721773021207 0ustar www-datawww-data# frozen_string_literal: true module Sentry VERSION = "5.28.0" end sentry-ruby-core-5.28.0/lib/sentry/rack.rb0000644000004100000410000000013015067721773020437 0ustar www-datawww-data# frozen_string_literal: true require "rack" require "sentry/rack/capture_exceptions" sentry-ruby-core-5.28.0/lib/sentry/linecache.rb0000644000004100000410000000212215067721773021435 0ustar www-datawww-data# frozen_string_literal: true module Sentry # @api private class LineCache def initialize @cache = {} end # Any linecache you provide to Sentry must implement this method. # Returns an Array of Strings representing the lines in the source # file. The number of lines retrieved is (2 * context) + 1, the middle # line should be the line requested by lineno. See specs for more information. def get_file_context(filename, lineno, context) return nil, nil, nil unless valid_path?(filename) lines = Array.new(2 * context + 1) do |i| getline(filename, lineno - context + i) end [lines[0..(context - 1)], lines[context], lines[(context + 1)..-1]] end private def valid_path?(path) lines = getlines(path) !lines.nil? end def getlines(path) @cache[path] ||= begin File.open(path, "r", &:readlines) rescue nil end end def getline(path, n) return nil if n < 1 lines = getlines(path) return nil if lines.nil? lines[n - 1] end end end sentry-ruby-core-5.28.0/lib/sentry/backtrace.rb0000644000004100000410000000625715067721773021456 0ustar www-datawww-data# frozen_string_literal: true require "rubygems" module Sentry # @api private class Backtrace # Handles backtrace parsing line by line class Line RB_EXTENSION = ".rb" # regexp (optional leading X: on windows, or JRuby9000 class-prefix) RUBY_INPUT_FORMAT = / ^ \s* (?: [a-zA-Z]: | uri:classloader: )? ([^:]+ | <.*>): (\d+) (?: :in\s('|`)(?:([\w:]+)\#)?([^']+)')?$ /x # org.jruby.runtime.callsite.CachingCallSite.call(CachingCallSite.java:170) JAVA_INPUT_FORMAT = /^([\w$.]+)\.([\w$]+)\(([\w$.]+):(\d+)\)$/ # The file portion of the line (such as app/models/user.rb) attr_reader :file # The line number portion of the line attr_reader :number # The method of the line (such as index) attr_reader :method # The module name (JRuby) attr_reader :module_name attr_reader :in_app_pattern # Parses a single line of a given backtrace # @param [String] unparsed_line The raw line from +caller+ or some backtrace # @return [Line] The parsed backtrace line def self.parse(unparsed_line, in_app_pattern = nil) ruby_match = unparsed_line.match(RUBY_INPUT_FORMAT) if ruby_match _, file, number, _, module_name, method = ruby_match.to_a file.sub!(/\.class$/, RB_EXTENSION) module_name = module_name else java_match = unparsed_line.match(JAVA_INPUT_FORMAT) _, module_name, method, file, number = java_match.to_a end new(file, number, method, module_name, in_app_pattern) end def initialize(file, number, method, module_name, in_app_pattern) @file = file @module_name = module_name @number = number.to_i @method = method @in_app_pattern = in_app_pattern end def in_app return false unless in_app_pattern if file =~ in_app_pattern true else false end end # Reconstructs the line in a readable fashion def to_s "#{file}:#{number}:in `#{method}'" end def ==(other) to_s == other.to_s end def inspect "" end end # holder for an Array of Backtrace::Line instances attr_reader :lines def self.parse(backtrace, project_root, app_dirs_pattern, &backtrace_cleanup_callback) ruby_lines = backtrace.is_a?(Array) ? backtrace : backtrace.split(/\n\s*/) ruby_lines = backtrace_cleanup_callback.call(ruby_lines) if backtrace_cleanup_callback in_app_pattern ||= begin Regexp.new("^(#{project_root}/)?#{app_dirs_pattern}") end lines = ruby_lines.to_a.map do |unparsed_line| Line.parse(unparsed_line, in_app_pattern) end new(lines) end def initialize(lines) @lines = lines end def inspect "" end def to_s content = [] lines.each do |line| content << line end content.join("\n") end def ==(other) if other.respond_to?(:lines) lines == other.lines else false end end end end sentry-ruby-core-5.28.0/lib/sentry/client.rb0000644000004100000410000003431715067721773021013 0ustar www-datawww-data# frozen_string_literal: true require "sentry/transport" require "sentry/log_event" require "sentry/log_event_buffer" require "sentry/utils/uuid" module Sentry class Client include LoggingHelper # The Transport object that'll send events for the client. # @return [Transport] attr_reader :transport # The Transport object that'll send events for the client. # @return [SpotlightTransport, nil] attr_reader :spotlight_transport # @!visibility private attr_reader :log_event_buffer # @!macro configuration attr_reader :configuration # @param configuration [Configuration] def initialize(configuration) @configuration = configuration @sdk_logger = configuration.sdk_logger if transport_class = configuration.transport.transport_class @transport = transport_class.new(configuration) else @transport = case configuration.dsn&.scheme when "http", "https" HTTPTransport.new(configuration) else DummyTransport.new(configuration) end end @spotlight_transport = SpotlightTransport.new(configuration) if configuration.spotlight if configuration.enable_logs @log_event_buffer = LogEventBuffer.new(configuration, self).start end end # Applies the given scope's data to the event and sends it to Sentry. # @param event [Event] the event to be sent. # @param scope [Scope] the scope with contextual data that'll be applied to the event before it's sent. # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors. # @return [Event, nil] def capture_event(event, scope, hint = {}) return unless configuration.sending_allowed? if event.is_a?(ErrorEvent) && !configuration.sample_allowed? transport.record_lost_event(:sample_rate, "error") return end event_type = event.is_a?(Event) ? event.type : event["type"] data_category = Envelope::Item.data_category(event_type) is_transaction = event.is_a?(TransactionEvent) spans_before = is_transaction ? event.spans.size : 0 event = scope.apply_to_event(event, hint) if event.nil? log_debug("Discarded event because one of the event processors returned nil") transport.record_lost_event(:event_processor, data_category) transport.record_lost_event(:event_processor, "span", num: spans_before + 1) if is_transaction return elsif is_transaction spans_delta = spans_before - event.spans.size transport.record_lost_event(:event_processor, "span", num: spans_delta) if spans_delta > 0 end if async_block = configuration.async dispatch_async_event(async_block, event, hint) elsif configuration.background_worker_threads != 0 && hint.fetch(:background, true) unless dispatch_background_event(event, hint) transport.record_lost_event(:queue_overflow, data_category) transport.record_lost_event(:queue_overflow, "span", num: spans_before + 1) if is_transaction end else send_event(event, hint) end event rescue => e log_error("Event capturing failed", e, debug: configuration.debug) nil end # Buffer a log event to be sent later with other logs in a single envelope # @param event [LogEvent] the log event to be buffered # @return [LogEvent] def buffer_log_event(event, scope) return unless event.is_a?(LogEvent) @log_event_buffer.add_event(scope.apply_to_event(event)) event end # Capture an envelope directly. # @param envelope [Envelope] the envelope to be captured. # @return [void] def capture_envelope(envelope) Sentry.background_worker.perform { send_envelope(envelope) } end # Flush pending events to Sentry. # @return [void] def flush transport.flush if configuration.sending_to_dsn_allowed? spotlight_transport.flush if spotlight_transport @log_event_buffer&.flush end # Initializes an Event object with the given exception. Returns `nil` if the exception's class is excluded from reporting. # @param exception [Exception] the exception to be reported. # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors. # @return [Event, nil] def event_from_exception(exception, hint = {}) return unless @configuration.sending_allowed? ignore_exclusions = hint.delete(:ignore_exclusions) { false } return if !ignore_exclusions && !@configuration.exception_class_allowed?(exception) integration_meta = Sentry.integrations[hint[:integration]] mechanism = hint.delete(:mechanism) { Mechanism.new } ErrorEvent.new(configuration: configuration, integration_meta: integration_meta).tap do |event| event.add_exception_interface(exception, mechanism: mechanism) event.add_threads_interface(crashed: true) event.level = :error end end # Initializes an Event object with the given message. # @param message [String] the message to be reported. # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors. # @return [Event] def event_from_message(message, hint = {}, backtrace: nil) return unless @configuration.sending_allowed? integration_meta = Sentry.integrations[hint[:integration]] event = ErrorEvent.new(configuration: configuration, integration_meta: integration_meta, message: message) event.add_threads_interface(backtrace: backtrace || caller) event.level = :error event end # Initializes a CheckInEvent object with the given options. # # @param slug [String] identifier of this monitor # @param status [Symbol] status of this check-in, one of {CheckInEvent::VALID_STATUSES} # @param hint [Hash] the hint data that'll be passed to `before_send` callback and the scope's event processors. # @param duration [Integer, nil] seconds elapsed since this monitor started # @param monitor_config [Cron::MonitorConfig, nil] configuration for this monitor # @param check_in_id [String, nil] for updating the status of an existing monitor # # @return [Event] def event_from_check_in( slug, status, hint = {}, duration: nil, monitor_config: nil, check_in_id: nil ) return unless configuration.sending_allowed? CheckInEvent.new( configuration: configuration, integration_meta: Sentry.integrations[hint[:integration]], slug: slug, status: status, duration: duration, monitor_config: monitor_config, check_in_id: check_in_id ) end # Initializes a LogEvent object with the given message and options # # @param message [String] the log message # @param level [Symbol] the log level (:trace, :debug, :info, :warn, :error, :fatal) # @param options [Hash] additional options # @option options [Array] :parameters Array of values to replace template tokens in the message # # @return [LogEvent] the created log event def event_from_log(message, level:, **options) return unless configuration.sending_allowed? attributes = options.reject { |k, _| k == :level || k == :severity || k == :origin } origin = options[:origin] LogEvent.new(level: level, body: message, attributes: attributes, origin: origin) end # Initializes an Event object with the given Transaction object. # @param transaction [Transaction] the transaction to be recorded. # @return [TransactionEvent] def event_from_transaction(transaction) TransactionEvent.new(configuration: configuration, transaction: transaction) end # @!macro send_event def send_event(event, hint = nil) event_type = event.is_a?(Event) ? event.type : event["type"] data_category = Envelope::Item.data_category(event_type) spans_before = event.is_a?(TransactionEvent) ? event.spans.size : 0 if event_type != TransactionEvent::TYPE && configuration.before_send event = configuration.before_send.call(event, hint) case event when ErrorEvent, CheckInEvent # do nothing when Hash log_debug(<<~MSG) Returning a Hash from before_send is deprecated and will be removed in the next major version. Please return a Sentry::ErrorEvent object instead. MSG else # Avoid serializing the event object in this case because we aren't sure what it is and what it contains log_debug(<<~MSG) Discarded event because before_send didn't return a Sentry::ErrorEvent object but an instance of #{event.class} MSG transport.record_lost_event(:before_send, data_category) return end end if event_type == TransactionEvent::TYPE && configuration.before_send_transaction event = configuration.before_send_transaction.call(event, hint) if event.is_a?(TransactionEvent) || event.is_a?(Hash) spans_after = event.is_a?(TransactionEvent) ? event.spans.size : 0 spans_delta = spans_before - spans_after transport.record_lost_event(:before_send, "span", num: spans_delta) if spans_delta > 0 if event.is_a?(Hash) log_debug(<<~MSG) Returning a Hash from before_send_transaction is deprecated and will be removed in the next major version. Please return a Sentry::TransactionEvent object instead. MSG end else # Avoid serializing the event object in this case because we aren't sure what it is and what it contains log_debug(<<~MSG) Discarded event because before_send_transaction didn't return a Sentry::TransactionEvent object but an instance of #{event.class} MSG transport.record_lost_event(:before_send, "transaction") transport.record_lost_event(:before_send, "span", num: spans_before + 1) return end end transport.send_event(event) if configuration.sending_to_dsn_allowed? spotlight_transport.send_event(event) if spotlight_transport event rescue => e log_error("Event sending failed", e, debug: configuration.debug) transport.record_lost_event(:network_error, data_category) transport.record_lost_event(:network_error, "span", num: spans_before + 1) if event.is_a?(TransactionEvent) raise end # Send an envelope with batched logs # @param log_events [Array] the log events to be sent # @api private # @return [void] def send_logs(log_events) envelope = Envelope.new( event_id: Sentry::Utils.uuid, sent_at: Sentry.utc_now.iso8601, dsn: configuration.dsn, sdk: Sentry.sdk_meta ) discarded_count = 0 envelope_items = [] if configuration.before_send_log log_events.each do |log_event| processed_log_event = configuration.before_send_log.call(log_event) if processed_log_event envelope_items << processed_log_event.to_hash else discarded_count += 1 end end envelope_items else envelope_items = log_events.map(&:to_hash) end envelope.add_item( { type: "log", item_count: envelope_items.size, content_type: "application/vnd.sentry.items.log+json" }, { items: envelope_items } ) send_envelope(envelope) unless discarded_count.zero? transport.record_lost_event(:before_send, "log_item", num: discarded_count) end end # Send an envelope directly to Sentry. # @param envelope [Envelope] the envelope to be sent. # @return [void] def send_envelope(envelope) transport.send_envelope(envelope) if configuration.sending_to_dsn_allowed? spotlight_transport.send_envelope(envelope) if spotlight_transport rescue => e log_error("Envelope sending failed", e, debug: configuration.debug) envelope.items.map(&:data_category).each do |data_category| transport.record_lost_event(:network_error, data_category) end raise end # @deprecated use Sentry.get_traceparent instead. # # Generates a Sentry trace for distribted tracing from the given Span. # Returns `nil` if `config.propagate_traces` is `false`. # @param span [Span] the span to generate trace from. # @return [String, nil] def generate_sentry_trace(span) return unless configuration.propagate_traces trace = span.to_sentry_trace log_debug("[Tracing] Adding #{SENTRY_TRACE_HEADER_NAME} header to outgoing request: #{trace}") trace end # @deprecated Use Sentry.get_baggage instead. # # Generates a W3C Baggage header for distributed tracing from the given Span. # Returns `nil` if `config.propagate_traces` is `false`. # @param span [Span] the span to generate trace from. # @return [String, nil] def generate_baggage(span) return unless configuration.propagate_traces baggage = span.to_baggage if baggage && !baggage.empty? log_debug("[Tracing] Adding #{BAGGAGE_HEADER_NAME} header to outgoing request: #{baggage}") end baggage end private def dispatch_background_event(event, hint) Sentry.background_worker.perform do send_event(event, hint) end end def dispatch_async_event(async_block, event, hint) # We have to convert to a JSON-like hash, because background job # processors (esp ActiveJob) may not like weird types in the event hash event_hash = begin event.to_json_compatible rescue => e log_error("Converting #{event.type} (#{event.event_id}) to JSON compatible hash failed", e, debug: configuration.debug) return end if async_block.arity == 2 hint = JSON.parse(JSON.generate(hint)) async_block.call(event_hash, hint) else async_block.call(event_hash) end rescue => e log_error("Async #{event_hash["type"]} sending failed", e, debug: configuration.debug) send_event(event, hint) end end end sentry-ruby-core-5.28.0/lib/sentry/std_lib_logger.rb0000644000004100000410000000272115067721773022506 0ustar www-datawww-data# frozen_string_literal: true module Sentry # Ruby Logger support Add commentMore actions # intercepts any logger instance and send the log to Sentry too. module StdLibLogger SEVERITY_MAP = { 0 => :debug, 1 => :info, 2 => :warn, 3 => :error, 4 => :fatal }.freeze ORIGIN = "auto.logger.ruby.std_logger" def add(severity, message = nil, progname = nil, &block) result = super return unless Sentry.initialized? && Sentry.get_current_hub # Only process logs that meet or exceed the logger's level return result if severity < level # exclude sentry SDK logs -- to prevent recursive log action, # do not process internal logs again if message.nil? && progname != Sentry::Logger::PROGNAME # handle different nature of Ruby Logger class: # inspo from Sentry::Breadcrumb::SentryLogger if block_given? message = yield else message = progname end message = message.to_s.strip if !message.nil? && message != Sentry::Logger::PROGNAME && method = SEVERITY_MAP[severity] Sentry.logger.send(method, message, origin: ORIGIN) end end result end end end Sentry.register_patch(:logger) do |config| if config.enable_logs ::Logger.prepend(Sentry::StdLibLogger) else config.sdk_logger.warn(":logger patch enabled but `enable_logs` is turned off - skipping applying patch") end end sentry-ruby-core-5.28.0/lib/sentry/excon.rb0000644000004100000410000000047315067721773020645 0ustar www-datawww-data# frozen_string_literal: true Sentry.register_patch(:excon) do if defined?(::Excon) require "sentry/excon/middleware" if Excon.defaults[:middlewares] Excon.defaults[:middlewares] << Sentry::Excon::Middleware unless Excon.defaults[:middlewares].include?(Sentry::Excon::Middleware) end end end sentry-ruby-core-5.28.0/lib/sentry/integrable.rb0000644000004100000410000000210015067721773021632 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Integrable def register_integration(name:, version:) Sentry.register_integration(name, version) @integration_name = name end def integration_name @integration_name end def capture_exception(exception, **options, &block) options[:hint] ||= {} options[:hint][:integration] = integration_name # within an integration, we usually intercept uncaught exceptions so we set handled to false. options[:hint][:mechanism] ||= Sentry::Mechanism.new(type: integration_name, handled: false) Sentry.capture_exception(exception, **options, &block) end def capture_message(message, **options, &block) options[:hint] ||= {} options[:hint][:integration] = integration_name Sentry.capture_message(message, **options, &block) end def capture_check_in(slug, status, **options, &block) options[:hint] ||= {} options[:hint][:integration] = integration_name Sentry.capture_check_in(slug, status, **options, &block) end end end sentry-ruby-core-5.28.0/lib/sentry/threaded_periodic_worker.rb0000644000004100000410000000131715067721773024556 0ustar www-datawww-data# frozen_string_literal: true module Sentry class ThreadedPeriodicWorker include LoggingHelper def initialize(sdk_logger, interval) @thread = nil @exited = false @interval = interval @sdk_logger = sdk_logger end def ensure_thread return false if @exited return true if @thread&.alive? @thread = Thread.new do loop do sleep(@interval) run end end true rescue ThreadError log_debug("[#{self.class.name}] thread creation failed") @exited = true false end def kill log_debug("[#{self.class.name}] thread killed") @exited = true @thread&.kill end end end sentry-ruby-core-5.28.0/lib/sentry/transport/0000755000004100000410000000000015067721773021234 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/transport/configuration.rb0000644000004100000410000000570615067721773024440 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Transport class Configuration # The timeout in seconds to open a connection to Sentry, in seconds. # Default value is 2. # # @return [Integer] attr_accessor :timeout # The timeout in seconds to read data from Sentry, in seconds. # Default value is 1. # # @return [Integer] attr_accessor :open_timeout # The proxy configuration to use to connect to Sentry. # Accepts either a URI formatted string, URI, or a hash with the `uri`, # `user`, and `password` keys. # # @example # # setup proxy using a string: # config.transport.proxy = "https://user:password@proxyhost:8080" # # # setup proxy using a URI: # config.transport.proxy = URI("https://user:password@proxyhost:8080") # # # setup proxy using a hash: # config.transport.proxy = { # uri: URI("https://proxyhost:8080"), # user: "user", # password: "password" # } # # If you're using the default transport (`Sentry::HTTPTransport`), # proxy settings will also automatically be read from tne environment # variables (`HTTP_PROXY`, `HTTPS_PROXY`, `NO_PROXY`). # # @return [String, URI, Hash, nil] attr_accessor :proxy # The SSL configuration to use to connect to Sentry. # You can either pass a `Hash` containing `ca_file` and `verification` keys, # or you can set those options directly on the `Sentry::HTTPTransport::Configuration` object: # # @example # config.transport.ssl = { # ca_file: "/path/to/ca_file", # verification: true # end # # @return [Hash, nil] attr_accessor :ssl # The path to the CA file to use to verify the SSL connection. # Default value is `nil`. # # @return [String, nil] attr_accessor :ssl_ca_file # Whether to verify that the peer certificate is valid in SSL connections. # Default value is `true`. # # @return [Boolean] attr_accessor :ssl_verification # The encoding to use to compress the request body. # Default value is `Sentry::HTTPTransport::GZIP_ENCODING`. # # @return [String] attr_accessor :encoding # The class to use as a transport to connect to Sentry. # If this option not set, it will return `nil`, and Sentry will use # `Sentry::HTTPTransport` by default. # # @return [Class, nil] attr_reader :transport_class def initialize @ssl_verification = true @open_timeout = 1 @timeout = 2 @encoding = HTTPTransport::GZIP_ENCODING end def transport_class=(klass) unless klass.is_a?(Class) raise Sentry::Error.new("config.transport.transport_class must a class. got: #{klass.class}") end @transport_class = klass end end end end sentry-ruby-core-5.28.0/lib/sentry/transport/debug_transport.rb0000644000004100000410000000340415067721773024764 0ustar www-datawww-data# frozen_string_literal: true require "json" require "fileutils" require "pathname" require "delegate" module Sentry # DebugTransport is a transport that logs events to a file for debugging purposes. # # It can optionally also send events to Sentry via HTTP transport if a real DSN # is provided. class DebugTransport < SimpleDelegator DEFAULT_LOG_FILE_PATH = File.join("log", "sentry_debug_events.log") attr_reader :log_file, :backend def initialize(configuration) @log_file = initialize_log_file(configuration) @backend = initialize_backend(configuration) super(@backend) end def send_event(event) log_envelope(envelope_from_event(event)) backend.send_event(event) end def log_envelope(envelope) envelope_json = { timestamp: Time.now.utc.iso8601, envelope_headers: envelope.headers, items: envelope.items.map do |item| { headers: item.headers, payload: item.payload } end } File.open(log_file, "a") { |file| file << JSON.dump(envelope_json) << "\n" } end def logged_envelopes return [] unless File.exist?(log_file) File.readlines(log_file).map do |line| JSON.parse(line) end end def clear File.write(log_file, "") log_debug("DebugTransport: Cleared events from #{log_file}") end private def initialize_backend(configuration) backend = configuration.dsn.local? ? DummyTransport : HTTPTransport backend.new(configuration) end def initialize_log_file(configuration) log_file = Pathname(configuration.sdk_debug_transport_log_file || DEFAULT_LOG_FILE_PATH) FileUtils.mkdir_p(log_file.dirname) unless log_file.dirname.exist? log_file end end end sentry-ruby-core-5.28.0/lib/sentry/transport/dummy_transport.rb0000644000004100000410000000054015067721773025027 0ustar www-datawww-data# frozen_string_literal: true module Sentry class DummyTransport < Transport attr_accessor :events, :envelopes def initialize(*) super @events = [] @envelopes = [] end def send_event(event) @events << event super end def send_envelope(envelope) @envelopes << envelope end end end sentry-ruby-core-5.28.0/lib/sentry/transport/spotlight_transport.rb0000644000004100000410000000217515067721773025717 0ustar www-datawww-data# frozen_string_literal: true require "net/http" require "zlib" module Sentry # Designed to just report events to Spotlight in development. class SpotlightTransport < HTTPTransport DEFAULT_SIDECAR_URL = "http://localhost:8969/stream" MAX_FAILED_REQUESTS = 3 def initialize(configuration) super @sidecar_url = configuration.spotlight.is_a?(String) ? configuration.spotlight : DEFAULT_SIDECAR_URL @failed = 0 @logged = false log_debug("[Spotlight] initialized for url #{@sidecar_url}") end def endpoint "/stream" end def send_data(data) if @failed >= MAX_FAILED_REQUESTS unless @logged log_debug("[Spotlight] disabling because of too many request failures") @logged = true end return end super end def on_error @failed += 1 end # Similar to HTTPTransport connection, but does not support Proxy and SSL def conn sidecar = URI(@sidecar_url) connection = ::Net::HTTP.new(sidecar.hostname, sidecar.port, nil) connection.use_ssl = false connection end end end sentry-ruby-core-5.28.0/lib/sentry/transport/http_transport.rb0000644000004100000410000001500015067721773024650 0ustar www-datawww-data# frozen_string_literal: true require "net/http" require "zlib" module Sentry class HTTPTransport < Transport GZIP_ENCODING = "gzip" GZIP_THRESHOLD = 1024 * 30 CONTENT_TYPE = "application/x-sentry-envelope" DEFAULT_DELAY = 60 RETRY_AFTER_HEADER = "retry-after" RATE_LIMIT_HEADER = "x-sentry-rate-limits" USER_AGENT = "sentry-ruby/#{Sentry::VERSION}" # The list of errors ::Net::HTTP is known to raise # See https://github.com/ruby/ruby/blob/b0c639f249165d759596f9579fa985cb30533de6/lib/bundler/fetcher.rb#L281-L286 HTTP_ERRORS = [ Timeout::Error, EOFError, SocketError, Errno::ENETDOWN, Errno::ENETUNREACH, Errno::EINVAL, Errno::ECONNRESET, Errno::ETIMEDOUT, Errno::EAGAIN, Net::HTTPBadResponse, Net::HTTPHeaderSyntaxError, Net::ProtocolError, Zlib::BufError, Errno::EHOSTUNREACH, Errno::ECONNREFUSED ].freeze def initialize(*args) super log_debug("Sentry HTTP Transport will connect to #{@dsn.server}") if @dsn end def send_data(data) encoding = "" if should_compress?(data) data = Zlib.gzip(data) encoding = GZIP_ENCODING end headers = { "Content-Type" => CONTENT_TYPE, "Content-Encoding" => encoding, "User-Agent" => USER_AGENT } auth_header = generate_auth_header headers["X-Sentry-Auth"] = auth_header if auth_header response = do_request(endpoint, headers, data) if response.code.match?(/\A2\d{2}/) handle_rate_limited_response(response) if has_rate_limited_header?(response) elsif response.code == "429" log_debug("the server responded with status 429") handle_rate_limited_response(response) else error_info = "the server responded with status #{response.code}" error_info += "\nbody: #{response.body}" error_info += " Error in headers is: #{response['x-sentry-error']}" if response["x-sentry-error"] raise Sentry::ExternalError, error_info end rescue SocketError, *HTTP_ERRORS => e on_error if respond_to?(:on_error) raise Sentry::ExternalError.new(e&.message) end def endpoint @dsn.envelope_endpoint end def generate_auth_header return nil unless @dsn now = Sentry.utc_now.to_i fields = { "sentry_version" => PROTOCOL_VERSION, "sentry_client" => USER_AGENT, "sentry_timestamp" => now, "sentry_key" => @dsn.public_key } fields["sentry_secret"] = @dsn.secret_key if @dsn.secret_key "Sentry " + fields.map { |key, value| "#{key}=#{value}" }.join(", ") end def conn server = URI(@dsn.server) # connection respects proxy setting from @transport_configuration, or environment variables (HTTP_PROXY, HTTPS_PROXY, NO_PROXY) # Net::HTTP will automatically read the env vars. # See https://ruby-doc.org/3.2.2/stdlibs/net/Net/HTTP.html#class-Net::HTTP-label-Proxies connection = if proxy = normalize_proxy(@transport_configuration.proxy) ::Net::HTTP.new(server.hostname, server.port, proxy[:uri].hostname, proxy[:uri].port, proxy[:user], proxy[:password]) else ::Net::HTTP.new(server.hostname, server.port) end connection.use_ssl = server.scheme == "https" connection.read_timeout = @transport_configuration.timeout connection.write_timeout = @transport_configuration.timeout if connection.respond_to?(:write_timeout) connection.open_timeout = @transport_configuration.open_timeout ssl_configuration.each do |key, value| connection.send("#{key}=", value) end connection end def do_request(endpoint, headers, body) conn.start do |http| request = ::Net::HTTP::Post.new(endpoint, headers) request.body = body http.request(request) end end private def has_rate_limited_header?(headers) headers[RETRY_AFTER_HEADER] || headers[RATE_LIMIT_HEADER] end def handle_rate_limited_response(headers) rate_limits = if rate_limits = headers[RATE_LIMIT_HEADER] parse_rate_limit_header(rate_limits) elsif retry_after = headers[RETRY_AFTER_HEADER] # although Sentry doesn't send a date string back # based on HTTP specification, this could be a date string (instead of an integer) retry_after = retry_after.to_i retry_after = DEFAULT_DELAY if retry_after == 0 { nil => Time.now + retry_after } else { nil => Time.now + DEFAULT_DELAY } end rate_limits.each do |category, limit| if current_limit = @rate_limits[category] if current_limit < limit @rate_limits[category] = limit end else @rate_limits[category] = limit end end end def parse_rate_limit_header(rate_limit_header) time = Time.now result = {} limits = rate_limit_header.split(",") limits.each do |limit| next if limit.nil? || limit.empty? begin retry_after, categories = limit.strip.split(":").first(2) retry_after = time + retry_after.to_i categories = categories.split(";") if categories.empty? result[nil] = retry_after else categories.each do |category| result[category] = retry_after end end rescue StandardError end end result end def should_compress?(data) @transport_configuration.encoding == GZIP_ENCODING && data.bytesize >= GZIP_THRESHOLD end # @param proxy [String, URI, Hash] Proxy config value passed into `config.transport`. # Accepts either a URI formatted string, URI, or a hash with the `uri`, `user`, and `password` keys. # @return [Hash] Normalized proxy config that will be passed into `Net::HTTP` def normalize_proxy(proxy) return proxy unless proxy case proxy when String uri = URI(proxy) { uri: uri, user: uri.user, password: uri.password } when URI { uri: proxy, user: proxy.user, password: proxy.password } when Hash proxy end end def ssl_configuration configuration = { verify: @transport_configuration.ssl_verification, ca_file: @transport_configuration.ssl_ca_file }.merge(@transport_configuration.ssl || {}) configuration[:verify_mode] = configuration.delete(:verify) ? OpenSSL::SSL::VERIFY_PEER : OpenSSL::SSL::VERIFY_NONE configuration end end end sentry-ruby-core-5.28.0/lib/sentry/scope.rb0000644000004100000410000002410615067721773020641 0ustar www-datawww-data# frozen_string_literal: true require "sentry/breadcrumb_buffer" require "sentry/propagation_context" require "sentry/attachment" require "etc" module Sentry class Scope include ArgumentCheckingHelper ATTRIBUTES = [ :transaction_name, :transaction_source, :contexts, :extra, :tags, :user, :level, :breadcrumbs, :fingerprint, :event_processors, :rack_env, :span, :session, :attachments, :propagation_context ] attr_reader(*ATTRIBUTES) # @param max_breadcrumbs [Integer] the maximum number of breadcrumbs to be stored in the scope. def initialize(max_breadcrumbs: nil) @max_breadcrumbs = max_breadcrumbs set_default_value end # Resets the scope's attributes to defaults. # @return [void] def clear set_default_value end # Applies stored attributes and event processors to the given event. # @param event [Event] # @param hint [Hash] the hint data that'll be passed to event processors. # @return [Event] def apply_to_event(event, hint = nil) unless event.is_a?(CheckInEvent) || event.is_a?(LogEvent) event.tags = tags.merge(event.tags) event.user = user.merge(event.user) event.extra = extra.merge(event.extra) event.contexts = contexts.merge(event.contexts) event.transaction = transaction_name if transaction_name event.transaction_info = { source: transaction_source } if transaction_source event.fingerprint = fingerprint event.level = level event.breadcrumbs = breadcrumbs event.rack_env = rack_env if rack_env event.attachments = attachments end if event.is_a?(LogEvent) event.user = user.merge(event.user) end if span event.contexts[:trace] ||= span.get_trace_context if event.respond_to?(:dynamic_sampling_context) event.dynamic_sampling_context ||= span.get_dynamic_sampling_context end else event.contexts[:trace] ||= propagation_context.get_trace_context if event.respond_to?(:dynamic_sampling_context) event.dynamic_sampling_context ||= propagation_context.get_dynamic_sampling_context end end all_event_processors = self.class.global_event_processors + @event_processors unless all_event_processors.empty? all_event_processors.each do |processor_block| event = processor_block.call(event, hint) end end event end # Adds the breadcrumb to the scope's breadcrumbs buffer. # @param breadcrumb [Breadcrumb] # @return [void] def add_breadcrumb(breadcrumb) breadcrumbs.record(breadcrumb) end # Clears the scope's breadcrumbs buffer # @return [void] def clear_breadcrumbs set_new_breadcrumb_buffer end # @return [Scope] def dup copy = super copy.breadcrumbs = breadcrumbs.dup copy.contexts = contexts.deep_dup copy.extra = extra.deep_dup copy.tags = tags.deep_dup copy.user = user.deep_dup copy.transaction_name = transaction_name.dup copy.transaction_source = transaction_source.dup copy.fingerprint = fingerprint.deep_dup copy.span = span.deep_dup copy.session = session.deep_dup copy.propagation_context = propagation_context.deep_dup copy.attachments = attachments.dup copy end # Updates the scope's data from a given scope. # @param scope [Scope] # @return [void] def update_from_scope(scope) self.breadcrumbs = scope.breadcrumbs self.contexts = scope.contexts self.extra = scope.extra self.tags = scope.tags self.user = scope.user self.transaction_name = scope.transaction_name self.transaction_source = scope.transaction_source self.fingerprint = scope.fingerprint self.span = scope.span self.propagation_context = scope.propagation_context self.attachments = scope.attachments end # Updates the scope's data from the given options. # @param contexts [Hash] # @param extras [Hash] # @param tags [Hash] # @param user [Hash] # @param level [String, Symbol] # @param fingerprint [Array] # @param attachments [Array] # @return [Array] def update_from_options( contexts: nil, extra: nil, tags: nil, user: nil, level: nil, fingerprint: nil, attachments: nil, **options ) self.contexts.merge!(contexts) if contexts self.extra.merge!(extra) if extra self.tags.merge!(tags) if tags self.user = user if user self.level = level if level self.fingerprint = fingerprint if fingerprint # Returns unsupported option keys so we can notify users. options.keys end # Sets the scope's rack_env attribute. # @param env [Hash] # @return [Hash] def set_rack_env(env) env = env || {} @rack_env = env end # Sets the scope's span attribute. # @param span [Span] # @return [Span] def set_span(span) check_argument_type!(span, Span) @span = span end # @!macro set_user def set_user(user_hash) check_argument_type!(user_hash, Hash) @user = user_hash end # @!macro set_extras def set_extras(extras_hash) check_argument_type!(extras_hash, Hash) @extra.merge!(extras_hash) end # Adds a new key-value pair to current extras. # @param key [String, Symbol] # @param value [Object] # @return [Hash] def set_extra(key, value) set_extras(key => value) end # @!macro set_tags def set_tags(tags_hash) check_argument_type!(tags_hash, Hash) @tags.merge!(tags_hash) end # Adds a new key-value pair to current tags. # @param key [String, Symbol] # @param value [Object] # @return [Hash] def set_tag(key, value) set_tags(key => value) end # Updates the scope's contexts attribute by merging with the old value. # @param contexts [Hash] # @return [Hash] def set_contexts(contexts_hash) check_argument_type!(contexts_hash, Hash) contexts_hash.values.each do |val| check_argument_type!(val, Hash) end @contexts.merge!(contexts_hash) do |key, old, new| old.merge(new) end end # @!macro set_context def set_context(key, value) check_argument_type!(value, Hash) set_contexts(key => value) end # Sets the scope's level attribute. # @param level [String, Symbol] # @return [void] def set_level(level) @level = level end # Appends a new transaction name to the scope. # The "transaction" here does not refer to `Transaction` objects. # @param transaction_name [String] # @return [void] def set_transaction_name(transaction_name, source: :custom) @transaction_name = transaction_name @transaction_source = source end # Sets the currently active session on the scope. # @param session [Session, nil] # @return [void] def set_session(session) @session = session end # These are high cardinality and thus bad. # @return [Boolean] def transaction_source_low_quality? transaction_source == :url end # Returns the associated Transaction object. # @return [Transaction, nil] def get_transaction span.transaction if span end # Returns the associated Span object. # @return [Span, nil] def get_span span end # Sets the scope's fingerprint attribute. # @param fingerprint [Array] # @return [Array] def set_fingerprint(fingerprint) check_argument_type!(fingerprint, Array) @fingerprint = fingerprint end # Adds a new event processor [Proc] to the scope. # @param block [Proc] # @return [void] def add_event_processor(&block) @event_processors << block end # Generate a new propagation context either from the incoming env headers or from scratch. # @param env [Hash, nil] # @return [void] def generate_propagation_context(env = nil) @propagation_context = PropagationContext.new(self, env) end # Add a new attachment to the scope. def add_attachment(**opts) attachments << (attachment = Attachment.new(**opts)) attachment end protected # for duplicating scopes internally attr_writer(*ATTRIBUTES) private def set_default_value @contexts = { os: self.class.os_context, runtime: self.class.runtime_context } @extra = {} @tags = {} @user = {} @level = :error @fingerprint = [] @transaction_name = nil @transaction_source = nil @event_processors = [] @rack_env = {} @span = nil @session = nil @attachments = [] generate_propagation_context set_new_breadcrumb_buffer end def set_new_breadcrumb_buffer @breadcrumbs = BreadcrumbBuffer.new(@max_breadcrumbs) end class << self # @return [Hash] def os_context @os_context ||= begin uname = Etc.uname { name: uname[:sysname] || RbConfig::CONFIG["host_os"], version: uname[:version], build: uname[:release], kernel_version: uname[:version], machine: uname[:machine] } end end # @return [Hash] def runtime_context @runtime_context ||= { name: RbConfig::CONFIG["ruby_install_name"], version: RUBY_DESCRIPTION || Sentry.sys_command("ruby -v") } end # Returns the global event processors array. # @return [Array] def global_event_processors @global_event_processors ||= [] end # Adds a new global event processor [Proc]. # Sometimes we need a global event processor without needing to configure scope. # These run before scope event processors. # # @param block [Proc] # @return [void] def add_global_event_processor(&block) global_event_processors << block end end end end sentry-ruby-core-5.28.0/lib/sentry/session_flusher.rb0000644000004100000410000000313615067721773022743 0ustar www-datawww-data# frozen_string_literal: true module Sentry class SessionFlusher < ThreadedPeriodicWorker FLUSH_INTERVAL = 60 def initialize(configuration, client) super(configuration.sdk_logger, FLUSH_INTERVAL) @client = client @pending_aggregates = {} @release = configuration.release @environment = configuration.environment @mutex = Mutex.new log_debug("[Sessions] Sessions won't be captured without a valid release") unless @release end def flush return if @pending_aggregates.empty? @client.capture_envelope(pending_envelope) end alias_method :run, :flush def add_session(session) return unless @release return unless ensure_thread return unless Session::AGGREGATE_STATUSES.include?(session.status) @pending_aggregates[session.aggregation_key] ||= init_aggregates(session.aggregation_key) @pending_aggregates[session.aggregation_key][session.status] += 1 end private def init_aggregates(aggregation_key) aggregates = { started: aggregation_key.iso8601 } Session::AGGREGATE_STATUSES.each { |k| aggregates[k] = 0 } aggregates end def pending_envelope aggregates = @mutex.synchronize do aggregates = @pending_aggregates.values @pending_aggregates = {} aggregates end envelope = Envelope.new header = { type: "sessions" } payload = { attrs: attrs, aggregates: aggregates } envelope.add_item(header, payload) envelope end def attrs { release: @release, environment: @environment } end end end sentry-ruby-core-5.28.0/lib/sentry/check_in_event.rb0000644000004100000410000000262215067721773022473 0ustar www-datawww-data# frozen_string_literal: true require "securerandom" require "sentry/cron/monitor_config" require "sentry/utils/uuid" module Sentry class CheckInEvent < Event TYPE = "check_in" # uuid to identify this check-in. # @return [String] attr_accessor :check_in_id # Identifier of the monitor for this check-in. # @return [String] attr_accessor :monitor_slug # Duration of this check since it has started in seconds. # @return [Integer, nil] attr_accessor :duration # Monitor configuration to support upserts. # @return [Cron::MonitorConfig, nil] attr_accessor :monitor_config # Status of this check-in. # @return [Symbol] attr_accessor :status VALID_STATUSES = %i[ok in_progress error] def initialize( slug:, status:, duration: nil, monitor_config: nil, check_in_id: nil, **options ) super(**options) self.monitor_slug = slug self.status = status self.duration = duration self.monitor_config = monitor_config self.check_in_id = check_in_id || Utils.uuid end # @return [Hash] def to_hash data = super data[:check_in_id] = check_in_id data[:monitor_slug] = monitor_slug data[:status] = status data[:duration] = duration if duration data[:monitor_config] = monitor_config.to_hash if monitor_config data end end end sentry-ruby-core-5.28.0/lib/sentry/transaction.rb0000644000004100000410000002675715067721773022073 0ustar www-datawww-data# frozen_string_literal: true require "sentry/baggage" require "sentry/profiler" require "sentry/utils/sample_rand" require "sentry/propagation_context" module Sentry class Transaction < Span # @deprecated Use Sentry::PropagationContext::SENTRY_TRACE_REGEXP instead. SENTRY_TRACE_REGEXP = PropagationContext::SENTRY_TRACE_REGEXP UNLABELD_NAME = "" MESSAGE_PREFIX = "[Tracing]" # https://develop.sentry.dev/sdk/event-payloads/transaction/#transaction-annotations SOURCES = %i[custom url route view component task] include LoggingHelper # The name of the transaction. # @return [String] attr_reader :name # The source of the transaction name. # @return [Symbol] attr_reader :source # The sampling decision of the parent transaction, which will be considered when making the current transaction's sampling decision. # @return [String] attr_reader :parent_sampled # The parsed incoming W3C baggage header. # This is only for accessing the current baggage variable. # Please use the #get_baggage method for interfacing outside this class. # @return [Baggage, nil] attr_reader :baggage # The measurements added to the transaction. # @return [Hash] attr_reader :measurements # @deprecated Use Sentry.get_current_hub instead. attr_reader :hub # @deprecated Use Sentry.configuration instead. attr_reader :configuration # The effective sample rate at which this transaction was sampled. # @return [Float, nil] attr_reader :effective_sample_rate # Additional contexts stored directly on the transaction object. # @return [Hash] attr_reader :contexts # The Profiler instance for this transaction. # @return [Profiler] attr_reader :profiler # Sample rand value generated from trace_id # @return [String] attr_reader :sample_rand def initialize( hub:, name: nil, source: :custom, parent_sampled: nil, baggage: nil, sample_rand: nil, **options ) super(transaction: self, **options) set_name(name, source: source) @parent_sampled = parent_sampled @hub = hub @baggage = baggage @configuration = hub.configuration # to be removed @tracing_enabled = hub.configuration.tracing_enabled? @traces_sampler = hub.configuration.traces_sampler @traces_sample_rate = hub.configuration.traces_sample_rate @sdk_logger = hub.configuration.sdk_logger @release = hub.configuration.release @environment = hub.configuration.environment @dsn = hub.configuration.dsn @effective_sample_rate = nil @contexts = {} @measurements = {} @sample_rand = sample_rand unless @hub.profiler_running? @profiler = @configuration.profiler_class.new(@configuration) end init_span_recorder unless @sample_rand generator = Utils::SampleRand.new(trace_id: @trace_id) @sample_rand = generator.generate_from_trace_id end end # @deprecated use Sentry.continue_trace instead. # # Initalizes a Transaction instance with a Sentry trace string from another transaction (usually from an external request). # # The original transaction will become the parent of the new Transaction instance. And they will share the same `trace_id`. # # The child transaction will also store the parent's sampling decision in its `parent_sampled` attribute. # @param sentry_trace [String] the trace string from the previous transaction. # @param baggage [String, nil] the incoming baggage header string. # @param hub [Hub] the hub that'll be responsible for sending this transaction when it's finished. # @param options [Hash] the options you want to use to initialize a Transaction instance. # @return [Transaction, nil] def self.from_sentry_trace(sentry_trace, baggage: nil, hub: Sentry.get_current_hub, **options) return unless hub.configuration.tracing_enabled? return unless sentry_trace sentry_trace_data = extract_sentry_trace(sentry_trace) return unless sentry_trace_data trace_id, parent_span_id, parent_sampled = sentry_trace_data baggage = if baggage && !baggage.empty? Baggage.from_incoming_header(baggage) else # If there's an incoming sentry-trace but no incoming baggage header, # for instance in traces coming from older SDKs, # baggage will be empty and frozen and won't be populated as head SDK. Baggage.new({}) end baggage.freeze! sample_rand = extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled) new( trace_id: trace_id, parent_span_id: parent_span_id, parent_sampled: parent_sampled, hub: hub, baggage: baggage, sample_rand: sample_rand, **options ) end # @deprecated Use Sentry::PropagationContext.extract_sentry_trace instead. # @return [Array, nil] def self.extract_sentry_trace(sentry_trace) PropagationContext.extract_sentry_trace(sentry_trace) end def self.extract_sample_rand_from_baggage(baggage, trace_id, parent_sampled) PropagationContext.extract_sample_rand_from_baggage(baggage, trace_id) || PropagationContext.generate_sample_rand(baggage, trace_id, parent_sampled) end # @return [Hash] def to_hash hash = super hash.merge!( name: @name, source: @source, sampled: @sampled, parent_sampled: @parent_sampled ) hash end def parent_sample_rate return unless @baggage&.items sample_rate_str = @baggage.items["sample_rate"] sample_rate_str&.to_f end # @return [Transaction] def deep_dup copy = super copy.init_span_recorder(@span_recorder.max_length) @span_recorder.spans.each do |span| # span_recorder's first span is the current span, which should not be added to the copy's spans next if span == self copy.span_recorder.add(span.dup) end copy end # Sets a custom measurement on the transaction. # @param name [String] name of the measurement # @param value [Float] value of the measurement # @param unit [String] unit of the measurement # @return [void] def set_measurement(name, value, unit = "") @measurements[name] = { value: value, unit: unit } end # Sets initial sampling decision of the transaction. # @param sampling_context [Hash] a context Hash that'll be passed to `traces_sampler` (if provided). # @return [void] def set_initial_sample_decision(sampling_context:) unless @tracing_enabled @sampled = false return end unless @sampled.nil? @effective_sample_rate = @sampled ? 1.0 : 0.0 return end sample_rate = if @traces_sampler.is_a?(Proc) @traces_sampler.call(sampling_context) elsif !sampling_context[:parent_sampled].nil? sampling_context[:parent_sampled] else @traces_sample_rate end transaction_description = generate_transaction_description if [true, false].include?(sample_rate) @effective_sample_rate = sample_rate ? 1.0 : 0.0 elsif sample_rate.is_a?(Numeric) && sample_rate >= 0.0 && sample_rate <= 1.0 @effective_sample_rate = sample_rate.to_f else @sampled = false log_warn("#{MESSAGE_PREFIX} Discarding #{transaction_description} because of invalid sample_rate: #{sample_rate}") return end if sample_rate == 0.0 || sample_rate == false @sampled = false log_debug("#{MESSAGE_PREFIX} Discarding #{transaction_description} because traces_sampler returned 0 or false") return end if sample_rate == true @sampled = true else if Sentry.backpressure_monitor factor = Sentry.backpressure_monitor.downsample_factor @effective_sample_rate /= 2**factor end @sampled = @sample_rand < @effective_sample_rate end if @sampled log_debug("#{MESSAGE_PREFIX} Starting #{transaction_description}") else log_debug( "#{MESSAGE_PREFIX} Discarding #{transaction_description} because it's not included in the random sample (sampling rate = #{sample_rate})" ) end end # Finishes the transaction's recording and send it to Sentry. # @param hub [Hub] the hub that'll send this transaction. (Deprecated) # @return [TransactionEvent] def finish(hub: nil, end_timestamp: nil) if hub log_warn( <<~MSG Specifying a different hub in `Transaction#finish` will be deprecated in version 5.0. Please use `Hub#start_transaction` with the designated hub. MSG ) end hub ||= @hub super(end_timestamp: end_timestamp) if @name.nil? @name = UNLABELD_NAME end @hub.stop_profiler!(self) if @sampled event = hub.current_client.event_from_transaction(self) hub.capture_event(event) else is_backpressure = Sentry.backpressure_monitor&.downsample_factor&.positive? reason = is_backpressure ? :backpressure : :sample_rate hub.current_client.transport.record_lost_event(reason, "transaction") hub.current_client.transport.record_lost_event(reason, "span") end end # Get the existing frozen incoming baggage # or populate one with sentry- items as the head SDK. # @return [Baggage] def get_baggage populate_head_baggage if @baggage.nil? || @baggage.mutable @baggage end # Set the transaction name directly. # Considered internal api since it bypasses the usual scope logic. # @param name [String] # @param source [Symbol] # @return [void] def set_name(name, source: :custom) @name = name @source = SOURCES.include?(source) ? source.to_sym : :custom end # Set contexts directly on the transaction. # @param key [String, Symbol] # @param value [Object] # @return [void] def set_context(key, value) @contexts[key] = value end # Start the profiler. # @return [void] def start_profiler! return unless profiler profiler.set_initial_sample_decision(sampled) profiler.start end # These are high cardinality and thus bad def source_low_quality? source == :url end protected def init_span_recorder(limit = 1000) @span_recorder = SpanRecorder.new(limit) @span_recorder.add(self) end private def generate_transaction_description result = op.nil? ? "" : "<#{@op}> " result += "transaction" result += " <#{@name}>" if @name result end def populate_head_baggage items = { "trace_id" => trace_id, "sample_rate" => effective_sample_rate&.to_s, "sample_rand" => Utils::SampleRand.format(@sample_rand), "sampled" => sampled&.to_s, "environment" => @environment, "release" => @release, "public_key" => @dsn&.public_key } items["transaction"] = name unless source_low_quality? items.compact! @baggage = Baggage.new(items, mutable: false) end class SpanRecorder attr_reader :max_length, :spans def initialize(max_length) @max_length = max_length @spans = [] end def add(span) if @spans.count < @max_length @spans << span end end end end end sentry-ruby-core-5.28.0/lib/sentry/log_event_buffer.rb0000644000004100000410000000311415067721773023037 0ustar www-datawww-data# frozen_string_literal: true require "sentry/threaded_periodic_worker" module Sentry # LogEventBuffer buffers log events and sends them to Sentry in a single envelope. # # This is used internally by the `Sentry::Client`. # # @!visibility private class LogEventBuffer < ThreadedPeriodicWorker FLUSH_INTERVAL = 5 # seconds DEFAULT_MAX_EVENTS = 100 # @!visibility private attr_reader :pending_events def initialize(configuration, client) super(configuration.sdk_logger, FLUSH_INTERVAL) @client = client @pending_events = [] @max_events = configuration.max_log_events || DEFAULT_MAX_EVENTS @mutex = Mutex.new log_debug("[Logging] Initialized buffer with max_events=#{@max_events}, flush_interval=#{FLUSH_INTERVAL}s") end def start ensure_thread self end def flush @mutex.synchronize do return if empty? log_debug("[LogEventBuffer] flushing #{size} log events") send_events end log_debug("[LogEventBuffer] flushed #{size} log events") self end alias_method :run, :flush def add_event(event) raise ArgumentError, "expected a LogEvent, got #{event.class}" unless event.is_a?(LogEvent) @mutex.synchronize do @pending_events << event send_events if size >= @max_events end self end def empty? @pending_events.empty? end def size @pending_events.size end private def send_events @client.send_logs(@pending_events) @pending_events.clear end end end sentry-ruby-core-5.28.0/lib/sentry/breadcrumb.rb0000644000004100000410000000371515067721773021641 0ustar www-datawww-data# frozen_string_literal: true module Sentry class Breadcrumb MAX_NESTING = 10 DATA_SERIALIZATION_ERROR_MESSAGE = "[data were removed due to serialization issues]" # @return [String, nil] attr_accessor :category # @return [Hash, nil] attr_accessor :data # @return [String, nil] attr_reader :level # @return [Time, Integer, nil] attr_accessor :timestamp # @return [String, nil] attr_accessor :type # @return [String, nil] attr_reader :message # @param category [String, nil] # @param data [Hash, nil] # @param message [String, nil] # @param timestamp [Time, Integer, nil] # @param level [String, nil] # @param type [String, nil] def initialize(category: nil, data: nil, message: nil, timestamp: nil, level: nil, type: nil) @category = category @data = data || {} @timestamp = timestamp || Sentry.utc_now.to_i @type = type self.message = message self.level = level end # @return [Hash] def to_hash { category: @category, data: serialized_data, level: @level, message: @message, timestamp: @timestamp, type: @type } end # @param message [String] # @return [void] def message=(message) @message = message && Utils::EncodingHelper.valid_utf_8?(message) ? message.byteslice(0..Event::MAX_MESSAGE_SIZE_IN_BYTES) : "" end # @param level [String] # @return [void] def level=(level) # needed to meet the Sentry spec @level = level == "warn" ? "warning" : level end private def serialized_data begin ::JSON.parse(::JSON.generate(@data, max_nesting: MAX_NESTING)) rescue Exception => e Sentry.sdk_logger.debug(LOGGER_PROGNAME) do <<~MSG can't serialize breadcrumb data because of error: #{e} data: #{@data} MSG end { error: DATA_SERIALIZATION_ERROR_MESSAGE } end end end end sentry-ruby-core-5.28.0/lib/sentry/rack/0000755000004100000410000000000015067721773020120 5ustar www-datawww-datasentry-ruby-core-5.28.0/lib/sentry/rack/capture_exceptions.rb0000644000004100000410000000462115067721773024354 0ustar www-datawww-data# frozen_string_literal: true module Sentry module Rack class CaptureExceptions ERROR_EVENT_ID_KEY = "sentry.error_event_id" MECHANISM_TYPE = "rack" SPAN_ORIGIN = "auto.http.rack" def initialize(app) @app = app end def call(env) return @app.call(env) unless Sentry.initialized? # make sure the current thread has a clean hub Sentry.clone_hub_to_current_thread Sentry.with_scope do |scope| Sentry.with_session_tracking do scope.clear_breadcrumbs scope.set_transaction_name(env["PATH_INFO"], source: :url) if env["PATH_INFO"] scope.set_rack_env(env) transaction = start_transaction(env, scope) scope.set_span(transaction) if transaction begin response = @app.call(env) rescue Sentry::Error finish_transaction(transaction, 500) raise # Don't capture Sentry errors rescue Exception => e capture_exception(e, env) finish_transaction(transaction, 500) raise end exception = collect_exception(env) capture_exception(exception, env) if exception finish_transaction(transaction, response[0]) response end end end private def collect_exception(env) env["rack.exception"] || env["sinatra.error"] end def transaction_op "http.server" end def capture_exception(exception, env) Sentry.capture_exception(exception, hint: { mechanism: mechanism }).tap do |event| env[ERROR_EVENT_ID_KEY] = event.event_id if event end end def start_transaction(env, scope) options = { name: scope.transaction_name, source: scope.transaction_source, op: transaction_op, origin: SPAN_ORIGIN } transaction = Sentry.continue_trace(env, **options) Sentry.start_transaction(transaction: transaction, custom_sampling_context: { env: env }, **options) end def finish_transaction(transaction, status_code) return unless transaction transaction.set_http_status(status_code) transaction.finish end def mechanism Sentry::Mechanism.new(type: MECHANISM_TYPE, handled: false) end end end end sentry-ruby-core-5.28.0/lib/sentry-ruby.rb0000644000004100000410000005204515067721773020512 0ustar www-datawww-data# frozen_string_literal: true require "English" require "forwardable" require "time" require "sentry/version" require "sentry/exceptions" require "sentry/core_ext/object/deep_dup" require "sentry/utils/argument_checking_helper" require "sentry/utils/encoding_helper" require "sentry/utils/logging_helper" require "sentry/utils/sample_rand" require "sentry/configuration" require "sentry/structured_logger" require "sentry/debug_structured_logger" require "sentry/event" require "sentry/error_event" require "sentry/transaction_event" require "sentry/check_in_event" require "sentry/span" require "sentry/transaction" require "sentry/hub" require "sentry/background_worker" require "sentry/threaded_periodic_worker" require "sentry/session_flusher" require "sentry/backpressure_monitor" require "sentry/cron/monitor_check_ins" require "sentry/metrics" require "sentry/vernier/profiler" [ "sentry/rake", "sentry/rack" ].each do |lib| begin require lib rescue LoadError end end module Sentry META = { "name" => "sentry.ruby", "version" => Sentry::VERSION }.freeze CAPTURED_SIGNATURE = :@__sentry_captured LOGGER_PROGNAME = "sentry" SENTRY_TRACE_HEADER_NAME = "sentry-trace" BAGGAGE_HEADER_NAME = "baggage" THREAD_LOCAL = :sentry_hub MUTEX = Mutex.new GLOBALS = %i[ main_hub logger session_flusher backpressure_monitor metrics_aggregator exception_locals_tp ].freeze class << self # @!visibility private def exception_locals_tp @exception_locals_tp ||= TracePoint.new(:raise) do |tp| exception = tp.raised_exception # don't collect locals again if the exception is re-raised next if exception.instance_variable_get(:@sentry_locals) next unless tp.binding locals = tp.binding.local_variables.each_with_object({}) do |local, result| result[local] = tp.binding.local_variable_get(local) end exception.instance_variable_set(:@sentry_locals, locals) end end # @!attribute [rw] background_worker # @return [BackgroundWorker] attr_accessor :background_worker # @!attribute [r] session_flusher # @return [SessionFlusher, nil] attr_reader :session_flusher # @!attribute [r] backpressure_monitor # @return [BackpressureMonitor, nil] attr_reader :backpressure_monitor # @!attribute [r] metrics_aggregator # @return [Metrics::Aggregator, nil] attr_reader :metrics_aggregator ##### Patch Registration ##### # @!visibility private def register_patch(key, patch = nil, target = nil, &block) if patch && block raise ArgumentError.new("Please provide either a patch and its target OR a block, but not both") end if block registered_patches[key] = block else registered_patches[key] = proc do target.send(:prepend, patch) unless target.ancestors.include?(patch) end end end # @!visibility private def apply_patches(config) registered_patches.each do |key, patch| patch.call(config) if config.enabled_patches.include?(key) end end # @!visibility private def registered_patches @registered_patches ||= {} end ##### Integrations ##### # Returns a hash that contains all the integrations that have been registered to the main SDK. # # @return [Hash{String=>Hash}] def integrations @integrations ||= {} end # Registers the SDK integration with its name and version. # # @param name [String] name of the integration # @param version [String] version of the integration def register_integration(name, version) if initialized? sdk_logger.warn(LOGGER_PROGNAME) do <<~MSG Integration '#{name}' is loaded after the SDK is initialized, which can cause unexpected behavior. Please make sure all integrations are loaded before SDK initialization. MSG end end meta = { name: "sentry.ruby.#{name}", version: version }.freeze integrations[name.to_s] = meta end ##### Method Delegation ##### extend Forwardable # @!macro [new] configuration # The Configuration object that's used for configuring the client and its transport. # @return [Configuration] # @!macro [new] send_event # Sends the event to Sentry. # @param event [Event] the event to be sent. # @param hint [Hash] the hint data that'll be passed to `before_send` callback. # @return [Event] # @!method configuration # @!macro configuration def configuration return unless initialized? get_current_client.configuration end # @!method send_event # @!macro send_event def send_event(*args) return unless initialized? get_current_client.send_event(*args) end # @!macro [new] set_extras # Updates the scope's extras attribute by merging with the old value. # @param extras [Hash] # @return [Hash] # @!macro [new] set_user # Sets the scope's user attribute. # @param user [Hash] # @return [Hash] # @!macro [new] set_context # Adds a new key-value pair to current contexts. # @param key [String, Symbol] # @param value [Object] # @return [Hash] # @!macro [new] set_tags # Updates the scope's tags attribute by merging with the old value. # @param tags [Hash] # @return [Hash] # @!method set_tags # @!macro set_tags def set_tags(*args) return unless initialized? get_current_scope.set_tags(*args) end # @!method set_extras # @!macro set_extras def set_extras(*args) return unless initialized? get_current_scope.set_extras(*args) end # @!method set_user # @!macro set_user def set_user(*args) return unless initialized? get_current_scope.set_user(*args) end # @!method set_context # @!macro set_context def set_context(*args) return unless initialized? get_current_scope.set_context(*args) end # @!method add_attachment # @!macro add_attachment def add_attachment(**opts) return unless initialized? get_current_scope.add_attachment(**opts) end ##### Main APIs ##### # Initializes the SDK with given configuration. # # @yieldparam config [Configuration] # @return [void] def init(&block) config = Configuration.new(&block) config.detect_release apply_patches(config) config.validate client = Client.new(config) scope = Scope.new(max_breadcrumbs: config.max_breadcrumbs) hub = Hub.new(client, scope) Thread.current.thread_variable_set(THREAD_LOCAL, hub) @main_hub = hub @background_worker = Sentry::BackgroundWorker.new(config) @session_flusher = config.session_tracking? ? Sentry::SessionFlusher.new(config, client) : nil @backpressure_monitor = config.enable_backpressure_handling ? Sentry::BackpressureMonitor.new(config, client) : nil @metrics_aggregator = config.metrics.enabled ? Sentry::Metrics::Aggregator.new(config, client) : nil exception_locals_tp.enable if config.include_local_variables at_exit { close } end # Flushes pending events and cleans up SDK state. # SDK will stop sending events and all top-level APIs will be no-ops after this. # # @return [void] def close if @session_flusher @session_flusher.flush @session_flusher.kill @session_flusher = nil end if @backpressure_monitor @backpressure_monitor.kill @backpressure_monitor = nil end if @metrics_aggregator @metrics_aggregator.flush(force: true) @metrics_aggregator.kill @metrics_aggregator = nil end if client = get_current_client client.flush if client.configuration.include_local_variables exception_locals_tp.disable end end @background_worker.shutdown MUTEX.synchronize do @main_hub = nil Thread.current.thread_variable_set(THREAD_LOCAL, nil) end end # Returns true if the SDK is initialized. # # @return [Boolean] def initialized? !!get_main_hub end # Returns an uri for security policy reporting that's generated from the given DSN # (To learn more about security policy reporting: https://docs.sentry.io/product/security-policy-reporting/) # # It returns nil if # - The SDK is not initialized yet. # - The DSN is not provided or is invalid. # # @return [String, nil] def csp_report_uri return unless initialized? configuration.csp_report_uri end # Returns the main thread's active hub. # # @return [Hub] def get_main_hub MUTEX.synchronize { @main_hub } rescue ThreadError # In some rare cases this may be called in a trap context so we need to handle it gracefully @main_hub end # Takes an instance of Sentry::Breadcrumb and stores it to the current active scope. # # @return [Breadcrumb, nil] def add_breadcrumb(breadcrumb, **options) return unless initialized? get_current_hub.add_breadcrumb(breadcrumb, **options) end # Returns the current active hub. # If the current thread doesn't have an active hub, it will clone the main thread's active hub, # stores it in the current thread, and then returns it. # # @return [Hub] def get_current_hub # we need to assign a hub to the current thread if it doesn't have one yet # # ideally, we should do this proactively whenever a new thread is created # but it's impossible for the SDK to keep track every new thread # so we need to use this rather passive way to make sure the app doesn't crash Thread.current.thread_variable_get(THREAD_LOCAL) || clone_hub_to_current_thread end # Returns the current active client. # @return [Client, nil] def get_current_client return unless initialized? get_current_hub.current_client end # Returns the current active scope. # # @return [Scope, nil] def get_current_scope return unless initialized? get_current_hub.current_scope end # Clones the main thread's active hub and stores it to the current thread. # # @return [void] def clone_hub_to_current_thread return unless initialized? Thread.current.thread_variable_set(THREAD_LOCAL, get_main_hub.clone) end # Takes a block and yields the current active scope. # # @example # Sentry.configure_scope do |scope| # scope.set_tags(foo: "bar") # end # # Sentry.capture_message("test message") # this event will have tags { foo: "bar" } # # @yieldparam scope [Scope] # @return [void] def configure_scope(&block) return unless initialized? get_current_hub.configure_scope(&block) end # Takes a block and yields a temporary scope. # The temporary scope will inherit all the attributes from the current active scope and replace it to be the active # scope inside the block. # # @example # Sentry.configure_scope do |scope| # scope.set_tags(foo: "bar") # end # # Sentry.capture_message("test message") # this event will have tags { foo: "bar" } # # Sentry.with_scope do |temp_scope| # temp_scope.set_tags(foo: "baz") # Sentry.capture_message("test message 2") # this event will have tags { foo: "baz" } # end # # Sentry.capture_message("test message 3") # this event will have tags { foo: "bar" } # # @yieldparam scope [Scope] # @return [void] def with_scope(&block) return yield unless initialized? get_current_hub.with_scope(&block) end # Wrap a given block with session tracking. # Aggregate sessions in minutely buckets will be recorded # around this block and flushed every minute. # # @example # Sentry.with_session_tracking do # a = 1 + 1 # new session recorded with :exited status # end # # Sentry.with_session_tracking do # 1 / 0 # rescue => e # Sentry.capture_exception(e) # new session recorded with :errored status # end # @return [void] def with_session_tracking(&block) return yield unless initialized? get_current_hub.with_session_tracking(&block) end # Takes an exception and reports it to Sentry via the currently active hub. # # @yieldparam scope [Scope] # @return [Event, nil] def capture_exception(exception, **options, &block) return unless initialized? get_current_hub.capture_exception(exception, **options, &block) end # Takes a block and evaluates it. If the block raised an exception, it reports the exception to Sentry and re-raises it. # If the block ran without exception, it returns the evaluation result. # # @example # Sentry.with_exception_captured do # 1/1 #=> 1 will be returned # end # # Sentry.with_exception_captured do # 1/0 #=> ZeroDivisionError will be reported and re-raised # end # def with_exception_captured(**options, &block) yield rescue Exception => e capture_exception(e, **options) raise end # Takes a message string and reports it to Sentry via the currently active hub. # # @yieldparam scope [Scope] # @return [Event, nil] def capture_message(message, **options, &block) return unless initialized? get_current_hub.capture_message(message, **options, &block) end # Takes an instance of Sentry::Event and dispatches it to the currently active hub. # # @return [Event, nil] def capture_event(event) return unless initialized? get_current_hub.capture_event(event) end # Captures a check-in and sends it to Sentry via the currently active hub. # # @param slug [String] identifier of this monitor # @param status [Symbol] status of this check-in, one of {CheckInEvent::VALID_STATUSES} # # @param [Hash] options extra check-in options # @option options [String] check_in_id for updating the status of an existing monitor # @option options [Integer] duration seconds elapsed since this monitor started # @option options [Cron::MonitorConfig] monitor_config configuration for this monitor # # @return [String, nil] The {CheckInEvent#check_in_id} to use for later updates on the same slug def capture_check_in(slug, status, **options) return unless initialized? get_current_hub.capture_check_in(slug, status, **options) end # Captures a log event and sends it to Sentry via the currently active hub. # This is the underlying method used by the StructuredLogger class. # # @param message [String] the log message # @param [Hash] options Extra log event options # @option options [Symbol] level The log level (:trace, :debug, :info, :warn, :error, :fatal) # @option options [Integer] severity The severity number according to the Sentry Logs Protocol # @option options [String] origin The origin of the log event (e.g., "auto.db.rails", "manual") # @option options [Hash] Additional attributes to include with the log # # @example Direct usage (prefer using Sentry.logger instead) # Sentry.capture_log("User logged in", level: :info, user_id: 123) # # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol # @return [LogEvent, nil] The created log event or nil if logging is disabled def capture_log(message, **options) return unless initialized? get_current_hub.capture_log_event(message, **options) end # Takes or initializes a new Sentry::Transaction and makes a sampling decision for it. # # @return [Transaction, nil] def start_transaction(**options) return unless initialized? get_current_hub.start_transaction(**options) end # Records the block's execution as a child of the current span. # If the current scope doesn't have a span, the block would still be executed but the yield param will be nil. # @param attributes [Hash] attributes for the child span. # @yieldparam child_span [Span, nil] # @return yield result # # @example # Sentry.with_child_span(op: "my operation") do |child_span| # child_span.set_data(operation_data) # child_span.set_description(operation_detail) # # result will be returned # end # def with_child_span(**attributes, &block) return yield(nil) unless Sentry.initialized? get_current_hub.with_child_span(**attributes, &block) end # Returns the id of the lastly reported Sentry::Event. # # @return [String, nil] def last_event_id return unless initialized? get_current_hub.last_event_id end # Checks if the exception object has been captured by the SDK. # # @return [Boolean] def exception_captured?(exc) return false unless initialized? !!exc.instance_variable_get(CAPTURED_SIGNATURE) end # Add a global event processor [Proc]. # These run before scope event processors. # # @yieldparam event [Event] # @yieldparam hint [Hash, nil] # @return [void] # # @example # Sentry.add_global_event_processor do |event, hint| # event.tags = { foo: 42 } # event # end # def add_global_event_processor(&block) Scope.add_global_event_processor(&block) end # Returns the traceparent (sentry-trace) header for distributed tracing. # Can be either from the currently active span or the propagation context. # # @return [String, nil] def get_traceparent return nil unless initialized? get_current_hub.get_traceparent end # Returns the baggage header for distributed tracing. # Can be either from the currently active span or the propagation context. # # @return [String, nil] def get_baggage return nil unless initialized? get_current_hub.get_baggage end # Returns the a Hash containing sentry-trace and baggage. # Can be either from the currently active span or the propagation context. # # @return [Hash, nil] def get_trace_propagation_headers return nil unless initialized? get_current_hub.get_trace_propagation_headers end # Returns the a Hash containing sentry-trace and baggage. # Can be either from the currently active span or the propagation context. # # @return [String] def get_trace_propagation_meta return "" unless initialized? get_current_hub.get_trace_propagation_meta end # Continue an incoming trace from a rack env like hash. # # @param env [Hash] # @return [Transaction, nil] def continue_trace(env, **options) return nil unless initialized? get_current_hub.continue_trace(env, **options) end # Returns the structured logger instance that implements Sentry's SDK telemetry logs protocol. # # This logger is only available when logs are enabled in the configuration. # # @example Enable logs in configuration # Sentry.init do |config| # config.dsn = "YOUR_DSN" # config.enable_logs = true # end # # @example Basic usage # Sentry.logger.info("User logged in successfully", user_id: 123) # Sentry.logger.error("Failed to process payment", # transaction_id: "tx_123", # error_code: "PAYMENT_FAILED" # ) # # @see https://develop.sentry.dev/sdk/telemetry/logs/ Sentry SDK Telemetry Logs Protocol # # @return [StructuredLogger, nil] The structured logger instance or nil if logs are disabled def logger @logger ||= if configuration.enable_logs # Initialize the public-facing Structured Logger if logs are enabled # Use configured structured logger class or default to StructuredLogger # @see https://develop.sentry.dev/sdk/telemetry/logs/ configuration.structured_logging.logger_class.new(configuration) else warn <<~STR [sentry] `Sentry.logger` will no longer be used as internal SDK logger when `enable_logs` feature is turned on. Use Sentry.configuration.sdk_logger for SDK-specific logging needs." Caller: #{caller.first} STR configuration.sdk_logger end end ##### Helpers ##### # @!visibility private def sys_command(command) result = `#{command} 2>&1` rescue nil return if result.nil? || result.empty? || ($CHILD_STATUS && $CHILD_STATUS.exitstatus != 0) result.strip end # @!visibility private def sdk_logger configuration.sdk_logger end # @!visibility private def sdk_meta META end # @!visibility private def utc_now Time.now.utc end # @!visibility private def dependency_installed?(name) Object.const_defined?(name) end end end # patches require "sentry/net/http" require "sentry/redis" require "sentry/puma" require "sentry/graphql" require "sentry/faraday" require "sentry/excon" require "sentry/std_lib_logger" sentry-ruby-core-5.28.0/LICENSE.txt0000644000004100000410000000206115067721773016730 0ustar www-datawww-dataThe MIT License (MIT) Copyright (c) 2020 Sentry Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. sentry-ruby-core-5.28.0/.yardopts0000644000004100000410000000007215067721773016753 0ustar www-datawww-data--exclude lib/sentry/utils/ --exclude lib/sentry/core_ext sentry-ruby-core-5.28.0/.rspec0000644000004100000410000000007515067721773016225 0ustar www-datawww-data--require spec_helper --format progress --color --order rand sentry-ruby-core-5.28.0/Rakefile0000644000004100000410000000064715067721773016562 0ustar www-datawww-data# frozen_string_literal: true require "rake/clean" CLOBBER.include "pkg" require "bundler/gem_helper" Bundler::GemHelper.install_tasks(name: "sentry-ruby") require_relative "../lib/sentry/test/rake_tasks" ISOLATED_SPECS = "spec/isolated/**/*_spec.rb" Sentry::Test::RakeTasks.define_spec_tasks( isolated_specs_pattern: ISOLATED_SPECS, spec_exclude_pattern: ISOLATED_SPECS ) task default: [:spec, :"spec:isolated"] sentry-ruby-core-5.28.0/sentry-ruby-core.gemspec0000644000004100000410000000166715067721773021716 0ustar www-datawww-data# frozen_string_literal: true require_relative "lib/sentry/version" Gem::Specification.new do |spec| spec.name = "sentry-ruby-core" spec.version = Sentry::VERSION spec.authors = ["Sentry Team"] spec.description = spec.summary = "A gem that provides a client interface for the Sentry error logger" spec.email = "accounts@sentry.io" spec.license = 'MIT' spec.homepage = "https://github.com/getsentry/sentry-ruby" spec.platform = Gem::Platform::RUBY spec.required_ruby_version = '>= 2.4' spec.extra_rdoc_files = ["README.md", "LICENSE.txt"] spec.files = `git ls-files | grep -Ev '^(spec|benchmarks|examples|\.rubocop\.yml)'`.split("\n") spec.metadata["homepage_uri"] = spec.homepage spec.metadata["source_code_uri"] = spec.homepage spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md" spec.add_dependency "sentry-ruby", Sentry::VERSION spec.add_dependency "concurrent-ruby" end sentry-ruby-core-5.28.0/Gemfile0000644000004100000410000000141615067721773016403 0ustar www-datawww-data# frozen_string_literal: true source "https://rubygems.org" git_source(:github) { |name| "https://github.com/#{name}.git" } eval_gemfile "../Gemfile.dev" gem "sentry-ruby", path: "./" rack_version = ENV["RACK_VERSION"] rack_version = "3.0.0" if rack_version.nil? gem "rack", "~> #{Gem::Version.new(rack_version)}" unless rack_version == "0" redis_rb_version = ENV.fetch("REDIS_RB_VERSION", "5.0") gem "redis", "~> #{redis_rb_version}" gem "puma" gem "timecop" gem "stackprof" unless RUBY_PLATFORM == "java" gem "vernier", platforms: :ruby if RUBY_VERSION >= "3.2.1" gem "graphql", ">= 2.2.6" if RUBY_VERSION.to_f >= 2.7 gem "benchmark-ips" gem "benchmark_driver" gem "benchmark-ipsa" gem "benchmark-memory" gem "yard" gem "webrick" gem "faraday" gem "excon" gem "webmock" sentry-ruby-core-5.28.0/README.md0000644000004100000410000002327715067721773016400 0ustar www-datawww-data

Sentry

_Bad software is everywhere, and we're tired of it. Sentry is on a mission to help developers write better software faster, so we can get back to enjoying technology. If you want to join us [**Check out our open positions**](https://sentry.io/careers/)_ Sentry SDK for Ruby =========== | Current version | Build | Coverage | API doc | | ---------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------- | | [![Gem Version](https://img.shields.io/gem/v/sentry-ruby?label=sentry-ruby)](https://rubygems.org/gems/sentry-ruby) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-ruby)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-ruby) | | [![Gem Version](https://img.shields.io/gem/v/sentry-rails?label=sentry-rails)](https://rubygems.org/gems/sentry-rails) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-rails)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-rails) | | [![Gem Version](https://img.shields.io/gem/v/sentry-sidekiq?label=sentry-sidekiq)](https://rubygems.org/gems/sentry-sidekiq) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-sidekiq)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-sidekiq) | | [![Gem Version](https://img.shields.io/gem/v/sentry-delayed_job?label=sentry-delayed_job)](https://rubygems.org/gems/sentry-delayed_job) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-delayed_job)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-delayed_job) | | [![Gem Version](https://img.shields.io/gem/v/sentry-resque?label=sentry-resque)](https://rubygems.org/gems/sentry-resque) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-resque)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-resque) | | [![Gem Version](https://img.shields.io/gem/v/sentry-opentelemetry?label=sentry-opentelemetry)](https://rubygems.org/gems/sentry-opentelemetry) | [![Build Status](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml/badge.svg)](https://github.com/getsentry/sentry-ruby/actions/workflows/tests.yml) | [![codecov](https://codecov.io/gh/getsentry/sentry-ruby/graph/badge.svg?token=ZePzrpZFP6&component=sentry-opentelemetry)](https://codecov.io/gh/getsentry/sentry-ruby) | [![API doc](https://img.shields.io/badge/API%20doc-rubydoc.info-blue)](https://www.rubydoc.info/gems/sentry-opentelemetry) | ## Migrate From sentry-raven **The old `sentry-raven` client has entered maintenance mode and was moved to [here](https://github.com/getsentry/sentry-ruby/tree/master/sentry-raven).** If you're using `sentry-raven`, we recommend you to migrate to this new SDK. You can find the benefits of migrating and how to do it in our [migration guide](https://docs.sentry.io/platforms/ruby/migration/). ## Requirements We test from Ruby 2.4 to Ruby 3.4 at the latest patchlevel/teeny version. We also support JRuby 9.0. If you use self-hosted Sentry, please also make sure its version is above `20.6.0`. ## Getting Started ### Install ```ruby gem "sentry-ruby" ``` and depends on the integrations you want to have, you might also want to install these: ```ruby gem "sentry-rails" gem "sentry-sidekiq" gem "sentry-delayed_job" gem "sentry-resque" gem "sentry-opentelemetry" ``` ### Configuration You need to use Sentry.init to initialize and configure your SDK: ```ruby require "sentry-ruby" Sentry.init do |config| config.dsn = "MY_DSN" end ``` To learn more about available configuration options, please visit the [official documentation](https://docs.sentry.io/platforms/ruby/configuration/options/). ### Performance Monitoring You can activate [performance monitoring](https://docs.sentry.io/platforms/ruby/performance) by enabling traces sampling: ```ruby Sentry.init do |config| # set a uniform sample rate between 0.0 and 1.0 config.traces_sample_rate = 0.2 # you can also use traces_sampler for more fine-grained sampling # please click the link below to learn more end ``` To learn more about sampling transactions, please visit the [official documentation](https://docs.sentry.io/platforms/ruby/configuration/sampling/#configuring-the-transaction-sample-rate). ### [Migration Guide](https://docs.sentry.io/platforms/ruby/migration/) ### Integrations - [Rack](https://docs.sentry.io/platforms/ruby/guides/rack/) - [Rails](https://docs.sentry.io/platforms/ruby/guides/rails/) - [Sidekiq](https://docs.sentry.io/platforms/ruby/guides/sidekiq/) - [DelayedJob](https://docs.sentry.io/platforms/ruby/guides/delayed_job/) - [Resque](https://docs.sentry.io/platforms/ruby/guides/resque/) - [OpenTelemetry](https://docs.sentry.io/platforms/ruby/performance/instrumentation/opentelemetry/) ### Enriching Events - [Add more data to the current scope](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/scopes/) - [Add custom breadcrumbs](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/breadcrumbs/) - [Add contextual data](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/context/) - [Add tags](https://docs.sentry.io/platforms/ruby/guides/rack/enriching-events/tags/) ## Resources * [![Ruby docs](https://img.shields.io/badge/documentation-sentry.io-green.svg?label=ruby%20docs)](https://docs.sentry.io/platforms/ruby/) * [![Forum](https://img.shields.io/badge/forum-sentry-green.svg)](https://forum.sentry.io/c/sdks) * [![Discord Chat](https://img.shields.io/discord/621778831602221064?logo=discord&logoColor=ffffff&color=7389D8)](https://discord.gg/PXa5Apfe7K) * [![Stack Overflow](https://img.shields.io/badge/stack%20overflow-sentry-green.svg)](https://stackoverflow.com/questions/tagged/sentry) * [![Twitter Follow](https://img.shields.io/twitter/follow/getsentry?label=getsentry&style=social)](https://twitter.com/intent/follow?screen_name=getsentry) ## Contributing to the SDK Please make sure to read the [CONTRIBUTING.md](https://github.com/getsentry/sentry-ruby/blob/master/CONTRIBUTING.md) before making a pull request. Thanks to everyone who has contributed to this project so far. > [!WARNING] > Example and sample code in sentry-rails/examples and sentry-rails/spec/dummy is unmaintained. Sample code may contain security vulnerabilities, should never be used in production, and exists only for illustrative purposes. sentry-ruby-core-5.28.0/CHANGELOG.md0000644000004100000410000003615015067721773016724 0ustar www-datawww-data# Changelog Individual gem's changelog has been deprecated. Please check the [project changelog](https://github.com/getsentry/sentry-ruby/blob/master/CHANGELOG.md). ## 4.4.2 - Fix NoMethodError when SDK's dsn is nil [#1433](https://github.com/getsentry/sentry-ruby/pull/1433) - fix: Update protocol version to 7 [#1434](https://github.com/getsentry/sentry-ruby/pull/1434) - Fixes [#867](https://github.com/getsentry/sentry-ruby/issues/867) ## 4.4.1 - Apply patches when initializing the SDK [#1432](https://github.com/getsentry/sentry-ruby/pull/1432) ## 4.4.0 ### Features #### Support category-based rate limiting [#1336](https://github.com/getsentry/sentry-ruby/pull/1336) Sentry rate limits different types of events. And when rate limiting is enabled, it sends back a `429` response to the SDK. Currently, the SDK would then raise an error like this: ``` Unable to record event with remote Sentry server (Sentry::Error - the server responded with status 429 body: {"detail":"event rejected due to rate limit"}): ``` This change improves the SDK's handling on such responses by: - Not treating them as errors, so you don't see the noise anymore. - Halting event sending for a while according to the duration provided in the response. And warns you with a message like: ``` Envelope [event] not sent: Excluded by random sample ``` #### Record request span from Net::HTTP library [#1381](https://github.com/getsentry/sentry-ruby/pull/1381) Now any outgoing requests will be recorded as a tracing span. Example: net:http span example #### Record breadcrumb for Net::HTTP requests [#1394](https://github.com/getsentry/sentry-ruby/pull/1394) With the new `http_logger` breadcrumbs logger: ```ruby config.breadcrumbs_logger = [:http_logger] ``` The SDK now records a new `net.http` breadcrumb whenever the user makes a request with the `Net::HTTP` library. net http breadcrumb #### Support config.debug configuration option [#1400](https://github.com/getsentry/sentry-ruby/pull/1400) It'll determine whether the SDK should run in the debugging mode. Default is `false`. When set to true, SDK errors will be logged with backtrace. #### Add the third tracing state [#1402](https://github.com/getsentry/sentry-ruby/pull/1402) - `rate == 0` - Tracing enabled. Rejects all locally created transactions but respects sentry-trace. - `1 > rate > 0` - Tracing enabled. Samples locally created transactions with the rate and respects sentry-trace. - `rate < 0` or `rate > 1` - Tracing disabled. ### Refactorings - Let Transaction constructor take an optional hub argument [#1384](https://github.com/getsentry/sentry-ruby/pull/1384) - Introduce LoggingHelper [#1385](https://github.com/getsentry/sentry-ruby/pull/1385) - Raise exception if a Transaction is initialized without a hub [#1391](https://github.com/getsentry/sentry-ruby/pull/1391) - Make hub a required argument for Transaction constructor [#1401](https://github.com/getsentry/sentry-ruby/pull/1401) ### Bug Fixes - Check `Scope#set_context`'s value argument [#1415](https://github.com/getsentry/sentry-ruby/pull/1415) - Disable tracing if events are not allowed to be sent [#1421](https://github.com/getsentry/sentry-ruby/pull/1421) ## 4.3.2 - Correct type attribute's usages [#1354](https://github.com/getsentry/sentry-ruby/pull/1354) - Fix sampling decision precedence [#1335](https://github.com/getsentry/sentry-ruby/pull/1335) - Fix set_contexts [#1375](https://github.com/getsentry/sentry-ruby/pull/1375) - Use thread variable instead of fiber variable to store the hub [#1380](https://github.com/getsentry/sentry-ruby/pull/1380) - Fixes [#1374](https://github.com/getsentry/sentry-ruby/issues/1374) - Fix Span/Transaction's nesting issue [#1382](https://github.com/getsentry/sentry-ruby/pull/1382) - Fixes [#1372](https://github.com/getsentry/sentry-ruby/issues/1372) ## 4.3.1 - Add Sentry.set_context helper [#1337](https://github.com/getsentry/sentry-ruby/pull/1337) - Fix handle the case where the logger messages is not of String type [#1341](https://github.com/getsentry/sentry-ruby/pull/1341) - Don't report Sentry::ExternalError to Sentry [#1353](https://github.com/getsentry/sentry-ruby/pull/1353) - Sentry.add_breadcrumb should call Hub#add_breadcrumb [#1358](https://github.com/getsentry/sentry-ruby/pull/1358) - Fixes [#1357](https://github.com/getsentry/sentry-ruby/issues/1357) ## 4.3.0 ### Features - Allow configuring BreadcrumbBuffer's size limit [#1310](https://github.com/getsentry/sentry-ruby/pull/1310) ```ruby # the SDK will only store 10 breadcrumbs (default is 100) config.max_breadcrumbs = 10 ``` - Compress event payload by default [#1314](https://github.com/getsentry/sentry-ruby/pull/1314) ### Refatorings - Refactor interface construction [#1296](https://github.com/getsentry/sentry-ruby/pull/1296) - Refactor tracing implementation [#1309](https://github.com/getsentry/sentry-ruby/pull/1309) ### Bug Fixes - Improve SDK's error handling [#1298](https://github.com/getsentry/sentry-ruby/pull/1298) - Fixes [#1246](https://github.com/getsentry/sentry-ruby/issues/1246) and [#1289](https://github.com/getsentry/sentry-ruby/issues/1289) - Please read [#1290](https://github.com/getsentry/sentry-ruby/issues/1290) to see the full specification - Treat query string as pii too [#1302](https://github.com/getsentry/sentry-ruby/pull/1302) - Fixes [#1301](https://github.com/getsentry/sentry-ruby/issues/1301) - Ignore sentry-trace when tracing is not enabled [#1308](https://github.com/getsentry/sentry-ruby/pull/1308) - Fixes [#1307](https://github.com/getsentry/sentry-ruby/issues/1307) - Return nil from logger methods instead of breadcrumb buffer [#1299](https://github.com/getsentry/sentry-ruby/pull/1299) - Exceptions with nil message shouldn't cause issues [#1327](https://github.com/getsentry/sentry-ruby/pull/1327) - Fixes [#1323](https://github.com/getsentry/sentry-ruby/issues/1323) - Fix sampling decision with sentry-trace and add more tests [#1326](https://github.com/getsentry/sentry-ruby/pull/1326) ## 4.2.2 - Add thread_id to Exception interface [#1291](https://github.com/getsentry/sentry-ruby/pull/1291) - always convert trusted proxies to string [#1288](https://github.com/getsentry/sentry-ruby/pull/1288) - fixes [#1274](https://github.com/getsentry/sentry-ruby/issues/1274) ## 4.2.1 ### Bug Fixes - Ignore invalid values for sentry-trace header that don't match the required format [#1265](https://github.com/getsentry/sentry-ruby/pull/1265) - Transaction created by `.from_sentry_trace` should inherit sampling decision [#1269](https://github.com/getsentry/sentry-ruby/pull/1269) - Transaction's sample rate should accept any numeric value [#1278](https://github.com/getsentry/sentry-ruby/pull/1278) ## 4.2.0 ### Features - Add configuration option for trusted proxies [#1126](https://github.com/getsentry/sentry-ruby/pull/1126) ```ruby config.trusted_proxies = ["2.2.2.2"] # this ip address will be skipped when computing users' ip addresses ``` - Add ThreadsInterface [#1178](https://github.com/getsentry/sentry-ruby/pull/1178) an exception event that has the new threads interface - Support `config.before_breadcrumb` [#1253](https://github.com/getsentry/sentry-ruby/pull/1253) ```ruby # this will be called before every breadcrumb is added to the breadcrumb buffer # you can use it to # - remove the data you don't want to send # - add additional info to the data config.before_breadcrumb = lambda do |breadcrumb, hint| breadcrumb.message = "foo" breadcrumb end ``` - Add ability to have many post initialization callbacks [#1261](https://github.com/getsentry/sentry-ruby/pull/1261) ### Bug Fixes - Inspect exception cause by default & don't exclude ActiveJob::DeserializationError [#1180](https://github.com/getsentry/sentry-ruby/pull/1180) - Fixes [#1071](https://github.com/getsentry/sentry-ruby/issues/1071) ## 4.1.6 - Don't detect project root for Rails apps [#1243](https://github.com/getsentry/sentry-ruby/pull/1243) - Separate individual breadcrumb's data serialization [#1250](https://github.com/getsentry/sentry-ruby/pull/1250) - Capture sentry-trace with the correct http header key [#1260](https://github.com/getsentry/sentry-ruby/pull/1260) ## 4.1.5 - Serialize event hint before passing it to the async block [#1231](https://github.com/getsentry/sentry-ruby/pull/1231) - Fixes [#1227](https://github.com/getsentry/sentry-ruby/issues/1227) - Require the English library [#1233](https://github.com/getsentry/sentry-ruby/pull/1233) (by @dentarg) - Allow `Sentry.init` without block argument [#1235](https://github.com/getsentry/sentry-ruby/pull/1235) (by @sue445) ## 4.1.5-beta.1 - No change ## 4.1.5-beta.0 - Inline global method [#1213](https://github.com/getsentry/sentry-ruby/pull/1213) (by @tricknotes) - Event message and exception message should have a size limit [#1221](https://github.com/getsentry/sentry-ruby/pull/1221) - Add sentry-ruby-core as a more flexible option [#1226](https://github.com/getsentry/sentry-ruby/pull/1226) ## 4.1.4 - Fix headers serialization for sentry-ruby [#1197](https://github.com/getsentry/sentry-ruby/pull/1197) (by @moofkit) - Support capturing "sentry-trace" header from the middleware [#1205](https://github.com/getsentry/sentry-ruby/pull/1205) - Document public APIs on the Sentry module [#1208](https://github.com/getsentry/sentry-ruby/pull/1208) - Check the argument type of capture_exception and capture_event helpers [#1209](https://github.com/getsentry/sentry-ruby/pull/1209) ## 4.1.3 - rm reference to old constant (from Rails v2.2) [#1184](https://github.com/getsentry/sentry-ruby/pull/1184) - Use copied env in events [#1186](https://github.com/getsentry/sentry-ruby/pull/1186) - Fixes [#1183](https://github.com/getsentry/sentry-ruby/issues/1183) - Refactor RequestInterface [#1187](https://github.com/getsentry/sentry-ruby/pull/1187) - Supply event hint to async callback when possible [#1189](https://github.com/getsentry/sentry-ruby/pull/1189) - Fixes [#1188](https://github.com/getsentry/sentry-ruby/issues/1188) - Refactor stacktrace parsing and increase test coverage [#1190](https://github.com/getsentry/sentry-ruby/pull/1190) - Sentry.send_event should also take a hint [#1192](https://github.com/getsentry/sentry-ruby/pull/1192) ## 4.1.2 - before_send callback shouldn't be applied to transaction events [#1167](https://github.com/getsentry/sentry-ruby/pull/1167) - Transaction improvements [#1170](https://github.com/getsentry/sentry-ruby/pull/1170) - Support Ruby 3 [#1172](https://github.com/getsentry/sentry-ruby/pull/1172) - Add Integrable module [#1177](https://github.com/getsentry/sentry-ruby/pull/1177) ## 4.1.1 - Fix NoMethodError when sending is not allowed [#1161](https://github.com/getsentry/sentry-ruby/pull/1161) - Add notification for users who still use deprecated middlewares [#1160](https://github.com/getsentry/sentry-ruby/pull/1160) - Improve top-level api safety [#1162](https://github.com/getsentry/sentry-ruby/pull/1162) ## 4.1.0 - Separate rack integration [#1138](https://github.com/getsentry/sentry-ruby/pull/1138) - Fixes [#1136](https://github.com/getsentry/sentry-ruby/pull/1136) - Fix event sampling [#1144](https://github.com/getsentry/sentry-ruby/pull/1144) - Merge & rename 2 Rack middlewares [#1147](https://github.com/getsentry/sentry-ruby/pull/1147) - Fixes [#1153](https://github.com/getsentry/sentry-ruby/pull/1153) - Removed `Sentry::Rack::Tracing` middleware and renamed `Sentry::Rack::CaptureException` to `Sentry::Rack::CaptureExceptions`. - Deep-copy spans [#1148](https://github.com/getsentry/sentry-ruby/pull/1148) - Move span recorder related code from Span to Transaction [#1149](https://github.com/getsentry/sentry-ruby/pull/1149) - Check SDK initialization before running integrations [#1151](https://github.com/getsentry/sentry-ruby/pull/1151) - Fixes [#1145](https://github.com/getsentry/sentry-ruby/pull/1145) - Refactor transport [#1154](https://github.com/getsentry/sentry-ruby/pull/1154) - Implement non-blocking event sending [#1155](https://github.com/getsentry/sentry-ruby/pull/1155) - Added `background_worker_threads` configuration option. ### Noticeable Changes #### Middleware Changes `Sentry::Rack::Tracing` is now removed. And `Sentry::Rack::CaptureException` has been renamed to `Sentry::Rack::CaptureExceptions`. #### Events Are Sent Asynchronously `sentry-ruby` now sends events asynchronously by default. The functionality works like this: 1. When the SDK is initialized, a `Sentry::BackgroundWorker` will be initialized too. 2. When an event is passed to `Client#capture_event`, instead of sending it directly with `Client#send_event`, we'll let the worker do it. 3. The worker will have a number of threads. And the one of the idle threads will pick the job and call `Client#send_event`. - If all the threads are busy, new jobs will be put into a queue, which has a limit of 30. - If the queue size is exceeded, new events will be dropped. However, if you still prefer to use your own async approach, that's totally fine. If you have `config.async` set, the worker won't initialize a thread pool and won't be used either. This functionality also introduces a new `background_worker_threads` config option. It allows you to decide how many threads should the worker hold. By default, the value will be the number of the processors your machine has. For example, if your machine has 4 processors, the value would be 4. Of course, you can always override the value to fit your use cases, like ```ruby config.background_worker_threads = 5 # the worker will have 5 threads for sending events ``` You can also disable this new non-blocking behaviour by giving a `0` value: ```ruby config.background_worker_threads = 0 # all events will be sent synchronously ``` ## 4.0.1 - Add rake integration: [1137](https://github.com/getsentry/sentry-ruby/pull/1137) - Make Event's interfaces accessible: [1135](https://github.com/getsentry/sentry-ruby/pull/1135) - ActiveSupportLogger should only record events that has a started time: [1132](https://github.com/getsentry/sentry-ruby/pull/1132) ## 4.0.0 - Only documents update for the official release and no API/feature changes. ## 0.3.0 - Major API changes: [1123](https://github.com/getsentry/sentry-ruby/pull/1123) - Support event hint: [1122](https://github.com/getsentry/sentry-ruby/pull/1122) - Add request-id tag to events: [1120](https://github.com/getsentry/sentry-ruby/pull/1120) (by @tvec) ## 0.2.0 - Multiple fixes and refactorings - Tracing support ## 0.1.3 Fix require reference ## 0.1.2 - Fix: Fix async callback [1098](https://github.com/getsentry/sentry-ruby/pull/1098) - Refactor: Some code cleanup [1100](https://github.com/getsentry/sentry-ruby/pull/1100) - Refactor: Remove Event options [1101](https://github.com/getsentry/sentry-ruby/pull/1101) ## 0.1.1 - Feature: Allow passing custom scope to Hub#capture* helpers [1086](https://github.com/getsentry/sentry-ruby/pull/1086) ## 0.1.0 First version sentry-ruby-core-5.28.0/Makefile0000644000004100000410000000013215067721773016542 0ustar www-datawww-databuild: bundle install gem build sentry-ruby-core.gemspec gem build sentry-ruby.gemspec