lapsoss 0.4.5 → 0.4.9

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5db26196bdbf6e69d90c126041f7310681e240ef22e61f0a3aee12035f7616da
4
- data.tar.gz: 93a2c75b6af04e776aca9c24f3704ddba51b4a2432d5d6db0f736874bef17c69
3
+ metadata.gz: 6e053057613e5678e160556abda63a224b056bb4b0b4b1d291282b2c1a8476ea
4
+ data.tar.gz: c6109d65cfb08dc095c45862565b6754a720e18d52f27814c0606a307964f328
5
5
  SHA512:
6
- metadata.gz: 38d9d2d1c1e2f67997b71cae8776b2c7ef8b3aca5d13d563080304c63b84b1db227eb923618b1ae078223cfc20dfebf5d1620dcf01eed92ee62d53461bf36e1e
7
- data.tar.gz: 1831cda3198ad594c850fd5579fed77aa9097ee4f497dd81374bb265ee237addc4d31fab0447d6164cff6fb64695869eba42366e0820647725d0725566158ff8
6
+ metadata.gz: 944d8581a4dc544e14f2daf5094d9af5d59e5f387802212a2342f8b33f88790e218fe365df882cc6b56444edeb72addf1815ff4851c6eb9f5e50ecf388d22797
7
+ data.tar.gz: e4c712a550906d722d7a954d04560de2f4d6119a51d8c5271ca73cf4b7b8ca8a45ea238eddc3fb11366dbd35bc9f3dbc5cb3dc85544f4bbee8ccdd38d78858ec
data/README.md CHANGED
@@ -23,10 +23,10 @@ Lapsoss.configure do |config|
23
23
  # Monday: Using Bugsnag
24
24
  config.use_bugsnag(api_key: ENV['BUGSNAG_KEY'])
25
25
 
26
- # Tuesday: Add Sentry for comparison
27
- config.use_sentry(dsn: ENV['SENTRY_DSN'])
26
+ # Tuesday: Add Telebugs for comparison
27
+ config.use_telebugs(dsn: ENV['TELEBUGS_DSN'])
28
28
 
29
- # Wednesday: Drop Bugsnag, keep Sentry
29
+ # Wednesday: Drop Bugsnag, keep Telebugs
30
30
  # Just remove the line. Zero code changes.
31
31
  end
32
32
  ```
@@ -64,7 +64,7 @@ That's it. No 500-line examples needed.
64
64
 
65
65
  ## Built for Rails, Not Around It
66
66
 
67
- Lapsoss integrates with Rails' native error reporting API introduced in Rails 7. No monkey-patching, no middleware gymnastics:
67
+ Lapsoss integrates with Rails' native error reporting API introduced in Rails 7. No monkey-patching, no global error handlers:
68
68
 
69
69
  ```ruby
70
70
  # It just works with Rails.error:
@@ -74,6 +74,56 @@ end
74
74
  # Automatically captured by whatever service you configured
75
75
  ```
76
76
 
77
+ ### Rails Integration Options
78
+
79
+ **Option 1: Automatic Rails.error Integration (Recommended)**
80
+ ```ruby
81
+ # config/initializers/lapsoss.rb
82
+ Lapsoss.configure do |config|
83
+ config.use_appsignal(push_api_key: ENV['APPSIGNAL_KEY'])
84
+ end
85
+
86
+ # That's it! All Rails errors are automatically captured
87
+ # Works with Rails.error.handle, Rails.error.record, Rails.error.report
88
+ # No code changes needed - just configure and go
89
+ ```
90
+
91
+ **Option 2: Add Controller Context (Optional)**
92
+ ```ruby
93
+ # app/controllers/application_controller.rb
94
+ class ApplicationController < ActionController::Base
95
+ include Lapsoss::RailsControllerContext
96
+ end
97
+
98
+ # Now all errors include controller/action context:
99
+ # { controller: "users", action: "show", controller_class: "UsersController" }
100
+ ```
101
+
102
+ **Option 3: Manual Error Reporting (Your Choice)**
103
+ ```ruby
104
+ # In controllers, jobs, or anywhere you want explicit control
105
+ begin
106
+ process_payment
107
+ rescue => e
108
+ Lapsoss.capture_exception(e, user_id: current_user.id)
109
+ # Handle gracefully...
110
+ end
111
+
112
+ # Or use Rails.error directly with your configured services
113
+ Rails.error.report(e, context: { user_id: current_user.id })
114
+ ```
115
+
116
+ ### No Global Patching Philosophy
117
+
118
+ Unlike other gems, Lapsoss **never** automatically captures all exceptions. You stay in control:
119
+
120
+ - ✅ **Rails.error integration only** - Uses Rails' official API
121
+ - ✅ **Explicit error handling** - You choose what to capture
122
+ - ✅ **No global hooks** - Your app behavior never changes
123
+ - ✅ **Optional controller context** - Include if you want it
124
+
125
+ This means your application behaves exactly the same with or without Lapsoss. No surprises, no changed behavior, no conflicts with other gems.
126
+
77
127
  ## Zero-Downtime Vendor Migration
78
128
 
79
129
  ```ruby
@@ -84,21 +134,22 @@ gem 'bugsnag' # Keep your existing gem for now
84
134
  # Step 2: Configure dual reporting
85
135
  Lapsoss.configure do |config|
86
136
  config.use_bugsnag(api_key: ENV['BUGSNAG_KEY'])
87
- config.use_sentry(dsn: ENV['SENTRY_DSN'])
137
+ config.use_telebugs(dsn: ENV['TELEBUGS_DSN'])
88
138
  end
89
139
 
90
140
  # Step 3: Gradually replace Bugsnag calls
91
141
  # Old: Bugsnag.notify(e)
92
142
  # New: Lapsoss.capture_exception(e)
93
143
 
94
- # Step 4: Remove bugsnag gem when ready
95
- # Your app keeps running, now on Sentry
144
+ # Step 4: Remove bugsnag gem when ready
145
+ # Your app keeps running, now on Telebugs
96
146
  ```
97
147
 
98
148
  ## Why Not Just Use Vendor SDKs?
99
149
 
100
150
  **Vendor SDKs monkey-patch your application:**
101
151
  - Sentry patches Net::HTTP, Redis, and 20+ other gems
152
+ - Bugsnag patches ActionController, ActiveJob, and more
102
153
  - Each vendor races to patch the same methods
103
154
  - Multiple SDKs = multiple layers of patches
104
155
  - Your app behavior changes based on load order
@@ -114,8 +165,8 @@ end
114
165
  ### GDPR Compliance
115
166
  ```ruby
116
167
  # Route EU data to EU servers, US data to US servers
117
- config.use_sentry(name: :us, dsn: ENV['US_DSN'])
118
- config.use_sentry(name: :eu, dsn: ENV['EU_DSN'])
168
+ config.use_sentry(name: :us, dsn: ENV['US_SENTRY_DSN'])
169
+ config.use_telebugs(name: :eu, dsn: ENV['EU_TELEBUGS_DSN'])
119
170
  ```
120
171
 
121
172
  ### A/B Testing Error Services
@@ -128,8 +179,8 @@ config.use_sentry(name: :candidate, dsn: ENV['SENTRY_DSN'])
128
179
  ### High Availability
129
180
  ```ruby
130
181
  # Multiple providers for redundancy
131
- config.use_sentry(name: :primary, dsn: ENV['PRIMARY_DSN'])
132
- config.use_rollbar(name: :backup, access_token: ENV['BACKUP_TOKEN'])
182
+ config.use_telebugs(name: :primary, dsn: ENV['PRIMARY_DSN'])
183
+ config.use_appsignal(name: :backup, push_api_key: ENV['APPSIGNAL_KEY'])
133
184
  ```
134
185
 
135
186
  ## Yes, We Require ActiveSupport
@@ -172,7 +223,7 @@ All adapters are pure Ruby implementations with no external SDK dependencies:
172
223
  ```ruby
173
224
  # config/initializers/lapsoss.rb
174
225
  Lapsoss.configure do |config|
175
- config.use_sentry(dsn: ENV["SENTRY_DSN"])
226
+ config.use_telebugs(dsn: ENV["TELEBUGS_DSN"])
176
227
  end
177
228
  ```
178
229
 
@@ -203,7 +254,7 @@ end
203
254
  ```ruby
204
255
  Lapsoss.configure do |config|
205
256
  # Adapter setup
206
- config.use_sentry(dsn: ENV['SENTRY_DSN'])
257
+ config.use_rollbar(access_token: ENV['ROLLBAR_TOKEN'])
207
258
 
208
259
  # Data scrubbing (uses Rails filter_parameters automatically)
209
260
  config.scrub_fields = %w[password credit_card ssn] # Or leave nil to use Rails defaults
@@ -122,7 +122,7 @@ module Lapsoss
122
122
  end
123
123
 
124
124
  def user_agent
125
- "Lapsoss/#{Lapsoss::VERSION} Ruby/#{RUBY_VERSION} Rails/#{Rails.version if defined?(Rails)}"
125
+ Base::USER_AGENT
126
126
  end
127
127
  end
128
128
  end
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support/core_ext/object/blank"
4
4
  require "uri"
5
+ require "lapsoss/runtime_context"
5
6
 
6
7
  module Lapsoss
7
8
  module Adapters
@@ -80,7 +81,7 @@ module Lapsoss
80
81
  content_type: "application/json"
81
82
  }
82
83
 
83
- item_payload = build_envelope_wrapper(event)
84
+ item_payload = build_sentry_event(event)
84
85
 
85
86
  # Sentry envelope is newline-delimited JSON
86
87
  [
@@ -90,6 +91,89 @@ module Lapsoss
90
91
  ].join("\n")
91
92
  end
92
93
 
94
+ # Build Sentry-compliant event structure
95
+ def build_sentry_event(event)
96
+ context = RuntimeContext.current
97
+ event_id = event.fingerprint.presence || SecureRandom.uuid
98
+
99
+ base_event = {
100
+ event_id: event_id,
101
+ timestamp: format_timestamp(event.timestamp),
102
+ platform: "ruby",
103
+ level: map_level(event.level),
104
+ environment: event.environment.presence || "production",
105
+ release: context.release,
106
+ server_name: context.server_name,
107
+ modules: context.modules,
108
+ contexts: context.to_contexts,
109
+ tags: event.tags.presence,
110
+ user: event.user_context.presence,
111
+ extra: event.extra.presence,
112
+ breadcrumbs: format_breadcrumbs(event.breadcrumbs),
113
+ transaction: event.transaction,
114
+ sdk: {
115
+ name: "lapsoss.ruby",
116
+ version: Lapsoss::VERSION
117
+ }
118
+ }.compact_blank
119
+
120
+ # Add event-specific data
121
+ case event.type
122
+ when :exception
123
+ base_event.merge(build_exception_envelope(event))
124
+ when :message
125
+ base_event.merge(
126
+ message: event.message,
127
+ level: map_level(event.level)
128
+ )
129
+ else
130
+ base_event
131
+ end
132
+ end
133
+
134
+ def build_exception_envelope(event)
135
+ {
136
+ exception: {
137
+ values: [ {
138
+ type: event.exception_type,
139
+ value: event.exception_message,
140
+ module: nil,
141
+ thread_id: Thread.current.object_id,
142
+ stacktrace: build_sentry_stacktrace(event),
143
+ mechanism: { type: "generic", handled: true }
144
+ } ]
145
+ },
146
+ threads: {
147
+ values: [ {
148
+ id: Thread.current.object_id,
149
+ name: Thread.current.name,
150
+ crashed: true,
151
+ current: true
152
+ } ]
153
+ }
154
+ }
155
+ end
156
+
157
+ def build_sentry_stacktrace(event)
158
+ return nil unless event.has_backtrace?
159
+
160
+ frames = event.backtrace_frames.map do |frame|
161
+ {
162
+ filename: frame.filename,
163
+ abs_path: frame.absolute_path || frame.filename,
164
+ function: frame.method_name || frame.function,
165
+ lineno: frame.line_number,
166
+ in_app: frame.in_app,
167
+ pre_context: frame.code_context&.dig(:pre_context),
168
+ context_line: frame.code_context&.dig(:context_line),
169
+ post_context: frame.code_context&.dig(:post_context)
170
+ }.compact
171
+ end
172
+
173
+ # Sentry expects frames in reverse order (oldest to newest)
174
+ { frames: frames.reverse }
175
+ end
176
+
93
177
  # Override serialization for Sentry's envelope format
94
178
  def serialize_payload(envelope_string)
95
179
  # Sentry envelopes are already formatted, just compress if needed
@@ -8,20 +8,25 @@ module Lapsoss
8
8
  # Telebugs is compatible with Sentry's API, so we inherit from SentryAdapter
9
9
  class TelebugsAdapter < SentryAdapter
10
10
  def initialize(name = :telebugs, settings = {})
11
+ debug_log "[TELEBUGS INIT] Initializing with settings: #{settings.inspect}"
11
12
  super(name, settings)
13
+ debug_log "[TELEBUGS INIT] Initialization complete, enabled: #{@enabled}"
12
14
  end
13
15
 
14
16
  private
15
17
 
16
18
  # Override to parse Telebugs DSN format
17
19
  def parse_dsn(dsn_string)
20
+ debug_log "[TELEBUGS DSN] Parsing DSN: #{dsn_string}"
18
21
  uri = URI.parse(dsn_string)
19
- {
22
+ parsed = {
20
23
  public_key: uri.user,
21
24
  project_id: uri.path.split("/").last,
22
25
  host: uri.host,
23
26
  path: uri.path
24
27
  }
28
+ debug_log "[TELEBUGS DSN] Parsed: #{parsed.inspect}"
29
+ parsed
25
30
  end
26
31
 
27
32
  # Override to build Telebugs-specific API path
@@ -42,8 +47,42 @@ module Lapsoss
42
47
  uri = URI.parse(@settings[:dsn])
43
48
  # For Telebug, we use the full URL without port (unless non-standard)
44
49
  port = (uri.port == 443 || uri.port == 80) ? "" : ":#{uri.port}"
45
- self.class.api_endpoint = "#{uri.scheme}://#{uri.host}#{port}"
46
- self.class.api_path = build_api_path(uri)
50
+ endpoint = "#{uri.scheme}://#{uri.host}#{port}"
51
+ api_path = build_api_path(uri)
52
+
53
+ debug_log "[TELEBUGS ENDPOINT] Setting endpoint: #{endpoint}"
54
+ debug_log "[TELEBUGS ENDPOINT] Setting API path: #{api_path}"
55
+
56
+ self.class.api_endpoint = endpoint
57
+ self.class.api_path = api_path
58
+ end
59
+
60
+ public
61
+
62
+ # Override capture to add debug logging
63
+ def capture(event)
64
+ debug_log "[TELEBUGS DEBUG] Capture called for event: #{event.type}"
65
+ debug_log "[TELEBUGS DEBUG] DSN configured: #{@dsn.inspect}"
66
+ debug_log "[TELEBUGS DEBUG] Endpoint: #{self.class.api_endpoint}"
67
+ debug_log "[TELEBUGS DEBUG] API Path: #{self.class.api_path}"
68
+
69
+ result = super(event)
70
+ debug_log "[TELEBUGS DEBUG] Event sent successfully, response: #{result.inspect}"
71
+ result
72
+ rescue => e
73
+ debug_log "[TELEBUGS ERROR] Failed to send: #{e.message}", :error
74
+ debug_log "[TELEBUGS ERROR] Backtrace: #{e.backtrace.first(5).join("\n")}", :error
75
+ raise
76
+ end
77
+
78
+ def debug_log(message, level = :info)
79
+ return unless @debug
80
+
81
+ if defined?(Rails) && Rails.respond_to?(:logger) && Rails.logger
82
+ Rails.logger.public_send(level, message)
83
+ elsif @logger
84
+ @logger.public_send(level, message)
85
+ end
47
86
  end
48
87
 
49
88
  # Override headers builder to add Telebugs-specific headers
@@ -3,6 +3,7 @@
3
3
  module Lapsoss
4
4
  BacktraceFrame = Data.define(
5
5
  :filename,
6
+ :absolute_path,
6
7
  :line_number,
7
8
  :method_name,
8
9
  :in_app,
@@ -19,6 +20,7 @@ module Lapsoss
19
20
  def to_h
20
21
  {
21
22
  filename: filename,
23
+ absolute_path: absolute_path,
22
24
  line_number: line_number,
23
25
  method: method_name,
24
26
  function: function,
@@ -30,9 +32,13 @@ module Lapsoss
30
32
  end
31
33
 
32
34
  def add_code_context(processor, context_lines = 3)
33
- return unless filename && line_number && File.exist?(filename)
35
+ return unless line_number
34
36
 
35
- with(code_context: processor.get_code_context(filename, line_number, context_lines))
37
+ # Use absolute path if available, otherwise try filename
38
+ path_to_read = absolute_path || filename
39
+ return unless path_to_read
40
+
41
+ with(code_context: processor.get_code_context(path_to_read, line_number, context_lines))
36
42
  end
37
43
 
38
44
  def valid?
@@ -75,10 +75,14 @@ module Lapsoss
75
75
  filename, line_number, method_name, function, module_name, block_info = parse_line_components
76
76
 
77
77
  in_app = determine_app_status(filename)
78
- filename = normalize_path(filename) if filename
78
+
79
+ # Keep both absolute and normalized paths
80
+ absolute_path = filename
81
+ normalized_filename = normalize_path(filename) if filename
79
82
 
80
83
  BacktraceFrame.new(
81
- filename: filename,
84
+ filename: normalized_filename,
85
+ absolute_path: absolute_path,
82
86
  line_number: line_number,
83
87
  method_name: method_name,
84
88
  in_app: in_app,
@@ -196,8 +196,7 @@ module Lapsoss
196
196
 
197
197
  # Get code context around a specific line number using ActiveSupport::Cache
198
198
  def get_code_context(filename, line_number, context_lines = 3)
199
- return nil unless filename && File.exist?(filename)
200
- return nil if File.size(filename) > (@config[:max_file_size] || (1024 * 1024))
199
+ return nil unless filename
201
200
 
202
201
  lines = @file_cache.fetch(filename) do
203
202
  read_file_safely(filename)
@@ -229,6 +228,9 @@ module Lapsoss
229
228
  private
230
229
 
231
230
  def read_file_safely(filename)
231
+ # Don't read huge files
232
+ return [] if File.exist?(filename) && File.size(filename) > (@config[:max_file_size] || (1024 * 1024))
233
+
232
234
  File.readlines(filename, chomp: true)
233
235
  rescue StandardError
234
236
  []
@@ -6,32 +6,35 @@ module Lapsoss
6
6
  class Client
7
7
  def initialize(configuration)
8
8
  @configuration = configuration
9
- @executor = Concurrent::FixedThreadPool.new(5) if @configuration.async
9
+ # Note: We're using Thread.new directly for async mode instead of a thread pool
10
+ # The Concurrent::FixedThreadPool had issues in Rails development mode
10
11
  end
11
12
 
12
13
  def capture_exception(exception, **context)
13
- return unless @configuration.enabled
14
+ return nil unless @configuration.enabled
14
15
 
15
16
  with_scope(context) do |scope|
16
17
  event = Event.build(
17
18
  type: :exception,
18
19
  level: :error,
19
20
  exception: exception,
20
- context: scope_to_context(scope)
21
+ context: scope_to_context(scope),
22
+ transaction: scope.transaction_name
21
23
  )
22
24
  capture_event(event)
23
25
  end
24
26
  end
25
27
 
26
28
  def capture_message(message, level: :info, **context)
27
- return unless @configuration.enabled
29
+ return nil unless @configuration.enabled
28
30
 
29
31
  with_scope(context) do |scope|
30
32
  event = Event.build(
31
33
  type: :message,
32
34
  level: level,
33
35
  message: message,
34
- context: scope_to_context(scope)
36
+ context: scope_to_context(scope),
37
+ transaction: scope.transaction_name
35
38
  )
36
39
  capture_event(event)
37
40
  end
@@ -58,30 +61,54 @@ module Lapsoss
58
61
  end
59
62
 
60
63
  def flush(timeout: 2)
61
- Registry.instance.flush(timeout: timeout)
64
+ @configuration.logger.debug("[LAPSOSS] Flush called with timeout: #{timeout}")
65
+ # Give threads a moment to complete
66
+ sleep(0.5) if @configuration.async
67
+
68
+ # Flush individual adapters if they support it
69
+ Registry.instance.active.each do |adapter|
70
+ adapter.flush(timeout: timeout) if adapter.respond_to?(:flush)
71
+ end
62
72
  end
63
73
 
64
74
  def shutdown
65
- @executor&.shutdown
66
75
  Registry.instance.shutdown
67
76
  end
68
77
 
69
78
  private
70
79
 
71
80
  def capture_event(event)
81
+ @configuration.logger.debug("[LAPSOSS] capture_event called, async: #{@configuration.async}, executor: #{@executor.inspect}")
82
+
72
83
  # Apply pipeline processing if enabled
73
84
  if @configuration.enable_pipeline && @configuration.pipeline
74
85
  event = @configuration.pipeline.call(event)
75
- return unless event
86
+ return nil unless event
76
87
  end
77
88
 
78
89
  event = run_before_send(event)
79
- return unless event
90
+ return nil unless event
80
91
 
81
92
  if @configuration.async
82
- @executor.post { Router.process_event(event) }
93
+ @configuration.logger.debug("[LAPSOSS ASYNC] About to process event asynchronously")
94
+
95
+ # Use Thread.new for now - the executor pool seems to have issues in Rails dev mode
96
+ thread = Thread.new do
97
+ begin
98
+ @configuration.logger.debug("[LAPSOSS ASYNC] Background thread started")
99
+ Router.process_event(event)
100
+ @configuration.logger.debug("[LAPSOSS ASYNC] Background thread completed")
101
+ rescue => e
102
+ @configuration.logger.error("[LAPSOSS ASYNC ERROR] Failed in background: #{e.message}")
103
+ @configuration.logger.error(e.backtrace.join("\n")) if @configuration.debug
104
+ end
105
+ end
106
+
107
+ thread
83
108
  else
109
+ @configuration.logger.debug("[LAPSOSS SYNC] Processing event synchronously")
84
110
  Router.process_event(event)
111
+ nil
85
112
  end
86
113
  rescue StandardError => e
87
114
  handle_capture_error(e)
@@ -59,6 +59,7 @@ module Lapsoss
59
59
  # Pipeline settings
60
60
  @enable_pipeline = true
61
61
  @pipeline_builder = nil
62
+ @pipeline = nil
62
63
  @sampling_strategy = nil
63
64
  # Rails error filtering
64
65
  @skip_rails_cache_errors = true
@@ -185,7 +186,12 @@ module Lapsoss
185
186
  end
186
187
 
187
188
  def pipeline
188
- @pipeline_builder&.pipeline
189
+ @pipeline || @pipeline_builder&.pipeline
190
+ end
191
+
192
+ def pipeline=(value)
193
+ validate_callable!(value, "pipeline") if value
194
+ @pipeline = value
189
195
  end
190
196
 
191
197
  # Sampling configuration
data/lib/lapsoss/event.rb CHANGED
@@ -16,7 +16,8 @@ module Lapsoss
16
16
  :context,
17
17
  :environment,
18
18
  :fingerprint,
19
- :backtrace_frames
19
+ :backtrace_frames,
20
+ :transaction # Controller#action or task name where event occurred
20
21
  ) do
21
22
  # Factory method with smart defaults
22
23
  def self.build(type:, level: :info, **attributes)
@@ -43,7 +44,8 @@ module Lapsoss
43
44
  context: context,
44
45
  environment: environment,
45
46
  fingerprint: fingerprint,
46
- backtrace_frames: backtrace_frames
47
+ backtrace_frames: backtrace_frames,
48
+ transaction: attributes[:transaction]
47
49
  )
48
50
  end
49
51
 
@@ -25,6 +25,24 @@ module Lapsoss
25
25
  @breadcrumbs ||= merge_breadcrumbs
26
26
  end
27
27
 
28
+ def transaction_name
29
+ # Check scope stack first (most recent wins)
30
+ @scope_stack.reverse_each do |context|
31
+ return context[:transaction_name] if context[:transaction_name]
32
+ end
33
+ # Fall back to base scope
34
+ @base_scope.transaction_name
35
+ end
36
+
37
+ def transaction_source
38
+ # Check scope stack first (most recent wins)
39
+ @scope_stack.reverse_each do |context|
40
+ return context[:transaction_source] if context[:transaction_source]
41
+ end
42
+ # Fall back to base scope
43
+ @base_scope.transaction_source
44
+ end
45
+
28
46
  def add_breadcrumb(message, type: :default, **metadata)
29
47
  breadcrumb = Breadcrumb.build(message, type: type, metadata: metadata)
30
48
  @own_breadcrumbs << breadcrumb
@@ -34,6 +52,10 @@ module Lapsoss
34
52
  @breadcrumbs = nil
35
53
  end
36
54
 
55
+ def set_transaction_name(name, source: nil)
56
+ @base_scope.set_transaction_name(name, source: source)
57
+ end
58
+
37
59
  private
38
60
 
39
61
  def merge_hash_contexts(key)
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ # Optional concern to add controller and action context to Rails.error
5
+ # Include this in ApplicationController or specific controllers to get more detailed context
6
+ #
7
+ # Example:
8
+ # class ApplicationController < ActionController::Base
9
+ # include Lapsoss::RailsControllerContext
10
+ # end
11
+ module RailsControllerContext
12
+ extend ActiveSupport::Concern
13
+
14
+ included do
15
+ prepend_before_action :set_lapsoss_controller_context
16
+ end
17
+
18
+ private
19
+
20
+ def set_lapsoss_controller_context
21
+ # Set context in Lapsoss scope if available
22
+ Lapsoss::Current.scope&.set_context("controller", {
23
+ controller: controller_name,
24
+ action: action_name,
25
+ controller_class: self.class.name
26
+ })
27
+
28
+ # Set context in Rails.error for ecosystem-wide availability
29
+ Rails.error.set_context(
30
+ controller: controller_name,
31
+ action: action_name,
32
+ controller_class: self.class.name
33
+ )
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lapsoss
4
+ module RailsControllerTransaction
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ around_action :lapsoss_capture_transaction
9
+ end
10
+
11
+ private
12
+
13
+ def lapsoss_capture_transaction
14
+ if Lapsoss.client
15
+ transaction_name = "#{self.class.name}##{action_name}"
16
+
17
+ # Set the transaction name in the current scope
18
+ Lapsoss::Current.scope.set_transaction_name(transaction_name, source: :view)
19
+
20
+ # Add breadcrumb for the action
21
+ Lapsoss::Current.scope.add_breadcrumb(
22
+ "Processing #{transaction_name}",
23
+ type: :navigation,
24
+ controller: self.class.name,
25
+ action: action_name,
26
+ params: request.filtered_parameters
27
+ )
28
+ end
29
+
30
+ yield
31
+ end
32
+ end
33
+ end
@@ -2,13 +2,7 @@
2
2
 
3
3
  module Lapsoss
4
4
  class Railtie < Rails::Railtie
5
- if ENV["DEBUG_LAPSOSS"]
6
- if Rails.logger.respond_to?(:tagged)
7
- Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Railtie loaded" }
8
- else
9
- Rails.logger.debug "[Lapsoss] Railtie loaded"
10
- end
11
- end
5
+ # Debug logging removed - will be handled by the configured logger
12
6
  config.lapsoss = ActiveSupport::OrderedOptions.new
13
7
 
14
8
  initializer "lapsoss.configure" do |_app|
@@ -20,12 +14,11 @@ module Lapsoss
20
14
  Rails.env
21
15
  end
22
16
 
23
- # Use tagged logger for all Lapsoss logs
24
- config.logger ||= if Rails.logger.respond_to?(:tagged)
25
- Rails.logger.tagged("Lapsoss")
26
- else
27
- ActiveSupport::TaggedLogging.new(Rails.logger).tagged("Lapsoss")
28
- end
17
+ # Use Rails logger if available
18
+ config.logger ||= Rails.logger
19
+
20
+ # Set debug level in development
21
+ config.debug = Rails.env.development?
29
22
 
30
23
  config.release ||= if Rails.application.respond_to?(:version)
31
24
  Rails.application.version.to_s
@@ -41,15 +34,16 @@ module Lapsoss
41
34
  end
42
35
  end
43
36
 
44
- initializer "lapsoss.add_middleware" do |app|
45
- require "lapsoss/rails_middleware"
46
37
 
47
- # Use config.middleware to ensure it's added during initialization
48
- app.config.middleware.use Lapsoss::RailsMiddleware
38
+ initializer "lapsoss.rails_error_subscriber" do |app|
39
+ Rails.error.subscribe(Lapsoss::RailsErrorSubscriber.new)
49
40
  end
50
41
 
51
- initializer "lapsoss.rails_error_subscriber", after: "lapsoss.add_middleware" do |app|
52
- Rails.error.subscribe(Lapsoss::RailsErrorSubscriber.new)
42
+ initializer "lapsoss.controller_transaction" do
43
+ ActiveSupport.on_load(:action_controller) do
44
+ require "lapsoss/rails_controller_transaction"
45
+ include Lapsoss::RailsControllerTransaction
46
+ end
53
47
  end
54
48
  end
55
49
  end
@@ -8,8 +8,13 @@ module Lapsoss
8
8
  #
9
9
  # @param event [Lapsoss::Event] The event to process.
10
10
  def process_event(event)
11
- Registry.instance.active.each do |adapter|
11
+ adapters = Registry.instance.active
12
+ Lapsoss.configuration.logger.debug("[LAPSOSS ROUTER] Processing event to #{adapters.length} adapters: #{adapters.map(&:name).join(', ')}")
13
+
14
+ adapters.each do |adapter|
15
+ Lapsoss.configuration.logger.info("[LAPSOSS ROUTER] About to call #{adapter.name}.capture")
12
16
  adapter.capture(event)
17
+ Lapsoss.configuration.logger.info("[LAPSOSS ROUTER] Adapter #{adapter.name} completed")
13
18
  rescue StandardError => e
14
19
  handle_adapter_error(adapter, event, e)
15
20
  end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rbconfig"
4
+ require "socket"
5
+
6
+ module Lapsoss
7
+ # Boot-time context collection using Data class
8
+ RuntimeContext = Data.define(:os, :runtime, :modules, :server_name, :release) do
9
+ def self.current
10
+ @current ||= new(
11
+ os: collect_os_context,
12
+ runtime: collect_runtime_context,
13
+ modules: collect_modules,
14
+ server_name: collect_server_name,
15
+ release: collect_release
16
+ )
17
+ end
18
+
19
+ def self.collect_os_context
20
+ {
21
+ name: RbConfig::CONFIG["host_os"],
22
+ version: `uname -r 2>/dev/null`.strip.presence,
23
+ build: `uname -v 2>/dev/null`.strip.presence,
24
+ kernel_version: `uname -a 2>/dev/null`.strip.presence,
25
+ machine: RbConfig::CONFIG["host_cpu"]
26
+ }.compact
27
+ rescue
28
+ { name: RbConfig::CONFIG["host_os"] }
29
+ end
30
+
31
+ def self.collect_runtime_context
32
+ {
33
+ name: "ruby",
34
+ version: RUBY_DESCRIPTION
35
+ }
36
+ end
37
+
38
+ def self.collect_modules
39
+ return {} unless defined?(Bundler)
40
+
41
+ Bundler.load.specs.each_with_object({}) do |spec, h|
42
+ h[spec.name] = spec.version.to_s
43
+ end
44
+ rescue
45
+ {}
46
+ end
47
+
48
+ def self.collect_server_name
49
+ Socket.gethostname
50
+ rescue
51
+ "unknown"
52
+ end
53
+
54
+ def self.collect_release
55
+ # Try to get from git if available
56
+ if File.exist?(".git")
57
+ `git rev-parse HEAD 2>/dev/null`.strip.presence
58
+ end
59
+ rescue
60
+ nil
61
+ end
62
+
63
+ def to_contexts
64
+ {
65
+ os: os,
66
+ runtime: runtime
67
+ }
68
+ end
69
+ end
70
+ end
data/lib/lapsoss/scope.rb CHANGED
@@ -3,12 +3,15 @@
3
3
  module Lapsoss
4
4
  class Scope
5
5
  attr_reader :breadcrumbs, :tags, :user, :extra
6
+ attr_accessor :transaction_name, :transaction_source
6
7
 
7
8
  def initialize
8
9
  @breadcrumbs = []
9
10
  @tags = {}
10
11
  @user = {}
11
12
  @extra = {}
13
+ @transaction_name = nil
14
+ @transaction_source = nil
12
15
  end
13
16
 
14
17
  def add_breadcrumb(message, type: :default, **metadata)
@@ -36,6 +39,8 @@ module Lapsoss
36
39
  @tags.clear
37
40
  @user.clear
38
41
  @extra.clear
42
+ @transaction_name = nil
43
+ @transaction_source = nil
39
44
  end
40
45
 
41
46
  def set_context(key, value)
@@ -61,5 +66,10 @@ module Lapsoss
61
66
  def set_extras(extras)
62
67
  @extra.merge!(extras)
63
68
  end
69
+
70
+ def set_transaction_name(name, source: nil)
71
+ @transaction_name = name
72
+ @transaction_source = source if source
73
+ end
64
74
  end
65
75
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Lapsoss
4
- VERSION = "0.4.5"
4
+ VERSION = "0.4.9"
5
5
  end
data/lib/lapsoss.rb CHANGED
@@ -37,6 +37,8 @@ module Lapsoss
37
37
  end
38
38
 
39
39
  def capture_exception(exception, **context)
40
+ configuration.logger.debug "[LAPSOSS] capture_exception called for #{exception.class}"
41
+ return unless client
40
42
  client.capture_exception(exception, **context)
41
43
  end
42
44
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lapsoss
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.5
4
+ version: 0.4.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -249,12 +249,14 @@ files:
249
249
  - lib/lapsoss/middleware/release_tracker.rb
250
250
  - lib/lapsoss/pipeline.rb
251
251
  - lib/lapsoss/pipeline_builder.rb
252
+ - lib/lapsoss/rails_controller_context.rb
253
+ - lib/lapsoss/rails_controller_transaction.rb
252
254
  - lib/lapsoss/rails_error_subscriber.rb
253
- - lib/lapsoss/rails_middleware.rb
254
255
  - lib/lapsoss/railtie.rb
255
256
  - lib/lapsoss/registry.rb
256
257
  - lib/lapsoss/release_tracker.rb
257
258
  - lib/lapsoss/router.rb
259
+ - lib/lapsoss/runtime_context.rb
258
260
  - lib/lapsoss/sampling/base.rb
259
261
  - lib/lapsoss/sampling/rate_limiter.rb
260
262
  - lib/lapsoss/sampling/uniform_sampler.rb
@@ -1,78 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Lapsoss
4
- class RailsMiddleware
5
- def initialize(app)
6
- @app = app
7
- end
8
-
9
- def call(env)
10
- Lapsoss::Current.with_clean_scope do
11
- # Add request context to current scope
12
- if Lapsoss.configuration.capture_request_context
13
- Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Adding request context" } if Rails.env.test?
14
- add_request_context(env)
15
- end
16
-
17
- begin
18
- @app.call(env)
19
- rescue Exception => e
20
- Rails.logger.tagged("Lapsoss") { Rails.logger.debug "Capturing exception: #{e.class} - #{e.message}" } if Rails.env.test?
21
- # Capture the exception
22
- Lapsoss.capture_exception(e)
23
- # Re-raise the exception to maintain Rails error handling
24
- raise
25
- end
26
- end
27
- end
28
-
29
- private
30
-
31
- def add_request_context(env)
32
- request = Rack::Request.new(env)
33
-
34
- return unless Lapsoss::Current.scope
35
-
36
- Lapsoss::Current.scope.set_context("request", {
37
- method: request.request_method,
38
- url: request.url,
39
- path: request.path,
40
- query_string: request.query_string,
41
- headers: extract_headers(env),
42
- ip: request.ip,
43
- user_agent: request.user_agent,
44
- referer: request.referer,
45
- request_id: env["action_dispatch.request_id"] || env["HTTP_X_REQUEST_ID"]
46
- })
47
-
48
- # Add user context if available
49
- return unless env["warden"]&.user
50
-
51
- user = env["warden"].user
52
- Lapsoss::Current.scope.set_user(
53
- id: user.id,
54
- email: user.respond_to?(:email) ? user.email : nil
55
- )
56
- end
57
-
58
- def extract_headers(env)
59
- headers = {}
60
-
61
- env.each do |key, value|
62
- if key.start_with?("HTTP_") && FILTERED_HEADERS.exclude?(key)
63
- header_name = key.sub(/^HTTP_/, "").split("_").map(&:capitalize).join("-")
64
- headers[header_name] = value
65
- end
66
- end
67
-
68
- headers
69
- end
70
-
71
- FILTERED_HEADERS = %w[
72
- HTTP_AUTHORIZATION
73
- HTTP_COOKIE
74
- HTTP_X_API_KEY
75
- HTTP_X_AUTH_TOKEN
76
- ].freeze
77
- end
78
- end