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,328 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'digest'
4
-
5
- module Lapsoss
6
- 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.zone.now
28
- @mutex = Mutex.new
29
- end
30
-
31
- def sample?(_event, _hint = {})
32
- @mutex.synchronize do
33
- now = Time.zone.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.zone.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}"
169
- return @schedule[hour_key] if @schedule.key?(hour_key)
170
-
171
- # Check day of week
172
- day_names = %i[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
- return @schedule[:weekends] if @schedule.key?(:weekends) && [0, 6].include?(day_of_week)
183
-
184
- @default_rate
185
- end
186
- end
187
-
188
- class CompositeSampler < Base
189
- def initialize(app = nil, samplers: [], strategy: :all)
190
- @app = app
191
- @samplers = samplers
192
- @strategy = strategy
193
- end
194
-
195
- def sample?(event, hint = {})
196
- case @strategy
197
- when :all
198
- @samplers.all? { |sampler| sampler.sample?(event, hint) }
199
- when :any
200
- @samplers.any? { |sampler| sampler.sample?(event, hint) }
201
- when :first
202
- @samplers.first&.sample?(event, hint) || true
203
- else
204
- raise ArgumentError, "Unknown strategy: #{@strategy}"
205
- end
206
- end
207
- end
208
-
209
- class AdaptiveSampler < Base
210
- def initialize(target_rate: 1.0, adjustment_period: 60)
211
- @target_rate = target_rate
212
- @adjustment_period = adjustment_period
213
- @current_rate = target_rate
214
- @events_count = 0
215
- @last_adjustment = Time.zone.now
216
- @mutex = Mutex.new
217
- end
218
-
219
- def sample?(_event, _hint = {})
220
- @mutex.synchronize do
221
- @events_count += 1
222
-
223
- # Adjust rate periodically
224
- now = Time.zone.now
225
- if now - @last_adjustment > @adjustment_period
226
- adjust_rate
227
- @last_adjustment = now
228
- @events_count = 0
229
- end
230
- end
231
-
232
- @current_rate > rand
233
- end
234
-
235
- attr_reader :current_rate
236
-
237
- private
238
-
239
- def adjust_rate
240
- # Simple adaptive logic - can be enhanced based on system metrics
241
- # For now, just ensure we don't drift too far from target
242
- if @events_count > 100 # High volume
243
- @current_rate = [@current_rate * 0.9, @target_rate * 0.1].max
244
- elsif @events_count < 10 # Low volume
245
- @current_rate = [@current_rate * 1.1, @target_rate].min
246
- end
247
- end
248
- end
249
-
250
- class HealthBasedSampler < Base
251
- def initialize(health_check:, high_rate: 1.0, low_rate: 0.1)
252
- @health_check = health_check
253
- @high_rate = high_rate
254
- @low_rate = low_rate
255
- end
256
-
257
- def sample?(_event, _hint = {})
258
- healthy = @health_check.call
259
- rate = healthy ? @high_rate : @low_rate
260
- rate > rand
261
- end
262
- end
263
-
264
- # Factory for creating common sampling configurations
265
- class SamplingFactory
266
- def self.create_production_sampling
267
- CompositeSampler.new(
268
- samplers: [
269
- # Rate limit to prevent overwhelming
270
- RateLimiter.new(max_events_per_second: 50),
271
-
272
- # Different rates for different exception types
273
- ExceptionTypeSampler.new(rates: {
274
- # Critical errors - always sample
275
- SecurityError => 1.0,
276
- SystemStackError => 1.0,
277
- NoMemoryError => 1.0,
278
-
279
- # Common errors - sample less
280
- ArgumentError => 0.1,
281
- TypeError => 0.1,
282
-
283
- # Network errors - medium sampling
284
- /timeout/i => 0.3,
285
- /connection/i => 0.3,
286
-
287
- # Default for unknown errors
288
- default: 0.5
289
- }),
290
-
291
- # Lower sampling during business hours
292
- TimeBasedSampler.new(schedule: {
293
- business_hours: 0.3,
294
- weekends: 0.8,
295
- default: 0.5
296
- })
297
- ],
298
- strategy: :all
299
- )
300
- end
301
-
302
- def self.create_development_sampling
303
- UniformSampler.new(1.0) # Sample everything in development
304
- end
305
-
306
- def self.create_user_focused_sampling
307
- CompositeSampler.new(
308
- samplers: [
309
- # Higher sampling for internal users
310
- UserBasedSampler.new(rates: {
311
- internal: 1.0,
312
- premium: 0.8,
313
- beta: 0.9,
314
- default: 0.1
315
- }),
316
-
317
- # Consistent sampling based on user ID
318
- ConsistentHashSampler.new(
319
- rate: 0.1,
320
- key_extractor: ->(event, _hint) { event.context.dig(:user, :id) }
321
- )
322
- ],
323
- strategy: :any
324
- )
325
- end
326
- end
327
- end
328
- end