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
@@ -0,0 +1,30 @@
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
@@ -0,0 +1,44 @@
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
@@ -0,0 +1,19 @@
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
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class RateLimiter < Base
6
+ def initialize(max_events_per_second: 10)
7
+ @max_events_per_second = max_events_per_second
8
+ @tokens = max_events_per_second
9
+ @last_refill = Time.zone.now
10
+ @mutex = Mutex.new
11
+ end
12
+
13
+ def sample?(_event, _hint = {})
14
+ @mutex.synchronize do
15
+ now = Time.zone.now
16
+ time_passed = now - @last_refill
17
+
18
+ # Refill tokens based on time passed
19
+ @tokens = [ @tokens + (time_passed * @max_events_per_second), @max_events_per_second ].min
20
+ @last_refill = now
21
+
22
+ if @tokens >= 1
23
+ @tokens -= 1
24
+ true
25
+ else
26
+ false
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,69 @@
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
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class TimeBasedSampler < Base
6
+ def initialize(schedule: {})
7
+ @schedule = schedule
8
+ @default_rate = schedule.fetch(:default, 1.0)
9
+ end
10
+
11
+ def sample?(_event, _hint = {})
12
+ now = Time.zone.now
13
+ rate = find_rate_for_time(now)
14
+ rate > rand
15
+ end
16
+
17
+ private
18
+
19
+ def find_rate_for_time(time)
20
+ hour = time.hour
21
+ day_of_week = time.wday # 0 = Sunday
22
+
23
+ # Check specific hour
24
+ hour_key = :"hour_#{hour}"
25
+ return @schedule[hour_key] if @schedule.key?(hour_key)
26
+
27
+ # Check day of week
28
+ day_names = %i[sunday monday tuesday wednesday thursday friday saturday]
29
+ day_key = day_names[day_of_week]
30
+ return @schedule[day_key] if @schedule.key?(day_key)
31
+
32
+ # Check business hours
33
+ if @schedule.key?(:business_hours) && (9..17).cover?(hour) && (1..5).cover?(day_of_week)
34
+ return @schedule[:business_hours]
35
+ end
36
+
37
+ # Check weekends
38
+ return @schedule[:weekends] if @schedule.key?(:weekends) && [ 0, 6 ].include?(day_of_week)
39
+
40
+ @default_rate
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class UniformSampler < Base
6
+ def initialize(rate)
7
+ @rate = rate
8
+ end
9
+
10
+ def sample?(_event, _hint = {})
11
+ rand < @rate
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module Sampling
5
+ class UserBasedSampler < 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
+ user = event.context[:user]
13
+ return @default_rate > rand unless user
14
+
15
+ rate = find_rate_for_user(user)
16
+ rate > rand
17
+ end
18
+
19
+ private
20
+
21
+ def find_rate_for_user(user)
22
+ # Check specific user ID
23
+ user_id = user[:id] || user["id"]
24
+ return @rates[user_id] if user_id && @rates.key?(user_id)
25
+
26
+ # Check user segments
27
+ @rates.each do |segment, rate|
28
+ case segment
29
+ when :internal, "internal"
30
+ return rate if user[:internal] || user["internal"]
31
+ when :premium, "premium"
32
+ return rate if user[:premium] || user["premium"]
33
+ when :beta, "beta"
34
+ return rate if user[:beta] || user["beta"]
35
+ end
36
+ end
37
+
38
+ @default_rate
39
+ end
40
+ end
41
+ end
42
+ end
data/lib/lapsoss/scope.rb CHANGED
@@ -42,65 +42,29 @@ module Lapsoss
42
42
  @user.clear
43
43
  @extra.clear
44
44
  end
45
- end
46
45
 
47
- # Performance-optimized scope that provides a merged view without cloning
48
- class MergedScope
49
- def initialize(scope_stack, base_scope)
50
- @scope_stack = scope_stack
51
- @base_scope = base_scope || Scope.new
52
- @own_breadcrumbs = []
46
+ def set_context(key, value)
47
+ @extra[key] = value
53
48
  end
54
49
 
55
- def tags
56
- @tags ||= merge_hash_contexts(:tags)
50
+ def set_user(user_info)
51
+ @user.merge!(user_info)
57
52
  end
58
53
 
59
- def user
60
- @user ||= merge_hash_contexts(:user)
54
+ def set_tag(key, value)
55
+ @tags[key] = value
61
56
  end
62
57
 
63
- def extra
64
- @extra ||= merge_hash_contexts(:extra)
58
+ def set_tags(tags)
59
+ @tags.merge!(tags)
65
60
  end
66
61
 
67
- def breadcrumbs
68
- @breadcrumbs ||= merge_breadcrumbs
62
+ def set_extra(key, value)
63
+ @extra[key] = value
69
64
  end
70
65
 
71
- def add_breadcrumb(message, type: :default, **metadata)
72
- breadcrumb = {
73
- message: message,
74
- type: type,
75
- metadata: metadata,
76
- timestamp: Time.now.utc
77
- }
78
- @own_breadcrumbs << breadcrumb
79
- # Keep breadcrumbs to a reasonable limit
80
- @own_breadcrumbs.shift if @own_breadcrumbs.length > 20
81
- # Clear cached breadcrumbs to force recomputation
82
- @breadcrumbs = nil
83
- end
84
-
85
- private
86
-
87
- def merge_hash_contexts(key)
88
- result = @base_scope.send(key).dup
89
- @scope_stack.each do |context|
90
- result.merge!(context[key] || {})
91
- end
92
- result
93
- end
94
-
95
- def merge_breadcrumbs
96
- result = @base_scope.breadcrumbs.dup
97
- @scope_stack.each do |context|
98
- result.concat(context[:breadcrumbs]) if context[:breadcrumbs]
99
- end
100
- # Add our own breadcrumbs
101
- result.concat(@own_breadcrumbs)
102
- # Keep breadcrumbs to a reasonable limit
103
- result.last(20)
66
+ def set_extras(extras)
67
+ @extra.merge!(extras)
104
68
  end
105
69
  end
106
70
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_support/parameter_filter'
3
+ require "active_support/parameter_filter"
4
4
 
5
5
  module Lapsoss
6
6
  class Scrubber
@@ -31,7 +31,7 @@ module Lapsoss
31
31
  @scrub_all = config[:scrub_all] || false
32
32
  @whitelist_fields = Array(config[:whitelist_fields] || [])
33
33
  @randomize_scrub_length = config[:randomize_scrub_length] || false
34
- @scrub_value = config[:scrub_value] || '**SCRUBBED**'
34
+ @scrub_value = config[:scrub_value] || "**SCRUBBED**"
35
35
  end
36
36
 
37
37
  def scrub(data)
@@ -68,13 +68,13 @@ module Lapsoss
68
68
 
69
69
  result[key] = if should_scrub_field?(key_string)
70
70
  generate_scrub_value(value)
71
- elsif value.is_a?(Hash)
71
+ elsif value.is_a?(Hash)
72
72
  scrub_recursive(value)
73
- elsif value.is_a?(Array)
73
+ elsif value.is_a?(Array)
74
74
  scrub_array(value)
75
- else
75
+ else
76
76
  scrub_value(value)
77
- end
77
+ end
78
78
  end
79
79
  end
80
80
 
@@ -146,7 +146,7 @@ module Lapsoss
146
146
 
147
147
  def generate_scrub_value(_original_value)
148
148
  if @randomize_scrub_length
149
- '*' * rand(6..12)
149
+ "*" * rand(6..12)
150
150
  else
151
151
  @scrub_value
152
152
  end