lapsoss 0.4.0 → 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.
- checksums.yaml +4 -4
- data/README.md +66 -7
- data/lib/lapsoss/client.rb +1 -3
- data/lib/lapsoss/configuration.rb +8 -17
- data/lib/lapsoss/fingerprinter.rb +52 -47
- data/lib/lapsoss/middleware/release_tracker.rb +11 -98
- data/lib/lapsoss/pipeline_builder.rb +2 -2
- data/lib/lapsoss/rails_middleware.rb +2 -2
- data/lib/lapsoss/railtie.rb +13 -2
- data/lib/lapsoss/registry.rb +7 -7
- data/lib/lapsoss/router.rb +1 -3
- data/lib/lapsoss/scrubber.rb +15 -152
- data/lib/lapsoss/validators.rb +48 -112
- data/lib/lapsoss/version.rb +1 -1
- metadata +1 -21
- data/lib/lapsoss/exclusion_configuration.rb +0 -30
- data/lib/lapsoss/exclusion_presets.rb +0 -249
- data/lib/lapsoss/middleware/sample_filter.rb +0 -23
- data/lib/lapsoss/middleware/sampling_middleware.rb +0 -18
- data/lib/lapsoss/middleware/user_context_enhancer.rb +0 -46
- data/lib/lapsoss/release_providers.rb +0 -110
- data/lib/lapsoss/sampling/adaptive_sampler.rb +0 -46
- data/lib/lapsoss/sampling/composite_sampler.rb +0 -26
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +0 -30
- data/lib/lapsoss/sampling/exception_type_sampler.rb +0 -44
- data/lib/lapsoss/sampling/health_based_sampler.rb +0 -19
- data/lib/lapsoss/sampling/sampling_factory.rb +0 -69
- data/lib/lapsoss/sampling/time_based_sampler.rb +0 -44
- data/lib/lapsoss/sampling/user_based_sampler.rb +0 -42
- data/lib/lapsoss/user_context.rb +0 -185
- data/lib/lapsoss/user_context_integrations.rb +0 -39
- data/lib/lapsoss/user_context_middleware.rb +0 -50
- data/lib/lapsoss/user_context_provider.rb +0 -93
- data/lib/lapsoss/utils.rb +0 -11
- data/lib/tasks/cassettes.rake +0 -50
@@ -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
|
@@ -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
|