lapsoss 0.3.1 → 0.4.2
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 +71 -7
- data/lib/lapsoss/adapters/appsignal_adapter.rb +18 -12
- data/lib/lapsoss/adapters/base.rb +19 -0
- data/lib/lapsoss/adapters/concerns/envelope_builder.rb +127 -0
- data/lib/lapsoss/adapters/concerns/http_delivery.rb +130 -0
- data/lib/lapsoss/adapters/concerns/level_mapping.rb +65 -0
- data/lib/lapsoss/adapters/insight_hub_adapter.rb +21 -21
- data/lib/lapsoss/adapters/rollbar_adapter.rb +64 -122
- data/lib/lapsoss/adapters/sentry_adapter.rb +77 -143
- data/lib/lapsoss/backtrace_processor.rb +1 -1
- data/lib/lapsoss/breadcrumb.rb +59 -0
- data/lib/lapsoss/client.rb +3 -5
- data/lib/lapsoss/configuration.rb +26 -31
- data/lib/lapsoss/event.rb +90 -96
- data/lib/lapsoss/fingerprinter.rb +57 -49
- data/lib/lapsoss/merged_scope.rb +1 -6
- data/lib/lapsoss/middleware/release_tracker.rb +11 -98
- data/lib/lapsoss/pipeline_builder.rb +2 -2
- data/lib/lapsoss/rails_error_subscriber.rb +3 -4
- data/lib/lapsoss/rails_middleware.rb +2 -2
- data/lib/lapsoss/railtie.rb +13 -2
- data/lib/lapsoss/registry.rb +7 -7
- data/lib/lapsoss/router.rb +1 -3
- data/lib/lapsoss/scope.rb +1 -6
- data/lib/lapsoss/scrubber.rb +15 -148
- data/lib/lapsoss/validators.rb +63 -92
- data/lib/lapsoss/version.rb +1 -1
- metadata +8 -24
- data/CHANGELOG.md +0 -5
- data/lib/lapsoss/exclusion_configuration.rb +0 -30
- data/lib/lapsoss/exclusion_presets.rb +0 -249
- data/lib/lapsoss/middleware/sample_filter.rb +0 -23
- data/lib/lapsoss/middleware/sampling_middleware.rb +0 -18
- data/lib/lapsoss/middleware/user_context_enhancer.rb +0 -46
- data/lib/lapsoss/release_providers.rb +0 -110
- data/lib/lapsoss/sampling/adaptive_sampler.rb +0 -46
- data/lib/lapsoss/sampling/composite_sampler.rb +0 -26
- data/lib/lapsoss/sampling/consistent_hash_sampler.rb +0 -30
- data/lib/lapsoss/sampling/exception_type_sampler.rb +0 -44
- data/lib/lapsoss/sampling/health_based_sampler.rb +0 -19
- data/lib/lapsoss/sampling/sampling_factory.rb +0 -69
- data/lib/lapsoss/sampling/time_based_sampler.rb +0 -44
- data/lib/lapsoss/sampling/user_based_sampler.rb +0 -42
- data/lib/lapsoss/user_context.rb +0 -175
- data/lib/lapsoss/user_context_integrations.rb +0 -39
- data/lib/lapsoss/user_context_middleware.rb +0 -50
- data/lib/lapsoss/user_context_provider.rb +0 -93
- data/lib/lapsoss/utils.rb +0 -13
@@ -21,7 +21,7 @@ module Lapsoss
|
|
21
21
|
def initialize
|
22
22
|
@adapter_configs = {}
|
23
23
|
@async = true
|
24
|
-
@logger = nil
|
24
|
+
@logger = Logger.new(nil) # Default null logger
|
25
25
|
@environment = nil
|
26
26
|
@enabled = true
|
27
27
|
@release = nil
|
@@ -193,17 +193,8 @@ module Lapsoss
|
|
193
193
|
|
194
194
|
def create_sampling_strategy
|
195
195
|
case @sampling_strategy
|
196
|
-
when
|
197
|
-
|
198
|
-
when :production
|
199
|
-
Sampling::SamplingFactory.create_production_sampling
|
200
|
-
when :development
|
201
|
-
Sampling::SamplingFactory.create_development_sampling
|
202
|
-
when :user_focused
|
203
|
-
Sampling::SamplingFactory.create_user_focused_sampling
|
204
|
-
else
|
205
|
-
Sampling::UniformSampler.new(@sample_rate)
|
206
|
-
end
|
196
|
+
when Numeric
|
197
|
+
Sampling::UniformSampler.new(@sampling_strategy)
|
207
198
|
when Proc
|
208
199
|
@sampling_strategy
|
209
200
|
when nil
|
@@ -272,42 +263,46 @@ module Lapsoss
|
|
272
263
|
@fingerprint_callback = value
|
273
264
|
end
|
274
265
|
|
275
|
-
# Configuration validation
|
266
|
+
# Configuration validation - just log warnings, don't fail
|
276
267
|
def validate!
|
277
|
-
|
268
|
+
# Check sample rate is between 0 and 1
|
269
|
+
if @sample_rate && (@sample_rate < 0 || @sample_rate > 1)
|
270
|
+
logger.warn "sample_rate should be between 0 and 1, got #{@sample_rate}"
|
271
|
+
end
|
272
|
+
|
273
|
+
# Check callables
|
278
274
|
validate_callable!(@before_send, "before_send")
|
279
275
|
validate_callable!(@error_handler, "error_handler")
|
280
276
|
validate_callable!(@fingerprint_callback, "fingerprint_callback")
|
277
|
+
|
278
|
+
# Log if environment looks unusual
|
281
279
|
validate_environment!(@environment, "environment") if @environment
|
282
280
|
|
283
|
-
#
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
validate_timeout!(@transport_max_backoff, "transport_max_backoff")
|
281
|
+
# Just log if transport settings look unusual
|
282
|
+
if @transport_timeout && @transport_timeout <= 0
|
283
|
+
logger.warn "transport_timeout should be positive, got #{@transport_timeout}"
|
284
|
+
end
|
288
285
|
|
289
|
-
if @
|
290
|
-
|
291
|
-
validate_numeric_range!(@transport_backoff_multiplier, 1.0..10.0, "transport_backoff_multiplier")
|
286
|
+
if @transport_max_retries && @transport_max_retries < 0
|
287
|
+
logger.warn "transport_max_retries should be non-negative, got #{@transport_max_retries}"
|
292
288
|
end
|
293
289
|
|
294
|
-
# Validate that initial backoff is less than max backoff
|
295
290
|
if @transport_initial_backoff && @transport_max_backoff && @transport_initial_backoff > @transport_max_backoff
|
296
|
-
|
297
|
-
"transport_initial_backoff (#{@transport_initial_backoff}) must be less than transport_max_backoff (#{@transport_max_backoff})"
|
291
|
+
logger.warn "transport_initial_backoff (#{@transport_initial_backoff}) should be less than transport_max_backoff (#{@transport_max_backoff})"
|
298
292
|
end
|
299
293
|
|
300
|
-
# Validate adapter configurations
|
294
|
+
# Validate adapter configurations exist
|
301
295
|
@adapter_configs.each do |name, config|
|
302
|
-
|
296
|
+
if config[:type].blank?
|
297
|
+
logger.warn "Adapter '#{name}' has no type specified"
|
298
|
+
end
|
303
299
|
end
|
300
|
+
|
301
|
+
true
|
304
302
|
end
|
305
303
|
|
306
304
|
private
|
307
305
|
|
308
|
-
|
309
|
-
validate_presence!(config[:type], "adapter type for '#{name}'")
|
310
|
-
validate_type!(config[:settings], [ Hash ], "adapter settings for '#{name}'")
|
311
|
-
end
|
306
|
+
# Adapter config validation moved to inline logging
|
312
307
|
end
|
313
308
|
end
|
data/lib/lapsoss/event.rb
CHANGED
@@ -1,131 +1,125 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
def initialize(type:, level: :info, **attributes)
|
9
|
-
@type = type
|
10
|
-
@level = level
|
11
|
-
@timestamp = Utils.current_time
|
12
|
-
@context = {}
|
13
|
-
@environment = Lapsoss.configuration.environment
|
14
|
-
|
15
|
-
attributes.each do |key, value|
|
16
|
-
instance_variable_set("@#{key}", value) if respond_to?("#{key}=")
|
17
|
-
end
|
18
|
-
|
19
|
-
# Process backtrace if we have an exception
|
20
|
-
@backtrace_frames = process_backtrace if @exception
|
21
|
-
|
22
|
-
# Set message from exception if not provided
|
23
|
-
@message ||= @exception.message if @exception
|
3
|
+
require "active_support/core_ext/hash"
|
4
|
+
require "active_support/core_ext/object/blank"
|
5
|
+
require "active_support/core_ext/time"
|
6
|
+
require "active_support/json"
|
24
7
|
|
25
|
-
|
26
|
-
|
27
|
-
|
8
|
+
module Lapsoss
|
9
|
+
# Immutable event structure using Ruby 3.3 Data class
|
10
|
+
Event = Data.define(
|
11
|
+
:type, # :exception, :message, :transaction
|
12
|
+
:level, # :debug, :info, :warning, :error, :fatal
|
13
|
+
:timestamp,
|
14
|
+
:message,
|
15
|
+
:exception,
|
16
|
+
:context,
|
17
|
+
:environment,
|
18
|
+
:fingerprint,
|
19
|
+
:backtrace_frames
|
20
|
+
) do
|
21
|
+
# Factory method with smart defaults
|
22
|
+
def self.build(type:, level: :info, **attributes)
|
23
|
+
timestamp = attributes[:timestamp] || Time.now
|
24
|
+
environment = attributes[:environment].presence || Lapsoss.configuration.environment
|
25
|
+
context = attributes[:context] || {}
|
26
|
+
|
27
|
+
# Process exception if present
|
28
|
+
exception = attributes[:exception]
|
29
|
+
message = attributes[:message].presence || exception&.message
|
30
|
+
backtrace_frames = process_backtrace(exception) if exception
|
31
|
+
|
32
|
+
# Generate fingerprint
|
33
|
+
fingerprint = attributes.fetch(:fingerprint) {
|
34
|
+
generate_fingerprint(type, message, exception, environment)
|
35
|
+
}
|
28
36
|
|
29
|
-
|
30
|
-
data = {
|
37
|
+
new(
|
31
38
|
type: type,
|
32
|
-
timestamp: timestamp,
|
33
39
|
level: level,
|
40
|
+
timestamp: timestamp,
|
34
41
|
message: message,
|
35
|
-
exception:
|
36
|
-
backtrace: backtrace_data,
|
42
|
+
exception: exception,
|
37
43
|
context: context,
|
38
44
|
environment: environment,
|
39
|
-
fingerprint: fingerprint
|
40
|
-
|
41
|
-
|
42
|
-
scrub_sensitive_data(data)
|
45
|
+
fingerprint: fingerprint,
|
46
|
+
backtrace_frames: backtrace_frames
|
47
|
+
)
|
43
48
|
end
|
44
49
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
scrub_fields: config.scrub_fields,
|
50
|
-
scrub_all: config.scrub_all,
|
51
|
-
whitelist_fields: config.whitelist_fields,
|
52
|
-
randomize_scrub_length: config.randomize_scrub_length
|
53
|
-
)
|
50
|
+
# ActiveSupport::JSON serialization
|
51
|
+
def as_json(options = nil)
|
52
|
+
to_h.compact_blank.as_json(options)
|
53
|
+
end
|
54
54
|
|
55
|
-
|
55
|
+
def to_json(options = nil)
|
56
|
+
ActiveSupport::JSON.encode(as_json(options))
|
56
57
|
end
|
57
58
|
|
58
|
-
|
59
|
-
|
59
|
+
# Helper methods
|
60
|
+
def exception_type = exception&.class&.name
|
60
61
|
|
61
|
-
|
62
|
-
custom_callback: config.fingerprint_callback,
|
63
|
-
patterns: config.fingerprint_patterns,
|
64
|
-
normalize_paths: config.normalize_fingerprint_paths,
|
65
|
-
normalize_ids: config.normalize_fingerprint_ids,
|
66
|
-
include_environment: config.fingerprint_include_environment
|
67
|
-
)
|
62
|
+
def exception_message = exception&.message
|
68
63
|
|
69
|
-
|
70
|
-
end
|
64
|
+
def has_exception? = exception.present?
|
71
65
|
|
72
|
-
def
|
73
|
-
return nil unless @exception&.backtrace
|
66
|
+
def has_backtrace? = backtrace_frames.present?
|
74
67
|
|
75
|
-
|
68
|
+
def backtrace = exception&.backtrace
|
76
69
|
|
77
|
-
|
78
|
-
context_lines: config.backtrace_context_lines,
|
79
|
-
max_frames: config.backtrace_max_frames,
|
80
|
-
enable_code_context: config.backtrace_enable_code_context,
|
81
|
-
strip_load_path: config.backtrace_strip_load_path,
|
82
|
-
in_app_patterns: config.backtrace_in_app_patterns,
|
83
|
-
exclude_patterns: config.backtrace_exclude_patterns
|
84
|
-
)
|
70
|
+
def request_context = context.dig(:extra, :request) || context.dig(:extra, "request")
|
85
71
|
|
86
|
-
|
87
|
-
end
|
72
|
+
def user_context = context[:user]
|
88
73
|
|
89
|
-
|
74
|
+
def tags = context[:tags] || {}
|
90
75
|
|
91
|
-
def
|
92
|
-
return nil unless exception
|
76
|
+
def extra = context[:extra] || {}
|
93
77
|
|
94
|
-
|
95
|
-
class: exception.class.name,
|
96
|
-
message: exception.message,
|
97
|
-
backtrace: exception.backtrace&.first(20)
|
98
|
-
}
|
99
|
-
end
|
78
|
+
def breadcrumbs = context[:breadcrumbs] || []
|
100
79
|
|
101
|
-
|
102
|
-
|
80
|
+
# Apply data scrubbing
|
81
|
+
def scrubbed
|
82
|
+
scrubber = Scrubber.new(
|
83
|
+
scrub_fields: Lapsoss.configuration.scrub_fields,
|
84
|
+
scrub_all: Lapsoss.configuration.scrub_all,
|
85
|
+
whitelist_fields: Lapsoss.configuration.whitelist_fields,
|
86
|
+
randomize_scrub_length: Lapsoss.configuration.randomize_scrub_length
|
87
|
+
)
|
103
88
|
|
104
|
-
|
89
|
+
with(context: scrubber.scrub(context))
|
105
90
|
end
|
106
91
|
|
107
|
-
|
92
|
+
private
|
108
93
|
|
109
|
-
def
|
110
|
-
exception&.
|
111
|
-
end
|
94
|
+
def self.process_backtrace(exception)
|
95
|
+
return nil unless exception&.backtrace.present?
|
112
96
|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
elsif exception
|
117
|
-
exception.backtrace
|
118
|
-
else
|
119
|
-
[]
|
120
|
-
end
|
97
|
+
config = Lapsoss.configuration
|
98
|
+
processor = BacktraceProcessor.new(config)
|
99
|
+
processor.process_exception_backtrace(exception)
|
121
100
|
end
|
122
101
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
102
|
+
def self.generate_fingerprint(type, message, exception, environment)
|
103
|
+
return nil unless Lapsoss.configuration.fingerprint_callback ||
|
104
|
+
Lapsoss.configuration.fingerprint_patterns.present?
|
105
|
+
|
106
|
+
fingerprinter = Fingerprinter.new(
|
107
|
+
custom_callback: Lapsoss.configuration.fingerprint_callback,
|
108
|
+
patterns: Lapsoss.configuration.fingerprint_patterns,
|
109
|
+
normalize_paths: Lapsoss.configuration.normalize_fingerprint_paths,
|
110
|
+
normalize_ids: Lapsoss.configuration.normalize_fingerprint_ids,
|
111
|
+
include_environment: Lapsoss.configuration.fingerprint_include_environment
|
112
|
+
)
|
113
|
+
|
114
|
+
# Create a temporary event-like object for fingerprinting
|
115
|
+
temp_event = Struct.new(:type, :message, :exception, :environment).new(
|
116
|
+
type,
|
117
|
+
message,
|
118
|
+
exception,
|
119
|
+
environment
|
120
|
+
)
|
127
121
|
|
128
|
-
|
122
|
+
fingerprinter.generate_fingerprint(temp_event)
|
129
123
|
end
|
130
124
|
end
|
131
125
|
end
|
@@ -4,8 +4,9 @@ require "digest"
|
|
4
4
|
|
5
5
|
module Lapsoss
|
6
6
|
class Fingerprinter
|
7
|
-
|
8
|
-
|
7
|
+
# Base patterns that are always available
|
8
|
+
BASE_PATTERNS = [
|
9
|
+
# Generic error message normalization
|
9
10
|
{
|
10
11
|
pattern: /User \d+ (not found|invalid|missing)/i,
|
11
12
|
fingerprint: "user-lookup-error"
|
@@ -15,21 +16,25 @@ module Lapsoss
|
|
15
16
|
fingerprint: "record-lookup-error"
|
16
17
|
},
|
17
18
|
|
18
|
-
#
|
19
|
+
# Network error patterns
|
19
20
|
{
|
20
|
-
pattern:
|
21
|
-
fingerprint: "
|
21
|
+
pattern: /Net::(TimeoutError|ReadTimeout|OpenTimeout)/,
|
22
|
+
fingerprint: "network-timeout"
|
22
23
|
},
|
23
24
|
{
|
24
|
-
pattern:
|
25
|
-
fingerprint: "
|
25
|
+
pattern: /Errno::(ECONNREFUSED|ECONNRESET|EHOSTUNREACH)/,
|
26
|
+
fingerprint: "network-connection-error"
|
26
27
|
},
|
27
28
|
|
28
|
-
#
|
29
|
+
# Memory/Resource patterns
|
29
30
|
{
|
30
|
-
pattern: /
|
31
|
-
fingerprint: "
|
32
|
-
}
|
31
|
+
pattern: /NoMemoryError|SystemStackError/,
|
32
|
+
fingerprint: "memory-resource-error"
|
33
|
+
}
|
34
|
+
].freeze
|
35
|
+
|
36
|
+
# ActiveRecord-specific patterns (only loaded if ActiveRecord is defined)
|
37
|
+
ACTIVERECORD_PATTERNS = [
|
33
38
|
{
|
34
39
|
pattern: /ActiveRecord::RecordNotFound/,
|
35
40
|
fingerprint: "record-not-found"
|
@@ -38,38 +43,34 @@ module Lapsoss
|
|
38
43
|
pattern: /ActiveRecord::StatementInvalid.*timeout/i,
|
39
44
|
fingerprint: "database-timeout"
|
40
45
|
},
|
41
|
-
|
42
|
-
# Network error patterns
|
43
|
-
{
|
44
|
-
pattern: /Net::(TimeoutError|ReadTimeout|OpenTimeout)/,
|
45
|
-
fingerprint: "network-timeout"
|
46
|
-
},
|
47
46
|
{
|
48
|
-
pattern: /
|
49
|
-
fingerprint: "
|
50
|
-
}
|
47
|
+
pattern: /ActiveRecord::ConnectionTimeoutError/,
|
48
|
+
fingerprint: "database-connection-timeout"
|
49
|
+
}
|
50
|
+
].freeze
|
51
51
|
|
52
|
-
|
52
|
+
# Database adapter patterns (only loaded if adapters are defined)
|
53
|
+
DATABASE_PATTERNS = [
|
53
54
|
{
|
54
|
-
pattern:
|
55
|
-
fingerprint: "
|
55
|
+
pattern: /PG::ConnectionBad/,
|
56
|
+
fingerprint: "postgres-connection-error",
|
57
|
+
condition: -> { defined?(PG) }
|
56
58
|
},
|
57
59
|
{
|
58
|
-
pattern: /
|
59
|
-
fingerprint: "
|
60
|
+
pattern: /Mysql2::Error/,
|
61
|
+
fingerprint: "mysql-connection-error",
|
62
|
+
condition: -> { defined?(Mysql2) }
|
60
63
|
},
|
61
|
-
|
62
|
-
# Memory/Resource patterns
|
63
64
|
{
|
64
|
-
pattern: /
|
65
|
-
fingerprint: "
|
65
|
+
pattern: /SQLite3::BusyException/,
|
66
|
+
fingerprint: "sqlite-busy-error",
|
67
|
+
condition: -> { defined?(SQLite3) }
|
66
68
|
}
|
67
69
|
].freeze
|
68
70
|
|
69
71
|
def initialize(config = {})
|
70
72
|
@custom_callback = config[:custom_callback]
|
71
|
-
@patterns = config[:patterns]
|
72
|
-
@normalize_paths = config.fetch(:normalize_paths, true)
|
73
|
+
@patterns = build_patterns(config[:patterns])
|
73
74
|
@normalize_ids = config.fetch(:normalize_ids, true)
|
74
75
|
@include_environment = config.fetch(:include_environment, false)
|
75
76
|
end
|
@@ -91,6 +92,23 @@ module Lapsoss
|
|
91
92
|
|
92
93
|
private
|
93
94
|
|
95
|
+
def build_patterns(custom_patterns)
|
96
|
+
return custom_patterns if custom_patterns
|
97
|
+
|
98
|
+
patterns = BASE_PATTERNS.dup
|
99
|
+
|
100
|
+
# Always include ActiveRecord patterns - they match on string names
|
101
|
+
patterns.concat(ACTIVERECORD_PATTERNS)
|
102
|
+
|
103
|
+
# Add database-specific patterns - they also match on string names
|
104
|
+
DATABASE_PATTERNS.each do |pattern_config|
|
105
|
+
# Skip the condition check - just match on error names
|
106
|
+
patterns << pattern_config.except(:condition)
|
107
|
+
end
|
108
|
+
|
109
|
+
patterns
|
110
|
+
end
|
111
|
+
|
94
112
|
def match_patterns(event)
|
95
113
|
full_error_text = build_error_text(event)
|
96
114
|
|
@@ -98,10 +116,13 @@ module Lapsoss
|
|
98
116
|
pattern = pattern_config[:pattern]
|
99
117
|
fingerprint = pattern_config[:fingerprint]
|
100
118
|
|
101
|
-
|
119
|
+
case pattern
|
120
|
+
in Regexp if pattern.match?(full_error_text)
|
102
121
|
return fingerprint
|
103
|
-
|
122
|
+
in String if full_error_text.include?(pattern)
|
104
123
|
return fingerprint
|
124
|
+
else
|
125
|
+
# Continue to next pattern
|
105
126
|
end
|
106
127
|
end
|
107
128
|
|
@@ -167,18 +188,9 @@ module Lapsoss
|
|
167
188
|
|
168
189
|
# Replace numeric IDs with placeholder (after UUIDs and hashes)
|
169
190
|
normalized.gsub!(/\b\d{3,}\b/, ":id")
|
170
|
-
end
|
171
|
-
|
172
|
-
if @normalize_paths
|
173
|
-
# Replace absolute file paths with placeholder
|
174
|
-
normalized.gsub!(%r{/[^/\s]+(?:/[^/\s]+)*\.[a-zA-Z0-9]+}, ":filepath")
|
175
|
-
normalized.gsub!(%r{/[^/\s]+(?:/[^/\s]+)+(?:/)?}, ":dirpath")
|
176
191
|
|
177
192
|
# Replace timestamps
|
178
193
|
normalized.gsub!(/\b\d{4}-\d{2}-\d{2}[T\s]\d{2}:\d{2}:\d{2}/, ":timestamp")
|
179
|
-
|
180
|
-
# Replace URLs with placeholder
|
181
|
-
normalized.gsub!(%r{https?://[^\s]+}, ":url")
|
182
194
|
end
|
183
195
|
|
184
196
|
# Clean up extra whitespace
|
@@ -198,13 +210,9 @@ module Lapsoss
|
|
198
210
|
|
199
211
|
line_to_use = app_line || backtrace.first
|
200
212
|
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
"#{::Regexp.last_match(1)}:#{::Regexp.last_match(2)}"
|
205
|
-
else
|
206
|
-
line_to_use
|
207
|
-
end
|
213
|
+
# Extract just filename:line_number
|
214
|
+
if line_to_use =~ %r{([^/]+):(\d+)}
|
215
|
+
"#{::Regexp.last_match(1)}:#{::Regexp.last_match(2)}"
|
208
216
|
else
|
209
217
|
line_to_use
|
210
218
|
end
|
data/lib/lapsoss/merged_scope.rb
CHANGED
@@ -26,12 +26,7 @@ module Lapsoss
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def add_breadcrumb(message, type: :default, **metadata)
|
29
|
-
breadcrumb =
|
30
|
-
message: message,
|
31
|
-
type: type,
|
32
|
-
metadata: metadata,
|
33
|
-
timestamp: Time.now.utc
|
34
|
-
}
|
29
|
+
breadcrumb = Breadcrumb.build(message, type: type, metadata: metadata)
|
35
30
|
@own_breadcrumbs << breadcrumb
|
36
31
|
# Keep breadcrumbs to a reasonable limit
|
37
32
|
@own_breadcrumbs.shift if @own_breadcrumbs.length > 20
|
@@ -3,114 +3,27 @@
|
|
3
3
|
module Lapsoss
|
4
4
|
module Middleware
|
5
5
|
class ReleaseTracker < Base
|
6
|
-
def initialize(app,
|
6
|
+
def initialize(app, release: nil)
|
7
7
|
super(app)
|
8
|
-
@
|
8
|
+
@release = release
|
9
9
|
end
|
10
10
|
|
11
11
|
def call(event, hint = {})
|
12
|
-
|
12
|
+
if release = detect_release
|
13
|
+
event.context[:release] = release
|
14
|
+
end
|
13
15
|
@app.call(event, hint)
|
14
16
|
end
|
15
17
|
|
16
18
|
private
|
17
19
|
|
18
|
-
def
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
def auto_detect_release
|
25
|
-
release_info = {}
|
26
|
-
|
27
|
-
# Try to detect Git information
|
28
|
-
if git_info = detect_git_info
|
29
|
-
release_info.merge!(git_info)
|
30
|
-
end
|
31
|
-
|
32
|
-
# Try to detect deployment info
|
33
|
-
if deployment_info = detect_deployment_info
|
34
|
-
release_info.merge!(deployment_info)
|
35
|
-
end
|
36
|
-
|
37
|
-
release_info.empty? ? nil : release_info
|
38
|
-
end
|
39
|
-
|
40
|
-
def detect_git_info
|
41
|
-
return nil unless File.exist?(".git")
|
42
|
-
|
43
|
-
begin
|
44
|
-
# Get current commit SHA
|
45
|
-
commit_sha = `git rev-parse HEAD`.strip
|
46
|
-
return nil if commit_sha.empty?
|
47
|
-
|
48
|
-
# Get branch name
|
49
|
-
branch = `git rev-parse --abbrev-ref HEAD`.strip
|
50
|
-
branch = nil if branch.empty? || branch == "HEAD"
|
51
|
-
|
52
|
-
# Get commit timestamp
|
53
|
-
commit_time = `git log -1 --format=%ct`.strip
|
54
|
-
commit_timestamp = commit_time.empty? ? nil : Time.zone.at(commit_time.to_i)
|
55
|
-
|
56
|
-
# Get tag if on a tag
|
57
|
-
tag = `git describe --exact-match --tags HEAD 2>/dev/null`.strip
|
58
|
-
tag = nil if tag.empty?
|
59
|
-
|
60
|
-
{
|
61
|
-
commit_sha: commit_sha,
|
62
|
-
branch: branch,
|
63
|
-
tag: tag,
|
64
|
-
commit_timestamp: commit_timestamp
|
65
|
-
}.compact
|
66
|
-
rescue StandardError
|
67
|
-
nil
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def detect_deployment_info
|
72
|
-
info = {}
|
73
|
-
|
74
|
-
# Check common deployment environment variables
|
75
|
-
info[:deployment_id] = ENV["DEPLOYMENT_ID"] if ENV["DEPLOYMENT_ID"]
|
76
|
-
info[:build_number] = ENV["BUILD_NUMBER"] if ENV["BUILD_NUMBER"]
|
77
|
-
info[:deployment_time] = parse_deployment_time(ENV["DEPLOYMENT_TIME"]) if ENV["DEPLOYMENT_TIME"]
|
78
|
-
|
79
|
-
# Check Heroku
|
80
|
-
if ENV["HEROKU_APP_NAME"]
|
81
|
-
info[:platform] = "heroku"
|
82
|
-
info[:app_name] = ENV["HEROKU_APP_NAME"]
|
83
|
-
info[:dyno] = ENV.fetch("DYNO", nil)
|
84
|
-
info[:slug_commit] = ENV.fetch("HEROKU_SLUG_COMMIT", nil)
|
85
|
-
end
|
86
|
-
|
87
|
-
# Check AWS
|
88
|
-
if ENV["AWS_EXECUTION_ENV"]
|
89
|
-
info[:platform] = "aws"
|
90
|
-
info[:execution_env] = ENV["AWS_EXECUTION_ENV"]
|
91
|
-
info[:region] = ENV.fetch("AWS_REGION", nil)
|
92
|
-
end
|
93
|
-
|
94
|
-
# Check Docker
|
95
|
-
if ENV["DOCKER_CONTAINER_ID"] || File.exist?("/.dockerenv")
|
96
|
-
info[:platform] = "docker"
|
97
|
-
info[:container_id] = ENV["DOCKER_CONTAINER_ID"]
|
98
|
-
end
|
99
|
-
|
100
|
-
# Check Kubernetes
|
101
|
-
if ENV["KUBERNETES_SERVICE_HOST"]
|
102
|
-
info[:platform] = "kubernetes"
|
103
|
-
info[:namespace] = ENV.fetch("KUBERNETES_NAMESPACE", nil)
|
104
|
-
info[:pod_name] = ENV.fetch("HOSTNAME", nil)
|
105
|
-
end
|
106
|
-
|
107
|
-
info.empty? ? nil : info
|
108
|
-
end
|
20
|
+
def detect_release
|
21
|
+
# Use configured release
|
22
|
+
return @release.call if @release.respond_to?(:call)
|
23
|
+
return @release if @release.present?
|
109
24
|
|
110
|
-
|
111
|
-
|
112
|
-
rescue StandardError
|
113
|
-
nil
|
25
|
+
# Use rails_app_version gem if available
|
26
|
+
Rails.application.version.to_s if defined?(Rails) && Rails.application.respond_to?(:version)
|
114
27
|
end
|
115
28
|
end
|
116
29
|
end
|
@@ -25,8 +25,8 @@ module Lapsoss
|
|
25
25
|
self
|
26
26
|
end
|
27
27
|
|
28
|
-
def track_releases(
|
29
|
-
@pipeline.use(Middleware::ReleaseTracker,
|
28
|
+
def track_releases(release: nil)
|
29
|
+
@pipeline.use(Middleware::ReleaseTracker, release: release)
|
30
30
|
self
|
31
31
|
end
|
32
32
|
|
@@ -22,10 +22,9 @@ module Lapsoss
|
|
22
22
|
private
|
23
23
|
|
24
24
|
def skip_error?(error, source)
|
25
|
-
# Skip cache-related
|
26
|
-
|
27
|
-
|
28
|
-
end
|
25
|
+
# Skip Rails cache-related errors using Rails error reporter source
|
26
|
+
# Avoid referencing backend-specific gems (e.g., Redis)
|
27
|
+
return true if Lapsoss.configuration.skip_rails_cache_errors && source&.include?("cache")
|
29
28
|
|
30
29
|
false
|
31
30
|
end
|
@@ -10,14 +10,14 @@ module Lapsoss
|
|
10
10
|
Lapsoss::Current.with_clean_scope do
|
11
11
|
# Add request context to current scope
|
12
12
|
if Lapsoss.configuration.capture_request_context
|
13
|
-
Rails.logger.debug "
|
13
|
+
Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Adding request context" } if Rails.env.test?
|
14
14
|
add_request_context(env)
|
15
15
|
end
|
16
16
|
|
17
17
|
begin
|
18
18
|
@app.call(env)
|
19
19
|
rescue Exception => e
|
20
|
-
Rails.logger.
|
20
|
+
Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Capturing exception: #{e.class} - #{e.message}" } if Rails.env.test?
|
21
21
|
# Capture the exception
|
22
22
|
Lapsoss.capture_exception(e)
|
23
23
|
# Re-raise the exception to maintain Rails error handling
|