lapsoss 0.1.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.
- checksums.yaml +4 -4
- data/README.md +153 -733
- data/lib/lapsoss/adapters/appsignal_adapter.rb +7 -8
- data/lib/lapsoss/adapters/base.rb +0 -3
- data/lib/lapsoss/adapters/bugsnag_adapter.rb +12 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +102 -101
- data/lib/lapsoss/adapters/logger_adapter.rb +7 -7
- data/lib/lapsoss/adapters/rollbar_adapter.rb +93 -54
- data/lib/lapsoss/adapters/sentry_adapter.rb +11 -17
- data/lib/lapsoss/backtrace_frame.rb +35 -214
- data/lib/lapsoss/backtrace_frame_factory.rb +228 -0
- data/lib/lapsoss/backtrace_processor.rb +37 -37
- data/lib/lapsoss/client.rb +2 -6
- data/lib/lapsoss/configuration.rb +25 -22
- data/lib/lapsoss/current.rb +9 -1
- data/lib/lapsoss/event.rb +30 -6
- data/lib/lapsoss/exception_backtrace_frame.rb +39 -0
- data/lib/lapsoss/exclusion_configuration.rb +30 -0
- data/lib/lapsoss/exclusion_filter.rb +156 -0
- data/lib/lapsoss/{exclusions.rb → exclusion_presets.rb} +1 -181
- data/lib/lapsoss/fingerprinter.rb +9 -13
- data/lib/lapsoss/http_client.rb +42 -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/middleware.rb +0 -347
- data/lib/lapsoss/pipeline.rb +1 -73
- 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 +34 -20
- data/lib/lapsoss/release_providers.rb +110 -0
- data/lib/lapsoss/release_tracker.rb +112 -207
- data/lib/lapsoss/router.rb +3 -5
- 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/sampling.rb +0 -326
- data/lib/lapsoss/scope.rb +17 -57
- data/lib/lapsoss/scrubber.rb +16 -18
- data/lib/lapsoss/user_context.rb +18 -198
- 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 +14 -27
- data/lib/lapsoss/version.rb +1 -1
- data/lib/lapsoss.rb +12 -25
- metadata +106 -21
@@ -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/sampling.rb
CHANGED
@@ -1,332 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "digest"
|
4
|
-
|
5
3
|
module Lapsoss
|
6
4
|
module Sampling
|
7
|
-
class Base
|
8
|
-
def sample?(event, hint = {})
|
9
|
-
true
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
class UniformSampler < Base
|
14
|
-
def initialize(rate)
|
15
|
-
@rate = rate
|
16
|
-
end
|
17
|
-
|
18
|
-
def sample?(event, hint = {})
|
19
|
-
rand < @rate
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
class RateLimiter < Base
|
24
|
-
def initialize(max_events_per_second: 10)
|
25
|
-
@max_events_per_second = max_events_per_second
|
26
|
-
@tokens = max_events_per_second
|
27
|
-
@last_refill = Time.now
|
28
|
-
@mutex = Mutex.new
|
29
|
-
end
|
30
|
-
|
31
|
-
def sample?(event, hint = {})
|
32
|
-
@mutex.synchronize do
|
33
|
-
now = Time.now
|
34
|
-
time_passed = now - @last_refill
|
35
|
-
|
36
|
-
# Refill tokens based on time passed
|
37
|
-
@tokens = [@tokens + (time_passed * @max_events_per_second), @max_events_per_second].min
|
38
|
-
@last_refill = now
|
39
|
-
|
40
|
-
if @tokens >= 1
|
41
|
-
@tokens -= 1
|
42
|
-
true
|
43
|
-
else
|
44
|
-
false
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
class ExceptionTypeSampler < Base
|
51
|
-
def initialize(rates: {})
|
52
|
-
@rates = rates
|
53
|
-
@default_rate = rates.fetch(:default, 1.0)
|
54
|
-
end
|
55
|
-
|
56
|
-
def sample?(event, hint = {})
|
57
|
-
return @default_rate > rand unless event.exception
|
58
|
-
|
59
|
-
exception_class = event.exception.class
|
60
|
-
rate = find_rate_for_exception(exception_class)
|
61
|
-
rate > rand
|
62
|
-
end
|
63
|
-
|
64
|
-
private
|
65
|
-
|
66
|
-
def find_rate_for_exception(exception_class)
|
67
|
-
# Check exact class match first
|
68
|
-
return @rates[exception_class] if @rates.key?(exception_class)
|
69
|
-
|
70
|
-
# Check inheritance hierarchy
|
71
|
-
@rates.each do |klass, rate|
|
72
|
-
return rate if klass.is_a?(Class) && exception_class <= klass
|
73
|
-
end
|
74
|
-
|
75
|
-
# Check string/regex patterns
|
76
|
-
@rates.each do |pattern, rate|
|
77
|
-
case pattern
|
78
|
-
when String
|
79
|
-
return rate if exception_class.name.include?(pattern)
|
80
|
-
when Regexp
|
81
|
-
return rate if exception_class.name.match?(pattern)
|
82
|
-
end
|
83
|
-
end
|
84
|
-
|
85
|
-
@default_rate
|
86
|
-
end
|
87
|
-
end
|
88
|
-
|
89
|
-
class UserBasedSampler < Base
|
90
|
-
def initialize(rates: {})
|
91
|
-
@rates = rates
|
92
|
-
@default_rate = rates.fetch(:default, 1.0)
|
93
|
-
end
|
94
|
-
|
95
|
-
def sample?(event, hint = {})
|
96
|
-
user = event.context[:user]
|
97
|
-
return @default_rate > rand unless user
|
98
|
-
|
99
|
-
rate = find_rate_for_user(user)
|
100
|
-
rate > rand
|
101
|
-
end
|
102
|
-
|
103
|
-
private
|
104
|
-
|
105
|
-
def find_rate_for_user(user)
|
106
|
-
# Check specific user ID
|
107
|
-
user_id = user[:id] || user["id"]
|
108
|
-
return @rates[user_id] if user_id && @rates.key?(user_id)
|
109
|
-
|
110
|
-
# Check user segments
|
111
|
-
@rates.each do |segment, rate|
|
112
|
-
case segment
|
113
|
-
when :internal, "internal"
|
114
|
-
return rate if user[:internal] || user["internal"]
|
115
|
-
when :premium, "premium"
|
116
|
-
return rate if user[:premium] || user["premium"]
|
117
|
-
when :beta, "beta"
|
118
|
-
return rate if user[:beta] || user["beta"]
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
@default_rate
|
123
|
-
end
|
124
|
-
end
|
125
|
-
|
126
|
-
class ConsistentHashSampler < Base
|
127
|
-
def initialize(rate:, key_extractor: nil)
|
128
|
-
@rate = rate
|
129
|
-
@key_extractor = key_extractor || method(:default_key_extractor)
|
130
|
-
@threshold = (rate * 0xFFFFFFFF).to_i
|
131
|
-
end
|
132
|
-
|
133
|
-
def sample?(event, hint = {})
|
134
|
-
key = @key_extractor.call(event, hint)
|
135
|
-
return @rate > rand unless key
|
136
|
-
|
137
|
-
hash_value = Digest::MD5.hexdigest(key.to_s)[0, 8].to_i(16)
|
138
|
-
hash_value <= @threshold
|
139
|
-
end
|
140
|
-
|
141
|
-
private
|
142
|
-
|
143
|
-
def default_key_extractor(event, hint)
|
144
|
-
# Use fingerprint for consistent sampling
|
145
|
-
event.fingerprint
|
146
|
-
end
|
147
|
-
end
|
148
|
-
|
149
|
-
class TimeBasedSampler < Base
|
150
|
-
def initialize(schedule: {})
|
151
|
-
@schedule = schedule
|
152
|
-
@default_rate = schedule.fetch(:default, 1.0)
|
153
|
-
end
|
154
|
-
|
155
|
-
def sample?(event, hint = {})
|
156
|
-
now = Time.now
|
157
|
-
rate = find_rate_for_time(now)
|
158
|
-
rate > rand
|
159
|
-
end
|
160
|
-
|
161
|
-
private
|
162
|
-
|
163
|
-
def find_rate_for_time(time)
|
164
|
-
hour = time.hour
|
165
|
-
day_of_week = time.wday # 0 = Sunday
|
166
|
-
|
167
|
-
# Check specific hour
|
168
|
-
hour_key = "hour_#{hour}".to_sym
|
169
|
-
return @schedule[hour_key] if @schedule.key?(hour_key)
|
170
|
-
|
171
|
-
# Check day of week
|
172
|
-
day_names = [:sunday, :monday, :tuesday, :wednesday, :thursday, :friday, :saturday]
|
173
|
-
day_key = day_names[day_of_week]
|
174
|
-
return @schedule[day_key] if @schedule.key?(day_key)
|
175
|
-
|
176
|
-
# Check business hours
|
177
|
-
if @schedule.key?(:business_hours) && (9..17).cover?(hour) && (1..5).cover?(day_of_week)
|
178
|
-
return @schedule[:business_hours]
|
179
|
-
end
|
180
|
-
|
181
|
-
# Check weekends
|
182
|
-
if @schedule.key?(:weekends) && [0, 6].include?(day_of_week)
|
183
|
-
return @schedule[:weekends]
|
184
|
-
end
|
185
|
-
|
186
|
-
@default_rate
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
class CompositeSampler < Base
|
191
|
-
def initialize(app = nil, samplers: [], strategy: :all)
|
192
|
-
@app = app
|
193
|
-
@samplers = samplers
|
194
|
-
@strategy = strategy
|
195
|
-
end
|
196
|
-
|
197
|
-
def sample?(event, hint = {})
|
198
|
-
case @strategy
|
199
|
-
when :all
|
200
|
-
@samplers.all? { |sampler| sampler.sample?(event, hint) }
|
201
|
-
when :any
|
202
|
-
@samplers.any? { |sampler| sampler.sample?(event, hint) }
|
203
|
-
when :first
|
204
|
-
@samplers.first&.sample?(event, hint) || true
|
205
|
-
else
|
206
|
-
raise ArgumentError, "Unknown strategy: #{@strategy}"
|
207
|
-
end
|
208
|
-
end
|
209
|
-
end
|
210
|
-
|
211
|
-
class AdaptiveSampler < Base
|
212
|
-
def initialize(target_rate: 1.0, adjustment_period: 60)
|
213
|
-
@target_rate = target_rate
|
214
|
-
@adjustment_period = adjustment_period
|
215
|
-
@current_rate = target_rate
|
216
|
-
@events_count = 0
|
217
|
-
@last_adjustment = Time.now
|
218
|
-
@mutex = Mutex.new
|
219
|
-
end
|
220
|
-
|
221
|
-
def sample?(event, hint = {})
|
222
|
-
@mutex.synchronize do
|
223
|
-
@events_count += 1
|
224
|
-
|
225
|
-
# Adjust rate periodically
|
226
|
-
now = Time.now
|
227
|
-
if now - @last_adjustment > @adjustment_period
|
228
|
-
adjust_rate
|
229
|
-
@last_adjustment = now
|
230
|
-
@events_count = 0
|
231
|
-
end
|
232
|
-
end
|
233
|
-
|
234
|
-
@current_rate > rand
|
235
|
-
end
|
236
|
-
|
237
|
-
def current_rate
|
238
|
-
@current_rate
|
239
|
-
end
|
240
|
-
|
241
|
-
private
|
242
|
-
|
243
|
-
def adjust_rate
|
244
|
-
# Simple adaptive logic - can be enhanced based on system metrics
|
245
|
-
# For now, just ensure we don't drift too far from target
|
246
|
-
if @events_count > 100 # High volume
|
247
|
-
@current_rate = [@current_rate * 0.9, @target_rate * 0.1].max
|
248
|
-
elsif @events_count < 10 # Low volume
|
249
|
-
@current_rate = [@current_rate * 1.1, @target_rate].min
|
250
|
-
end
|
251
|
-
end
|
252
|
-
end
|
253
|
-
|
254
|
-
class HealthBasedSampler < Base
|
255
|
-
def initialize(health_check:, high_rate: 1.0, low_rate: 0.1)
|
256
|
-
@health_check = health_check
|
257
|
-
@high_rate = high_rate
|
258
|
-
@low_rate = low_rate
|
259
|
-
end
|
260
|
-
|
261
|
-
def sample?(event, hint = {})
|
262
|
-
healthy = @health_check.call
|
263
|
-
rate = healthy ? @high_rate : @low_rate
|
264
|
-
rate > rand
|
265
|
-
end
|
266
|
-
end
|
267
|
-
|
268
|
-
# Factory for creating common sampling configurations
|
269
|
-
class SamplingFactory
|
270
|
-
def self.create_production_sampling
|
271
|
-
CompositeSampler.new(
|
272
|
-
samplers: [
|
273
|
-
# Rate limit to prevent overwhelming
|
274
|
-
RateLimiter.new(max_events_per_second: 50),
|
275
|
-
|
276
|
-
# Different rates for different exception types
|
277
|
-
ExceptionTypeSampler.new(rates: {
|
278
|
-
# Critical errors - always sample
|
279
|
-
SecurityError => 1.0,
|
280
|
-
SystemStackError => 1.0,
|
281
|
-
NoMemoryError => 1.0,
|
282
|
-
|
283
|
-
# Common errors - sample less
|
284
|
-
ArgumentError => 0.1,
|
285
|
-
TypeError => 0.1,
|
286
|
-
|
287
|
-
# Network errors - medium sampling
|
288
|
-
/timeout/i => 0.3,
|
289
|
-
/connection/i => 0.3,
|
290
|
-
|
291
|
-
# Default for unknown errors
|
292
|
-
default: 0.5
|
293
|
-
}),
|
294
|
-
|
295
|
-
# Lower sampling during business hours
|
296
|
-
TimeBasedSampler.new(schedule: {
|
297
|
-
business_hours: 0.3,
|
298
|
-
weekends: 0.8,
|
299
|
-
default: 0.5
|
300
|
-
})
|
301
|
-
],
|
302
|
-
strategy: :all
|
303
|
-
)
|
304
|
-
end
|
305
|
-
|
306
|
-
def self.create_development_sampling
|
307
|
-
UniformSampler.new(1.0) # Sample everything in development
|
308
|
-
end
|
309
|
-
|
310
|
-
def self.create_user_focused_sampling
|
311
|
-
CompositeSampler.new(
|
312
|
-
samplers: [
|
313
|
-
# Higher sampling for internal users
|
314
|
-
UserBasedSampler.new(rates: {
|
315
|
-
internal: 1.0,
|
316
|
-
premium: 0.8,
|
317
|
-
beta: 0.9,
|
318
|
-
default: 0.1
|
319
|
-
}),
|
320
|
-
|
321
|
-
# Consistent sampling based on user ID
|
322
|
-
ConsistentHashSampler.new(
|
323
|
-
rate: 0.1,
|
324
|
-
key_extractor: ->(event, hint) { event.context.dig(:user, :id) }
|
325
|
-
)
|
326
|
-
],
|
327
|
-
strategy: :any
|
328
|
-
)
|
329
|
-
end
|
330
|
-
end
|
331
5
|
end
|
332
6
|
end
|
data/lib/lapsoss/scope.rb
CHANGED
@@ -29,13 +29,11 @@ module Lapsoss
|
|
29
29
|
@extra.merge!(context[:extra] || {})
|
30
30
|
|
31
31
|
# Handle breadcrumbs if provided
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
end
|
38
|
-
end
|
32
|
+
return unless context[:breadcrumbs]
|
33
|
+
|
34
|
+
@breadcrumbs.concat(context[:breadcrumbs])
|
35
|
+
# Keep breadcrumbs to a reasonable limit
|
36
|
+
@breadcrumbs.shift while @breadcrumbs.length > 20
|
39
37
|
end
|
40
38
|
|
41
39
|
def clear
|
@@ -44,67 +42,29 @@ module Lapsoss
|
|
44
42
|
@user.clear
|
45
43
|
@extra.clear
|
46
44
|
end
|
47
|
-
end
|
48
45
|
|
49
|
-
|
50
|
-
|
51
|
-
def initialize(scope_stack, base_scope)
|
52
|
-
@scope_stack = scope_stack
|
53
|
-
@base_scope = base_scope || Scope.new
|
54
|
-
@own_breadcrumbs = []
|
46
|
+
def set_context(key, value)
|
47
|
+
@extra[key] = value
|
55
48
|
end
|
56
49
|
|
57
|
-
def
|
58
|
-
@
|
50
|
+
def set_user(user_info)
|
51
|
+
@user.merge!(user_info)
|
59
52
|
end
|
60
53
|
|
61
|
-
def
|
62
|
-
@
|
54
|
+
def set_tag(key, value)
|
55
|
+
@tags[key] = value
|
63
56
|
end
|
64
57
|
|
65
|
-
def
|
66
|
-
@
|
58
|
+
def set_tags(tags)
|
59
|
+
@tags.merge!(tags)
|
67
60
|
end
|
68
61
|
|
69
|
-
def
|
70
|
-
@
|
62
|
+
def set_extra(key, value)
|
63
|
+
@extra[key] = value
|
71
64
|
end
|
72
65
|
|
73
|
-
def
|
74
|
-
|
75
|
-
message: message,
|
76
|
-
type: type,
|
77
|
-
metadata: metadata,
|
78
|
-
timestamp: Time.now.utc
|
79
|
-
}
|
80
|
-
@own_breadcrumbs << breadcrumb
|
81
|
-
# Keep breadcrumbs to a reasonable limit
|
82
|
-
@own_breadcrumbs.shift if @own_breadcrumbs.length > 20
|
83
|
-
# Clear cached breadcrumbs to force recomputation
|
84
|
-
@breadcrumbs = nil
|
85
|
-
end
|
86
|
-
|
87
|
-
private
|
88
|
-
|
89
|
-
def merge_hash_contexts(key)
|
90
|
-
result = @base_scope.send(key).dup
|
91
|
-
@scope_stack.each do |context|
|
92
|
-
result.merge!(context[key] || {})
|
93
|
-
end
|
94
|
-
result
|
95
|
-
end
|
96
|
-
|
97
|
-
def merge_breadcrumbs
|
98
|
-
result = @base_scope.breadcrumbs.dup
|
99
|
-
@scope_stack.each do |context|
|
100
|
-
if context[:breadcrumbs]
|
101
|
-
result.concat(context[:breadcrumbs])
|
102
|
-
end
|
103
|
-
end
|
104
|
-
# Add our own breadcrumbs
|
105
|
-
result.concat(@own_breadcrumbs)
|
106
|
-
# Keep breadcrumbs to a reasonable limit
|
107
|
-
result.last(20)
|
66
|
+
def set_extras(extras)
|
67
|
+
@extra.merge!(extras)
|
108
68
|
end
|
109
69
|
end
|
110
70
|
end
|