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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lapsoss
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 0.4.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -225,6 +225,7 @@ files:
225
225
  - lib/lapsoss/adapters/logger_adapter.rb
226
226
  - lib/lapsoss/adapters/rollbar_adapter.rb
227
227
  - lib/lapsoss/adapters/sentry_adapter.rb
228
+ - lib/lapsoss/adapters/telebug_adapter.rb
228
229
  - lib/lapsoss/backtrace_frame.rb
229
230
  - lib/lapsoss/backtrace_frame_factory.rb
230
231
  - lib/lapsoss/backtrace_processor.rb
@@ -234,9 +235,7 @@ files:
234
235
  - lib/lapsoss/current.rb
235
236
  - lib/lapsoss/event.rb
236
237
  - lib/lapsoss/exception_backtrace_frame.rb
237
- - lib/lapsoss/exclusion_configuration.rb
238
238
  - lib/lapsoss/exclusion_filter.rb
239
- - lib/lapsoss/exclusion_presets.rb
240
239
  - lib/lapsoss/fingerprinter.rb
241
240
  - lib/lapsoss/http_client.rb
242
241
  - lib/lapsoss/merged_scope.rb
@@ -248,39 +247,21 @@ files:
248
247
  - lib/lapsoss/middleware/metrics_collector.rb
249
248
  - lib/lapsoss/middleware/rate_limiter.rb
250
249
  - lib/lapsoss/middleware/release_tracker.rb
251
- - lib/lapsoss/middleware/sample_filter.rb
252
- - lib/lapsoss/middleware/sampling_middleware.rb
253
- - lib/lapsoss/middleware/user_context_enhancer.rb
254
250
  - lib/lapsoss/pipeline.rb
255
251
  - lib/lapsoss/pipeline_builder.rb
256
252
  - lib/lapsoss/rails_error_subscriber.rb
257
253
  - lib/lapsoss/rails_middleware.rb
258
254
  - lib/lapsoss/railtie.rb
259
255
  - lib/lapsoss/registry.rb
260
- - lib/lapsoss/release_providers.rb
261
256
  - lib/lapsoss/release_tracker.rb
262
257
  - lib/lapsoss/router.rb
263
- - lib/lapsoss/sampling/adaptive_sampler.rb
264
258
  - lib/lapsoss/sampling/base.rb
265
- - lib/lapsoss/sampling/composite_sampler.rb
266
- - lib/lapsoss/sampling/consistent_hash_sampler.rb
267
- - lib/lapsoss/sampling/exception_type_sampler.rb
268
- - lib/lapsoss/sampling/health_based_sampler.rb
269
259
  - lib/lapsoss/sampling/rate_limiter.rb
270
- - lib/lapsoss/sampling/sampling_factory.rb
271
- - lib/lapsoss/sampling/time_based_sampler.rb
272
260
  - lib/lapsoss/sampling/uniform_sampler.rb
273
- - lib/lapsoss/sampling/user_based_sampler.rb
274
261
  - lib/lapsoss/scope.rb
275
262
  - lib/lapsoss/scrubber.rb
276
- - lib/lapsoss/user_context.rb
277
- - lib/lapsoss/user_context_integrations.rb
278
- - lib/lapsoss/user_context_middleware.rb
279
- - lib/lapsoss/user_context_provider.rb
280
- - lib/lapsoss/utils.rb
281
263
  - lib/lapsoss/validators.rb
282
264
  - lib/lapsoss/version.rb
283
- - lib/tasks/cassettes.rake
284
265
  homepage: https://github.com/seuros/lapsoss
285
266
  licenses:
286
267
  - MIT
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- # Configuration helper for exclusions
5
- module ExclusionConfiguration
6
- def self.configure_exclusions(config, preset: nil, **custom_config)
7
- exclusion_config = if preset
8
- case preset
9
- when Array
10
- ExclusionPresets.combined(preset)
11
- else
12
- ExclusionPresets.send(preset)
13
- end
14
- else
15
- {}
16
- end
17
-
18
- # Merge custom configuration
19
- exclusion_config.merge!(custom_config)
20
-
21
- # Create exclusion filter
22
- exclusion_filter = ExclusionFilter.new(exclusion_config)
23
-
24
- # Add to configuration
25
- config.exclusion_filter = exclusion_filter
26
-
27
- exclusion_filter
28
- end
29
- end
30
- end
@@ -1,249 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- # Predefined exclusion configurations for common use cases
5
- class ExclusionPresets
6
- def self.development
7
- {
8
- excluded_exceptions: [
9
- # Test-related exceptions
10
- "RSpec::Expectations::ExpectationNotMetError",
11
- "Minitest::Assertion",
12
-
13
- # Development tools
14
- "Pry::CommandError",
15
- "Byebug::CommandError"
16
- ],
17
- excluded_patterns: [
18
- /test/i,
19
- /spec/i,
20
- /debug/i,
21
- /development/i
22
- ],
23
- excluded_environments: %w[test]
24
- }
25
- end
26
-
27
- def self.production
28
- {
29
- excluded_exceptions: [
30
- # Common Rails exceptions that are usually not actionable
31
- "ActionController::RoutingError",
32
- "ActionController::UnknownFormat",
33
- "ActionController::BadRequest",
34
- "ActionController::ParameterMissing",
35
-
36
- # ActiveRecord exceptions for common user errors
37
- "ActiveRecord::RecordNotFound",
38
- "ActiveRecord::RecordInvalid",
39
-
40
- # Network timeouts that are expected
41
- "Net::ReadTimeout",
42
- "Net::OpenTimeout",
43
- "Timeout::Error"
44
- ],
45
- excluded_patterns: [
46
- # Bot and crawler patterns
47
- /bot/i,
48
- /crawler/i,
49
- /spider/i,
50
- /scraper/i,
51
-
52
- # Security scanning patterns
53
- /sql.*injection/i,
54
- /xss/i,
55
- /csrf/i,
56
-
57
- # Common attack patterns
58
- /\.php$/i,
59
- /\.asp$/i,
60
- /wp-admin/i,
61
- /wp-login/i
62
- ],
63
- excluded_messages: [
64
- # Common spam/attack messages
65
- "No route matches",
66
- "Invalid authenticity token",
67
- "Forbidden",
68
- "Unauthorized"
69
- ]
70
- }
71
- end
72
-
73
- def self.staging
74
- {
75
- excluded_exceptions: [
76
- # Test data related errors
77
- "ActiveRecord::RecordNotFound",
78
- "ArgumentError"
79
- ],
80
- excluded_patterns: [
81
- /test/i,
82
- /staging/i,
83
- /dummy/i,
84
- /fake/i
85
- ],
86
- excluded_environments: %w[test development]
87
- }
88
- end
89
-
90
- def self.security_focused
91
- {
92
- excluded_patterns: [
93
- # Exclude common security scanning attempts
94
- /\.php$/i,
95
- /\.asp$/i,
96
- /\.jsp$/i,
97
- /wp-admin/i,
98
- /wp-login/i,
99
- /phpmyadmin/i,
100
- /admin/i,
101
- /login\.php/i,
102
- /index\.php/i,
103
-
104
- # SQL injection attempts
105
- /union.*select/i,
106
- /insert.*into/i,
107
- /drop.*table/i,
108
- /delete.*from/i,
109
-
110
- # XSS attempts
111
- /<script/i,
112
- /javascript:/i,
113
- /onclick=/i,
114
- /onerror=/i
115
- ],
116
- excluded_messages: [
117
- "Invalid authenticity token",
118
- "Forbidden",
119
- "Unauthorized",
120
- "Access denied"
121
- ],
122
- custom_filters: [
123
- # Exclude requests from known bot user agents
124
- lambda do |event|
125
- user_agent = event.context.dig(:request, :headers, "User-Agent")
126
- return false unless user_agent
127
-
128
- bot_patterns = [
129
- /googlebot/i,
130
- /bingbot/i,
131
- /slurp/i,
132
- /crawler/i,
133
- /spider/i,
134
- /bot/i
135
- ]
136
-
137
- bot_patterns.any? { |pattern| user_agent.match?(pattern) }
138
- end
139
- ]
140
- }
141
- end
142
-
143
- def self.performance_focused
144
- {
145
- excluded_exceptions: [
146
- # Timeout exceptions that are expected under load
147
- "Net::ReadTimeout",
148
- "Net::OpenTimeout",
149
- "Timeout::Error",
150
- "Redis::TimeoutError",
151
-
152
- # Memory and resource limits
153
- "NoMemoryError",
154
- "SystemStackError"
155
- ],
156
- excluded_patterns: [
157
- /timeout/i,
158
- /memory/i,
159
- /resource/i,
160
- /limit/i
161
- ],
162
- custom_filters: [
163
- # Exclude high-frequency errors during peak times
164
- lambda do |event|
165
- now = Time.zone.now
166
- peak_hours = (9..17).cover?(now.hour) && (1..5).cover?(now.wday)
167
-
168
- if peak_hours
169
- # During peak hours, exclude common performance-related errors
170
- return true if event.exception.is_a?(Timeout::Error)
171
- return true if event.exception.message.match?(/timeout/i)
172
- end
173
-
174
- false
175
- end
176
- ]
177
- }
178
- end
179
-
180
- def self.user_error_focused
181
- {
182
- excluded_exceptions: [
183
- # User input validation errors
184
- "ActiveModel::ValidationError",
185
- "ActiveRecord::RecordInvalid",
186
- "ActionController::ParameterMissing",
187
- "ArgumentError",
188
- "TypeError"
189
- ],
190
- excluded_patterns: [
191
- /validation/i,
192
- /invalid/i,
193
- /missing/i,
194
- /required/i,
195
- /format/i
196
- ],
197
- custom_filters: [
198
- # Exclude errors from invalid user input
199
- lambda do |event|
200
- return false unless event.exception
201
-
202
- # Check if error is from user input validation
203
- message = event.exception.message.downcase
204
- validation_keywords = %w[invalid required missing format validation]
205
-
206
- validation_keywords.any? { |keyword| message.include?(keyword) }
207
- end
208
- ]
209
- }
210
- end
211
-
212
- def self.combined(presets)
213
- combined_config = {
214
- excluded_exceptions: [],
215
- excluded_patterns: [],
216
- excluded_messages: [],
217
- excluded_environments: [],
218
- custom_filters: []
219
- }
220
-
221
- presets.each do |preset|
222
- config = case preset
223
- when :development then development
224
- when :production then production
225
- when :staging then staging
226
- when :security_focused then security_focused
227
- when :performance_focused then performance_focused
228
- when :user_error_focused then user_error_focused
229
- when Hash then preset
230
- else raise ArgumentError, "Unknown preset: #{preset}"
231
- end
232
-
233
- combined_config[:excluded_exceptions].concat(config[:excluded_exceptions] || [])
234
- combined_config[:excluded_patterns].concat(config[:excluded_patterns] || [])
235
- combined_config[:excluded_messages].concat(config[:excluded_messages] || [])
236
- combined_config[:excluded_environments].concat(config[:excluded_environments] || [])
237
- combined_config[:custom_filters].concat(config[:custom_filters] || [])
238
- end
239
-
240
- # Remove duplicates
241
- combined_config[:excluded_exceptions].uniq!
242
- combined_config[:excluded_patterns].uniq!
243
- combined_config[:excluded_messages].uniq!
244
- combined_config[:excluded_environments].uniq!
245
-
246
- combined_config
247
- end
248
- end
249
- end
@@ -1,23 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Middleware
5
- class SampleFilter < Base
6
- def initialize(app, sample_rate: 1.0, sample_callback: nil)
7
- super(app)
8
- @sample_rate = sample_rate
9
- @sample_callback = sample_callback
10
- end
11
-
12
- def call(event, hint = {})
13
- # Apply custom sampling logic first
14
- return nil if @sample_callback && !@sample_callback.call(event, hint)
15
-
16
- # Apply rate-based sampling
17
- return nil if (@sample_rate < 1.0) && (rand > @sample_rate)
18
-
19
- @app.call(event, hint)
20
- end
21
- end
22
- end
23
- end
@@ -1,18 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Middleware
5
- class SamplingMiddleware < Base
6
- def initialize(app, sampler)
7
- super(app)
8
- @sampler = sampler
9
- end
10
-
11
- def call(event, hint = {})
12
- return nil unless @sampler.sample?(event, hint)
13
-
14
- @app.call(event, hint)
15
- end
16
- end
17
- end
18
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Middleware
5
- class UserContextEnhancer < Base
6
- def initialize(app, user_provider: nil, privacy_mode: false)
7
- super(app)
8
- @user_provider = user_provider
9
- @privacy_mode = privacy_mode
10
- end
11
-
12
- def call(event, hint = {})
13
- enhance_user_context(event, hint)
14
- @app.call(event, hint)
15
- end
16
-
17
- private
18
-
19
- def enhance_user_context(event, hint)
20
- # Get user from provider if available
21
- user_data = @user_provider&.call(event, hint) || {}
22
-
23
- # Merge with existing user context
24
- existing_user = event.context[:user] || {}
25
- enhanced_user = existing_user.merge(user_data)
26
-
27
- # Apply privacy filtering if enabled
28
- enhanced_user = apply_privacy_filtering(enhanced_user) if @privacy_mode
29
-
30
- event.context[:user] = enhanced_user unless enhanced_user.empty?
31
- end
32
-
33
- def apply_privacy_filtering(user_data)
34
- # Remove sensitive fields in privacy mode
35
- sensitive_fields = %i[email phone address ssn credit_card]
36
- filtered = user_data.dup
37
-
38
- sensitive_fields.each do |field|
39
- filtered[field] = "[FILTERED]" if filtered.key?(field)
40
- end
41
-
42
- filtered
43
- end
44
- end
45
- end
46
- end
@@ -1,110 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "json"
4
-
5
- module Lapsoss
6
- # Built-in release providers for common scenarios
7
- class ReleaseProviders
8
- def self.from_file(file_path)
9
- lambda do
10
- return nil unless File.exist?(file_path)
11
-
12
- content = File.read(file_path).strip
13
- return nil if content.empty?
14
-
15
- # Try to parse as JSON first
16
- begin
17
- JSON.parse(content)
18
- rescue JSON::ParserError
19
- # Treat as plain text version
20
- { version: content }
21
- end
22
- end
23
- end
24
-
25
- def self.from_ruby_constant(constant_name)
26
- lambda do
27
- constant = Object.const_get(constant_name)
28
- { version: constant.to_s }
29
- rescue NameError
30
- nil
31
- end
32
- end
33
-
34
- def self.from_gemfile_lock
35
- lambda do
36
- return nil unless File.exist?("Gemfile.lock")
37
-
38
- content = File.read("Gemfile.lock")
39
-
40
- # Extract gems with versions
41
- gems = {}
42
- content.scan(/^\s{4}(\w+)\s+\(([^)]+)\)/).each do |name, version|
43
- gems[name] = version
44
- end
45
-
46
- { gems: gems }
47
- end
48
- end
49
-
50
- def self.from_package_json
51
- lambda do
52
- return nil unless File.exist?("package.json")
53
-
54
- begin
55
- package_info = JSON.parse(File.read("package.json"))
56
- {
57
- version: package_info["version"],
58
- name: package_info["name"],
59
- dependencies: package_info["dependencies"]&.keys
60
- }.compact
61
- rescue JSON::ParserError
62
- nil
63
- end
64
- end
65
- end
66
-
67
- def self.from_rails_application
68
- lambda do
69
- return nil unless defined?(Rails) && Rails.respond_to?(:application)
70
-
71
- app = Rails.application
72
- return nil unless app
73
-
74
- info = {
75
- rails_version: Rails.version,
76
- environment: Rails.env,
77
- root: Rails.root.to_s
78
- }
79
-
80
- # Get application version if defined
81
- info[:app_version] = app.class.version if app.class.respond_to?(:version)
82
-
83
- # Get application name
84
- info[:app_name] = app.class.name if app.class.respond_to?(:name)
85
-
86
- info
87
- end
88
- end
89
-
90
- def self.from_capistrano
91
- lambda do
92
- # Check for Capistrano deployment files
93
- %w[REVISION current/REVISION].each do |file|
94
- next unless File.exist?(file)
95
-
96
- revision = File.read(file).strip
97
- next if revision.empty?
98
-
99
- return {
100
- revision: revision,
101
- deployed_at: File.mtime(file),
102
- deployment_method: "capistrano"
103
- }
104
- end
105
-
106
- nil
107
- end
108
- end
109
- end
110
- end
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Sampling
5
- class AdaptiveSampler < Base
6
- def initialize(target_rate: 1.0, adjustment_period: 60)
7
- @target_rate = target_rate
8
- @adjustment_period = adjustment_period
9
- @current_rate = target_rate
10
- @events_count = 0
11
- @last_adjustment = Time.zone.now
12
- @mutex = Mutex.new
13
- end
14
-
15
- def sample?(_event, _hint = {})
16
- @mutex.synchronize do
17
- @events_count += 1
18
-
19
- # Adjust rate periodically
20
- now = Time.zone.now
21
- if now - @last_adjustment > @adjustment_period
22
- adjust_rate
23
- @last_adjustment = now
24
- @events_count = 0
25
- end
26
- end
27
-
28
- @current_rate > rand
29
- end
30
-
31
- attr_reader :current_rate
32
-
33
- private
34
-
35
- def adjust_rate
36
- # Simple adaptive logic - can be enhanced based on system metrics
37
- # For now, just ensure we don't drift too far from target
38
- if @events_count > 100 # High volume
39
- @current_rate = [ @current_rate * 0.9, @target_rate * 0.1 ].max
40
- elsif @events_count < 10 # Low volume
41
- @current_rate = [ @current_rate * 1.1, @target_rate ].min
42
- end
43
- end
44
- end
45
- end
46
- end
@@ -1,26 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Sampling
5
- class CompositeSampler < Base
6
- def initialize(app = nil, samplers: [], strategy: :all)
7
- @app = app
8
- @samplers = samplers
9
- @strategy = strategy
10
- end
11
-
12
- def sample?(event, hint = {})
13
- case @strategy
14
- when :all
15
- @samplers.all? { |sampler| sampler.sample?(event, hint) }
16
- when :any
17
- @samplers.any? { |sampler| sampler.sample?(event, hint) }
18
- when :first
19
- @samplers.first&.sample?(event, hint) || true
20
- else
21
- raise ArgumentError, "Unknown strategy: #{@strategy}"
22
- end
23
- end
24
- end
25
- end
26
- end
@@ -1,30 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "digest"
4
-
5
- module Lapsoss
6
- module Sampling
7
- class ConsistentHashSampler < Base
8
- def initialize(rate:, key_extractor: nil)
9
- @rate = rate
10
- @key_extractor = key_extractor || method(:default_key_extractor)
11
- @threshold = (rate * 0xFFFFFFFF).to_i
12
- end
13
-
14
- def sample?(event, hint = {})
15
- key = @key_extractor.call(event, hint)
16
- return @rate > rand unless key
17
-
18
- hash_value = Digest::MD5.hexdigest(key.to_s)[0, 8].to_i(16)
19
- hash_value <= @threshold
20
- end
21
-
22
- private
23
-
24
- def default_key_extractor(event, _hint)
25
- # Use fingerprint for consistent sampling
26
- event.fingerprint
27
- end
28
- end
29
- end
30
- end
@@ -1,44 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- module Sampling
5
- class ExceptionTypeSampler < 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
- return @default_rate > rand unless event.exception
13
-
14
- exception_class = event.exception.class
15
- rate = find_rate_for_exception(exception_class)
16
- rate > rand
17
- end
18
-
19
- private
20
-
21
- def find_rate_for_exception(exception_class)
22
- # Check exact class match first
23
- return @rates[exception_class] if @rates.key?(exception_class)
24
-
25
- # Check inheritance hierarchy
26
- @rates.each do |klass, rate|
27
- return rate if klass.is_a?(Class) && exception_class <= klass
28
- end
29
-
30
- # Check string/regex patterns
31
- @rates.each do |pattern, rate|
32
- case pattern
33
- when String
34
- return rate if exception_class.name.include?(pattern)
35
- when Regexp
36
- return rate if exception_class.name.match?(pattern)
37
- end
38
- end
39
-
40
- @default_rate
41
- end
42
- end
43
- end
44
- end