lapsoss 0.3.1 → 0.4.0
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 +5 -0
- data/lib/lapsoss/adapters/appsignal_adapter.rb +18 -12
- data/lib/lapsoss/adapters/base.rb +19 -0
- data/lib/lapsoss/adapters/concerns/envelope_builder.rb +127 -0
- data/lib/lapsoss/adapters/concerns/http_delivery.rb +130 -0
- data/lib/lapsoss/adapters/concerns/level_mapping.rb +65 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +21 -21
- data/lib/lapsoss/adapters/rollbar_adapter.rb +64 -122
- data/lib/lapsoss/adapters/sentry_adapter.rb +77 -143
- data/lib/lapsoss/backtrace_processor.rb +1 -1
- data/lib/lapsoss/breadcrumb.rb +59 -0
- data/lib/lapsoss/client.rb +2 -2
- data/lib/lapsoss/configuration.rb +23 -19
- data/lib/lapsoss/event.rb +90 -96
- data/lib/lapsoss/fingerprinter.rb +5 -2
- data/lib/lapsoss/merged_scope.rb +1 -6
- data/lib/lapsoss/rails_error_subscriber.rb +3 -4
- data/lib/lapsoss/scope.rb +1 -6
- data/lib/lapsoss/scrubber.rb +15 -11
- data/lib/lapsoss/user_context.rb +15 -5
- data/lib/lapsoss/utils.rb +0 -2
- data/lib/lapsoss/validators.rb +111 -76
- data/lib/lapsoss/version.rb +1 -1
- data/lib/tasks/cassettes.rake +50 -0
- metadata +9 -5
- data/CHANGELOG.md +0 -5
data/lib/lapsoss/event.rb
CHANGED
@@ -1,131 +1,125 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(type:, level: :info, **attributes)
|
9
|
-
@type = type
|
10
|
-
@level = level
|
11
|
-
@timestamp = Utils.current_time
|
12
|
-
@context = {}
|
13
|
-
@environment = Lapsoss.configuration.environment
|
14
|
-
|
15
|
-
attributes.each do |key, value|
|
16
|
-
instance_variable_set("@#{key}", value) if respond_to?("#{key}=")
|
17
|
-
end
|
18
|
-
|
19
|
-
# Process backtrace if we have an exception
|
20
|
-
@backtrace_frames = process_backtrace if @exception
|
21
|
-
|
22
|
-
# Set message from exception if not provided
|
23
|
-
@message ||= @exception.message if @exception
|
3
|
+
require "active_support/core_ext/hash"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/time"
|
6
|
+
require "active_support/json"
|
24
7
|
|
25
|
-
|
26
|
-
|
27
|
-
|
8
|
+
module Lapsoss
|
9
|
+
# Immutable event structure using Ruby 3.3 Data class
|
10
|
+
Event = Data.define(
|
11
|
+
:type, # :exception, :message, :transaction
|
12
|
+
:level, # :debug, :info, :warning, :error, :fatal
|
13
|
+
:timestamp,
|
14
|
+
:message,
|
15
|
+
:exception,
|
16
|
+
:context,
|
17
|
+
:environment,
|
18
|
+
:fingerprint,
|
19
|
+
:backtrace_frames
|
20
|
+
) do
|
21
|
+
# Factory method with smart defaults
|
22
|
+
def self.build(type:, level: :info, **attributes)
|
23
|
+
timestamp = attributes[:timestamp] || Time.now
|
24
|
+
environment = attributes[:environment].presence || Lapsoss.configuration.environment
|
25
|
+
context = attributes[:context] || {}
|
26
|
+
|
27
|
+
# Process exception if present
|
28
|
+
exception = attributes[:exception]
|
29
|
+
message = attributes[:message].presence || exception&.message
|
30
|
+
backtrace_frames = process_backtrace(exception) if exception
|
31
|
+
|
32
|
+
# Generate fingerprint
|
33
|
+
fingerprint = attributes.fetch(:fingerprint) {
|
34
|
+
generate_fingerprint(type, message, exception, environment)
|
35
|
+
}
|
28
36
|
|
29
|
-
|
30
|
-
data = {
|
37
|
+
new(
|
31
38
|
type: type,
|
32
|
-
timestamp: timestamp,
|
33
39
|
level: level,
|
40
|
+
timestamp: timestamp,
|
34
41
|
message: message,
|
35
|
-
exception:
|
36
|
-
backtrace: backtrace_data,
|
42
|
+
exception: exception,
|
37
43
|
context: context,
|
38
44
|
environment: environment,
|
39
|
-
fingerprint: fingerprint
|
40
|
-
|
41
|
-
|
42
|
-
scrub_sensitive_data(data)
|
45
|
+
fingerprint: fingerprint,
|
46
|
+
backtrace_frames: backtrace_frames
|
47
|
+
)
|
43
48
|
end
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
scrub_fields: config.scrub_fields,
|
50
|
-
scrub_all: config.scrub_all,
|
51
|
-
whitelist_fields: config.whitelist_fields,
|
52
|
-
randomize_scrub_length: config.randomize_scrub_length
|
53
|
-
)
|
50
|
+
# ActiveSupport::JSON serialization
|
51
|
+
def as_json(options = nil)
|
52
|
+
to_h.compact_blank.as_json(options)
|
53
|
+
end
|
54
54
|
|
55
|
-
|
55
|
+
def to_json(options = nil)
|
56
|
+
ActiveSupport::JSON.encode(as_json(options))
|
56
57
|
end
|
57
58
|
|
58
|
-
|
59
|
-
|
59
|
+
# Helper methods
|
60
|
+
def exception_type = exception&.class&.name
|
60
61
|
|
61
|
-
|
62
|
-
custom_callback: config.fingerprint_callback,
|
63
|
-
patterns: config.fingerprint_patterns,
|
64
|
-
normalize_paths: config.normalize_fingerprint_paths,
|
65
|
-
normalize_ids: config.normalize_fingerprint_ids,
|
66
|
-
include_environment: config.fingerprint_include_environment
|
67
|
-
)
|
62
|
+
def exception_message = exception&.message
|
68
63
|
|
69
|
-
|
70
|
-
end
|
64
|
+
def has_exception? = exception.present?
|
71
65
|
|
72
|
-
def
|
73
|
-
return nil unless @exception&.backtrace
|
66
|
+
def has_backtrace? = backtrace_frames.present?
|
74
67
|
|
75
|
-
|
68
|
+
def backtrace = exception&.backtrace
|
76
69
|
|
77
|
-
|
78
|
-
context_lines: config.backtrace_context_lines,
|
79
|
-
max_frames: config.backtrace_max_frames,
|
80
|
-
enable_code_context: config.backtrace_enable_code_context,
|
81
|
-
strip_load_path: config.backtrace_strip_load_path,
|
82
|
-
in_app_patterns: config.backtrace_in_app_patterns,
|
83
|
-
exclude_patterns: config.backtrace_exclude_patterns
|
84
|
-
)
|
70
|
+
def request_context = context.dig(:extra, :request) || context.dig(:extra, "request")
|
85
71
|
|
86
|
-
|
87
|
-
end
|
72
|
+
def user_context = context[:user]
|
88
73
|
|
89
|
-
|
74
|
+
def tags = context[:tags] || {}
|
90
75
|
|
91
|
-
def
|
92
|
-
return nil unless exception
|
76
|
+
def extra = context[:extra] || {}
|
93
77
|
|
94
|
-
|
95
|
-
class: exception.class.name,
|
96
|
-
message: exception.message,
|
97
|
-
backtrace: exception.backtrace&.first(20)
|
98
|
-
}
|
99
|
-
end
|
78
|
+
def breadcrumbs = context[:breadcrumbs] || []
|
100
79
|
|
101
|
-
|
102
|
-
|
80
|
+
# Apply data scrubbing
|
81
|
+
def scrubbed
|
82
|
+
scrubber = Scrubber.new(
|
83
|
+
scrub_fields: Lapsoss.configuration.scrub_fields,
|
84
|
+
scrub_all: Lapsoss.configuration.scrub_all,
|
85
|
+
whitelist_fields: Lapsoss.configuration.whitelist_fields,
|
86
|
+
randomize_scrub_length: Lapsoss.configuration.randomize_scrub_length
|
87
|
+
)
|
103
88
|
|
104
|
-
|
89
|
+
with(context: scrubber.scrub(context))
|
105
90
|
end
|
106
91
|
|
107
|
-
|
92
|
+
private
|
108
93
|
|
109
|
-
def
|
110
|
-
exception&.
|
111
|
-
end
|
94
|
+
def self.process_backtrace(exception)
|
95
|
+
return nil unless exception&.backtrace.present?
|
112
96
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
elsif exception
|
117
|
-
exception.backtrace
|
118
|
-
else
|
119
|
-
[]
|
120
|
-
end
|
97
|
+
config = Lapsoss.configuration
|
98
|
+
processor = BacktraceProcessor.new(config)
|
99
|
+
processor.process_exception_backtrace(exception)
|
121
100
|
end
|
122
101
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
102
|
+
def self.generate_fingerprint(type, message, exception, environment)
|
103
|
+
return nil unless Lapsoss.configuration.fingerprint_callback ||
|
104
|
+
Lapsoss.configuration.fingerprint_patterns.present?
|
105
|
+
|
106
|
+
fingerprinter = Fingerprinter.new(
|
107
|
+
custom_callback: Lapsoss.configuration.fingerprint_callback,
|
108
|
+
patterns: Lapsoss.configuration.fingerprint_patterns,
|
109
|
+
normalize_paths: Lapsoss.configuration.normalize_fingerprint_paths,
|
110
|
+
normalize_ids: Lapsoss.configuration.normalize_fingerprint_ids,
|
111
|
+
include_environment: Lapsoss.configuration.fingerprint_include_environment
|
112
|
+
)
|
113
|
+
|
114
|
+
# Create a temporary event-like object for fingerprinting
|
115
|
+
temp_event = Struct.new(:type, :message, :exception, :environment).new(
|
116
|
+
type,
|
117
|
+
message,
|
118
|
+
exception,
|
119
|
+
environment
|
120
|
+
)
|
127
121
|
|
128
|
-
|
122
|
+
fingerprinter.generate_fingerprint(temp_event)
|
129
123
|
end
|
130
124
|
end
|
131
125
|
end
|
@@ -98,10 +98,13 @@ module Lapsoss
|
|
98
98
|
pattern = pattern_config[:pattern]
|
99
99
|
fingerprint = pattern_config[:fingerprint]
|
100
100
|
|
101
|
-
|
101
|
+
case pattern
|
102
|
+
in Regexp if pattern.match?(full_error_text)
|
102
103
|
return fingerprint
|
103
|
-
|
104
|
+
in String if full_error_text.include?(pattern)
|
104
105
|
return fingerprint
|
106
|
+
else
|
107
|
+
# Continue to next pattern
|
105
108
|
end
|
106
109
|
end
|
107
110
|
|
data/lib/lapsoss/merged_scope.rb
CHANGED
@@ -26,12 +26,7 @@ module Lapsoss
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def add_breadcrumb(message, type: :default, **metadata)
|
29
|
-
breadcrumb =
|
30
|
-
message: message,
|
31
|
-
type: type,
|
32
|
-
metadata: metadata,
|
33
|
-
timestamp: Time.now.utc
|
34
|
-
}
|
29
|
+
breadcrumb = Breadcrumb.build(message, type: type, metadata: metadata)
|
35
30
|
@own_breadcrumbs << breadcrumb
|
36
31
|
# Keep breadcrumbs to a reasonable limit
|
37
32
|
@own_breadcrumbs.shift if @own_breadcrumbs.length > 20
|
@@ -22,10 +22,9 @@ module Lapsoss
|
|
22
22
|
private
|
23
23
|
|
24
24
|
def skip_error?(error, source)
|
25
|
-
# Skip cache-related
|
26
|
-
|
27
|
-
|
28
|
-
end
|
25
|
+
# Skip Rails cache-related errors using Rails error reporter source
|
26
|
+
# Avoid referencing backend-specific gems (e.g., Redis)
|
27
|
+
return true if Lapsoss.configuration.skip_rails_cache_errors && source&.include?("cache")
|
29
28
|
|
30
29
|
false
|
31
30
|
end
|
data/lib/lapsoss/scope.rb
CHANGED
@@ -12,12 +12,7 @@ module Lapsoss
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def add_breadcrumb(message, type: :default, **metadata)
|
15
|
-
breadcrumb =
|
16
|
-
message: message,
|
17
|
-
type: type,
|
18
|
-
metadata: metadata,
|
19
|
-
timestamp: Time.now.utc
|
20
|
-
}
|
15
|
+
breadcrumb = Breadcrumb.build(message, type: type, metadata: metadata)
|
21
16
|
@breadcrumbs << breadcrumb
|
22
17
|
# Keep breadcrumbs to a reasonable limit
|
23
18
|
@breadcrumbs.shift if @breadcrumbs.length > 20
|
data/lib/lapsoss/scrubber.rb
CHANGED
@@ -53,10 +53,10 @@ module Lapsoss
|
|
53
53
|
@scrubbed_objects[data] = true
|
54
54
|
|
55
55
|
case data
|
56
|
-
|
57
|
-
scrub_hash(
|
58
|
-
|
59
|
-
scrub_array(
|
56
|
+
in Hash => hash
|
57
|
+
scrub_hash(hash)
|
58
|
+
in Array => array
|
59
|
+
scrub_array(array)
|
60
60
|
else
|
61
61
|
scrub_value(data)
|
62
62
|
end
|
@@ -68,12 +68,15 @@ module Lapsoss
|
|
68
68
|
|
69
69
|
result[key] = if should_scrub_field?(key_string)
|
70
70
|
generate_scrub_value(value)
|
71
|
-
elsif value.is_a?(Hash)
|
72
|
-
scrub_recursive(value)
|
73
|
-
elsif value.is_a?(Array)
|
74
|
-
scrub_array(value)
|
75
71
|
else
|
76
|
-
|
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
|
77
80
|
end
|
78
81
|
end
|
79
82
|
end
|
@@ -101,8 +104,9 @@ module Lapsoss
|
|
101
104
|
end
|
102
105
|
|
103
106
|
def field_matches_pattern?(field_name, pattern)
|
104
|
-
|
105
|
-
|
107
|
+
case pattern
|
108
|
+
in Regexp => regex
|
109
|
+
regex.match?(field_name)
|
106
110
|
else
|
107
111
|
field_name.include?(pattern.to_s.downcase)
|
108
112
|
end
|
data/lib/lapsoss/user_context.rb
CHANGED
@@ -96,12 +96,22 @@ module Lapsoss
|
|
96
96
|
|
97
97
|
sanitized[key] = if sensitive_field?(key_sym)
|
98
98
|
"[REDACTED]"
|
99
|
-
elsif value.is_a?(Hash)
|
100
|
-
sanitize_for_logging(value)
|
101
|
-
elsif value.is_a?(Array)
|
102
|
-
value.map { |v| v.is_a?(Hash) ? sanitize_for_logging(v) : v }
|
103
99
|
else
|
104
|
-
value
|
100
|
+
case value
|
101
|
+
in Hash => h
|
102
|
+
sanitize_for_logging(h)
|
103
|
+
in Array => arr
|
104
|
+
arr.map do |v|
|
105
|
+
case v
|
106
|
+
in Hash => h
|
107
|
+
sanitize_for_logging(h)
|
108
|
+
else
|
109
|
+
v
|
110
|
+
end
|
111
|
+
end
|
112
|
+
else
|
113
|
+
value
|
114
|
+
end
|
105
115
|
end
|
106
116
|
end
|
107
117
|
|
data/lib/lapsoss/utils.rb
CHANGED
data/lib/lapsoss/validators.rb
CHANGED
@@ -1,129 +1,164 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "uri"
|
4
|
-
|
5
3
|
module Lapsoss
|
6
4
|
module Validators
|
7
5
|
class ValidationError < StandardError; end
|
8
6
|
|
9
7
|
module_function
|
10
8
|
|
9
|
+
# Simple presence check - just ensure it's not blank
|
11
10
|
def validate_presence!(value, name)
|
12
|
-
|
13
|
-
raise ValidationError, "#{name} is required and cannot be nil or empty"
|
14
|
-
end
|
15
|
-
end
|
11
|
+
return unless value.blank?
|
16
12
|
|
17
|
-
|
18
|
-
|
19
|
-
unless types.any? { |type| value.is_a?(type) }
|
20
|
-
type_names = types.map(&:name).join(" or ")
|
21
|
-
raise ValidationError, "#{name} must be a #{type_names}, got #{value.class} (#{value.inspect})"
|
22
|
-
end
|
13
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} is missing or blank"
|
14
|
+
false
|
23
15
|
end
|
24
16
|
|
17
|
+
# Check if callable, log warning if not
|
25
18
|
def validate_callable!(value, name)
|
26
|
-
|
27
|
-
|
19
|
+
return true if value.nil? || value.respond_to?(:call)
|
20
|
+
|
21
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be callable but got #{value.class}"
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
# Just log DSN issues, don't fail
|
26
|
+
def validate_dsn!(dsn_string, name = "DSN")
|
27
|
+
return true if dsn_string.blank?
|
28
|
+
|
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
|
28
44
|
end
|
29
45
|
end
|
30
46
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
47
|
+
# Validate sample rate is between 0 and 1
|
48
|
+
def validate_sample_rate!(value, name)
|
49
|
+
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}"
|
35
53
|
end
|
54
|
+
true
|
36
55
|
end
|
37
56
|
|
38
|
-
|
39
|
-
|
57
|
+
# Validate timeout values
|
58
|
+
def validate_timeout!(value, name)
|
59
|
+
return true if value.nil?
|
40
60
|
|
41
|
-
|
61
|
+
if value <= 0
|
62
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be positive, got #{value}"
|
63
|
+
end
|
64
|
+
true
|
65
|
+
end
|
42
66
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
67
|
+
# Validate retry count
|
68
|
+
def validate_retries!(value, name)
|
69
|
+
return true if value.nil?
|
70
|
+
|
71
|
+
if value < 0
|
72
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be non-negative, got #{value}"
|
49
73
|
end
|
74
|
+
true
|
50
75
|
end
|
51
76
|
|
52
|
-
|
53
|
-
|
77
|
+
# Validate environment string
|
78
|
+
def validate_environment!(value, name)
|
79
|
+
return true if value.nil?
|
54
80
|
|
55
|
-
|
81
|
+
if value.to_s.strip.empty?
|
82
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should not be empty"
|
83
|
+
end
|
84
|
+
true
|
85
|
+
end
|
56
86
|
|
57
|
-
|
58
|
-
|
87
|
+
# Validate type
|
88
|
+
def validate_type!(value, expected_types, name)
|
89
|
+
return true if value.nil?
|
59
90
|
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
95
|
+
end
|
64
96
|
|
65
|
-
|
66
|
-
|
97
|
+
# Validate numeric range
|
98
|
+
def validate_numeric_range!(value, range, name)
|
99
|
+
return true if value.nil?
|
67
100
|
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
101
|
+
unless range.include?(value)
|
102
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be within #{range}, got #{value}"
|
103
|
+
end
|
104
|
+
true
|
105
|
+
end
|
72
106
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
107
|
+
# Validate boolean
|
108
|
+
def validate_boolean!(value, name)
|
109
|
+
return true if value.nil?
|
110
|
+
|
111
|
+
unless [ true, false ].include?(value)
|
112
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} should be true or false, got #{value}"
|
79
113
|
end
|
114
|
+
true
|
80
115
|
end
|
81
116
|
|
117
|
+
# Just check presence, don't validate format
|
82
118
|
def validate_api_key!(value, name, format: nil)
|
83
|
-
|
84
|
-
|
119
|
+
if value.blank?
|
120
|
+
Lapsoss.configuration.logger&.warn "[Lapsoss] #{name} is missing"
|
121
|
+
return false
|
122
|
+
end
|
85
123
|
|
124
|
+
# Optional format hint for logging only
|
86
125
|
case format
|
87
126
|
when :uuid
|
88
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)
|
89
|
-
|
128
|
+
Lapsoss.configuration.logger&.info "[Lapsoss] #{name} doesn't look like a UUID, but continuing anyway"
|
90
129
|
end
|
91
|
-
when :hex
|
92
|
-
raise ValidationError, "#{name} must be a valid hexadecimal string" unless value.match?(/\A[0-9a-f]+\z/i)
|
93
130
|
when :alphanumeric
|
94
|
-
|
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
|
95
134
|
end
|
135
|
+
|
136
|
+
true
|
96
137
|
end
|
97
138
|
|
139
|
+
# Environment validation - just log if unusual
|
98
140
|
def validate_environment!(value, name = "environment")
|
99
|
-
return if value.
|
100
|
-
|
101
|
-
validate_type!(value, [ String, Symbol ], name)
|
141
|
+
return true if value.blank?
|
102
142
|
|
103
|
-
|
104
|
-
unless
|
105
|
-
|
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(', ')})"
|
106
146
|
end
|
107
|
-
end
|
108
|
-
|
109
|
-
def validate_sample_rate!(value, name = "sample_rate")
|
110
|
-
return if value.nil?
|
111
|
-
|
112
|
-
validate_numeric_range!(value, 0.0..1.0, name)
|
113
|
-
end
|
114
|
-
|
115
|
-
def validate_timeout!(value, name = "timeout")
|
116
|
-
return if value.nil?
|
117
147
|
|
118
|
-
|
119
|
-
raise ValidationError, "#{name} must be positive, got #{value}" if value <= 0
|
148
|
+
true
|
120
149
|
end
|
121
150
|
|
122
|
-
|
123
|
-
|
151
|
+
# URL validation - just check parsability
|
152
|
+
def validate_url!(value, name)
|
153
|
+
return true if value.nil?
|
124
154
|
|
125
|
-
|
126
|
-
|
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
|
127
162
|
end
|
128
163
|
end
|
129
164
|
end
|
data/lib/lapsoss/version.rb
CHANGED
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "yaml"
|
4
|
+
|
5
|
+
namespace :cassettes do
|
6
|
+
desc "Sanitize VCR cassettes by scrubbing sensitive data"
|
7
|
+
task :sanitize do
|
8
|
+
dir = File.expand_path("../../test/cassettes", __dir__)
|
9
|
+
files = Dir[File.join(dir, "*.yml")]
|
10
|
+
puts "Sanitizing #{files.size} cassette(s) in #{dir}..."
|
11
|
+
|
12
|
+
email_re = /\b[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\b/i
|
13
|
+
hostname_key_re = /"hostname":"[^"]+"/
|
14
|
+
home_path_re = Regexp.new(Regexp.escape(Dir.home))
|
15
|
+
api_key_pairs = [
|
16
|
+
[ /"apiKey":"[^"]+"/, '"apiKey":"<INSIGHT_HUB_API_KEY>"' ],
|
17
|
+
[ /"access_token":"[^"]+"/, '"access_token":"<ROLLBAR_ACCESS_TOKEN>"' ],
|
18
|
+
[ /api_key=[^&]+/, "api_key=<APPSIGNAL_FRONTEND_API_KEY>" ]
|
19
|
+
]
|
20
|
+
|
21
|
+
header_key_map = {
|
22
|
+
"X-Rollbar-Access-Token" => "<ROLLBAR_ACCESS_TOKEN>",
|
23
|
+
"Bugsnag-Api-Key" => "<INSIGHT_HUB_API_KEY>",
|
24
|
+
"Authorization" => "<AUTHORIZATION>"
|
25
|
+
}
|
26
|
+
|
27
|
+
files.each do |file|
|
28
|
+
content = File.read(file)
|
29
|
+
|
30
|
+
# Replace patterns in raw content safely
|
31
|
+
content = content.gsub(email_re, "<EMAIL>")
|
32
|
+
content = content.gsub(hostname_key_re, '"hostname":"<HOSTNAME>"')
|
33
|
+
content = content.gsub(home_path_re, "/home/user")
|
34
|
+
api_key_pairs.each { |(re, rep)| content = content.gsub(re, rep) }
|
35
|
+
|
36
|
+
# Normalize User-Agent references to avoid machine leakage
|
37
|
+
content = content.gsub(/User-Agent:\n\s+-\s+.+/, "User-Agent:\n - lapsoss/x.y.z") rescue nil
|
38
|
+
|
39
|
+
# Rewrite known header values
|
40
|
+
header_key_map.each do |hdr, placeholder|
|
41
|
+
content = content.gsub(/#{hdr}:(?:\n\s+-\s+.*)/, "#{hdr}:\n - \"#{placeholder}\"")
|
42
|
+
end
|
43
|
+
|
44
|
+
File.write(file, content)
|
45
|
+
puts " scrubbed: #{File.basename(file)}"
|
46
|
+
end
|
47
|
+
|
48
|
+
puts "Done."
|
49
|
+
end
|
50
|
+
end
|