lapsoss 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +153 -733
- data/lib/lapsoss/adapters/appsignal_adapter.rb +22 -22
- data/lib/lapsoss/adapters/base.rb +0 -3
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
- data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
- data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
- data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
- data/lib/lapsoss/backtrace_frame.rb +37 -206
- data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
- data/lib/lapsoss/backtrace_processor.rb +27 -23
- data/lib/lapsoss/client.rb +2 -4
- data/lib/lapsoss/configuration.rb +28 -32
- data/lib/lapsoss/current.rb +10 -2
- data/lib/lapsoss/event.rb +28 -5
- data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
- data/lib/lapsoss/exclusion_configuration.rb +30 -0
- data/lib/lapsoss/exclusion_filter.rb +0 -273
- data/lib/lapsoss/exclusion_presets.rb +249 -0
- data/lib/lapsoss/fingerprinter.rb +28 -28
- data/lib/lapsoss/http_client.rb +8 -8
- data/lib/lapsoss/merged_scope.rb +63 -0
- data/lib/lapsoss/middleware/base.rb +15 -0
- data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
- data/lib/lapsoss/middleware/event_enricher.rb +19 -0
- data/lib/lapsoss/middleware/event_transformer.rb +19 -0
- data/lib/lapsoss/middleware/exception_filter.rb +43 -0
- data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
- data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
- data/lib/lapsoss/middleware/release_tracker.rb +117 -0
- data/lib/lapsoss/middleware/sample_filter.rb +23 -0
- data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
- data/lib/lapsoss/pipeline.rb +0 -68
- data/lib/lapsoss/pipeline_builder.rb +69 -0
- data/lib/lapsoss/rails_error_subscriber.rb +42 -0
- data/lib/lapsoss/rails_middleware.rb +78 -0
- data/lib/lapsoss/railtie.rb +22 -50
- data/lib/lapsoss/registry.rb +18 -5
- data/lib/lapsoss/release_providers.rb +110 -0
- data/lib/lapsoss/release_tracker.rb +159 -232
- data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
- data/lib/lapsoss/sampling/base.rb +11 -0
- data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
- data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
- data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
- data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
- data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
- data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
- data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
- data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
- data/lib/lapsoss/scope.rb +12 -48
- data/lib/lapsoss/scrubber.rb +7 -7
- data/lib/lapsoss/user_context.rb +30 -203
- data/lib/lapsoss/user_context_integrations.rb +39 -0
- data/lib/lapsoss/user_context_middleware.rb +50 -0
- data/lib/lapsoss/user_context_provider.rb +93 -0
- data/lib/lapsoss/utils.rb +13 -0
- data/lib/lapsoss/validators.rb +15 -15
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +3 -3
- metadata +60 -7
- data/lib/lapsoss/middleware.rb +0 -345
- data/lib/lapsoss/sampling.rb +0 -328
@@ -0,0 +1,78 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lapsoss
|
4
|
+
class RailsMiddleware
|
5
|
+
def initialize(app)
|
6
|
+
@app = app
|
7
|
+
end
|
8
|
+
|
9
|
+
def call(env)
|
10
|
+
Lapsoss::Current.with_clean_scope do
|
11
|
+
# Add request context to current scope
|
12
|
+
if Lapsoss.configuration.capture_request_context
|
13
|
+
Rails.logger.debug "[Lapsoss] Adding request context" if Rails.env.test?
|
14
|
+
add_request_context(env)
|
15
|
+
end
|
16
|
+
|
17
|
+
begin
|
18
|
+
@app.call(env)
|
19
|
+
rescue Exception => e
|
20
|
+
Rails.logger.debug { "[Lapsoss] Capturing exception: #{e.class} - #{e.message}" } if Rails.env.test?
|
21
|
+
# Capture the exception
|
22
|
+
Lapsoss.capture_exception(e)
|
23
|
+
# Re-raise the exception to maintain Rails error handling
|
24
|
+
raise
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def add_request_context(env)
|
32
|
+
request = Rack::Request.new(env)
|
33
|
+
|
34
|
+
return unless Lapsoss::Current.scope
|
35
|
+
|
36
|
+
Lapsoss::Current.scope.set_context("request", {
|
37
|
+
method: request.request_method,
|
38
|
+
url: request.url,
|
39
|
+
path: request.path,
|
40
|
+
query_string: request.query_string,
|
41
|
+
headers: extract_headers(env),
|
42
|
+
ip: request.ip,
|
43
|
+
user_agent: request.user_agent,
|
44
|
+
referer: request.referer,
|
45
|
+
request_id: env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
|
46
|
+
})
|
47
|
+
|
48
|
+
# Add user context if available
|
49
|
+
return unless env["warden"]&.user
|
50
|
+
|
51
|
+
user = env["warden"].user
|
52
|
+
Lapsoss::Current.scope.set_user(
|
53
|
+
id: user.id,
|
54
|
+
email: user.respond_to?(:email) ? user.email : nil
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
def extract_headers(env)
|
59
|
+
headers = {}
|
60
|
+
|
61
|
+
env.each do |key, value|
|
62
|
+
if key.start_with?("HTTP_") && FILTERED_HEADERS.exclude?(key)
|
63
|
+
header_name = key.sub(/^HTTP_/, "").split("_").map(&:capitalize).join("-")
|
64
|
+
headers[header_name] = value
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
headers
|
69
|
+
end
|
70
|
+
|
71
|
+
FILTERED_HEADERS = %w[
|
72
|
+
HTTP_AUTHORIZATION
|
73
|
+
HTTP_COOKIE
|
74
|
+
HTTP_X_API_KEY
|
75
|
+
HTTP_X_AUTH_TOKEN
|
76
|
+
].freeze
|
77
|
+
end
|
78
|
+
end
|
data/lib/lapsoss/railtie.rb
CHANGED
@@ -2,71 +2,43 @@
|
|
2
2
|
|
3
3
|
module Lapsoss
|
4
4
|
class Railtie < Rails::Railtie
|
5
|
+
Rails.logger.debug "[Lapsoss] Railtie loaded" if ENV["DEBUG_LAPSOSS"]
|
5
6
|
config.lapsoss = ActiveSupport::OrderedOptions.new
|
6
7
|
|
7
|
-
initializer
|
8
|
+
initializer "lapsoss.configure" do |_app|
|
8
9
|
Lapsoss.configure do |config|
|
9
|
-
|
10
|
+
# Use rails_app_version gem if available, otherwise fallback to Rails defaults
|
11
|
+
config.environment ||= if Rails.application.respond_to?(:env)
|
12
|
+
Rails.application.env
|
13
|
+
else
|
14
|
+
Rails.env
|
15
|
+
end
|
16
|
+
|
10
17
|
config.logger ||= Rails.logger
|
11
|
-
|
18
|
+
|
19
|
+
config.release ||= if Rails.application.respond_to?(:version)
|
20
|
+
Rails.application.version.to_s
|
21
|
+
else
|
22
|
+
Rails.application.config.try(:release)
|
23
|
+
end
|
12
24
|
|
13
25
|
# Set default tags
|
14
26
|
config.default_tags = {
|
15
|
-
rails_env:
|
27
|
+
rails_env: config.environment,
|
16
28
|
rails_version: Rails.version
|
17
29
|
}
|
18
30
|
end
|
19
31
|
end
|
20
32
|
|
21
|
-
initializer
|
22
|
-
|
23
|
-
end
|
24
|
-
|
25
|
-
initializer 'lapsoss.rails_error_subscriber', after: 'lapsoss.add_middleware' do |app|
|
26
|
-
app.executor.error_reporter.subscribe(Lapsoss::RailsErrorSubscriber.new)
|
27
|
-
end
|
28
|
-
|
29
|
-
rake_tasks do
|
30
|
-
# Add any Lapsoss-specific rake tasks here
|
31
|
-
end
|
32
|
-
end
|
33
|
-
|
34
|
-
class RailsErrorSubscriber
|
35
|
-
def report(error, handled:, severity:, context:, source: nil)
|
36
|
-
# Skip certain framework errors
|
37
|
-
return if skip_error?(error, source)
|
38
|
-
|
39
|
-
level = map_severity(severity)
|
33
|
+
initializer "lapsoss.add_middleware" do |app|
|
34
|
+
require "lapsoss/rails_middleware"
|
40
35
|
|
41
|
-
|
42
|
-
|
43
|
-
level: level,
|
44
|
-
tags: {
|
45
|
-
handled: handled,
|
46
|
-
source: source || 'rails'
|
47
|
-
},
|
48
|
-
context: context
|
49
|
-
)
|
36
|
+
# Use config.middleware to ensure it's added during initialization
|
37
|
+
app.config.middleware.use Lapsoss::RailsMiddleware
|
50
38
|
end
|
51
39
|
|
52
|
-
|
53
|
-
|
54
|
-
def skip_error?(error, source)
|
55
|
-
# Skip cache-related Redis errors if configured to do so
|
56
|
-
if Lapsoss.configuration.skip_rails_cache_errors && source&.include?('cache') && error.is_a?(Redis::CannotConnectError)
|
57
|
-
return true
|
58
|
-
end
|
59
|
-
|
60
|
-
false
|
61
|
-
end
|
62
|
-
|
63
|
-
def map_severity(severity)
|
64
|
-
case severity
|
65
|
-
when :error then :error
|
66
|
-
when :warning then :warning
|
67
|
-
when :info then :info
|
68
|
-
else :error
|
69
|
-
end
|
40
|
+
initializer "lapsoss.rails_error_subscriber", after: "lapsoss.add_middleware" do |app|
|
41
|
+
app.executor.error_reporter.subscribe(Lapsoss::RailsErrorSubscriber.new)
|
70
42
|
end
|
71
43
|
end
|
72
44
|
end
|
data/lib/lapsoss/registry.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
3
|
+
require "singleton"
|
4
|
+
require "concurrent"
|
5
5
|
|
6
6
|
module Lapsoss
|
7
7
|
class Registry
|
@@ -36,11 +36,17 @@ module Lapsoss
|
|
36
36
|
#
|
37
37
|
# @param adapter [Adapter] The adapter instance to register
|
38
38
|
def register_adapter(adapter)
|
39
|
+
# Ensure we're getting an adapter instance, not a config hash
|
40
|
+
raise ArgumentError, "Expected an adapter instance, got #{adapter.class}" unless adapter.respond_to?(:capture)
|
41
|
+
|
39
42
|
name = if adapter.respond_to?(:name) && adapter.name
|
40
43
|
adapter.name.to_sym
|
41
|
-
|
42
|
-
adapter.class.name.split(
|
43
|
-
|
44
|
+
elsif adapter.class.name
|
45
|
+
adapter.class.name.split("::").last.to_sym
|
46
|
+
else
|
47
|
+
# Generate a unique name if class name is nil (anonymous class)
|
48
|
+
:"adapter_#{adapter.object_id}"
|
49
|
+
end
|
44
50
|
@adapters[name] = adapter
|
45
51
|
end
|
46
52
|
|
@@ -98,6 +104,13 @@ module Lapsoss
|
|
98
104
|
@adapters.keys
|
99
105
|
end
|
100
106
|
|
107
|
+
# Get all registered adapters (alias for all)
|
108
|
+
#
|
109
|
+
# @return [Array<Adapter>] All adapter instances
|
110
|
+
def adapters
|
111
|
+
all
|
112
|
+
end
|
113
|
+
|
101
114
|
private
|
102
115
|
|
103
116
|
# Resolve adapter type to class
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
module Lapsoss
|
6
|
+
# Built-in release providers for common scenarios
|
7
|
+
class ReleaseProviders
|
8
|
+
def self.from_file(file_path)
|
9
|
+
lambda do
|
10
|
+
return nil unless File.exist?(file_path)
|
11
|
+
|
12
|
+
content = File.read(file_path).strip
|
13
|
+
return nil if content.empty?
|
14
|
+
|
15
|
+
# Try to parse as JSON first
|
16
|
+
begin
|
17
|
+
JSON.parse(content)
|
18
|
+
rescue JSON::ParserError
|
19
|
+
# Treat as plain text version
|
20
|
+
{ version: content }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.from_ruby_constant(constant_name)
|
26
|
+
lambda do
|
27
|
+
constant = Object.const_get(constant_name)
|
28
|
+
{ version: constant.to_s }
|
29
|
+
rescue NameError
|
30
|
+
nil
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.from_gemfile_lock
|
35
|
+
lambda do
|
36
|
+
return nil unless File.exist?("Gemfile.lock")
|
37
|
+
|
38
|
+
content = File.read("Gemfile.lock")
|
39
|
+
|
40
|
+
# Extract gems with versions
|
41
|
+
gems = {}
|
42
|
+
content.scan(/^\s{4}(\w+)\s+\(([^)]+)\)/).each do |name, version|
|
43
|
+
gems[name] = version
|
44
|
+
end
|
45
|
+
|
46
|
+
{ gems: gems }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.from_package_json
|
51
|
+
lambda do
|
52
|
+
return nil unless File.exist?("package.json")
|
53
|
+
|
54
|
+
begin
|
55
|
+
package_info = JSON.parse(File.read("package.json"))
|
56
|
+
{
|
57
|
+
version: package_info["version"],
|
58
|
+
name: package_info["name"],
|
59
|
+
dependencies: package_info["dependencies"]&.keys
|
60
|
+
}.compact
|
61
|
+
rescue JSON::ParserError
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def self.from_rails_application
|
68
|
+
lambda do
|
69
|
+
return nil unless defined?(Rails) && Rails.respond_to?(:application)
|
70
|
+
|
71
|
+
app = Rails.application
|
72
|
+
return nil unless app
|
73
|
+
|
74
|
+
info = {
|
75
|
+
rails_version: Rails.version,
|
76
|
+
environment: Rails.env,
|
77
|
+
root: Rails.root.to_s
|
78
|
+
}
|
79
|
+
|
80
|
+
# Get application version if defined
|
81
|
+
info[:app_version] = app.class.version if app.class.respond_to?(:version)
|
82
|
+
|
83
|
+
# Get application name
|
84
|
+
info[:app_name] = app.class.name if app.class.respond_to?(:name)
|
85
|
+
|
86
|
+
info
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.from_capistrano
|
91
|
+
lambda do
|
92
|
+
# Check for Capistrano deployment files
|
93
|
+
%w[REVISION current/REVISION].each do |file|
|
94
|
+
next unless File.exist?(file)
|
95
|
+
|
96
|
+
revision = File.read(file).strip
|
97
|
+
next if revision.empty?
|
98
|
+
|
99
|
+
return {
|
100
|
+
revision: revision,
|
101
|
+
deployed_at: File.mtime(file),
|
102
|
+
deployment_method: "capistrano"
|
103
|
+
}
|
104
|
+
end
|
105
|
+
|
106
|
+
nil
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|