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
@@ -1,19 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Sampling
5
- class HealthBasedSampler < Base
6
- def initialize(health_check:, high_rate: 1.0, low_rate: 0.1)
7
- @health_check = health_check
8
- @high_rate = high_rate
9
- @low_rate = low_rate
10
- end
11
-
12
- def sample?(_event, _hint = {})
13
- healthy = @health_check.call
14
- rate = healthy ? @high_rate : @low_rate
15
- rate > rand
16
- end
17
- end
18
- end
19
- end
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Sampling
5
- # Factory for creating common sampling configurations
6
- class SamplingFactory
7
- def self.create_production_sampling
8
- CompositeSampler.new(
9
- samplers: [
10
- # Rate limit to prevent overwhelming
11
- RateLimiter.new(max_events_per_second: 50),
12
-
13
- # Different rates for different exception types
14
- ExceptionTypeSampler.new(rates: {
15
- # Critical errors - always sample
16
- SecurityError => 1.0,
17
- SystemStackError => 1.0,
18
- NoMemoryError => 1.0,
19
-
20
- # Common errors - sample less
21
- ArgumentError => 0.1,
22
- TypeError => 0.1,
23
-
24
- # Network errors - medium sampling
25
- /timeout/i => 0.3,
26
- /connection/i => 0.3,
27
-
28
- # Default for unknown errors
29
- default: 0.5
30
- }),
31
-
32
- # Lower sampling during business hours
33
- TimeBasedSampler.new(schedule: {
34
- business_hours: 0.3,
35
- weekends: 0.8,
36
- default: 0.5
37
- })
38
- ],
39
- strategy: :all
40
- )
41
- end
42
-
43
- def self.create_development_sampling
44
- UniformSampler.new(1.0) # Sample everything in development
45
- end
46
-
47
- def self.create_user_focused_sampling
48
- CompositeSampler.new(
49
- samplers: [
50
- # Higher sampling for internal users
51
- UserBasedSampler.new(rates: {
52
- internal: 1.0,
53
- premium: 0.8,
54
- beta: 0.9,
55
- default: 0.1
56
- }),
57
-
58
- # Consistent sampling based on user ID
59
- ConsistentHashSampler.new(
60
- rate: 0.1,
61
- key_extractor: ->(event, _hint) { event.context.dig(:user, :id) }
62
- )
63
- ],
64
- strategy: :any
65
- )
66
- end
67
- end
68
- end
69
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Sampling
5
- class TimeBasedSampler < Base
6
- def initialize(schedule: {})
7
- @schedule = schedule
8
- @default_rate = schedule.fetch(:default, 1.0)
9
- end
10
-
11
- def sample?(_event, _hint = {})
12
- now = Time.zone.now
13
- rate = find_rate_for_time(now)
14
- rate > rand
15
- end
16
-
17
- private
18
-
19
- def find_rate_for_time(time)
20
- hour = time.hour
21
- day_of_week = time.wday # 0 = Sunday
22
-
23
- # Check specific hour
24
- hour_key = :"hour_#{hour}"
25
- return @schedule[hour_key] if @schedule.key?(hour_key)
26
-
27
- # Check day of week
28
- day_names = %i[sunday monday tuesday wednesday thursday friday saturday]
29
- day_key = day_names[day_of_week]
30
- return @schedule[day_key] if @schedule.key?(day_key)
31
-
32
- # Check business hours
33
- if @schedule.key?(:business_hours) && (9..17).cover?(hour) && (1..5).cover?(day_of_week)
34
- return @schedule[:business_hours]
35
- end
36
-
37
- # Check weekends
38
- return @schedule[:weekends] if @schedule.key?(:weekends) && [ 0, 6 ].include?(day_of_week)
39
-
40
- @default_rate
41
- end
42
- end
43
- end
44
- end
@@ -1,42 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Sampling
5
- class UserBasedSampler < Base
6
- def initialize(rates: {})
7
- @rates = rates
8
- @default_rate = rates.fetch(:default, 1.0)
9
- end
10
-
11
- def sample?(event, _hint = {})
12
- user = event.context[:user]
13
- return @default_rate > rand unless user
14
-
15
- rate = find_rate_for_user(user)
16
- rate > rand
17
- end
18
-
19
- private
20
-
21
- def find_rate_for_user(user)
22
- # Check specific user ID
23
- user_id = user[:id] || user["id"]
24
- return @rates[user_id] if user_id && @rates.key?(user_id)
25
-
26
- # Check user segments
27
- @rates.each do |segment, rate|
28
- case segment
29
- when :internal, "internal"
30
- return rate if user[:internal] || user["internal"]
31
- when :premium, "premium"
32
- return rate if user[:premium] || user["premium"]
33
- when :beta, "beta"
34
- return rate if user[:beta] || user["beta"]
35
- end
36
- end
37
-
38
- @default_rate
39
- end
40
- end
41
- end
42
- end
@@ -1,185 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- # Enhanced user context handling with privacy controls
5
- class UserContext
6
- SENSITIVE_FIELDS = %i[
7
- email phone mobile telephone
8
- address street city state zip postal_code country
9
- ssn social_security_number
10
- credit_card card_number cvv
11
- password password_confirmation
12
- secret token api_key
13
- ip_address last_login_ip
14
- birth_date date_of_birth dob
15
- salary income wage
16
- ].freeze
17
-
18
- def initialize(privacy_mode: false, allowed_fields: nil, field_transformers: {})
19
- @privacy_mode = privacy_mode
20
- @allowed_fields = allowed_fields&.map(&:to_sym)
21
- @field_transformers = field_transformers
22
- end
23
-
24
- def process_user_data(user_data)
25
- return {} unless user_data.is_a?(Hash)
26
-
27
- processed = {}
28
-
29
- user_data.each do |key, value|
30
- key_sym = key.to_sym
31
-
32
- # Skip if not in allowed fields list (when specified)
33
- next if @allowed_fields&.exclude?(key_sym)
34
-
35
- # Apply privacy filtering
36
- processed[key] = if @privacy_mode && sensitive_field?(key_sym)
37
- apply_privacy_filter(key_sym, value)
38
- else
39
- transform_field(key_sym, value)
40
- end
41
- end
42
-
43
- processed
44
- end
45
-
46
- def merge_user_data(existing_data, new_data)
47
- existing_processed = process_user_data(existing_data || {})
48
- new_processed = process_user_data(new_data || {})
49
-
50
- existing_processed.merge(new_processed)
51
- end
52
-
53
- def extract_user_id(user_data)
54
- return nil unless user_data.is_a?(Hash)
55
-
56
- # Try common user ID fields in order of preference
57
- %i[id user_id uuid guid].each do |field|
58
- value = user_data[field] || user_data[field.to_s]
59
- return value if value
60
- end
61
-
62
- nil
63
- end
64
-
65
- def extract_user_segment(user_data)
66
- return nil unless user_data.is_a?(Hash)
67
-
68
- segments = {}
69
-
70
- # Check for common user segments
71
- segments[:internal] = !(user_data[:internal] || user_data["internal"]).nil?
72
- segments[:premium] = !(user_data[:premium] || user_data["premium"]).nil?
73
- segments[:beta] = !(user_data[:beta] || user_data["beta"]).nil?
74
- segments[:admin] = !(user_data[:admin] || user_data["admin"]).nil?
75
-
76
- # Check role-based segments
77
- if role = user_data[:role] || user_data["role"]
78
- segments[:role] = role.to_s.downcase
79
- end
80
-
81
- # Check plan-based segments
82
- if plan = user_data[:plan] || user_data["plan"]
83
- segments[:plan] = plan.to_s.downcase
84
- end
85
-
86
- segments.compact
87
- end
88
-
89
- def sanitize_for_logging(user_data)
90
- return {} unless user_data.is_a?(Hash)
91
-
92
- sanitized = {}
93
-
94
- user_data.each do |key, value|
95
- key_sym = key.to_sym
96
-
97
- sanitized[key] = if sensitive_field?(key_sym)
98
- "[REDACTED]"
99
- else
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
115
- end
116
- end
117
-
118
- sanitized
119
- end
120
-
121
- private
122
-
123
- def sensitive_field?(field)
124
- SENSITIVE_FIELDS.include?(field) || field.to_s.match?(/password|secret|token|key|ssn|credit|card/i)
125
- end
126
-
127
- def apply_privacy_filter(field, value)
128
- case field
129
- when :email
130
- mask_email(value)
131
- when :phone, :mobile, :telephone
132
- mask_phone(value)
133
- when :ip_address, :last_login_ip
134
- mask_ip(value)
135
- else
136
- "[FILTERED]"
137
- end
138
- end
139
-
140
- def transform_field(field, value)
141
- if transformer = @field_transformers[field]
142
- transformer.call(value)
143
- else
144
- value
145
- end
146
- end
147
-
148
- def mask_email(email)
149
- return "[INVALID_EMAIL]" unless email.is_a?(String) && email.include?("@")
150
-
151
- local, domain = email.split("@", 2)
152
- masked_local = local.length > 2 ? "#{local[0]}***#{local[-1]}" : "***"
153
- "#{masked_local}@#{domain}"
154
- end
155
-
156
- def mask_phone(phone)
157
- return "[INVALID_PHONE]" unless phone.is_a?(String)
158
-
159
- # Remove all non-digits
160
- digits = phone.gsub(/\D/, "")
161
- return "[INVALID_PHONE]" if digits.length < 4
162
-
163
- # Show last 4 digits
164
- ("*" * (digits.length - 4)) + digits[-4..]
165
- end
166
-
167
- def mask_ip(ip)
168
- return "[INVALID_IP]" unless ip.is_a?(String)
169
-
170
- if ip.include?(":")
171
- # IPv6 - mask last 4 groups
172
- parts = ip.split(":")
173
- parts[-4..-1] = [ "****" ] * 4 if parts.length >= 4
174
- parts.join(":")
175
- else
176
- # IPv4 - mask last octet
177
- parts = ip.split(".")
178
- return "[INVALID_IP]" if parts.length != 4
179
-
180
- parts[-1] = "***"
181
- parts.join(".")
182
- end
183
- end
184
- end
185
- end
@@ -1,39 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- # Integration with popular authentication gems
5
- module UserContextIntegrations
6
- def self.setup_devise_integration
7
- return unless defined?(Devise)
8
-
9
- # Add middleware to capture user context
10
- Rails.application.config.middleware.use(UserContextMiddleware) if defined?(Rails)
11
- end
12
-
13
- def self.setup_clearance_integration
14
- return unless defined?(Clearance)
15
-
16
- # Clearance integration
17
- return unless defined?(Rails)
18
-
19
- Rails.application.config.middleware.use(UserContextMiddleware) do |middleware|
20
- middleware.user_provider = lambda do |request|
21
- request.env[:clearance].current_user if request.env[:clearance]
22
- end
23
- end
24
- end
25
-
26
- def self.setup_authlogic_integration
27
- return unless defined?(Authlogic)
28
-
29
- # Authlogic integration
30
- return unless defined?(Rails)
31
-
32
- Rails.application.config.middleware.use(UserContextMiddleware) do |middleware|
33
- middleware.user_provider = lambda do |_request|
34
- UserSession.find&.user if defined?(UserSession)
35
- end
36
- end
37
- end
38
- end
39
- end
@@ -1,50 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- # Middleware to automatically capture user context
5
- class UserContextMiddleware
6
- def initialize(app, user_provider: nil)
7
- @app = app
8
- @user_provider = user_provider
9
- end
10
-
11
- def call(env)
12
- request = Rack::Request.new(env)
13
-
14
- # Capture user context
15
- user_context = extract_user_context(request)
16
-
17
- # Store in thread-local for access during request processing
18
- Thread.current[:lapsoss_user_context] = user_context
19
-
20
- @app.call(env)
21
- ensure
22
- Thread.current[:lapsoss_user_context] = nil
23
- end
24
-
25
- private
26
-
27
- def extract_user_context(request)
28
- if @user_provider
29
- user = @user_provider.call(request)
30
- return {} unless user
31
-
32
- context = {
33
- id: user.id,
34
- email: user.email,
35
- username: user.respond_to?(:username) ? user.username : nil
36
- }
37
-
38
- # Add role information if available
39
- context[:role] = user.role if user.respond_to?(:role)
40
-
41
- # Add plan information if available
42
- context[:plan] = user.plan if user.respond_to?(:plan)
43
-
44
- context.compact
45
- else
46
- {}
47
- end
48
- end
49
- end
50
- end
@@ -1,93 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- # User context provider that integrates with various authentication systems
5
- class UserContextProvider
6
- def initialize(providers: {})
7
- @providers = providers
8
- end
9
-
10
- def get_user_context(event, hint = {})
11
- context = {}
12
-
13
- # Try each provider in order
14
- @providers.each do |name, provider|
15
- if provider_context = provider.call(event, hint)
16
- context.merge!(provider_context)
17
- end
18
- rescue StandardError => e
19
- # Log provider error but don't fail
20
- warn "User context provider #{name} failed: #{e.message}"
21
- end
22
-
23
- context
24
- end
25
-
26
- # Built-in providers for common authentication systems
27
- def self.devise_provider
28
- lambda do |_event, hint|
29
- return {} unless defined?(Devise) && defined?(Warden)
30
-
31
- # Try to get user from Warden (used by Devise)
32
- if request = hint[:request]
33
- user = request.env["warden"]&.user
34
- return {} unless user
35
-
36
- {
37
- id: user.id,
38
- email: user.email,
39
- username: user.respond_to?(:username) ? user.username : nil,
40
- created_at: user.created_at,
41
- role: user.respond_to?(:role) ? user.role : nil
42
- }.compact
43
- end
44
-
45
- {}
46
- end
47
- end
48
-
49
- def self.omniauth_provider
50
- lambda do |_event, hint|
51
- return {} unless defined?(OmniAuth)
52
-
53
- if (request = hint[:request]) && (auth_info = request.env["omniauth.auth"])
54
- {
55
- provider: auth_info["provider"],
56
- uid: auth_info["uid"],
57
- name: auth_info.dig("info", "name"),
58
- email: auth_info.dig("info", "email"),
59
- username: auth_info.dig("info", "nickname")
60
- }.compact
61
- end
62
-
63
- {}
64
- end
65
- end
66
-
67
- def self.session_provider
68
- lambda do |_event, hint|
69
- return {} unless hint[:request]
70
-
71
- request = hint[:request]
72
- session = begin
73
- request.session
74
- rescue StandardError
75
- {}
76
- end
77
-
78
- {
79
- session_id: session[:session_id] || session["session_id"],
80
- user_id: session[:user_id] || session["user_id"],
81
- csrf_token: session[:_csrf_token] || session["_csrf_token"]
82
- }.compact
83
- end
84
- end
85
-
86
- def self.thread_local_provider
87
- lambda do |_event, _hint|
88
- # Get user from thread-local storage
89
- Thread.current[:current_user] || {}
90
- end
91
- end
92
- end
93
- end
data/lib/lapsoss/utils.rb DELETED
@@ -1,11 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Utils
5
- module_function
6
-
7
- def current_time
8
- Time.zone.now
9
- end
10
- end
11
- end
@@ -1,50 +0,0 @@
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