lapsoss 0.3.1 → 0.4.2

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 (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +71 -7
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +18 -12
  4. data/lib/lapsoss/adapters/base.rb +19 -0
  5. data/lib/lapsoss/adapters/concerns/envelope_builder.rb +127 -0
  6. data/lib/lapsoss/adapters/concerns/http_delivery.rb +130 -0
  7. data/lib/lapsoss/adapters/concerns/level_mapping.rb +65 -0
  8. data/lib/lapsoss/adapters/insight_hub_adapter.rb +21 -21
  9. data/lib/lapsoss/adapters/rollbar_adapter.rb +64 -122
  10. data/lib/lapsoss/adapters/sentry_adapter.rb +77 -143
  11. data/lib/lapsoss/backtrace_processor.rb +1 -1
  12. data/lib/lapsoss/breadcrumb.rb +59 -0
  13. data/lib/lapsoss/client.rb +3 -5
  14. data/lib/lapsoss/configuration.rb +26 -31
  15. data/lib/lapsoss/event.rb +90 -96
  16. data/lib/lapsoss/fingerprinter.rb +57 -49
  17. data/lib/lapsoss/merged_scope.rb +1 -6
  18. data/lib/lapsoss/middleware/release_tracker.rb +11 -98
  19. data/lib/lapsoss/pipeline_builder.rb +2 -2
  20. data/lib/lapsoss/rails_error_subscriber.rb +3 -4
  21. data/lib/lapsoss/rails_middleware.rb +2 -2
  22. data/lib/lapsoss/railtie.rb +13 -2
  23. data/lib/lapsoss/registry.rb +7 -7
  24. data/lib/lapsoss/router.rb +1 -3
  25. data/lib/lapsoss/scope.rb +1 -6
  26. data/lib/lapsoss/scrubber.rb +15 -148
  27. data/lib/lapsoss/validators.rb +63 -92
  28. data/lib/lapsoss/version.rb +1 -1
  29. metadata +8 -24
  30. data/CHANGELOG.md +0 -5
  31. data/lib/lapsoss/exclusion_configuration.rb +0 -30
  32. data/lib/lapsoss/exclusion_presets.rb +0 -249
  33. data/lib/lapsoss/middleware/sample_filter.rb +0 -23
  34. data/lib/lapsoss/middleware/sampling_middleware.rb +0 -18
  35. data/lib/lapsoss/middleware/user_context_enhancer.rb +0 -46
  36. data/lib/lapsoss/release_providers.rb +0 -110
  37. data/lib/lapsoss/sampling/adaptive_sampler.rb +0 -46
  38. data/lib/lapsoss/sampling/composite_sampler.rb +0 -26
  39. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +0 -30
  40. data/lib/lapsoss/sampling/exception_type_sampler.rb +0 -44
  41. data/lib/lapsoss/sampling/health_based_sampler.rb +0 -19
  42. data/lib/lapsoss/sampling/sampling_factory.rb +0 -69
  43. data/lib/lapsoss/sampling/time_based_sampler.rb +0 -44
  44. data/lib/lapsoss/sampling/user_based_sampler.rb +0 -42
  45. data/lib/lapsoss/user_context.rb +0 -175
  46. data/lib/lapsoss/user_context_integrations.rb +0 -39
  47. data/lib/lapsoss/user_context_middleware.rb +0 -50
  48. data/lib/lapsoss/user_context_provider.rb +0 -93
  49. data/lib/lapsoss/utils.rb +0 -13
@@ -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,175 +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
- 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
- else
104
- value
105
- end
106
- end
107
-
108
- sanitized
109
- end
110
-
111
- private
112
-
113
- def sensitive_field?(field)
114
- SENSITIVE_FIELDS.include?(field) || field.to_s.match?(/password|secret|token|key|ssn|credit|card/i)
115
- end
116
-
117
- def apply_privacy_filter(field, value)
118
- case field
119
- when :email
120
- mask_email(value)
121
- when :phone, :mobile, :telephone
122
- mask_phone(value)
123
- when :ip_address, :last_login_ip
124
- mask_ip(value)
125
- else
126
- "[FILTERED]"
127
- end
128
- end
129
-
130
- def transform_field(field, value)
131
- if transformer = @field_transformers[field]
132
- transformer.call(value)
133
- else
134
- value
135
- end
136
- end
137
-
138
- def mask_email(email)
139
- return "[INVALID_EMAIL]" unless email.is_a?(String) && email.include?("@")
140
-
141
- local, domain = email.split("@", 2)
142
- masked_local = local.length > 2 ? "#{local[0]}***#{local[-1]}" : "***"
143
- "#{masked_local}@#{domain}"
144
- end
145
-
146
- def mask_phone(phone)
147
- return "[INVALID_PHONE]" unless phone.is_a?(String)
148
-
149
- # Remove all non-digits
150
- digits = phone.gsub(/\D/, "")
151
- return "[INVALID_PHONE]" if digits.length < 4
152
-
153
- # Show last 4 digits
154
- ("*" * (digits.length - 4)) + digits[-4..]
155
- end
156
-
157
- def mask_ip(ip)
158
- return "[INVALID_IP]" unless ip.is_a?(String)
159
-
160
- if ip.include?(":")
161
- # IPv6 - mask last 4 groups
162
- parts = ip.split(":")
163
- parts[-4..-1] = [ "****" ] * 4 if parts.length >= 4
164
- parts.join(":")
165
- else
166
- # IPv4 - mask last octet
167
- parts = ip.split(".")
168
- return "[INVALID_IP]" if parts.length != 4
169
-
170
- parts[-1] = "***"
171
- parts.join(".")
172
- end
173
- end
174
- end
175
- 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,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Utils
5
- module_function
6
-
7
- def current_time
8
- if Time.respond_to?(:zone) && Time.zone
9
- end
10
- Time.zone.now
11
- end
12
- end
13
- end