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
@@ -1,8 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/cache'
4
- require 'active_support/core_ext/numeric/time'
5
- require_relative 'backtrace_frame'
3
+ require "active_support/cache"
4
+ require "active_support/core_ext/numeric/time"
6
5
 
7
6
  module Lapsoss
8
7
  class BacktraceProcessor
@@ -42,10 +41,10 @@ module Lapsoss
42
41
  exclude_patterns: config.backtrace_exclude_patterns,
43
42
  strip_load_path: config.backtrace_strip_load_path
44
43
  }
45
- else
44
+ else
46
45
  # Hash passed
47
46
  config
48
- end
47
+ end
49
48
 
50
49
  @config = DEFAULT_CONFIG.merge(config_hash)
51
50
  @file_cache = ActiveSupport::Cache::MemoryStore.new(
@@ -83,10 +82,13 @@ module Lapsoss
83
82
 
84
83
  frames = process_backtrace(exception.backtrace)
85
84
 
86
- # Add exception-specific context
87
- frames.each_with_index do |frame, index|
88
- frame.define_singleton_method(:crash_frame?) { index.zero? }
89
- frame.define_singleton_method(:exception_class) { exception.class.name }
85
+ # Wrap frames with exception-specific context
86
+ frames = frames.map.with_index do |frame, index|
87
+ ExceptionBacktraceFrame.new(
88
+ frame,
89
+ exception_class: exception.class.name,
90
+ is_crash_frame: index.zero?
91
+ )
90
92
  end
91
93
 
92
94
  # Follow exception causes if requested
@@ -179,7 +181,7 @@ module Lapsoss
179
181
  file_cache: {
180
182
  # ActiveSupport::Cache::MemoryStore doesn't expose detailed stats
181
183
  # but we can provide basic info
182
- type: 'ActiveSupport::Cache::MemoryStore',
184
+ type: "ActiveSupport::Cache::MemoryStore",
183
185
  configured_size: @file_cache.options[:size]
184
186
  },
185
187
  config: @config,
@@ -207,8 +209,8 @@ module Lapsoss
207
209
  return nil if line_index.negative? || line_index >= lines.length
208
210
 
209
211
  # Calculate context range
210
- start_line = [0, line_index - context_lines].max
211
- end_line = [lines.length - 1, line_index + context_lines].min
212
+ start_line = [ 0, line_index - context_lines ].max
213
+ end_line = [ lines.length - 1, line_index + context_lines ].min
212
214
 
213
215
  {
214
216
  pre_context: lines[start_line...line_index],
@@ -234,7 +236,7 @@ module Lapsoss
234
236
  def parse_frames(backtrace)
235
237
  load_paths = determine_load_paths
236
238
  frames = backtrace.map do |line|
237
- BacktraceFrame.new(
239
+ BacktraceFrameFactory.from_raw_line(
238
240
  line,
239
241
  in_app_patterns: @config[:in_app_patterns],
240
242
  exclude_patterns: @config[:exclude_patterns],
@@ -248,7 +250,7 @@ module Lapsoss
248
250
 
249
251
  def filter_frames(frames)
250
252
  # Remove excluded frames
251
- frames = frames.reject(&:excluded?)
253
+ frames = frames.reject { |frame| frame.excluded?(@config[:exclude_patterns]) }
252
254
 
253
255
  # Filter gems if configured
254
256
  unless @config[:include_gems_in_context]
@@ -284,9 +286,9 @@ module Lapsoss
284
286
  # Get tail frames (original cause)
285
287
  tail_frames = if tail_count.positive?
286
288
  frames.last(tail_count)
287
- else
289
+ else
288
290
  []
289
- end
291
+ end
290
292
 
291
293
  head_frames + tail_frames
292
294
  end
@@ -296,8 +298,9 @@ module Lapsoss
296
298
  context_frames = frames.select(&:app_frame?)
297
299
  context_frames += frames.select(&:library_frame?).first(3)
298
300
 
299
- context_frames.each do |frame|
300
- frame.add_code_context(self, @config[:context_lines])
301
+ context_frames.each_with_index do |frame, _index|
302
+ updated_frame = frame.add_code_context(self, @config[:context_lines])
303
+ frames[frames.index(frame)] = updated_frame if updated_frame
301
304
  end
302
305
  end
303
306
 
@@ -305,7 +308,7 @@ module Lapsoss
305
308
  seen = Set.new
306
309
  frames.select do |frame|
307
310
  # Create a key based on filename, line, and method
308
- key = [frame.filename, frame.line_number, frame.function].join(':')
311
+ key = [ frame.filename, frame.line_number, frame.function ].join(":")
309
312
 
310
313
  if seen.include?(key)
311
314
  false
@@ -325,16 +328,16 @@ module Lapsoss
325
328
  # Add common Rails paths if in Rails
326
329
  if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
327
330
  paths << Rails.root.to_s
328
- paths << Rails.root.join('app').to_s
329
- paths << Rails.root.join('lib').to_s
330
- paths << Rails.root.join('config').to_s
331
+ paths << Rails.root.join("app").to_s
332
+ paths << Rails.root.join("lib").to_s
333
+ paths << Rails.root.join("config").to_s
331
334
  end
332
335
 
333
336
  # Add current working directory
334
337
  paths << Dir.pwd
335
338
 
336
339
  # Add gem paths
337
- paths.concat(Gem.path.map { |p| File.join(p, 'gems') }) if defined?(Gem)
340
+ paths.concat(Gem.path.map { |p| File.join(p, "gems") }) if defined?(Gem)
338
341
 
339
342
  # Sort by length (longest first) for better matching
340
343
  paths.uniq.sort_by(&:length).reverse
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'concurrent'
4
- require_relative 'scope'
5
- require_relative 'current'
3
+ require "concurrent"
6
4
 
7
5
  module Lapsoss
8
6
  class Client
@@ -47,7 +45,7 @@ module Lapsoss
47
45
  original_scope = current_scope
48
46
 
49
47
  # Create a merged scope with the new context
50
- merged_scope = MergedScope.new([context], original_scope)
48
+ merged_scope = MergedScope.new([ context ], original_scope)
51
49
  Current.scope = merged_scope
52
50
 
53
51
  yield(merged_scope)
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'validators'
4
- require 'active_support/configurable'
3
+ require "active_support/configurable"
5
4
 
6
5
  module Lapsoss
7
6
  class Configuration
@@ -15,7 +14,7 @@ module Lapsoss
15
14
  :backtrace_context_lines, :backtrace_in_app_patterns, :backtrace_exclude_patterns,
16
15
  :backtrace_strip_load_path, :backtrace_max_frames, :backtrace_enable_code_context,
17
16
  :enable_pipeline, :pipeline_builder, :sampling_strategy,
18
- :skip_rails_cache_errors, :force_sync_http
17
+ :skip_rails_cache_errors, :force_sync_http, :capture_request_context
19
18
  attr_reader :fingerprint_callback, :environment, :before_send, :sample_rate, :error_handler, :transport_timeout,
20
19
  :transport_max_retries, :transport_initial_backoff, :transport_max_backoff, :transport_backoff_multiplier, :transport_ssl_verify, :default_context, :adapter_configs
21
20
 
@@ -64,6 +63,8 @@ module Lapsoss
64
63
  @skip_rails_cache_errors = true
65
64
  # HTTP client settings
66
65
  @force_sync_http = false
66
+ # Capture request context in middleware
67
+ @capture_request_context = true
67
68
  end
68
69
 
69
70
  # Register a named adapter configuration
@@ -172,7 +173,6 @@ module Lapsoss
172
173
 
173
174
  # Pipeline configuration
174
175
  def configure_pipeline
175
- require_relative 'pipeline'
176
176
  @pipeline_builder = PipelineBuilder.new
177
177
  yield(@pipeline_builder) if block_given?
178
178
  @pipeline_builder
@@ -184,8 +184,6 @@ module Lapsoss
184
184
 
185
185
  # Sampling configuration
186
186
  def configure_sampling(strategy = nil, &block)
187
- require_relative 'sampling'
188
-
189
187
  if strategy
190
188
  @sampling_strategy = strategy
191
189
  elsif block_given?
@@ -194,8 +192,6 @@ module Lapsoss
194
192
  end
195
193
 
196
194
  def create_sampling_strategy
197
- require_relative 'sampling'
198
-
199
195
  case @sampling_strategy
200
196
  when Symbol
201
197
  case @sampling_strategy
@@ -219,80 +215,80 @@ module Lapsoss
219
215
 
220
216
  # Validation and setter overrides
221
217
  def sample_rate=(value)
222
- validate_sample_rate!(value, 'sample_rate') if value
218
+ validate_sample_rate!(value, "sample_rate") if value
223
219
  @sample_rate = value
224
220
  end
225
221
 
226
222
  def before_send=(value)
227
- validate_callable!(value, 'before_send')
223
+ validate_callable!(value, "before_send")
228
224
  @before_send = value
229
225
  end
230
226
 
231
227
  def error_handler=(value)
232
- validate_callable!(value, 'error_handler')
228
+ validate_callable!(value, "error_handler")
233
229
  @error_handler = value
234
230
  end
235
231
 
236
232
  def environment=(value)
237
- validate_environment!(value, 'environment') if value
233
+ validate_environment!(value, "environment") if value
238
234
  @environment = value&.to_s
239
235
  end
240
236
 
241
237
  def transport_timeout=(value)
242
- validate_timeout!(value, 'transport_timeout') if value
238
+ validate_timeout!(value, "transport_timeout") if value
243
239
  @transport_timeout = value
244
240
  end
245
241
 
246
242
  def transport_max_retries=(value)
247
- validate_retries!(value, 'transport_max_retries') if value
243
+ validate_retries!(value, "transport_max_retries") if value
248
244
  @transport_max_retries = value
249
245
  end
250
246
 
251
247
  def transport_initial_backoff=(value)
252
- validate_timeout!(value, 'transport_initial_backoff') if value
248
+ validate_timeout!(value, "transport_initial_backoff") if value
253
249
  @transport_initial_backoff = value
254
250
  end
255
251
 
256
252
  def transport_max_backoff=(value)
257
- validate_timeout!(value, 'transport_max_backoff') if value
253
+ validate_timeout!(value, "transport_max_backoff") if value
258
254
  @transport_max_backoff = value
259
255
  end
260
256
 
261
257
  def transport_backoff_multiplier=(value)
262
258
  if value
263
- validate_type!(value, [Numeric], 'transport_backoff_multiplier')
264
- validate_numeric_range!(value, 1.0..10.0, 'transport_backoff_multiplier')
259
+ validate_type!(value, [ Numeric ], "transport_backoff_multiplier")
260
+ validate_numeric_range!(value, 1.0..10.0, "transport_backoff_multiplier")
265
261
  end
266
262
  @transport_backoff_multiplier = value
267
263
  end
268
264
 
269
265
  def transport_ssl_verify=(value)
270
- validate_boolean!(value, 'transport_ssl_verify') if value
266
+ validate_boolean!(value, "transport_ssl_verify") if value
271
267
  @transport_ssl_verify = value
272
268
  end
273
269
 
274
270
  def fingerprint_callback=(value)
275
- validate_callable!(value, 'fingerprint_callback')
271
+ validate_callable!(value, "fingerprint_callback")
276
272
  @fingerprint_callback = value
277
273
  end
278
274
 
279
275
  # Configuration validation
280
276
  def validate!
281
- validate_sample_rate!(@sample_rate, 'sample_rate') if @sample_rate
282
- validate_callable!(@before_send, 'before_send')
283
- validate_callable!(@error_handler, 'error_handler')
284
- validate_callable!(@fingerprint_callback, 'fingerprint_callback')
285
- validate_environment!(@environment, 'environment') if @environment
277
+ validate_sample_rate!(@sample_rate, "sample_rate") if @sample_rate
278
+ validate_callable!(@before_send, "before_send")
279
+ validate_callable!(@error_handler, "error_handler")
280
+ validate_callable!(@fingerprint_callback, "fingerprint_callback")
281
+ validate_environment!(@environment, "environment") if @environment
286
282
 
287
283
  # Validate transport settings
288
- validate_timeout!(@transport_timeout, 'transport_timeout')
289
- validate_retries!(@transport_max_retries, 'transport_max_retries')
290
- validate_timeout!(@transport_initial_backoff, 'transport_initial_backoff')
291
- validate_timeout!(@transport_max_backoff, 'transport_max_backoff')
284
+ validate_timeout!(@transport_timeout, "transport_timeout")
285
+ validate_retries!(@transport_max_retries, "transport_max_retries")
286
+ validate_timeout!(@transport_initial_backoff, "transport_initial_backoff")
287
+ validate_timeout!(@transport_max_backoff, "transport_max_backoff")
292
288
 
293
289
  if @transport_backoff_multiplier
294
- validate_type!(@transport_backoff_multiplier, [Numeric], 'transport_backoff_multiplier')
295
- validate_numeric_range!(@transport_backoff_multiplier, 1.0..10.0, 'transport_backoff_multiplier')
290
+ validate_type!(@transport_backoff_multiplier, [ Numeric ], "transport_backoff_multiplier")
291
+ validate_numeric_range!(@transport_backoff_multiplier, 1.0..10.0, "transport_backoff_multiplier")
296
292
  end
297
293
 
298
294
  # Validate that initial backoff is less than max backoff
@@ -311,7 +307,7 @@ module Lapsoss
311
307
 
312
308
  def validate_adapter_config!(name, config)
313
309
  validate_presence!(config[:type], "adapter type for '#{name}'")
314
- validate_type!(config[:settings], [Hash], "adapter settings for '#{name}'")
310
+ validate_type!(config[:settings], [ Hash ], "adapter settings for '#{name}'")
315
311
  end
316
312
  end
317
313
  end
@@ -1,9 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/all'
3
+ require "active_support/all"
4
4
 
5
5
  module Lapsoss
6
6
  class Current < ActiveSupport::CurrentAttributes
7
- attribute :scope
7
+ attribute :scope, default: -> { Scope.new }
8
+
9
+ def self.with_clean_scope
10
+ previous_scope = scope
11
+ self.scope = Scope.new
12
+ yield
13
+ ensure
14
+ self.scope = previous_scope
15
+ end
8
16
  end
9
17
  end
data/lib/lapsoss/event.rb CHANGED
@@ -1,9 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'scrubber'
4
- require_relative 'fingerprinter'
5
- require_relative 'backtrace_processor'
6
-
7
3
  module Lapsoss
8
4
  class Event
9
5
  attr_accessor :type, :timestamp, :level, :message, :exception, :context, :environment, :fingerprint,
@@ -12,7 +8,7 @@ module Lapsoss
12
8
  def initialize(type:, level: :info, **attributes)
13
9
  @type = type
14
10
  @level = level
15
- @timestamp = Time.zone.now
11
+ @timestamp = Utils.current_time
16
12
  @context = {}
17
13
  @environment = Lapsoss.configuration.environment
18
14
 
@@ -23,6 +19,9 @@ module Lapsoss
23
19
  # Process backtrace if we have an exception
24
20
  @backtrace_frames = process_backtrace if @exception
25
21
 
22
+ # Set message from exception if not provided
23
+ @message ||= @exception.message if @exception
24
+
26
25
  # Generate fingerprint after all attributes are set (unless explicitly set to nil or a value)
27
26
  @fingerprint = generate_fingerprint if @fingerprint.nil? && !attributes.key?(:fingerprint)
28
27
  end
@@ -104,5 +103,29 @@ module Lapsoss
104
103
 
105
104
  @backtrace_frames.map(&:to_h)
106
105
  end
106
+
107
+ public
108
+
109
+ def exception_type
110
+ exception&.class&.name
111
+ end
112
+
113
+ def backtrace
114
+ if @backtrace_frames
115
+ @backtrace_frames.map(&:raw_line)
116
+ elsif exception
117
+ exception.backtrace
118
+ else
119
+ []
120
+ end
121
+ end
122
+
123
+ def request_context
124
+ # Request context is stored in extra by the middleware
125
+ # Check both string and symbol keys for compatibility
126
+ return nil unless context[:extra]
127
+
128
+ context[:extra]["request"] || context[:extra][:request]
129
+ end
107
130
  end
108
131
  end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ # Wrapper for BacktraceFrame that adds exception-specific metadata
5
+ class ExceptionBacktraceFrame
6
+ attr_reader :frame, :exception_class, :is_crash_frame
7
+
8
+ def initialize(frame, exception_class: nil, is_crash_frame: false)
9
+ @frame = frame
10
+ @exception_class = exception_class
11
+ @is_crash_frame = is_crash_frame
12
+ end
13
+
14
+ def crash_frame?
15
+ @is_crash_frame
16
+ end
17
+
18
+ # Delegate all other methods to the wrapped frame
19
+ def method_missing(method, *, &)
20
+ if @frame.respond_to?(method)
21
+ @frame.send(method, *, &)
22
+ else
23
+ super
24
+ end
25
+ end
26
+
27
+ def respond_to_missing?(method, include_private = false)
28
+ @frame.respond_to?(method, include_private) || super
29
+ end
30
+
31
+ # Override to_h to include exception metadata
32
+ def to_h
33
+ @frame.to_h.merge(
34
+ exception_class: @exception_class,
35
+ crash_frame: @is_crash_frame
36
+ ).compact
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,30 @@
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