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.
- checksums.yaml +4 -4
- data/README.md +195 -7
- data/lib/lapsoss/adapters/concerns/level_mapping.rb +1 -0
- data/lib/lapsoss/adapters/telebug_adapter.rb +58 -0
- data/lib/lapsoss/client.rb +1 -3
- data/lib/lapsoss/configuration.rb +14 -17
- data/lib/lapsoss/fingerprinter.rb +52 -47
- data/lib/lapsoss/middleware/release_tracker.rb +11 -98
- data/lib/lapsoss/pipeline_builder.rb +2 -2
- data/lib/lapsoss/rails_middleware.rb +2 -2
- data/lib/lapsoss/railtie.rb +14 -3
- data/lib/lapsoss/registry.rb +7 -7
- data/lib/lapsoss/router.rb +1 -3
- data/lib/lapsoss/scrubber.rb +15 -152
- data/lib/lapsoss/validators.rb +48 -112
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +23 -0
- metadata +2 -21
- data/lib/lapsoss/exclusion_configuration.rb +0 -30
- data/lib/lapsoss/exclusion_presets.rb +0 -249
- data/lib/lapsoss/middleware/sample_filter.rb +0 -23
- data/lib/lapsoss/middleware/sampling_middleware.rb +0 -18
- data/lib/lapsoss/middleware/user_context_enhancer.rb +0 -46
- data/lib/lapsoss/release_providers.rb +0 -110
- data/lib/lapsoss/sampling/adaptive_sampler.rb +0 -46
- data/lib/lapsoss/sampling/composite_sampler.rb +0 -26
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +0 -30
- data/lib/lapsoss/sampling/exception_type_sampler.rb +0 -44
- data/lib/lapsoss/sampling/health_based_sampler.rb +0 -19
- data/lib/lapsoss/sampling/sampling_factory.rb +0 -69
- data/lib/lapsoss/sampling/time_based_sampler.rb +0 -44
- data/lib/lapsoss/sampling/user_based_sampler.rb +0 -42
- data/lib/lapsoss/user_context.rb +0 -185
- data/lib/lapsoss/user_context_integrations.rb +0 -39
- data/lib/lapsoss/user_context_middleware.rb +0 -50
- data/lib/lapsoss/user_context_provider.rb +0 -93
- data/lib/lapsoss/utils.rb +0 -11
- 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,
|
6
|
+
def initialize(app, release: nil)
|
7
7
|
super(app)
|
8
|
-
@
|
8
|
+
@release = release
|
9
9
|
end
|
10
10
|
|
11
11
|
def call(event, hint = {})
|
12
|
-
|
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
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
-
|
111
|
-
|
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(
|
29
|
-
@pipeline.use(Middleware::ReleaseTracker,
|
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 "
|
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.
|
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
|
data/lib/lapsoss/railtie.rb
CHANGED
@@ -2,7 +2,13 @@
|
|
2
2
|
|
3
3
|
module Lapsoss
|
4
4
|
class Railtie < Rails::Railtie
|
5
|
-
|
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
|
-
|
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
|
-
|
52
|
+
Rails.error.subscribe(Lapsoss::RailsErrorSubscriber.new)
|
42
53
|
end
|
43
54
|
end
|
44
55
|
end
|
data/lib/lapsoss/registry.rb
CHANGED
@@ -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.
|
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.
|
120
|
+
class_name = "#{type.to_s.camelize}Adapter"
|
121
|
+
full_class_name = "Lapsoss::Adapters::#{class_name}"
|
120
122
|
|
121
|
-
|
122
|
-
|
123
|
-
|
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
|
data/lib/lapsoss/router.rb
CHANGED
@@ -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
|
-
"
|
23
|
+
"Adapter '#{adapter.name}' failed to capture event (type: #{event.type}): #{error.message}"
|
26
24
|
)
|
27
25
|
|
28
26
|
# Call error handler if configured
|
data/lib/lapsoss/scrubber.rb
CHANGED
@@ -4,169 +4,32 @@ require "active_support/parameter_filter"
|
|
4
4
|
|
5
5
|
module Lapsoss
|
6
6
|
class Scrubber
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
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
|
-
|
26
|
-
|
27
|
-
|
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
|
-
|
18
|
+
DEFAULT_SCRUB_FIELDS
|
62
19
|
end
|
63
|
-
end
|
64
20
|
|
65
|
-
|
66
|
-
|
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
|
-
|
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
|
-
@
|
27
|
+
@filter = ActiveSupport::ParameterFilter.new(filter_params)
|
104
28
|
end
|
105
29
|
|
106
|
-
def
|
107
|
-
|
108
|
-
|
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
|
data/lib/lapsoss/validators.rb
CHANGED
@@ -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
|
-
|
7
|
+
extend ActiveSupport::Concern
|
6
8
|
|
7
9
|
module_function
|
8
10
|
|
9
|
-
|
10
|
-
|
11
|
-
|
11
|
+
def logger
|
12
|
+
Lapsoss.configuration.logger
|
13
|
+
end
|
12
14
|
|
13
|
-
|
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
|
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
|
-
#
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
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
|
-
|
52
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
72
|
-
|
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
|
-
#
|
98
|
-
def
|
99
|
-
return true if value.
|
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
|
-
|
108
|
-
|
109
|
-
return true if value.nil?
|
68
|
+
value_str = value.to_s.strip
|
69
|
+
return true if value_str.present?
|
110
70
|
|
111
|
-
|
112
|
-
|
113
|
-
end
|
114
|
-
true
|
71
|
+
logger.warn "#{name} should not be empty"
|
72
|
+
false
|
115
73
|
end
|
116
74
|
|
117
|
-
#
|
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
|
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
|
90
|
+
# URL validation
|
152
91
|
def validate_url!(value, name)
|
153
92
|
return true if value.nil?
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
data/lib/lapsoss/version.rb
CHANGED
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
|