Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,22 @@ Lapsoss.configure do |config|
end
```

### Pipeline & Sampling (optional)

```ruby
Lapsoss.configure do |config|
# Build a middleware pipeline for every event
config.configure_pipeline do |pipeline|
pipeline.sample(rate: 0.1) # Drop 90% of events

pipeline.enhance_user_context(
provider: ->(event, _) { current_user&.slice(:id, :email) },
privacy_mode: true # keep only ids
)
end
end
```

### Filtering Errors

You decide what errors to track. Lapsoss doesn't make assumptions:
Expand Down Expand Up @@ -351,6 +367,30 @@ Rails.application.config.filter_parameters += [:password, :token]
# Lapsoss automatically uses these filters - no additional configuration needed!
```

Additional controls:

```ruby
Lapsoss.configure do |config|
config.scrub_fields = %w[credit_card ssn api_key]
config.scrub_all = true # Mask everything by default
config.whitelist_fields = %w[user_id request_id] # Keep these fields as-is
config.randomize_scrub_length = true # Avoid fixed "[FILTERED]" marker
end
```

### Error Handler Hook

Get a callback when an adapter fails to deliver:

```ruby
Lapsoss.configure do |config|
config.error_handler = lambda do |adapter, event, error|
Rails.logger.error("Delivery failed for #{adapter.name}: #{error.message}")
Rails.logger.error(event.to_h) if Rails.env.development?
end
end
```

### Custom Fingerprinting

Control how errors are grouped:
Expand Down
18 changes: 18 additions & 0 deletions lib/lapsoss.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,24 @@ def flush(timeout: 2)
client.flush(timeout: timeout)
end

def call_error_handler(adapter:, event:, error:)
handler = configuration.error_handler
return unless handler

case handler.arity
when 3
handler.call(adapter, event, error)
when 2
handler.call(event, error)
when 1, 0, -1
handler.call(error)
else
handler.call(adapter, event, error)
end
rescue => handler_error
configuration.logger&.error("[LAPSOSS] Error handler failed: #{handler_error.message}")
end

delegate :shutdown, to: :client
end
end
26 changes: 19 additions & 7 deletions lib/lapsoss/adapters/concerns/http_delivery.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ module HttpDelivery
extend ActiveSupport::Concern

included do
class_attribute :api_endpoint, instance_writer: false
class_attribute :api_path, default: "/", instance_writer: false
# Instance-level endpoint configuration for adapter isolation
attr_reader :api_endpoint, :api_path

# Memoized git info using AS
mattr_accessor :git_info_cache, default: {}
Expand All @@ -39,7 +39,7 @@ def deliver(event)
handle_response(response)
end
rescue => error
handle_delivery_error(error)
handle_delivery_error(error, event)
end

# Common headers for all adapters
Expand Down Expand Up @@ -101,18 +101,30 @@ def handle_client_error(response)
raise DeliveryError.new("Client error: #{message}", response: response)
end

def handle_delivery_error(error)
def handle_delivery_error(error, event = nil)
ActiveSupport::Notifications.instrument("error.lapsoss",
adapter: self.class.name,
error: error.class.name,
message: error.message
)

Lapsoss.configuration.logger&.error("[#{self.class.name}] Delivery failed: #{error.message}")
Lapsoss.configuration.error_handler&.call(error)
Lapsoss.call_error_handler(adapter: self, event: event, error: error)
mark_error_handled(error)

raise error if error.is_a?(DeliveryError)
raise DeliveryError.new("Delivery failed: #{error.message}", cause: error)
if error.is_a?(DeliveryError)
raise error
else
delivery_error = DeliveryError.new("Delivery failed: #{error.message}", cause: error)
mark_error_handled(delivery_error)
raise delivery_error
end
end

def mark_error_handled(error)
error.instance_variable_set(:@lapsoss_error_handled, true)
rescue StandardError
# If setting the flag fails, we still continue
end

private
Expand Down
6 changes: 4 additions & 2 deletions lib/lapsoss/adapters/rollbar_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@ class RollbarAdapter < Base
include Concerns::HttpDelivery

self.level_mapping_type = :rollbar
self.api_endpoint = "https://api.rollbar.com"
self.api_path = "/api/1/item/"
DEFAULT_API_ENDPOINT = "https://api.rollbar.com"
DEFAULT_API_PATH = "/api/1/item/"

def initialize(name, settings = {})
super
@api_endpoint = DEFAULT_API_ENDPOINT
@api_path = DEFAULT_API_PATH
@access_token = settings[:access_token].presence || ENV["ROLLBAR_ACCESS_TOKEN"]

if @access_token.blank?
Expand Down
4 changes: 2 additions & 2 deletions lib/lapsoss/adapters/sentry_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,8 @@ def capabilities

def setup_endpoint
uri = URI.parse(@settings[:dsn])
self.class.api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}"
self.class.api_path = build_api_path(uri)
@api_endpoint = "#{uri.scheme}://#{uri.host}:#{uri.port}"
@api_path = build_api_path(uri)
end

def build_api_path(uri)
Expand Down
8 changes: 4 additions & 4 deletions lib/lapsoss/adapters/telebugs_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ def setup_endpoint
debug_log "[TELEBUGS ENDPOINT] Setting endpoint: #{endpoint}"
debug_log "[TELEBUGS ENDPOINT] Setting API path: #{api_path}"

self.class.api_endpoint = endpoint
self.class.api_path = api_path
@api_endpoint = endpoint
@api_path = api_path
end

public
Expand All @@ -63,8 +63,8 @@ def setup_endpoint
def capture(event)
debug_log "[TELEBUGS DEBUG] Capture called for event: #{event.type}"
debug_log "[TELEBUGS DEBUG] DSN configured: #{@dsn.inspect}"
debug_log "[TELEBUGS DEBUG] Endpoint: #{self.class.api_endpoint}"
debug_log "[TELEBUGS DEBUG] API Path: #{self.class.api_path}"
debug_log "[TELEBUGS DEBUG] Endpoint: #{@api_endpoint}"
debug_log "[TELEBUGS DEBUG] API Path: #{@api_path}"

result = super(event)
debug_log "[TELEBUGS DEBUG] Event sent successfully, response: #{result.inspect}"
Expand Down
34 changes: 26 additions & 8 deletions lib/lapsoss/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,16 @@ def initialize(configuration)
# The Concurrent::FixedThreadPool had issues in Rails development mode
end

def capture_exception(exception, **context)
def capture_exception(exception, level: :error, **context)
return nil unless @configuration.enabled

extra_context = context.delete(:context)
with_scope(context) do |scope|
event = Event.build(
type: :exception,
level: :error,
level: level,
exception: exception,
context: scope_to_context(scope),
context: merge_context(scope_to_context(scope), extra_context),
transaction: scope.transaction_name
)
capture_event(event)
Expand All @@ -28,12 +29,13 @@ def capture_exception(exception, **context)
def capture_message(message, level: :info, **context)
return nil unless @configuration.enabled

extra_context = context.delete(:context)
with_scope(context) do |scope|
event = Event.build(
type: :message,
level: level,
message: message,
context: scope_to_context(scope),
context: merge_context(scope_to_context(scope), extra_context),
transaction: scope.transaction_name
)
capture_event(event)
Expand Down Expand Up @@ -86,6 +88,11 @@ def capture_event(event)
return nil unless event
end

if (filter = @configuration.exclusion_filter) && filter.should_exclude?(event)
@configuration.logger.debug("[LAPSOSS] Event excluded by configured exclusion_filter")
return nil
end

event = run_before_send(event)
return nil unless event

Expand Down Expand Up @@ -121,12 +128,23 @@ def run_before_send(event)
end

def scope_to_context(scope)
defaults = @configuration.default_context
{
tags: scope.tags,
user: scope.user,
extra: scope.extra,
tags: (defaults[:tags] || {}).merge(scope.tags),
user: (defaults[:user] || {}).merge(scope.user),
extra: (defaults[:extra] || {}).merge(scope.extra),
breadcrumbs: scope.breadcrumbs
}
}.tap do |ctx|
ctx[:environment] ||= @configuration.environment if @configuration.environment
end
end

def merge_context(scope_context, extra_context)
return scope_context unless extra_context

merged = scope_context.dup
merged[:context] = (scope_context[:context] || {}).merge(extra_context)
merged
end

def handle_capture_error(error)
Expand Down
5 changes: 4 additions & 1 deletion lib/lapsoss/configuration.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ class Configuration
:backtrace_context_lines, :backtrace_in_app_patterns, :backtrace_exclude_patterns,
:backtrace_strip_load_path, :backtrace_max_frames, :backtrace_enable_code_context,
:enable_pipeline, :pipeline_builder, :sampling_strategy,
:skip_rails_cache_errors, :force_sync_http, :capture_request_context
:skip_rails_cache_errors, :force_sync_http, :capture_request_context,
:exclusion_filter
attr_reader :fingerprint_callback, :environment, :before_send, :sample_rate, :error_handler, :transport_timeout,
:transport_max_retries, :transport_initial_backoff, :transport_max_backoff, :transport_backoff_multiplier, :transport_ssl_verify, :default_context, :adapter_configs

Expand Down Expand Up @@ -65,6 +66,8 @@ def initialize
@force_sync_http = false
# Capture request context in middleware
@capture_request_context = true
# Exclusion filter
@exclusion_filter = nil
end

# Register a named adapter configuration
Expand Down
32 changes: 32 additions & 0 deletions lib/lapsoss/middleware/sample_filter.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# frozen_string_literal: true

module Lapsoss
module Middleware
# Drops events based on sampling strategy or rate.
class SampleFilter < Base
def initialize(app, sample_rate: 1.0, sample_callback: nil, sampler: nil)
super(app)
@sampler =
sampler ||
sample_callback ||
Sampling::UniformSampler.new(sample_rate)
end

def call(event, hint = {})
return nil unless sample?(event, hint)

@app.call(event, hint)
end

private

def sample?(event, hint)
if @sampler.respond_to?(:sample?)
@sampler.sample?(event, hint)
else
@sampler.call(event, hint)
end
end
end
end
end
44 changes: 44 additions & 0 deletions lib/lapsoss/middleware/user_context_enhancer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# frozen_string_literal: true

module Lapsoss
module Middleware
# Adds user info to the event context using a callable provider.
class UserContextEnhancer < Base
def initialize(app, user_provider:, privacy_mode: false)
super(app)
@user_provider = user_provider
@privacy_mode = privacy_mode
end

def call(event, hint = {})
user_data = fetch_user(event, hint)
return @app.call(event, hint) unless user_data

merged_user = (event.context[:user] || {}).merge(user_data)
merged_user = sanitize_for_privacy(merged_user) if @privacy_mode

updated_context = event.context.merge(user: merged_user)
@app.call(event.with(context: updated_context), hint)
end

private

def fetch_user(event, hint)
return nil unless @user_provider

if @user_provider.respond_to?(:call)
@user_provider.call(event, hint)
elsif @user_provider.is_a?(Hash)
@user_provider
end
rescue StandardError
nil
end

def sanitize_for_privacy(user_hash)
allowed_keys = %i[id uuid user_id]
user_hash.slice(*allowed_keys)
end
end
end
end
5 changes: 3 additions & 2 deletions lib/lapsoss/router.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ def handle_adapter_error(adapter, event, error)
)

# Call error handler if configured
handler = Lapsoss.configuration.error_handler
handler&.call(adapter, event, error)
handled = error.instance_variable_defined?(:@lapsoss_error_handled) &&
error.instance_variable_get(:@lapsoss_error_handled)
Lapsoss.call_error_handler(adapter: adapter, event: event, error: error) unless handled
end
end
end
Expand Down
Loading