lapsoss 0.4.0 → 0.4.4

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 (38) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +195 -7
  3. data/lib/lapsoss/adapters/concerns/level_mapping.rb +1 -0
  4. data/lib/lapsoss/adapters/telebug_adapter.rb +58 -0
  5. data/lib/lapsoss/client.rb +1 -3
  6. data/lib/lapsoss/configuration.rb +14 -17
  7. data/lib/lapsoss/fingerprinter.rb +52 -47
  8. data/lib/lapsoss/middleware/release_tracker.rb +11 -98
  9. data/lib/lapsoss/pipeline_builder.rb +2 -2
  10. data/lib/lapsoss/rails_middleware.rb +2 -2
  11. data/lib/lapsoss/railtie.rb +14 -3
  12. data/lib/lapsoss/registry.rb +7 -7
  13. data/lib/lapsoss/router.rb +1 -3
  14. data/lib/lapsoss/scrubber.rb +15 -152
  15. data/lib/lapsoss/validators.rb +48 -112
  16. data/lib/lapsoss/version.rb +1 -1
  17. data/lib/lapsoss.rb +23 -0
  18. metadata +2 -21
  19. data/lib/lapsoss/exclusion_configuration.rb +0 -30
  20. data/lib/lapsoss/exclusion_presets.rb +0 -249
  21. data/lib/lapsoss/middleware/sample_filter.rb +0 -23
  22. data/lib/lapsoss/middleware/sampling_middleware.rb +0 -18
  23. data/lib/lapsoss/middleware/user_context_enhancer.rb +0 -46
  24. data/lib/lapsoss/release_providers.rb +0 -110
  25. data/lib/lapsoss/sampling/adaptive_sampler.rb +0 -46
  26. data/lib/lapsoss/sampling/composite_sampler.rb +0 -26
  27. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +0 -30
  28. data/lib/lapsoss/sampling/exception_type_sampler.rb +0 -44
  29. data/lib/lapsoss/sampling/health_based_sampler.rb +0 -19
  30. data/lib/lapsoss/sampling/sampling_factory.rb +0 -69
  31. data/lib/lapsoss/sampling/time_based_sampler.rb +0 -44
  32. data/lib/lapsoss/sampling/user_based_sampler.rb +0 -42
  33. data/lib/lapsoss/user_context.rb +0 -185
  34. data/lib/lapsoss/user_context_integrations.rb +0 -39
  35. data/lib/lapsoss/user_context_middleware.rb +0 -50
  36. data/lib/lapsoss/user_context_provider.rb +0 -93
  37. data/lib/lapsoss/utils.rb +0 -11
  38. data/lib/tasks/cassettes.rake +0 -50
@@ -3,114 +3,27 @@
3
3
  module Lapsoss
4
4
  module Middleware
5
5
  class ReleaseTracker < Base
6
- def initialize(app, release_provider: nil)
6
+ def initialize(app, release: nil)
7
7
  super(app)
8
- @release_provider = release_provider
8
+ @release = release
9
9
  end
10
10
 
11
11
  def call(event, hint = {})
12
- add_release_info(event, hint)
12
+ if release = detect_release
13
+ event.context[:release] = release
14
+ end
13
15
  @app.call(event, hint)
14
16
  end
15
17
 
16
18
  private
17
19
 
18
- def add_release_info(event, hint)
19
- release_info = @release_provider&.call(event, hint) || auto_detect_release
20
-
21
- event.context[:release] = release_info if release_info
22
- end
23
-
24
- def auto_detect_release
25
- release_info = {}
26
-
27
- # Try to detect Git information
28
- if git_info = detect_git_info
29
- release_info.merge!(git_info)
30
- end
31
-
32
- # Try to detect deployment info
33
- if deployment_info = detect_deployment_info
34
- release_info.merge!(deployment_info)
35
- end
36
-
37
- release_info.empty? ? nil : release_info
38
- end
39
-
40
- def detect_git_info
41
- return nil unless File.exist?(".git")
42
-
43
- begin
44
- # Get current commit SHA
45
- commit_sha = `git rev-parse HEAD`.strip
46
- return nil if commit_sha.empty?
47
-
48
- # Get branch name
49
- branch = `git rev-parse --abbrev-ref HEAD`.strip
50
- branch = nil if branch.empty? || branch == "HEAD"
51
-
52
- # Get commit timestamp
53
- commit_time = `git log -1 --format=%ct`.strip
54
- commit_timestamp = commit_time.empty? ? nil : Time.zone.at(commit_time.to_i)
55
-
56
- # Get tag if on a tag
57
- tag = `git describe --exact-match --tags HEAD 2>/dev/null`.strip
58
- tag = nil if tag.empty?
59
-
60
- {
61
- commit_sha: commit_sha,
62
- branch: branch,
63
- tag: tag,
64
- commit_timestamp: commit_timestamp
65
- }.compact
66
- rescue StandardError
67
- nil
68
- end
69
- end
70
-
71
- def detect_deployment_info
72
- info = {}
73
-
74
- # Check common deployment environment variables
75
- info[:deployment_id] = ENV["DEPLOYMENT_ID"] if ENV["DEPLOYMENT_ID"]
76
- info[:build_number] = ENV["BUILD_NUMBER"] if ENV["BUILD_NUMBER"]
77
- info[:deployment_time] = parse_deployment_time(ENV["DEPLOYMENT_TIME"]) if ENV["DEPLOYMENT_TIME"]
78
-
79
- # Check Heroku
80
- if ENV["HEROKU_APP_NAME"]
81
- info[:platform] = "heroku"
82
- info[:app_name] = ENV["HEROKU_APP_NAME"]
83
- info[:dyno] = ENV.fetch("DYNO", nil)
84
- info[:slug_commit] = ENV.fetch("HEROKU_SLUG_COMMIT", nil)
85
- end
86
-
87
- # Check AWS
88
- if ENV["AWS_EXECUTION_ENV"]
89
- info[:platform] = "aws"
90
- info[:execution_env] = ENV["AWS_EXECUTION_ENV"]
91
- info[:region] = ENV.fetch("AWS_REGION", nil)
92
- end
93
-
94
- # Check Docker
95
- if ENV["DOCKER_CONTAINER_ID"] || File.exist?("/.dockerenv")
96
- info[:platform] = "docker"
97
- info[:container_id] = ENV["DOCKER_CONTAINER_ID"]
98
- end
99
-
100
- # Check Kubernetes
101
- if ENV["KUBERNETES_SERVICE_HOST"]
102
- info[:platform] = "kubernetes"
103
- info[:namespace] = ENV.fetch("KUBERNETES_NAMESPACE", nil)
104
- info[:pod_name] = ENV.fetch("HOSTNAME", nil)
105
- end
106
-
107
- info.empty? ? nil : info
108
- end
20
+ def detect_release
21
+ # Use configured release
22
+ return @release.call if @release.respond_to?(:call)
23
+ return @release if @release.present?
109
24
 
110
- def parse_deployment_time(time_str)
111
- Time.zone.parse(time_str)
112
- rescue StandardError
113
- nil
25
+ # Use rails_app_version gem if available
26
+ Rails.application.version.to_s if defined?(Rails) && Rails.application.respond_to?(:version)
114
27
  end
115
28
  end
116
29
  end
@@ -25,8 +25,8 @@ module Lapsoss
25
25
  self
26
26
  end
27
27
 
28
- def track_releases(provider: nil)
29
- @pipeline.use(Middleware::ReleaseTracker, release_provider: provider)
28
+ def track_releases(release: nil)
29
+ @pipeline.use(Middleware::ReleaseTracker, release: release)
30
30
  self
31
31
  end
32
32
 
@@ -10,14 +10,14 @@ module Lapsoss
10
10
  Lapsoss::Current.with_clean_scope do
11
11
  # Add request context to current scope
12
12
  if Lapsoss.configuration.capture_request_context
13
- Rails.logger.debug "[Lapsoss] Adding request context" if Rails.env.test?
13
+ Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Adding request context" } if Rails.env.test?
14
14
  add_request_context(env)
15
15
  end
16
16
 
17
17
  begin
18
18
  @app.call(env)
19
19
  rescue Exception => e
20
- Rails.logger.debug { "[Lapsoss] Capturing exception: #{e.class} - #{e.message}" } if Rails.env.test?
20
+ Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Capturing exception: #{e.class} - #{e.message}" } if Rails.env.test?
21
21
  # Capture the exception
22
22
  Lapsoss.capture_exception(e)
23
23
  # Re-raise the exception to maintain Rails error handling
@@ -2,7 +2,13 @@
2
2
 
3
3
  module Lapsoss
4
4
  class Railtie < Rails::Railtie
5
- Rails.logger.debug "[Lapsoss] Railtie loaded" if ENV["DEBUG_LAPSOSS"]
5
+ if ENV["DEBUG_LAPSOSS"]
6
+ if Rails.logger.respond_to?(:tagged)
7
+ Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Railtie loaded" }
8
+ else
9
+ Rails.logger.debug "[Lapsoss] Railtie loaded"
10
+ end
11
+ end
6
12
  config.lapsoss = ActiveSupport::OrderedOptions.new
7
13
 
8
14
  initializer "lapsoss.configure" do |_app|
@@ -14,7 +20,12 @@ module Lapsoss
14
20
  Rails.env
15
21
  end
16
22
 
17
- config.logger ||= Rails.logger
23
+ # Use tagged logger for all Lapsoss logs
24
+ config.logger ||= if Rails.logger.respond_to?(:tagged)
25
+ Rails.logger.tagged("Lapsoss")
26
+ else
27
+ ActiveSupport::TaggedLogging.new(Rails.logger).tagged("Lapsoss")
28
+ end
18
29
 
19
30
  config.release ||= if Rails.application.respond_to?(:version)
20
31
  Rails.application.version.to_s
@@ -38,7 +49,7 @@ module Lapsoss
38
49
  end
39
50
 
40
51
  initializer "lapsoss.rails_error_subscriber", after: "lapsoss.add_middleware" do |app|
41
- app.executor.error_reporter.subscribe(Lapsoss::RailsErrorSubscriber.new)
52
+ Rails.error.subscribe(Lapsoss::RailsErrorSubscriber.new)
42
53
  end
43
54
  end
44
55
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "singleton"
4
4
  require "concurrent"
5
+ require "active_support/core_ext/string/inflections"
5
6
 
6
7
  module Lapsoss
7
8
  class Registry
@@ -42,7 +43,7 @@ module Lapsoss
42
43
  name = if adapter.respond_to?(:name) && adapter.name
43
44
  adapter.name.to_sym
44
45
  elsif adapter.class.name
45
- adapter.class.name.split("::").last.to_sym
46
+ adapter.class.name.demodulize.underscore.to_sym
46
47
  else
47
48
  # Generate a unique name if class name is nil (anonymous class)
48
49
  :"adapter_#{adapter.object_id}"
@@ -116,13 +117,12 @@ module Lapsoss
116
117
  # Resolve adapter type to class
117
118
  def resolve_adapter_class(type)
118
119
  # Try to get the class by convention: Adapters::{Type}Adapter
119
- class_name = "#{type.to_s.split('_').map(&:capitalize).join}Adapter"
120
+ class_name = "#{type.to_s.camelize}Adapter"
121
+ full_class_name = "Lapsoss::Adapters::#{class_name}"
120
122
 
121
- begin
122
- Adapters.const_get(class_name)
123
- rescue NameError
124
- raise AdapterNotFoundError, "Unknown adapter type: #{type}. Expected class: Lapsoss::Adapters::#{class_name}"
125
- end
123
+ full_class_name.constantize
124
+ rescue NameError
125
+ raise AdapterNotFoundError, "Unknown adapter type: #{type}. Expected class: #{full_class_name}"
126
126
  end
127
127
  end
128
128
  end
@@ -19,10 +19,8 @@ module Lapsoss
19
19
 
20
20
  # Handle adapter errors gracefully
21
21
  def handle_adapter_error(adapter, event, error)
22
- return unless Lapsoss.configuration.logger
23
-
24
22
  Lapsoss.configuration.logger.error(
25
- "[Lapsoss] Adapter '#{adapter.name}' failed to capture event (type: #{event.type}): #{error.message}"
23
+ "Adapter '#{adapter.name}' failed to capture event (type: #{event.type}): #{error.message}"
26
24
  )
27
25
 
28
26
  # Call error handler if configured
@@ -4,169 +4,32 @@ require "active_support/parameter_filter"
4
4
 
5
5
  module Lapsoss
6
6
  class Scrubber
7
- DEFAULT_SCRUB_FIELDS = %w[
8
- password passwd pwd secret token key api_key access_token
9
- authorization auth_token session_token csrf_token
10
- credit_card cc_number card_number ssn social_security_number
11
- phone mobile email_address
12
- ].freeze
13
-
14
- PROTECTED_EVENT_FIELDS = %w[
15
- type timestamp level message exception environment context
16
- ].freeze
17
-
18
- ATTACHMENT_CLASSES = %w[
19
- ActionDispatch::Http::UploadedFile
20
- Rack::Multipart::UploadedFile
21
- Tempfile
7
+ # Match Rails conventions - these are only used when Rails is not available
8
+ # Rails uses partial matching, so 'passw' matches 'password'
9
+ DEFAULT_SCRUB_FIELDS = %i[
10
+ passw email secret token _key crypt salt certificate otp ssn cvv cvc
22
11
  ].freeze
23
12
 
24
13
  def initialize(config = {})
25
- @rails_parameter_filter = rails_parameter_filter
26
-
27
- # Only use custom scrubbing if Rails parameter filter is not available
28
- return if @rails_parameter_filter
29
-
30
- @scrub_fields = Array(config[:scrub_fields] || DEFAULT_SCRUB_FIELDS)
31
- @scrub_all = config[:scrub_all] || false
32
- @whitelist_fields = Array(config[:whitelist_fields] || [])
33
- @randomize_scrub_length = config[:randomize_scrub_length] || false
34
- @scrub_value = config[:scrub_value] || "**SCRUBBED**"
35
- end
36
-
37
- def scrub(data)
38
- return data if data.nil?
39
-
40
- # If Rails parameter filter is available, use it exclusively
41
- return @rails_parameter_filter.filter(data) if @rails_parameter_filter
42
-
43
- # Fallback to custom scrubbing logic only if Rails filter is not available
44
- @scrubbed_objects = {}.compare_by_identity
45
- scrub_recursive(data)
46
- end
47
-
48
- private
49
-
50
- def scrub_recursive(data)
51
- return data if @scrubbed_objects.key?(data)
52
-
53
- @scrubbed_objects[data] = true
54
-
55
- case data
56
- in Hash => hash
57
- scrub_hash(hash)
58
- in Array => array
59
- scrub_array(array)
14
+ # Combine: Rails filter parameters + custom fields (if provided)
15
+ base_params = if defined?(Rails) && Rails.respond_to?(:application) && Rails.application
16
+ Rails.application.config.filter_parameters.presence || DEFAULT_SCRUB_FIELDS
60
17
  else
61
- scrub_value(data)
18
+ DEFAULT_SCRUB_FIELDS
62
19
  end
63
- end
64
20
 
65
- def scrub_hash(hash)
66
- hash.each_with_object({}) do |(key, value), result|
67
- key_string = key.to_s.downcase
68
-
69
- result[key] = if should_scrub_field?(key_string)
70
- generate_scrub_value(value)
71
- else
72
- case value
73
- in Hash => h
74
- scrub_recursive(h)
75
- in Array => a
76
- scrub_array(a)
77
- else
78
- scrub_value(value)
79
- end
80
- end
81
- end
82
- end
83
-
84
- def scrub_array(array)
85
- array.map do |item|
86
- scrub_recursive(item)
87
- end
88
- end
89
-
90
- def scrub_value(value)
91
- if attachment_value?(value)
92
- scrub_attachment(value)
21
+ filter_params = if config[:scrub_fields]
22
+ Array(base_params) + Array(config[:scrub_fields])
93
23
  else
94
- value
24
+ base_params
95
25
  end
96
- end
97
-
98
- def should_scrub_field?(field_name)
99
- return false if whitelisted_field?(field_name)
100
- return false if protected_event_field?(field_name)
101
- return true if @scrub_all
102
26
 
103
- @scrub_fields.any? { |pattern| field_matches_pattern?(field_name, pattern) }
27
+ @filter = ActiveSupport::ParameterFilter.new(filter_params)
104
28
  end
105
29
 
106
- def field_matches_pattern?(field_name, pattern)
107
- case pattern
108
- in Regexp => regex
109
- regex.match?(field_name)
110
- else
111
- field_name.include?(pattern.to_s.downcase)
112
- end
113
- end
114
-
115
- def whitelisted_field?(field_name)
116
- @whitelist_fields.any? { |pattern| field_matches_pattern?(field_name, pattern) }
117
- end
118
-
119
- def protected_event_field?(field_name)
120
- PROTECTED_EVENT_FIELDS.include?(field_name.to_s)
121
- end
122
-
123
- def whitelisted_value?(_value)
124
- # Basic implementation - could be extended
125
- false
126
- end
127
-
128
- def attachment_value?(value)
129
- return false unless value.respond_to?(:class)
130
-
131
- ATTACHMENT_CLASSES.include?(value.class.name)
132
- end
133
-
134
- def scrub_attachment(attachment)
135
- {
136
- __attachment__: true,
137
- content_type: safe_call(attachment, :content_type),
138
- original_filename: safe_call(attachment, :original_filename),
139
- size: safe_call(attachment, :size) || safe_call(attachment, :tempfile, :size)
140
- }
141
- rescue StandardError => e
142
- { __attachment__: true, error: "Failed to extract attachment info: #{e.message}" }
143
- end
144
-
145
- def safe_call(object, *methods)
146
- methods.reduce(object) do |obj, method|
147
- obj.respond_to?(method) ? obj.public_send(method) : nil
148
- end
149
- end
150
-
151
- def generate_scrub_value(_original_value)
152
- if @randomize_scrub_length
153
- "*" * rand(6..12)
154
- else
155
- @scrub_value
156
- end
157
- end
158
-
159
- def rails_parameter_filter
160
- return nil unless defined?(Rails) && Rails.respond_to?(:application) && Rails.application
161
- return nil unless defined?(ActiveSupport::ParameterFilter)
162
-
163
- filter_params = Rails.application.config.filter_parameters
164
- return nil if filter_params.empty?
165
-
166
- ActiveSupport::ParameterFilter.new(filter_params)
167
- rescue StandardError
168
- # Fallback silently if Rails config is not available
169
- nil
30
+ def scrub(data)
31
+ return data if data.nil?
32
+ @filter.filter(data)
170
33
  end
171
34
  end
172
35
  end
@@ -1,164 +1,100 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/object/blank"
4
+
3
5
  module Lapsoss
4
6
  module Validators
5
- class ValidationError < StandardError; end
7
+ extend ActiveSupport::Concern
6
8
 
7
9
  module_function
8
10
 
9
- # Simple presence check - just ensure it's not blank
10
- def validate_presence!(value, name)
11
- return unless value.blank?
11
+ def logger
12
+ Lapsoss.configuration.logger
13
+ end
12
14
 
13
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} is missing or blank"
15
+ # Simple presence check using AS blank?
16
+ def validate_presence!(value, name)
17
+ return true if value.present?
18
+ logger.warn "#{name} is missing or blank"
14
19
  false
15
20
  end
16
21
 
17
- # Check if callable, log warning if not
22
+ # Check if callable
18
23
  def validate_callable!(value, name)
19
24
  return true if value.nil? || value.respond_to?(:call)
20
-
21
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be callable but got #{value.class}"
25
+ logger.warn "#{name} should be callable but got #{value.class}"
22
26
  false
23
27
  end
24
28
 
25
- # Just log DSN issues, don't fail
29
+ # DSN validation - just log issues
26
30
  def validate_dsn!(dsn_string, name = "DSN")
27
31
  return true if dsn_string.blank?
28
32
 
29
- begin
30
- uri = URI.parse(dsn_string)
31
-
32
- if uri.user.blank?
33
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} appears to be missing public key"
34
- end
35
-
36
- if uri.host.blank?
37
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} appears to be missing host"
38
- end
39
-
40
- true
41
- rescue URI::InvalidURIError => e
42
- Lapsoss.configuration.logger&.error "[Lapsoss] #{name} couldn't be parsed: #{e.message}"
43
- false
44
- end
33
+ uri = URI.parse(dsn_string)
34
+ logger.warn "#{name} appears to be missing public key" if uri.user.blank?
35
+ logger.warn "#{name} appears to be missing host" if uri.host.blank?
36
+ true
37
+ rescue URI::InvalidURIError => e
38
+ logger.error "#{name} couldn't be parsed: #{e.message}"
39
+ false
45
40
  end
46
41
 
47
- # Validate sample rate is between 0 and 1
42
+ # Validate numeric ranges using AS Range#cover?
48
43
  def validate_sample_rate!(value, name)
49
44
  return true if value.nil?
50
-
51
- if value < 0 || value > 1
52
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be between 0 and 1, got #{value}"
53
- end
54
- true
45
+ return true if (0..1).cover?(value)
46
+ logger.warn "#{name} should be between 0 and 1, got #{value}"
47
+ false
55
48
  end
56
49
 
57
- # Validate timeout values
58
50
  def validate_timeout!(value, name)
59
51
  return true if value.nil?
60
-
61
- if value <= 0
62
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be positive, got #{value}"
63
- end
64
- true
52
+ return true if value.positive?
53
+ logger.warn "#{name} should be positive, got #{value}"
54
+ false
65
55
  end
66
56
 
67
- # Validate retry count
68
57
  def validate_retries!(value, name)
69
58
  return true if value.nil?
70
-
71
- if value < 0
72
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be non-negative, got #{value}"
73
- end
74
- true
75
- end
76
-
77
- # Validate environment string
78
- def validate_environment!(value, name)
79
- return true if value.nil?
80
-
81
- if value.to_s.strip.empty?
82
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should not be empty"
83
- end
84
- true
85
- end
86
-
87
- # Validate type
88
- def validate_type!(value, expected_types, name)
89
- return true if value.nil?
90
-
91
- unless expected_types.any? { |type| value.is_a?(type) }
92
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be one of #{expected_types.join(', ')}, got #{value.class}"
93
- end
94
- true
59
+ return true if value >= 0
60
+ logger.warn "#{name} should be non-negative, got #{value}"
61
+ false
95
62
  end
96
63
 
97
- # Validate numeric range
98
- def validate_numeric_range!(value, range, name)
99
- return true if value.nil?
100
-
101
- unless range.include?(value)
102
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be within #{range}, got #{value}"
103
- end
104
- true
105
- end
64
+ # Environment validation using AS presence
65
+ def validate_environment!(value, name = "environment")
66
+ return true if value.blank?
106
67
 
107
- # Validate boolean
108
- def validate_boolean!(value, name)
109
- return true if value.nil?
68
+ value_str = value.to_s.strip
69
+ return true if value_str.present?
110
70
 
111
- unless [ true, false ].include?(value)
112
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be true or false, got #{value}"
113
- end
114
- true
71
+ logger.warn "#{name} should not be empty"
72
+ false
115
73
  end
116
74
 
117
- # Just check presence, don't validate format
75
+ # API key validation using AS blank?
118
76
  def validate_api_key!(value, name, format: nil)
119
- if value.blank?
120
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} is missing"
121
- return false
122
- end
77
+ return false if value.blank? && logger.warn("#{name} is missing")
123
78
 
124
- # Optional format hint for logging only
79
+ # Optional format hints
125
80
  case format
126
81
  when :uuid
127
- unless value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
128
- Lapsoss.configuration.logger&.info "[Lapsoss] #{name} doesn't look like a UUID, but continuing anyway"
129
- end
82
+ logger.info "#{name} doesn't look like a UUID, but continuing anyway" unless value.match?(/\A[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\z/i)
130
83
  when :alphanumeric
131
- unless value.match?(/\A[a-z0-9]+\z/i)
132
- Lapsoss.configuration.logger&.info "[Lapsoss] #{name} contains special characters, but continuing anyway"
133
- end
134
- end
135
-
136
- true
137
- end
138
-
139
- # Environment validation - just log if unusual
140
- def validate_environment!(value, name = "environment")
141
- return true if value.blank?
142
-
143
- common_envs = %w[development test staging production]
144
- unless common_envs.include?(value.to_s.downcase)
145
- Lapsoss.configuration.logger&.info "[Lapsoss] #{name} '#{value}' is non-standard (expected one of: #{common_envs.join(', ')})"
84
+ logger.info "#{name} contains special characters, but continuing anyway" unless value.match?(/\A[a-z0-9]+\z/i)
146
85
  end
147
86
 
148
87
  true
149
88
  end
150
89
 
151
- # URL validation - just check parsability
90
+ # URL validation
152
91
  def validate_url!(value, name)
153
92
  return true if value.nil?
154
-
155
- begin
156
- URI.parse(value)
157
- true
158
- rescue URI::InvalidURIError => e
159
- Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} couldn't be parsed as URL: #{e.message}"
160
- false
161
- end
93
+ URI.parse(value)
94
+ true
95
+ rescue URI::InvalidURIError => e
96
+ logger.warn "#{name} couldn't be parsed as URL: #{e.message}"
97
+ false
162
98
  end
163
99
  end
164
100
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lapsoss
4
- VERSION = "0.4.0"
4
+ VERSION = "0.4.4"
5
5
  end
data/lib/lapsoss.rb CHANGED
@@ -44,6 +44,29 @@ module Lapsoss
44
44
  client.capture_message(message, level: level, **context)
45
45
  end
46
46
 
47
+ # Rails.error-compatible methods for non-Rails environments
48
+
49
+ # Handle errors and swallow them (like Rails.error.handle)
50
+ def handle(error_class = StandardError, fallback: nil, **context)
51
+ yield
52
+ rescue error_class => e
53
+ capture_exception(e, **context.merge(handled: true))
54
+ fallback.respond_to?(:call) ? fallback.call : fallback
55
+ end
56
+
57
+ # Record errors and re-raise them (like Rails.error.record)
58
+ def record(error_class = StandardError, **context)
59
+ yield
60
+ rescue error_class => e
61
+ capture_exception(e, **context.merge(handled: false))
62
+ raise
63
+ end
64
+
65
+ # Report an error manually (like Rails.error.report)
66
+ def report(exception, handled: true, **context)
67
+ capture_exception(exception, **context.merge(handled: handled))
68
+ end
69
+
47
70
  def add_breadcrumb(message, type: :default, **metadata)
48
71
  client.add_breadcrumb(message, type: type, **metadata)
49
72
  end