lapsoss 0.2.0 → 0.3.1
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 +153 -733
- data/lib/lapsoss/adapters/appsignal_adapter.rb +22 -22
- data/lib/lapsoss/adapters/base.rb +0 -3
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
- data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
- data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
- data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
- data/lib/lapsoss/backtrace_frame.rb +37 -206
- data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
- data/lib/lapsoss/backtrace_processor.rb +27 -23
- data/lib/lapsoss/client.rb +2 -4
- data/lib/lapsoss/configuration.rb +28 -32
- data/lib/lapsoss/current.rb +10 -2
- data/lib/lapsoss/event.rb +28 -5
- data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
- data/lib/lapsoss/exclusion_configuration.rb +30 -0
- data/lib/lapsoss/exclusion_filter.rb +0 -273
- data/lib/lapsoss/exclusion_presets.rb +249 -0
- data/lib/lapsoss/fingerprinter.rb +28 -28
- data/lib/lapsoss/http_client.rb +8 -8
- data/lib/lapsoss/merged_scope.rb +63 -0
- data/lib/lapsoss/middleware/base.rb +15 -0
- data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
- data/lib/lapsoss/middleware/event_enricher.rb +19 -0
- data/lib/lapsoss/middleware/event_transformer.rb +19 -0
- data/lib/lapsoss/middleware/exception_filter.rb +43 -0
- data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
- data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
- data/lib/lapsoss/middleware/release_tracker.rb +117 -0
- data/lib/lapsoss/middleware/sample_filter.rb +23 -0
- data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
- data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
- data/lib/lapsoss/pipeline.rb +0 -68
- data/lib/lapsoss/pipeline_builder.rb +69 -0
- data/lib/lapsoss/rails_error_subscriber.rb +42 -0
- data/lib/lapsoss/rails_middleware.rb +78 -0
- data/lib/lapsoss/railtie.rb +22 -50
- data/lib/lapsoss/registry.rb +18 -5
- data/lib/lapsoss/release_providers.rb +110 -0
- data/lib/lapsoss/release_tracker.rb +159 -232
- data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
- data/lib/lapsoss/sampling/base.rb +11 -0
- data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
- data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
- data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
- data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
- data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
- data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
- data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
- data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
- data/lib/lapsoss/scope.rb +12 -48
- data/lib/lapsoss/scrubber.rb +7 -7
- data/lib/lapsoss/user_context.rb +30 -203
- data/lib/lapsoss/user_context_integrations.rb +39 -0
- data/lib/lapsoss/user_context_middleware.rb +50 -0
- data/lib/lapsoss/user_context_provider.rb +93 -0
- data/lib/lapsoss/utils.rb +13 -0
- data/lib/lapsoss/validators.rb +15 -15
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +3 -3
- metadata +60 -7
- data/lib/lapsoss/middleware.rb +0 -345
- data/lib/lapsoss/sampling.rb +0 -328
@@ -153,277 +153,4 @@ module Lapsoss
|
|
153
153
|
@custom_filters.any? { |filter| filter.call(event) }
|
154
154
|
end
|
155
155
|
end
|
156
|
-
|
157
|
-
# Predefined exclusion configurations for common use cases
|
158
|
-
class ExclusionPresets
|
159
|
-
def self.development
|
160
|
-
{
|
161
|
-
excluded_exceptions: [
|
162
|
-
# Test-related exceptions
|
163
|
-
'RSpec::Expectations::ExpectationNotMetError',
|
164
|
-
'Minitest::Assertion',
|
165
|
-
|
166
|
-
# Development tools
|
167
|
-
'Pry::CommandError',
|
168
|
-
'Byebug::CommandError'
|
169
|
-
],
|
170
|
-
excluded_patterns: [
|
171
|
-
/test/i,
|
172
|
-
/spec/i,
|
173
|
-
/debug/i,
|
174
|
-
/development/i
|
175
|
-
],
|
176
|
-
excluded_environments: %w[test]
|
177
|
-
}
|
178
|
-
end
|
179
|
-
|
180
|
-
def self.production
|
181
|
-
{
|
182
|
-
excluded_exceptions: [
|
183
|
-
# Common Rails exceptions that are usually not actionable
|
184
|
-
'ActionController::RoutingError',
|
185
|
-
'ActionController::UnknownFormat',
|
186
|
-
'ActionController::BadRequest',
|
187
|
-
'ActionController::ParameterMissing',
|
188
|
-
|
189
|
-
# ActiveRecord exceptions for common user errors
|
190
|
-
'ActiveRecord::RecordNotFound',
|
191
|
-
'ActiveRecord::RecordInvalid',
|
192
|
-
|
193
|
-
# Network timeouts that are expected
|
194
|
-
'Net::ReadTimeout',
|
195
|
-
'Net::OpenTimeout',
|
196
|
-
'Timeout::Error'
|
197
|
-
],
|
198
|
-
excluded_patterns: [
|
199
|
-
# Bot and crawler patterns
|
200
|
-
/bot/i,
|
201
|
-
/crawler/i,
|
202
|
-
/spider/i,
|
203
|
-
/scraper/i,
|
204
|
-
|
205
|
-
# Security scanning patterns
|
206
|
-
/sql.*injection/i,
|
207
|
-
/xss/i,
|
208
|
-
/csrf/i,
|
209
|
-
|
210
|
-
# Common attack patterns
|
211
|
-
/\.php$/i,
|
212
|
-
/\.asp$/i,
|
213
|
-
/wp-admin/i,
|
214
|
-
/wp-login/i
|
215
|
-
],
|
216
|
-
excluded_messages: [
|
217
|
-
# Common spam/attack messages
|
218
|
-
'No route matches',
|
219
|
-
'Invalid authenticity token',
|
220
|
-
'Forbidden',
|
221
|
-
'Unauthorized'
|
222
|
-
]
|
223
|
-
}
|
224
|
-
end
|
225
|
-
|
226
|
-
def self.staging
|
227
|
-
{
|
228
|
-
excluded_exceptions: [
|
229
|
-
# Test data related errors
|
230
|
-
'ActiveRecord::RecordNotFound',
|
231
|
-
'ArgumentError'
|
232
|
-
],
|
233
|
-
excluded_patterns: [
|
234
|
-
/test/i,
|
235
|
-
/staging/i,
|
236
|
-
/dummy/i,
|
237
|
-
/fake/i
|
238
|
-
],
|
239
|
-
excluded_environments: %w[test development]
|
240
|
-
}
|
241
|
-
end
|
242
|
-
|
243
|
-
def self.security_focused
|
244
|
-
{
|
245
|
-
excluded_patterns: [
|
246
|
-
# Exclude common security scanning attempts
|
247
|
-
/\.php$/i,
|
248
|
-
/\.asp$/i,
|
249
|
-
/\.jsp$/i,
|
250
|
-
/wp-admin/i,
|
251
|
-
/wp-login/i,
|
252
|
-
/phpmyadmin/i,
|
253
|
-
/admin/i,
|
254
|
-
/login\.php/i,
|
255
|
-
/index\.php/i,
|
256
|
-
|
257
|
-
# SQL injection attempts
|
258
|
-
/union.*select/i,
|
259
|
-
/insert.*into/i,
|
260
|
-
/drop.*table/i,
|
261
|
-
/delete.*from/i,
|
262
|
-
|
263
|
-
# XSS attempts
|
264
|
-
/<script/i,
|
265
|
-
/javascript:/i,
|
266
|
-
/onclick=/i,
|
267
|
-
/onerror=/i
|
268
|
-
],
|
269
|
-
excluded_messages: [
|
270
|
-
'Invalid authenticity token',
|
271
|
-
'Forbidden',
|
272
|
-
'Unauthorized',
|
273
|
-
'Access denied'
|
274
|
-
],
|
275
|
-
custom_filters: [
|
276
|
-
# Exclude requests from known bot user agents
|
277
|
-
lambda do |event|
|
278
|
-
user_agent = event.context.dig(:request, :headers, 'User-Agent')
|
279
|
-
return false unless user_agent
|
280
|
-
|
281
|
-
bot_patterns = [
|
282
|
-
/googlebot/i,
|
283
|
-
/bingbot/i,
|
284
|
-
/slurp/i,
|
285
|
-
/crawler/i,
|
286
|
-
/spider/i,
|
287
|
-
/bot/i
|
288
|
-
]
|
289
|
-
|
290
|
-
bot_patterns.any? { |pattern| user_agent.match?(pattern) }
|
291
|
-
end
|
292
|
-
]
|
293
|
-
}
|
294
|
-
end
|
295
|
-
|
296
|
-
def self.performance_focused
|
297
|
-
{
|
298
|
-
excluded_exceptions: [
|
299
|
-
# Timeout exceptions that are expected under load
|
300
|
-
'Net::ReadTimeout',
|
301
|
-
'Net::OpenTimeout',
|
302
|
-
'Timeout::Error',
|
303
|
-
'Redis::TimeoutError',
|
304
|
-
|
305
|
-
# Memory and resource limits
|
306
|
-
'NoMemoryError',
|
307
|
-
'SystemStackError'
|
308
|
-
],
|
309
|
-
excluded_patterns: [
|
310
|
-
/timeout/i,
|
311
|
-
/memory/i,
|
312
|
-
/resource/i,
|
313
|
-
/limit/i
|
314
|
-
],
|
315
|
-
custom_filters: [
|
316
|
-
# Exclude high-frequency errors during peak times
|
317
|
-
lambda do |event|
|
318
|
-
now = Time.zone.now
|
319
|
-
peak_hours = (9..17).cover?(now.hour) && (1..5).cover?(now.wday)
|
320
|
-
|
321
|
-
if peak_hours
|
322
|
-
# During peak hours, exclude common performance-related errors
|
323
|
-
return true if event.exception.is_a?(Timeout::Error)
|
324
|
-
return true if event.exception.message.match?(/timeout/i)
|
325
|
-
end
|
326
|
-
|
327
|
-
false
|
328
|
-
end
|
329
|
-
]
|
330
|
-
}
|
331
|
-
end
|
332
|
-
|
333
|
-
def self.user_error_focused
|
334
|
-
{
|
335
|
-
excluded_exceptions: [
|
336
|
-
# User input validation errors
|
337
|
-
'ActiveModel::ValidationError',
|
338
|
-
'ActiveRecord::RecordInvalid',
|
339
|
-
'ActionController::ParameterMissing',
|
340
|
-
'ArgumentError',
|
341
|
-
'TypeError'
|
342
|
-
],
|
343
|
-
excluded_patterns: [
|
344
|
-
/validation/i,
|
345
|
-
/invalid/i,
|
346
|
-
/missing/i,
|
347
|
-
/required/i,
|
348
|
-
/format/i
|
349
|
-
],
|
350
|
-
custom_filters: [
|
351
|
-
# Exclude errors from invalid user input
|
352
|
-
lambda do |event|
|
353
|
-
return false unless event.exception
|
354
|
-
|
355
|
-
# Check if error is from user input validation
|
356
|
-
message = event.exception.message.downcase
|
357
|
-
validation_keywords = %w[invalid required missing format validation]
|
358
|
-
|
359
|
-
validation_keywords.any? { |keyword| message.include?(keyword) }
|
360
|
-
end
|
361
|
-
]
|
362
|
-
}
|
363
|
-
end
|
364
|
-
|
365
|
-
def self.combined(presets)
|
366
|
-
combined_config = {
|
367
|
-
excluded_exceptions: [],
|
368
|
-
excluded_patterns: [],
|
369
|
-
excluded_messages: [],
|
370
|
-
excluded_environments: [],
|
371
|
-
custom_filters: []
|
372
|
-
}
|
373
|
-
|
374
|
-
presets.each do |preset|
|
375
|
-
config = case preset
|
376
|
-
when :development then development
|
377
|
-
when :production then production
|
378
|
-
when :staging then staging
|
379
|
-
when :security_focused then security_focused
|
380
|
-
when :performance_focused then performance_focused
|
381
|
-
when :user_error_focused then user_error_focused
|
382
|
-
when Hash then preset
|
383
|
-
else raise ArgumentError, "Unknown preset: #{preset}"
|
384
|
-
end
|
385
|
-
|
386
|
-
combined_config[:excluded_exceptions].concat(config[:excluded_exceptions] || [])
|
387
|
-
combined_config[:excluded_patterns].concat(config[:excluded_patterns] || [])
|
388
|
-
combined_config[:excluded_messages].concat(config[:excluded_messages] || [])
|
389
|
-
combined_config[:excluded_environments].concat(config[:excluded_environments] || [])
|
390
|
-
combined_config[:custom_filters].concat(config[:custom_filters] || [])
|
391
|
-
end
|
392
|
-
|
393
|
-
# Remove duplicates
|
394
|
-
combined_config[:excluded_exceptions].uniq!
|
395
|
-
combined_config[:excluded_patterns].uniq!
|
396
|
-
combined_config[:excluded_messages].uniq!
|
397
|
-
combined_config[:excluded_environments].uniq!
|
398
|
-
|
399
|
-
combined_config
|
400
|
-
end
|
401
|
-
end
|
402
|
-
|
403
|
-
# Configuration helper for exclusions
|
404
|
-
module ExclusionConfiguration
|
405
|
-
def self.configure_exclusions(config, preset: nil, **custom_config)
|
406
|
-
exclusion_config = if preset
|
407
|
-
case preset
|
408
|
-
when Array
|
409
|
-
ExclusionPresets.combined(preset)
|
410
|
-
else
|
411
|
-
ExclusionPresets.send(preset)
|
412
|
-
end
|
413
|
-
else
|
414
|
-
{}
|
415
|
-
end
|
416
|
-
|
417
|
-
# Merge custom configuration
|
418
|
-
exclusion_config.merge!(custom_config)
|
419
|
-
|
420
|
-
# Create exclusion filter
|
421
|
-
exclusion_filter = ExclusionFilter.new(exclusion_config)
|
422
|
-
|
423
|
-
# Add to configuration
|
424
|
-
config.exclusion_filter = exclusion_filter
|
425
|
-
|
426
|
-
exclusion_filter
|
427
|
-
end
|
428
|
-
end
|
429
156
|
end
|
@@ -0,0 +1,249 @@
|
|
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,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
3
|
+
require "digest"
|
4
4
|
|
5
5
|
module Lapsoss
|
6
6
|
class Fingerprinter
|
@@ -8,61 +8,61 @@ module Lapsoss
|
|
8
8
|
# User/ID normalization patterns
|
9
9
|
{
|
10
10
|
pattern: /User \d+ (not found|invalid|missing)/i,
|
11
|
-
fingerprint:
|
11
|
+
fingerprint: "user-lookup-error"
|
12
12
|
},
|
13
13
|
{
|
14
14
|
pattern: /Record \d+ (not found|invalid|missing)/i,
|
15
|
-
fingerprint:
|
15
|
+
fingerprint: "record-lookup-error"
|
16
16
|
},
|
17
17
|
|
18
18
|
# URL/Path normalization patterns
|
19
19
|
{
|
20
20
|
pattern: %r{/users/\d+(/.*)?},
|
21
|
-
fingerprint:
|
21
|
+
fingerprint: "users-id-endpoint"
|
22
22
|
},
|
23
23
|
{
|
24
24
|
pattern: %r{/api/v\d+/.*},
|
25
|
-
fingerprint:
|
25
|
+
fingerprint: "api-endpoint"
|
26
26
|
},
|
27
27
|
|
28
28
|
# Database error patterns
|
29
29
|
{
|
30
30
|
pattern: /PG::ConnectionBad|Mysql2::Error|SQLite3::BusyException/,
|
31
|
-
fingerprint:
|
31
|
+
fingerprint: "database-connection-error"
|
32
32
|
},
|
33
33
|
{
|
34
34
|
pattern: /ActiveRecord::RecordNotFound/,
|
35
|
-
fingerprint:
|
35
|
+
fingerprint: "record-not-found"
|
36
36
|
},
|
37
37
|
{
|
38
38
|
pattern: /ActiveRecord::StatementInvalid.*timeout/i,
|
39
|
-
fingerprint:
|
39
|
+
fingerprint: "database-timeout"
|
40
40
|
},
|
41
41
|
|
42
42
|
# Network error patterns
|
43
43
|
{
|
44
44
|
pattern: /Net::(TimeoutError|ReadTimeout|OpenTimeout)/,
|
45
|
-
fingerprint:
|
45
|
+
fingerprint: "network-timeout"
|
46
46
|
},
|
47
47
|
{
|
48
48
|
pattern: /Errno::(ECONNREFUSED|ECONNRESET|EHOSTUNREACH)/,
|
49
|
-
fingerprint:
|
49
|
+
fingerprint: "network-connection-error"
|
50
50
|
},
|
51
51
|
|
52
52
|
# File system patterns
|
53
53
|
{
|
54
54
|
pattern: %r{Errno::(ENOENT|EACCES).*/tmp/},
|
55
|
-
fingerprint:
|
55
|
+
fingerprint: "tmp-file-error"
|
56
56
|
},
|
57
57
|
{
|
58
58
|
pattern: /No such file or directory.*\.log/,
|
59
|
-
fingerprint:
|
59
|
+
fingerprint: "log-file-missing"
|
60
60
|
},
|
61
61
|
|
62
62
|
# Memory/Resource patterns
|
63
63
|
{
|
64
64
|
pattern: /NoMemoryError|SystemStackError/,
|
65
|
-
fingerprint:
|
65
|
+
fingerprint: "memory-resource-error"
|
66
66
|
}
|
67
67
|
].freeze
|
68
68
|
|
@@ -123,7 +123,7 @@ module Lapsoss
|
|
123
123
|
# Include first few backtrace lines for context
|
124
124
|
parts.concat(event.exception.backtrace.first(3)) if event.exception&.backtrace
|
125
125
|
|
126
|
-
parts.compact.join(
|
126
|
+
parts.compact.join(" ")
|
127
127
|
end
|
128
128
|
|
129
129
|
def generate_default_fingerprint(event)
|
@@ -141,7 +141,7 @@ module Lapsoss
|
|
141
141
|
primary_location = extract_primary_location(event.exception.backtrace)
|
142
142
|
components << primary_location if primary_location
|
143
143
|
elsif event.message
|
144
|
-
components <<
|
144
|
+
components << "message"
|
145
145
|
components << normalize_message(event.message)
|
146
146
|
end
|
147
147
|
|
@@ -149,7 +149,7 @@ module Lapsoss
|
|
149
149
|
components << event.environment if @include_environment && event.environment
|
150
150
|
|
151
151
|
# Generate hash from components
|
152
|
-
content = components.compact.join(
|
152
|
+
content = components.compact.join("|")
|
153
153
|
Digest::SHA256.hexdigest(content)[0, 16] # Use first 16 chars for readability
|
154
154
|
end
|
155
155
|
|
@@ -160,29 +160,29 @@ module Lapsoss
|
|
160
160
|
|
161
161
|
if @normalize_ids
|
162
162
|
# Replace UUIDs first (before numeric IDs)
|
163
|
-
normalized.gsub!(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i,
|
163
|
+
normalized.gsub!(/\b[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\b/i, ":uuid")
|
164
164
|
|
165
165
|
# Replace hex hashes with placeholder
|
166
|
-
normalized.gsub!(/\b[0-9a-f]{32,}\b/i,
|
166
|
+
normalized.gsub!(/\b[0-9a-f]{32,}\b/i, ":hash")
|
167
167
|
|
168
168
|
# Replace numeric IDs with placeholder (after UUIDs and hashes)
|
169
|
-
normalized.gsub!(/\b\d{3,}\b/,
|
169
|
+
normalized.gsub!(/\b\d{3,}\b/, ":id")
|
170
170
|
end
|
171
171
|
|
172
172
|
if @normalize_paths
|
173
173
|
# Replace absolute file paths with placeholder
|
174
|
-
normalized.gsub!(%r{/[^/\s]+(?:/[^/\s]+)*\.[a-zA-Z0-9]+},
|
175
|
-
normalized.gsub!(%r{/[^/\s]+(?:/[^/\s]+)+(?:/)?},
|
174
|
+
normalized.gsub!(%r{/[^/\s]+(?:/[^/\s]+)*\.[a-zA-Z0-9]+}, ":filepath")
|
175
|
+
normalized.gsub!(%r{/[^/\s]+(?:/[^/\s]+)+(?:/)?}, ":dirpath")
|
176
176
|
|
177
177
|
# Replace timestamps
|
178
|
-
normalized.gsub!(/\b\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}/,
|
178
|
+
normalized.gsub!(/\b\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}/, ":timestamp")
|
179
179
|
|
180
180
|
# Replace URLs with placeholder
|
181
|
-
normalized.gsub!(%r{https?://[^\s]+},
|
181
|
+
normalized.gsub!(%r{https?://[^\s]+}, ":url")
|
182
182
|
end
|
183
183
|
|
184
184
|
# Clean up extra whitespace
|
185
|
-
normalized.strip.squeeze(
|
185
|
+
normalized.strip.squeeze(" ")
|
186
186
|
end
|
187
187
|
|
188
188
|
def extract_primary_location(backtrace)
|
@@ -190,10 +190,10 @@ module Lapsoss
|
|
190
190
|
|
191
191
|
# Find first non-gem, non-framework line
|
192
192
|
app_line = backtrace.find do |line|
|
193
|
-
line.exclude?(
|
194
|
-
line.exclude?(
|
195
|
-
line.exclude?(
|
196
|
-
!line.start_with?(
|
193
|
+
line.exclude?("/gems/") &&
|
194
|
+
line.exclude?("/ruby/") &&
|
195
|
+
line.exclude?("(eval)") &&
|
196
|
+
!line.start_with?("[")
|
197
197
|
end
|
198
198
|
|
199
199
|
line_to_use = app_line || backtrace.first
|