lapsoss 0.2.0 → 0.3.0

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +153 -733
  3. data/lib/lapsoss/adapters/appsignal_adapter.rb +22 -22
  4. data/lib/lapsoss/adapters/base.rb +0 -3
  5. data/lib/lapsoss/adapters/insight_hub_adapter.rb +108 -104
  6. data/lib/lapsoss/adapters/logger_adapter.rb +1 -1
  7. data/lib/lapsoss/adapters/rollbar_adapter.rb +108 -68
  8. data/lib/lapsoss/adapters/sentry_adapter.rb +24 -24
  9. data/lib/lapsoss/backtrace_frame.rb +37 -206
  10. data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
  11. data/lib/lapsoss/backtrace_processor.rb +26 -23
  12. data/lib/lapsoss/client.rb +2 -4
  13. data/lib/lapsoss/configuration.rb +28 -32
  14. data/lib/lapsoss/current.rb +10 -2
  15. data/lib/lapsoss/event.rb +28 -5
  16. data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
  17. data/lib/lapsoss/exclusion_configuration.rb +30 -0
  18. data/lib/lapsoss/exclusion_filter.rb +0 -273
  19. data/lib/lapsoss/exclusion_presets.rb +249 -0
  20. data/lib/lapsoss/fingerprinter.rb +28 -28
  21. data/lib/lapsoss/http_client.rb +8 -8
  22. data/lib/lapsoss/merged_scope.rb +63 -0
  23. data/lib/lapsoss/middleware/base.rb +15 -0
  24. data/lib/lapsoss/middleware/conditional_filter.rb +18 -0
  25. data/lib/lapsoss/middleware/event_enricher.rb +19 -0
  26. data/lib/lapsoss/middleware/event_transformer.rb +19 -0
  27. data/lib/lapsoss/middleware/exception_filter.rb +43 -0
  28. data/lib/lapsoss/middleware/metrics_collector.rb +44 -0
  29. data/lib/lapsoss/middleware/rate_limiter.rb +31 -0
  30. data/lib/lapsoss/middleware/release_tracker.rb +117 -0
  31. data/lib/lapsoss/middleware/sample_filter.rb +23 -0
  32. data/lib/lapsoss/middleware/sampling_middleware.rb +18 -0
  33. data/lib/lapsoss/middleware/user_context_enhancer.rb +46 -0
  34. data/lib/lapsoss/middleware.rb +0 -339
  35. data/lib/lapsoss/pipeline.rb +0 -68
  36. data/lib/lapsoss/pipeline_builder.rb +69 -0
  37. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  38. data/lib/lapsoss/rails_middleware.rb +78 -0
  39. data/lib/lapsoss/railtie.rb +22 -50
  40. data/lib/lapsoss/registry.rb +18 -5
  41. data/lib/lapsoss/release_providers.rb +110 -0
  42. data/lib/lapsoss/release_tracker.rb +159 -232
  43. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  44. data/lib/lapsoss/sampling/base.rb +11 -0
  45. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  46. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  47. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  48. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  49. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  50. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  51. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  52. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  53. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  54. data/lib/lapsoss/sampling.rb +0 -322
  55. data/lib/lapsoss/scope.rb +12 -48
  56. data/lib/lapsoss/scrubber.rb +7 -7
  57. data/lib/lapsoss/user_context.rb +30 -203
  58. data/lib/lapsoss/user_context_integrations.rb +39 -0
  59. data/lib/lapsoss/user_context_middleware.rb +50 -0
  60. data/lib/lapsoss/user_context_provider.rb +93 -0
  61. data/lib/lapsoss/utils.rb +13 -0
  62. data/lib/lapsoss/validators.rb +15 -15
  63. data/lib/lapsoss/version.rb +1 -1
  64. data/lib/lapsoss.rb +3 -3
  65. metadata +54 -5
@@ -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 'digest'
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: 'user-lookup-error'
11
+ fingerprint: "user-lookup-error"
12
12
  },
13
13
  {
14
14
  pattern: /Record \d+ (not found|invalid|missing)/i,
15
- fingerprint: 'record-lookup-error'
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: 'users-id-endpoint'
21
+ fingerprint: "users-id-endpoint"
22
22
  },
23
23
  {
24
24
  pattern: %r{/api/v\d+/.*},
25
- fingerprint: 'api-endpoint'
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: 'database-connection-error'
31
+ fingerprint: "database-connection-error"
32
32
  },
33
33
  {
34
34
  pattern: /ActiveRecord::RecordNotFound/,
35
- fingerprint: 'record-not-found'
35
+ fingerprint: "record-not-found"
36
36
  },
37
37
  {
38
38
  pattern: /ActiveRecord::StatementInvalid.*timeout/i,
39
- fingerprint: 'database-timeout'
39
+ fingerprint: "database-timeout"
40
40
  },
41
41
 
42
42
  # Network error patterns
43
43
  {
44
44
  pattern: /Net::(TimeoutError|ReadTimeout|OpenTimeout)/,
45
- fingerprint: 'network-timeout'
45
+ fingerprint: "network-timeout"
46
46
  },
47
47
  {
48
48
  pattern: /Errno::(ECONNREFUSED|ECONNRESET|EHOSTUNREACH)/,
49
- fingerprint: 'network-connection-error'
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: 'tmp-file-error'
55
+ fingerprint: "tmp-file-error"
56
56
  },
57
57
  {
58
58
  pattern: /No such file or directory.*\.log/,
59
- fingerprint: 'log-file-missing'
59
+ fingerprint: "log-file-missing"
60
60
  },
61
61
 
62
62
  # Memory/Resource patterns
63
63
  {
64
64
  pattern: /NoMemoryError|SystemStackError/,
65
- fingerprint: 'memory-resource-error'
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 << 'message'
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, ':uuid')
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, ':hash')
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/, ':id')
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]+}, ':filepath')
175
- normalized.gsub!(%r{/[^/\s]+(?:/[^/\s]+)+(?:/)?}, ':dirpath')
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}/, ':timestamp')
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]+}, ':url')
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?('/gems/') &&
194
- line.exclude?('/ruby/') &&
195
- line.exclude?('(eval)') &&
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