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.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +22 -22
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
  6. data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
  7. data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
  8. data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
  9. data/lib/lapsoss/backtrace_frame.rb +37 -206
  10. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  11. data/lib/lapsoss/backtrace_processor.rb +27 -23
  12. data/lib/lapsoss/client.rb +2 -4
  13. data/lib/lapsoss/configuration.rb +28 -32
  14. data/lib/lapsoss/current.rb +10 -2
  15. data/lib/lapsoss/event.rb +28 -5
  16. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  17. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  18. data/lib/lapsoss/exclusion_filter.rb +0 -273
  19. data/lib/lapsoss/exclusion_presets.rb +249 -0
  20. data/lib/lapsoss/fingerprinter.rb +28 -28
  21. data/lib/lapsoss/http_client.rb +8 -8
  22. data/lib/lapsoss/merged_scope.rb +63 -0
  23. data/lib/lapsoss/middleware/base.rb +15 -0
  24. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  25. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  26. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  27. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  28. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  29. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  30. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  31. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  32. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  33. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  34. data/lib/lapsoss/pipeline.rb +0 -68
  35. data/lib/lapsoss/pipeline_builder.rb +69 -0
  36. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  37. data/lib/lapsoss/rails_middleware.rb +78 -0
  38. data/lib/lapsoss/railtie.rb +22 -50
  39. data/lib/lapsoss/registry.rb +18 -5
  40. data/lib/lapsoss/release_providers.rb +110 -0
  41. data/lib/lapsoss/release_tracker.rb +159 -232
  42. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  43. data/lib/lapsoss/sampling/base.rb +11 -0
  44. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  45. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  46. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  47. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  48. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  49. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  50. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  51. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  52. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  53. data/lib/lapsoss/scope.rb +12 -48
  54. data/lib/lapsoss/scrubber.rb +7 -7
  55. data/lib/lapsoss/user_context.rb +30 -203
  56. data/lib/lapsoss/user_context_integrations.rb +39 -0
  57. data/lib/lapsoss/user_context_middleware.rb +50 -0
  58. data/lib/lapsoss/user_context_provider.rb +93 -0
  59. data/lib/lapsoss/utils.rb +13 -0
  60. data/lib/lapsoss/validators.rb +15 -15
  61. data/lib/lapsoss/version.rb +1 -1
  62. data/lib/lapsoss.rb +3 -3
  63. metadata +60 -7
  64. data/lib/lapsoss/middleware.rb +0 -345
  65. 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
@@ -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 'lapsoss.configure' do |_app|
8
+ initializer "lapsoss.configure" do |_app|
8
9
  Lapsoss.configure do |config|
9
- config.environment ||= Rails.env
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
- config.release ||= Rails.application.config.try(:release)
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: 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 'lapsoss.add_middleware', after: 'lapsoss.configure' do |app|
22
- app.middleware.insert_before ActionDispatch::ShowExceptions, Lapsoss::Middleware
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
- Lapsoss.capture_exception(
42
- error,
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
- private
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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'singleton'
4
- require 'concurrent'
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
- else
42
- adapter.class.name.split('::').last.to_sym
43
- end
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