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.
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 +27 -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/pipeline.rb +0 -68
  35. data/lib/lapsoss/pipeline_builder.rb +69 -0
  36. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  37. data/lib/lapsoss/rails_middleware.rb +78 -0
  38. data/lib/lapsoss/railtie.rb +22 -50
  39. data/lib/lapsoss/registry.rb +18 -5
  40. data/lib/lapsoss/release_providers.rb +110 -0
  41. data/lib/lapsoss/release_tracker.rb +159 -232
  42. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  43. data/lib/lapsoss/sampling/base.rb +11 -0
  44. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  45. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  46. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  47. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  48. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  49. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  50. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  51. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  52. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  53. data/lib/lapsoss/scope.rb +12 -48
  54. data/lib/lapsoss/scrubber.rb +7 -7
  55. data/lib/lapsoss/user_context.rb +30 -203
  56. data/lib/lapsoss/user_context_integrations.rb +39 -0
  57. data/lib/lapsoss/user_context_middleware.rb +50 -0
  58. data/lib/lapsoss/user_context_provider.rb +93 -0
  59. data/lib/lapsoss/utils.rb +13 -0
  60. data/lib/lapsoss/validators.rb +15 -15
  61. data/lib/lapsoss/version.rb +1 -1
  62. data/lib/lapsoss.rb +3 -3
  63. metadata +60 -7
  64. data/lib/lapsoss/middleware.rb +0 -345
  65. data/lib/lapsoss/sampling.rb +0 -328
@@ -1,8 +1,8 @@
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 "set"
4
+ require "active_support/cache"
5
+ require "active_support/core_ext/numeric/time"
6
6
 
7
7
  module Lapsoss
8
8
  class BacktraceProcessor
@@ -42,10 +42,10 @@ module Lapsoss
42
42
  exclude_patterns: config.backtrace_exclude_patterns,
43
43
  strip_load_path: config.backtrace_strip_load_path
44
44
  }
45
- else
45
+ else
46
46
  # Hash passed
47
47
  config
48
- end
48
+ end
49
49
 
50
50
  @config = DEFAULT_CONFIG.merge(config_hash)
51
51
  @file_cache = ActiveSupport::Cache::MemoryStore.new(
@@ -83,10 +83,13 @@ module Lapsoss
83
83
 
84
84
  frames = process_backtrace(exception.backtrace)
85
85
 
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 }
86
+ # Wrap frames with exception-specific context
87
+ frames = frames.map.with_index do |frame, index|
88
+ ExceptionBacktraceFrame.new(
89
+ frame,
90
+ exception_class: exception.class.name,
91
+ is_crash_frame: index.zero?
92
+ )
90
93
  end
91
94
 
92
95
  # Follow exception causes if requested
@@ -179,7 +182,7 @@ module Lapsoss
179
182
  file_cache: {
180
183
  # ActiveSupport::Cache::MemoryStore doesn't expose detailed stats
181
184
  # but we can provide basic info
182
- type: 'ActiveSupport::Cache::MemoryStore',
185
+ type: "ActiveSupport::Cache::MemoryStore",
183
186
  configured_size: @file_cache.options[:size]
184
187
  },
185
188
  config: @config,
@@ -207,8 +210,8 @@ module Lapsoss
207
210
  return nil if line_index.negative? || line_index >= lines.length
208
211
 
209
212
  # Calculate context range
210
- start_line = [0, line_index - context_lines].max
211
- end_line = [lines.length - 1, line_index + context_lines].min
213
+ start_line = [ 0, line_index - context_lines ].max
214
+ end_line = [ lines.length - 1, line_index + context_lines ].min
212
215
 
213
216
  {
214
217
  pre_context: lines[start_line...line_index],
@@ -234,7 +237,7 @@ module Lapsoss
234
237
  def parse_frames(backtrace)
235
238
  load_paths = determine_load_paths
236
239
  frames = backtrace.map do |line|
237
- BacktraceFrame.new(
240
+ BacktraceFrameFactory.from_raw_line(
238
241
  line,
239
242
  in_app_patterns: @config[:in_app_patterns],
240
243
  exclude_patterns: @config[:exclude_patterns],
@@ -248,7 +251,7 @@ module Lapsoss
248
251
 
249
252
  def filter_frames(frames)
250
253
  # Remove excluded frames
251
- frames = frames.reject(&:excluded?)
254
+ frames = frames.reject { |frame| frame.excluded?(@config[:exclude_patterns]) }
252
255
 
253
256
  # Filter gems if configured
254
257
  unless @config[:include_gems_in_context]
@@ -284,9 +287,9 @@ module Lapsoss
284
287
  # Get tail frames (original cause)
285
288
  tail_frames = if tail_count.positive?
286
289
  frames.last(tail_count)
287
- else
290
+ else
288
291
  []
289
- end
292
+ end
290
293
 
291
294
  head_frames + tail_frames
292
295
  end
@@ -296,8 +299,9 @@ module Lapsoss
296
299
  context_frames = frames.select(&:app_frame?)
297
300
  context_frames += frames.select(&:library_frame?).first(3)
298
301
 
299
- context_frames.each do |frame|
300
- frame.add_code_context(self, @config[:context_lines])
302
+ context_frames.each_with_index do |frame, _index|
303
+ updated_frame = frame.add_code_context(self, @config[:context_lines])
304
+ frames[frames.index(frame)] = updated_frame if updated_frame
301
305
  end
302
306
  end
303
307
 
@@ -305,7 +309,7 @@ module Lapsoss
305
309
  seen = Set.new
306
310
  frames.select do |frame|
307
311
  # Create a key based on filename, line, and method
308
- key = [frame.filename, frame.line_number, frame.function].join(':')
312
+ key = [ frame.filename, frame.line_number, frame.function ].join(":")
309
313
 
310
314
  if seen.include?(key)
311
315
  false
@@ -325,16 +329,16 @@ module Lapsoss
325
329
  # Add common Rails paths if in Rails
326
330
  if defined?(Rails) && Rails.respond_to?(:root) && Rails.root
327
331
  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
332
+ paths << Rails.root.join("app").to_s
333
+ paths << Rails.root.join("lib").to_s
334
+ paths << Rails.root.join("config").to_s
331
335
  end
332
336
 
333
337
  # Add current working directory
334
338
  paths << Dir.pwd
335
339
 
336
340
  # Add gem paths
337
- paths.concat(Gem.path.map { |p| File.join(p, 'gems') }) if defined?(Gem)
341
+ paths.concat(Gem.path.map { |p| File.join(p, "gems") }) if defined?(Gem)
338
342
 
339
343
  # Sort by length (longest first) for better matching
340
344
  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