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
@@ -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,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
|
-
|
48
|
-
|
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
|
56
|
-
@
|
50
|
+
def set_user(user_info)
|
51
|
+
@user.merge!(user_info)
|
57
52
|
end
|
58
53
|
|
59
|
-
def
|
60
|
-
@
|
54
|
+
def set_tag(key, value)
|
55
|
+
@tags[key] = value
|
61
56
|
end
|
62
57
|
|
63
|
-
def
|
64
|
-
@
|
58
|
+
def set_tags(tags)
|
59
|
+
@tags.merge!(tags)
|
65
60
|
end
|
66
61
|
|
67
|
-
def
|
68
|
-
@
|
62
|
+
def set_extra(key, value)
|
63
|
+
@extra[key] = value
|
69
64
|
end
|
70
65
|
|
71
|
-
def
|
72
|
-
|
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
|
data/lib/lapsoss/scrubber.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
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] ||
|
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
|
-
|
71
|
+
elsif value.is_a?(Hash)
|
72
72
|
scrub_recursive(value)
|
73
|
-
|
73
|
+
elsif value.is_a?(Array)
|
74
74
|
scrub_array(value)
|
75
|
-
|
75
|
+
else
|
76
76
|
scrub_value(value)
|
77
|
-
|
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
|
-
|
149
|
+
"*" * rand(6..12)
|
150
150
|
else
|
151
151
|
@scrub_value
|
152
152
|
end
|