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.
data/lib/lapsoss/event.rb CHANGED
@@ -1,131 +1,125 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Lapsoss
4
- class Event
5
- attr_accessor :type, :timestamp, :level, :message, :exception, :context, :environment, :fingerprint,
6
- :backtrace_frames
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
- # Generate fingerprint after all attributes are set (unless explicitly set to nil or a value)
26
- @fingerprint = generate_fingerprint if @fingerprint.nil? && !attributes.key?(:fingerprint)
27
- end
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
- def to_h
30
- data = {
37
+ new(
31
38
  type: type,
32
- timestamp: timestamp,
33
39
  level: level,
40
+ timestamp: timestamp,
34
41
  message: message,
35
- exception: exception_data,
36
- backtrace: backtrace_data,
42
+ exception: exception,
37
43
  context: context,
38
44
  environment: environment,
39
- fingerprint: fingerprint
40
- }.compact
41
-
42
- scrub_sensitive_data(data)
45
+ fingerprint: fingerprint,
46
+ backtrace_frames: backtrace_frames
47
+ )
43
48
  end
44
49
 
45
- def scrub_sensitive_data(data)
46
- config = Lapsoss.configuration
47
-
48
- scrubber = Scrubber.new(
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
- scrubber.scrub(data)
55
+ def to_json(options = nil)
56
+ ActiveSupport::JSON.encode(as_json(options))
56
57
  end
57
58
 
58
- def generate_fingerprint
59
- config = Lapsoss.configuration
59
+ # Helper methods
60
+ def exception_type = exception&.class&.name
60
61
 
61
- fingerprinter = Fingerprinter.new(
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
- fingerprinter.generate_fingerprint(self)
70
- end
64
+ def has_exception? = exception.present?
71
65
 
72
- def process_backtrace
73
- return nil unless @exception&.backtrace
66
+ def has_backtrace? = backtrace_frames.present?
74
67
 
75
- config = Lapsoss.configuration
68
+ def backtrace = exception&.backtrace
76
69
 
77
- processor = BacktraceProcessor.new(
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
- processor.process_exception_backtrace(@exception)
87
- end
72
+ def user_context = context[:user]
88
73
 
89
- private
74
+ def tags = context[:tags] || {}
90
75
 
91
- def exception_data
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
- def backtrace_data
102
- return nil unless @backtrace_frames&.any?
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
- @backtrace_frames.map(&:to_h)
89
+ with(context: scrubber.scrub(context))
105
90
  end
106
91
 
107
- public
92
+ private
108
93
 
109
- def exception_type
110
- exception&.class&.name
111
- end
94
+ def self.process_backtrace(exception)
95
+ return nil unless exception&.backtrace.present?
112
96
 
113
- def backtrace
114
- if @backtrace_frames
115
- @backtrace_frames.map(&:raw_line)
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 request_context
124
- # Request context is stored in extra by the middleware
125
- # Check both string and symbol keys for compatibility
126
- return nil unless context[:extra]
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
- context[:extra]["request"] || context[:extra][:request]
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
- if pattern.is_a?(Regexp) && full_error_text.match?(pattern)
101
+ case pattern
102
+ in Regexp if pattern.match?(full_error_text)
102
103
  return fingerprint
103
- elsif pattern.is_a?(String) && full_error_text.include?(pattern)
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
 
@@ -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 Redis errors if configured to do so
26
- if Lapsoss.configuration.skip_rails_cache_errors && source&.include?("cache") && defined?(Redis::CannotConnectError) && error.is_a?(Redis::CannotConnectError)
27
- return true
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
@@ -53,10 +53,10 @@ module Lapsoss
53
53
  @scrubbed_objects[data] = true
54
54
 
55
55
  case data
56
- when Hash
57
- scrub_hash(data)
58
- when Array
59
- scrub_array(data)
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
- scrub_value(value)
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
- if pattern.is_a?(Regexp)
105
- pattern.match?(field_name)
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
@@ -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
@@ -5,8 +5,6 @@ module Lapsoss
5
5
  module_function
6
6
 
7
7
  def current_time
8
- if Time.respond_to?(:zone) && Time.zone
9
- end
10
8
  Time.zone.now
11
9
  end
12
10
  end
@@ -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
- if value.nil? || (value.respond_to?(:empty?) && value.empty?)
13
- raise ValidationError, "#{name} is required and cannot be nil or empty"
14
- end
15
- end
11
+ return unless value.blank?
16
12
 
17
- def validate_type!(value, types, name)
18
- types = Array(types)
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
- unless value.nil? || value.respond_to?(:call)
27
- raise ValidationError, "#{name} must be callable (respond_to? :call) or nil, got #{value.class}"
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
- def validate_numeric_range!(value, range, name)
32
- validate_type!(value, [ Numeric ], name)
33
- unless range.cover?(value)
34
- raise ValidationError, "#{name} must be between #{range.min} and #{range.max}, got #{value}"
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
- def validate_url!(value, name)
39
- return if value.nil?
57
+ # Validate timeout values
58
+ def validate_timeout!(value, name)
59
+ return true if value.nil?
40
60
 
41
- validate_type!(value, [ String ], name)
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
- begin
44
- uri = URI.parse(value)
45
- raise ValidationError, "#{name} must be a valid URL with scheme and host" unless uri.scheme && uri.host
46
- raise ValidationError, "#{name} must use http or https scheme" unless %w[http https].include?(uri.scheme)
47
- rescue URI::InvalidURIError => e
48
- raise ValidationError, "#{name} is not a valid URL: #{e.message}"
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
- def validate_dsn!(dsn_string, name = "DSN")
53
- return if dsn_string.nil?
77
+ # Validate environment string
78
+ def validate_environment!(value, name)
79
+ return true if value.nil?
54
80
 
55
- validate_type!(dsn_string, [ String ], name)
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
- begin
58
- uri = URI.parse(dsn_string)
87
+ # Validate type
88
+ def validate_type!(value, expected_types, name)
89
+ return true if value.nil?
59
90
 
60
- # Check required components
61
- validate_presence!(uri.scheme, "#{name} scheme")
62
- validate_presence!(uri.host, "#{name} host")
63
- validate_presence!(uri.user, "#{name} public key")
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
- # Validate scheme
66
- raise ValidationError, "#{name} must use http or https scheme" unless %w[http https].include?(uri.scheme)
97
+ # Validate numeric range
98
+ def validate_numeric_range!(value, range, name)
99
+ return true if value.nil?
67
100
 
68
- # Extract project ID from path
69
- path_parts = uri.path&.split("/") || []
70
- project_id = path_parts.last
71
- validate_presence!(project_id, "#{name} project ID")
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
- # Validate project ID is numeric
74
- unless project_id.match?(/^\d+$/)
75
- raise ValidationError, "#{name} project ID must be numeric, got '#{project_id}'"
76
- end
77
- rescue URI::InvalidURIError => e
78
- raise ValidationError, "#{name} is not a valid URI: #{e.message}"
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
- validate_presence!(value, name)
84
- validate_type!(value, [ String ], name)
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
- raise ValidationError, "#{name} must be a valid UUID format"
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
- raise ValidationError, "#{name} must be alphanumeric" unless value.match?(/\A[a-z0-9]+\z/i)
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.nil?
100
-
101
- validate_type!(value, [ String, Symbol ], name)
141
+ return true if value.blank?
102
142
 
103
- env_string = value.to_s
104
- unless env_string.match?(/\A[a-z0-9_-]+\z/i)
105
- raise ValidationError, "#{name} must contain only alphanumeric characters, underscores, and hyphens"
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
- validate_type!(value, [ Numeric ], name)
119
- raise ValidationError, "#{name} must be positive, got #{value}" if value <= 0
148
+ true
120
149
  end
121
150
 
122
- def validate_retries!(value, name = "max_retries")
123
- return if value.nil?
151
+ # URL validation - just check parsability
152
+ def validate_url!(value, name)
153
+ return true if value.nil?
124
154
 
125
- validate_type!(value, [ Integer ], name)
126
- validate_numeric_range!(value, 0..10, name)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lapsoss
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
@@ -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