lapsoss 0.2.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.
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 +26 -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/middleware.rb +0 -339
  35. data/lib/lapsoss/pipeline.rb +0 -68
  36. data/lib/lapsoss/pipeline_builder.rb +69 -0
  37. data/lib/lapsoss/rails_error_subscriber.rb +42 -0
  38. data/lib/lapsoss/rails_middleware.rb +78 -0
  39. data/lib/lapsoss/railtie.rb +22 -50
  40. data/lib/lapsoss/registry.rb +18 -5
  41. data/lib/lapsoss/release_providers.rb +110 -0
  42. data/lib/lapsoss/release_tracker.rb +159 -232
  43. data/lib/lapsoss/sampling/adaptive_sampler.rb +46 -0
  44. data/lib/lapsoss/sampling/base.rb +11 -0
  45. data/lib/lapsoss/sampling/composite_sampler.rb +26 -0
  46. data/lib/lapsoss/sampling/consistent_hash_sampler.rb +30 -0
  47. data/lib/lapsoss/sampling/exception_type_sampler.rb +44 -0
  48. data/lib/lapsoss/sampling/health_based_sampler.rb +19 -0
  49. data/lib/lapsoss/sampling/rate_limiter.rb +32 -0
  50. data/lib/lapsoss/sampling/sampling_factory.rb +69 -0
  51. data/lib/lapsoss/sampling/time_based_sampler.rb +44 -0
  52. data/lib/lapsoss/sampling/uniform_sampler.rb +15 -0
  53. data/lib/lapsoss/sampling/user_based_sampler.rb +42 -0
  54. data/lib/lapsoss/sampling.rb +0 -322
  55. data/lib/lapsoss/scope.rb +12 -48
  56. data/lib/lapsoss/scrubber.rb +7 -7
  57. data/lib/lapsoss/user_context.rb +30 -203
  58. data/lib/lapsoss/user_context_integrations.rb +39 -0
  59. data/lib/lapsoss/user_context_middleware.rb +50 -0
  60. data/lib/lapsoss/user_context_provider.rb +93 -0
  61. data/lib/lapsoss/utils.rb +13 -0
  62. data/lib/lapsoss/validators.rb +15 -15
  63. data/lib/lapsoss/version.rb +1 -1
  64. data/lib/lapsoss.rb +3 -3
  65. metadata +54 -5
@@ -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
@@ -1,328 +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.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
5
  end
328
6
  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