Module: Lapsoss

Defined in:
lib/lapsoss.rb,
lib/lapsoss/event.rb,
lib/lapsoss/scope.rb,
lib/lapsoss/client.rb,
lib/lapsoss/router.rb,
lib/lapsoss/current.rb,
lib/lapsoss/railtie.rb,
lib/lapsoss/version.rb,
lib/lapsoss/pipeline.rb,
lib/lapsoss/registry.rb,
lib/lapsoss/scrubber.rb,
lib/lapsoss/breadcrumb.rb,
lib/lapsoss/validators.rb,
lib/lapsoss/http_client.rb,
lib/lapsoss/merged_scope.rb,
lib/lapsoss/adapters/base.rb,
lib/lapsoss/configuration.rb,
lib/lapsoss/fingerprinter.rb,
lib/lapsoss/sampling/base.rb,
lib/lapsoss/backtrace_frame.rb,
lib/lapsoss/middleware/base.rb,
lib/lapsoss/release_tracker.rb,
lib/lapsoss/runtime_context.rb,
lib/lapsoss/exclusion_filter.rb,
lib/lapsoss/pipeline_builder.rb,
lib/lapsoss/backtrace_processor.rb,
lib/lapsoss/sampling/rate_limiter.rb,
lib/lapsoss/rails_error_subscriber.rb,
lib/lapsoss/adapters/logger_adapter.rb,
lib/lapsoss/adapters/sentry_adapter.rb,
lib/lapsoss/backtrace_frame_factory.rb,
lib/lapsoss/middleware/rate_limiter.rb,
lib/lapsoss/adapters/bugsnag_adapter.rb,
lib/lapsoss/adapters/rollbar_adapter.rb,
lib/lapsoss/rails_controller_context.rb,
lib/lapsoss/sampling/uniform_sampler.rb,
lib/lapsoss/adapters/telebugs_adapter.rb,
lib/lapsoss/exception_backtrace_frame.rb,
lib/lapsoss/middleware/event_enricher.rb,
lib/lapsoss/adapters/appsignal_adapter.rb,
lib/lapsoss/middleware/release_tracker.rb,
lib/lapsoss/middleware/exception_filter.rb,
lib/lapsoss/adapters/insight_hub_adapter.rb,
lib/lapsoss/middleware/event_transformer.rb,
lib/lapsoss/middleware/metrics_collector.rb,
lib/lapsoss/rails_controller_transaction.rb,
lib/lapsoss/middleware/conditional_filter.rb,
lib/lapsoss/adapters/concerns/http_delivery.rb,
lib/lapsoss/adapters/concerns/level_mapping.rb,
lib/lapsoss/adapters/concerns/envelope_builder.rb

Defined Under Namespace

Modules: Adapters, Breadcrumb, Middleware, RailsControllerContext, RailsControllerTransaction, Sampling, Validators Classes: BacktraceFrameFactory, BacktraceProcessor, Client, Configuration, Current, DeliveryError, Error, ExceptionBacktraceFrame, ExclusionFilter, Fingerprinter, HttpClient, MergedScope, Pipeline, PipelineBuilder, RailsErrorSubscriber, Railtie, Registry, ReleaseTracker, Router, Scope, Scrubber

Constant Summary collapse

Event =

Immutable event structure using Ruby 3.3 Data class

Data.define(
  :type,           # :exception, :message, :transaction
  :level,          # :debug, :info, :warning, :error, :fatal
  :timestamp,
  :message,
  :exception,
  :context,
  :environment,
  :fingerprint,
  :backtrace_frames,
  :transaction     # Controller#action or task name where event occurred
) do
  # Factory method with smart defaults
  def self.build(type:, level: :info, **attributes)
    timestamp = attributes[:timestamp] || Time.now
    environment = attributes[:environment].presence || Lapsoss.configuration.environment
    context = attributes[:context] || {}

    # Process exception if present
    exception = attributes[:exception]
    message = attributes[:message].presence || exception&.message
    backtrace_frames = process_backtrace(exception) if exception

    # Generate fingerprint
    fingerprint = attributes.fetch(:fingerprint) {
      generate_fingerprint(type, message, exception, environment)
    }

    new(
      type: type,
      level: level,
      timestamp: timestamp,
      message: message,
      exception: exception,
      context: context,
      environment: environment,
      fingerprint: fingerprint,
      backtrace_frames: backtrace_frames,
      transaction: attributes[:transaction]
    )
  end

  # ActiveSupport::JSON serialization
  def as_json(options = nil)
    to_h.compact_blank.as_json(options)
  end

  def to_json(options = nil)
    ActiveSupport::JSON.encode(as_json(options))
  end

  # Helper methods
  def exception_type = exception&.class&.name

  def exception_message = exception&.message

  def has_exception? = exception.present?

  def has_backtrace? = backtrace_frames.present?

  def backtrace = exception&.backtrace

  def request_context = context.dig(:extra, :request) || context.dig(:extra, "request")

  def user_context = context[:user]

  def tags = context[:tags] || {}

  def extra = context[:extra] || {}

  def breadcrumbs = context[:breadcrumbs] || []

  # Apply data scrubbing
  def scrubbed
    scrubber = Scrubber.new(
      scrub_fields: Lapsoss.configuration.scrub_fields,
      scrub_all: Lapsoss.configuration.scrub_all,
      whitelist_fields: Lapsoss.configuration.whitelist_fields,
      randomize_scrub_length: Lapsoss.configuration.randomize_scrub_length
    )

    with(context: scrubber.scrub(context))
  end

  private

  def self.process_backtrace(exception)
    return nil unless exception&.backtrace.present?

    config = Lapsoss.configuration
    processor = BacktraceProcessor.new(config)
    processor.process_exception_backtrace(exception)
  end

  def self.generate_fingerprint(type, message, exception, environment)
    return nil unless Lapsoss.configuration.fingerprint_callback ||
                     Lapsoss.configuration.fingerprint_patterns.present?

    fingerprinter = Fingerprinter.new(
      custom_callback: Lapsoss.configuration.fingerprint_callback,
      patterns: Lapsoss.configuration.fingerprint_patterns,
      normalize_paths: Lapsoss.configuration.normalize_fingerprint_paths,
      normalize_ids: Lapsoss.configuration.normalize_fingerprint_ids,
      include_environment: Lapsoss.configuration.fingerprint_include_environment
    )

    # Create a temporary event-like object for fingerprinting
    temp_event = Struct.new(:type, :message, :exception, :environment).new(
      type,
      message,
      exception,
      environment
    )

    fingerprinter.generate_fingerprint(temp_event)
  end
end
VERSION =
"0.4.10"
BacktraceFrame =
Data.define(
  :filename,
  :absolute_path,
  :line_number,
  :method_name,
  :in_app,
  :raw_line,
  :function,
  :module_name,
  :code_context,
  :block_info
) do
  # Backward compatibility aliases
  alias_method :lineno, :line_number
  alias_method :raw, :raw_line

  def to_h
    {
      filename: filename,
      absolute_path: absolute_path,
      line_number: line_number,
      method: method_name,
      function: function,
      module: module_name,
      in_app: in_app,
      code_context: code_context,
      raw: raw_line
    }.compact
  end

  def add_code_context(processor, context_lines = 3)
    return unless line_number

    # Use absolute path if available, otherwise try filename
    path_to_read = absolute_path || filename
    return unless path_to_read

    with(code_context: processor.get_code_context(path_to_read, line_number, context_lines))
  end

  def valid?
    filename && (line_number.nil? || line_number >= 0)
  end

  def library_frame?
    !in_app
  end

  def app_frame?
    in_app
  end

  def excluded?(exclude_patterns = [])
    return false if exclude_patterns.empty?

    exclude_patterns.any? do |pattern|
      case pattern
      when Regexp
        raw_line.match?(pattern)
      when String
        raw_line.include?(pattern)
      else
        false
      end
    end
  end

  def relative_filename(load_paths = [])
    return filename unless filename && load_paths.any?

    # Try to make path relative to load paths
    load_paths.each do |load_path|
      if filename.start_with?(load_path)
        relative = filename.sub(%r{^#{Regexp.escape(load_path)}/?}, "")
        return relative unless relative.empty?
      end
    end

    filename
  end
end
RuntimeContext =

Boot-time context collection using Data class

Data.define(:os, :runtime, :modules, :server_name, :release) do
  def self.current
    @current ||= new(
      os: collect_os_context,
      runtime: collect_runtime_context,
      modules: collect_modules,
      server_name: collect_server_name,
      release: collect_release
    )
  end

  def self.collect_os_context
    {
      name: RbConfig::CONFIG["host_os"],
      version: `uname -r 2>/dev/null`.strip.presence,
      build: `uname -v 2>/dev/null`.strip.presence,
      kernel_version: `uname -a 2>/dev/null`.strip.presence,
      machine: RbConfig::CONFIG["host_cpu"]
    }.compact
  rescue
    { name: RbConfig::CONFIG["host_os"] }
  end

  def self.collect_runtime_context
    {
      name: "ruby",
      version: RUBY_DESCRIPTION
    }
  end

  def self.collect_modules
    return {} unless defined?(Bundler)

    Bundler.load.specs.each_with_object({}) do |spec, h|
      h[spec.name] = spec.version.to_s
    end
  rescue
    {}
  end

  def self.collect_server_name
    Socket.gethostname
  rescue
    "unknown"
  end

  def self.collect_release
    # Try to get from git if available
    if File.exist?(".git")
      `git rev-parse HEAD 2>/dev/null`.strip.presence
    end
  rescue
    nil
  end

  def to_contexts
    {
      os: os,
      runtime: runtime
    }
  end
end

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.clientObject (readonly)

Returns the value of attribute client.



26
27
28
# File 'lib/lapsoss.rb', line 26

def client
  @client
end

Class Method Details

.add_breadcrumb(message, type: :default, **metadata) ⇒ Object



72
73
74
# File 'lib/lapsoss.rb', line 72

def add_breadcrumb(message, type: :default, **)
  client.add_breadcrumb(message, type: type, **)
end

.capture_exception(exception, **context) ⇒ Object



39
40
41
42
43
# File 'lib/lapsoss.rb', line 39

def capture_exception(exception, **context)
  configuration.logger.debug "[LAPSOSS] capture_exception called for #{exception.class}"
  return unless client
  client.capture_exception(exception, **context)
end

.capture_message(message, level: :info, **context) ⇒ Object



45
46
47
# File 'lib/lapsoss.rb', line 45

def capture_message(message, level: :info, **context)
  client.capture_message(message, level: level, **context)
end

.configurationObject



28
29
30
# File 'lib/lapsoss.rb', line 28

def configuration
  @configuration ||= Configuration.new
end

.configure {|configuration| ... } ⇒ Object

Yields:



32
33
34
35
36
37
# File 'lib/lapsoss.rb', line 32

def configure
  yield(configuration)
  configuration.validate!
  configuration.apply!
  @client = Client.new(configuration)
end

.flush(timeout: 2) ⇒ Object



82
83
84
# File 'lib/lapsoss.rb', line 82

def flush(timeout: 2)
  client.flush(timeout: timeout)
end

.handle(error_class = StandardError, fallback: nil, **context) ⇒ Object

Handle errors and swallow them (like Rails.error.handle)



52
53
54
55
56
57
# File 'lib/lapsoss.rb', line 52

def handle(error_class = StandardError, fallback: nil, **context)
  yield
rescue error_class => e
  capture_exception(e, **context.merge(handled: true))
  fallback.respond_to?(:call) ? fallback.call : fallback
end

.record(error_class = StandardError, **context) ⇒ Object

Record errors and re-raise them (like Rails.error.record)



60
61
62
63
64
65
# File 'lib/lapsoss.rb', line 60

def record(error_class = StandardError, **context)
  yield
rescue error_class => e
  capture_exception(e, **context.merge(handled: false))
  raise
end

.report(exception, handled: true, **context) ⇒ Object

Report an error manually (like Rails.error.report)



68
69
70
# File 'lib/lapsoss.rb', line 68

def report(exception, handled: true, **context)
  capture_exception(exception, **context.merge(handled: handled))
end

.with_scope(context = {}) ⇒ Object



76
77
78
# File 'lib/lapsoss.rb', line 76

def with_scope(context = {}, &)
  client.with_scope(context, &)
end