railswatch_gem 0.1.1 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: cb0eec72b0b4b1904079261950d3780b713f068f25dc2347c1d606369b86b083
4
- data.tar.gz: f390ff3575ed2c6b6df73fc0dbb2ee64f176169d51a7cdb733e4d7cc0f591036
3
+ metadata.gz: c3c2f8707dde0136f3ec790ae46558c384646a71d656b3659f46473732a77b5c
4
+ data.tar.gz: edec55e909ad43675ba436fac4337ee23a228988b8348cba5a8b9e99322c2f20
5
5
  SHA512:
6
- metadata.gz: 549bd28842b55b51cb78112d5c03e971abce96cb5d15983defa211f7814f984ef869b1575dc022b93c9c554ceec4e02907671b48de001bf691bdaa38dd4f3401
7
- data.tar.gz: 3b7d77f1529eb2a8b35f745426867ea2c8e202e75f5044bf2ea126690be17963862709099665beafc3b0ce546cffb3ae8ce2e7e098320406e87f90f8b64e5e92
6
+ metadata.gz: 189aad8fdc06a51766bb19e4b4ded13f2b037839ae14dff7e2db632b8f1ae7a20b52d46dc81e60c95acf7603db804c46eae5240e31da6081b0d49e8786f77846
7
+ data.tar.gz: 2d7c7ebeaf988bdffd24265bc5eb1cafe659f35b91e5bb5537725279e99de42db9ec646bd92f25b0623b32ca3924e8aa0d062d07d307a8fd0dab5c431ef99be3
@@ -25,16 +25,21 @@ module RailswatchGem
25
25
  name = event.name # e.g., "cache_read.active_support"
26
26
 
27
27
  # Extract action from name: cache_read.active_support -> read
28
- # Common actions: read, write, delete, fetch_hit, generate
29
28
  action = name.split(".").first.sub("cache_", "")
30
29
 
30
+ # Capture source location (Where did we call Rails.cache.read?)
31
+ # We use the cleaner to ignore framework lines and find user code.
32
+ app_trace = ::Rails.backtrace_cleaner.clean(caller)
33
+
31
34
  data = {
32
35
  event_type: "cache",
33
- # FIX: Handle Float timestamps safely
34
- timestamp: Time.at(event.end).utc.iso8601,
36
+ timestamp: Time.at(event.end).utc.iso8601(6),
35
37
  action: action,
36
38
  key: payload[:key],
37
39
 
40
+ # Add Trace for debugging
41
+ backtrace: app_trace,
42
+
38
43
  # Performance
39
44
  duration_ms: event.duration.round(2)
40
45
  }
@@ -11,7 +11,7 @@ module RailswatchGem
11
11
  end
12
12
 
13
13
  def start
14
- # Hook into Thor to capture CLI commands (e.g., rails runner, rails generate, custom CLI tools)
14
+ # Hook into Thor to capture CLI commands (e.g., rails runner, rails generate)
15
15
  if defined?(::Thor::Command)
16
16
  patch_thor_commands
17
17
 
@@ -37,15 +37,14 @@ module RailswatchGem
37
37
 
38
38
  data = {
39
39
  event_type: "command",
40
- # FIX: Handle Float timestamps safely using Time.at()
41
- timestamp: Time.at(event.end).utc.iso8601,
40
+ timestamp: Time.at(event.end).utc.iso8601(6),
42
41
 
43
42
  # Command Identity
44
43
  name: command_name,
45
44
  args: payload[:args],
46
45
  options: payload[:options],
47
46
 
48
- # Context (Optional: class name of the tool running, e.g., 'Rails::Generators::ModelGenerator')
47
+ # Context
49
48
  tool: payload[:tool_class],
50
49
 
51
50
  # Performance
@@ -71,9 +70,6 @@ module RailswatchGem
71
70
  # ----------------------------------------------------------------
72
71
  module ThorCommandPatch
73
72
  def run(instance, args = [])
74
- # Thor::Command#run(instance, args)
75
- # 'name' is available on the command object
76
-
77
73
  # Capture options if available on the instance (parsed flags)
78
74
  opts = instance.respond_to?(:options) ? instance.options : nil
79
75
  tool_class = instance.class.name
@@ -9,9 +9,6 @@ module RailswatchGem
9
9
  end
10
10
 
11
11
  def start
12
- # This instrumenter relies on the Rails 7.0+ Error Reporter interface.
13
- # It allows capturing both unhandled exceptions and manually reported errors
14
- # (e.g., via Rails.error.handle { ... }).
15
12
  if defined?(::Rails) && ::Rails.respond_to?(:error)
16
13
  ::Rails.error.subscribe(ErrorSubscriber.new(@client))
17
14
  end
@@ -22,42 +19,35 @@ module RailswatchGem
22
19
  @client = client
23
20
  end
24
21
 
25
- # The signature required by Rails.error.subscribe
26
22
  def report(error, handled:, severity:, context:, source: nil)
27
- # 1. Infinite Loop Protection:
28
- # Don't report errors originating from our own gem to prevent recursion.
23
+ # 1. Infinite Loop Protection
29
24
  return if error.class.name.start_with?("RailswatchGem::")
30
25
 
31
- # 2. Context Correlation:
32
- # Try to link this error to the current HTTP request if possible.
33
- # We check the passed context first, then fall back to our thread-local storage.
26
+ # 2. Context Correlation
34
27
  request_id = context[:request_id] || Thread.current[:railswatch_request_id]
35
28
 
36
29
  data = {
37
30
  event_type: "error",
38
- timestamp: Time.now.utc.iso8601,
31
+ timestamp: Time.now.utc.iso8601(6),
39
32
 
40
33
  # Exception Details
41
34
  class: error.class.name,
42
35
  message: error.message,
43
- # Limit backtrace to save bandwidth; the dashboard usually only needs the top frames.
36
+ # Limit backtrace to save bandwidth
44
37
  backtrace: (error.backtrace || []).first(25),
45
38
 
46
39
  # Rails Error Reporter Context
47
40
  handled: handled,
48
41
  severity: severity,
49
- source: source, # e.g., "application.job"
42
+ source: source,
50
43
 
51
44
  # Correlation
52
45
  request_id: request_id,
53
-
54
- # Extra context passed to Rails.error.report(e, context: { user_id: 1 })
55
46
  context: context
56
47
  }
57
48
 
58
49
  @client.record(data)
59
50
  rescue => e
60
- # Absolute safety net: If reporting the error fails, print to stderr and move on.
61
51
  warn "RailswatchGem: Failed to report error event: #{e.message}"
62
52
  end
63
53
  end
@@ -12,7 +12,6 @@ module RailswatchGem
12
12
 
13
13
  def start
14
14
  # Subscribe to the execution event.
15
- # "enqueue.active_job" is also available if you want to track queue depth/latency separately.
16
15
  ActiveSupport::Notifications.subscribe("perform.active_job") do |*args|
17
16
  event = ActiveSupport::Notifications::Event.new(*args)
18
17
  process_event(event)
@@ -25,13 +24,21 @@ module RailswatchGem
25
24
  payload = event.payload
26
25
  job = payload[:job]
27
26
 
28
- # Guard against malformed payloads (though rare in ActiveJob)
29
27
  return unless job
30
28
 
29
+ # Safely serialize arguments (GlobalID or primitives)
30
+ # We use standard ActiveJob serialization to get readable values
31
+ serialized_args = begin
32
+ job.arguments.map do |arg|
33
+ arg.respond_to?(:to_global_id) ? arg.to_global_id.to_s : arg.inspect
34
+ end
35
+ rescue
36
+ ["(unserializable args)"]
37
+ end
38
+
31
39
  data = {
32
40
  event_type: "job",
33
- # FIX: Handle Float timestamps safely
34
- timestamp: Time.at(event.end).utc.iso8601,
41
+ timestamp: Time.at(event.end).utc.iso8601(6),
35
42
 
36
43
  # Job Identity
37
44
  job_class: job.class.name,
@@ -39,6 +46,9 @@ module RailswatchGem
39
46
  queue_name: job.queue_name,
40
47
  priority: job.priority,
41
48
 
49
+ # Arguments allow debugging "Which user failed?"
50
+ arguments: serialized_args,
51
+
42
52
  # Execution Context
43
53
  adapter: payload[:adapter]&.class&.name,
44
54
  executions: job.executions,
@@ -47,12 +57,11 @@ module RailswatchGem
47
57
  duration_ms: event.duration.round(2)
48
58
  }
49
59
 
50
- # Calculate Queue Latency: Time spent waiting in the queue
60
+ # Calculate Queue Latency
51
61
  if job.enqueued_at
52
62
  enqueued_time = Time.iso8601(job.enqueued_at) rescue nil
53
63
  if enqueued_time
54
- # Start of processing - Time enqueued
55
- # FIX: Convert event.time (Float) to Time before subtraction
64
+ # Fix: Ensure event.time is converted to Time object
56
65
  latency = (Time.at(event.time) - enqueued_time) * 1000
57
66
  data[:queue_latency_ms] = latency.round(2)
58
67
  end
@@ -60,7 +69,6 @@ module RailswatchGem
60
69
 
61
70
  # Error handling
62
71
  if payload[:exception]
63
- # payload[:exception] is [ClassName, Message]
64
72
  data[:error_class] = payload[:exception].first.to_s
65
73
  data[:error_message] = payload[:exception].last.to_s
66
74
  end
@@ -11,20 +11,12 @@ module RailswatchGem
11
11
  end
12
12
 
13
13
  def start
14
- # Only attach if Rails logger is available
15
14
  return unless defined?(::Rails) && ::Rails.logger
16
15
 
17
16
  target_logger = ::Rails.logger
18
-
19
- # Avoid double patching if called multiple times
20
17
  return if target_logger.respond_to?(:_railswatch_instrumented?)
21
18
 
22
- # Create the interception module bound to our client
23
19
  interceptor = create_interceptor(@client)
24
-
25
- # Extend the specific logger instance.
26
- # This inserts the module into the object's singleton class inheritance chain,
27
- # effectively wrapping the 'add' method.
28
20
  target_logger.extend(interceptor)
29
21
  end
30
22
 
@@ -32,21 +24,15 @@ module RailswatchGem
32
24
 
33
25
  def create_interceptor(client)
34
26
  Module.new do
35
- # FIX: We use define_method here to create a closure over the 'client' variable.
36
- # Standard 'def' methods create a new scope and cannot access outer local variables.
37
- # We expose client as a private method so 'add' can access it.
38
27
  define_method(:_railswatch_client) { client }
39
28
  private :_railswatch_client
40
29
 
41
- # The primary entry point for Ruby Logger
42
30
  def add(severity, message = nil, progname = nil, &block)
43
- # PERFORMANCE: Check level first.
44
31
  if respond_to?(:level) && severity < level
45
32
  return super
46
33
  end
47
34
 
48
35
  begin
49
- # Resolve the message content.
50
36
  log_message = message
51
37
  if log_message.nil?
52
38
  if block_given?
@@ -58,13 +44,11 @@ module RailswatchGem
58
44
 
59
45
  # INFINITE LOOP PROTECTION
60
46
  unless log_message.to_s.include?("[Railswatch]")
61
-
62
47
  request_id = Thread.current[:railswatch_request_id]
63
48
 
64
- # FIX: Access client via the closure-captured helper method
65
49
  _railswatch_client.record({
66
50
  event_type: "log",
67
- timestamp: Time.now.utc.iso8601,
51
+ timestamp: Time.now.utc.iso8601(6),
68
52
  severity: format_severity_level(severity),
69
53
  message: log_message.to_s,
70
54
  progname: progname,
@@ -74,13 +58,12 @@ module RailswatchGem
74
58
 
75
59
  super(severity, log_message, nil)
76
60
  rescue => e
77
- # Fallback: If our instrumentation fails, ensure the app's logging continues.
61
+ # Fail safe: don't crash the app if logging fails
78
62
  $stderr.puts "RailswatchGem: LogsInstrumenter error: #{e.message}"
79
63
  super
80
64
  end
81
65
  end
82
66
 
83
- # Marker method to prevent double-patching
84
67
  def _railswatch_instrumented?
85
68
  true
86
69
  end
@@ -11,8 +11,6 @@ module RailswatchGem
11
11
  end
12
12
 
13
13
  def start
14
- # Subscribe to the delivery event.
15
- # "process.action_mailer" is also available if you want to track template rendering time separately.
16
14
  ActiveSupport::Notifications.subscribe("deliver.action_mailer") do |*args|
17
15
  event = ActiveSupport::Notifications::Event.new(*args)
18
16
  process_event(event)
@@ -24,35 +22,35 @@ module RailswatchGem
24
22
  def process_event(event)
25
23
  payload = event.payload
26
24
 
27
- # Payload keys depend on Rails version, but generally include:
28
- # :mailer, :message_id, :subject, :to, :from, :bcc, :cc, :date
29
-
30
- # Calculate recipient count for metrics
31
25
  to_count = Array(payload[:to]).size
32
26
  cc_count = Array(payload[:cc]).size
33
27
  bcc_count = Array(payload[:bcc]).size
34
28
  total_recipients = to_count + cc_count + bcc_count
35
29
 
30
+ # Attempt to correlate with parent request (e.g. "Reset Password" controller action)
31
+ request_id = Thread.current[:railswatch_request_id]
32
+
36
33
  data = {
37
34
  event_type: "mail",
38
- # FIX: Handle Float timestamps safely
39
- timestamp: Time.at(event.end).utc.iso8601,
35
+ timestamp: Time.at(event.end).utc.iso8601(6),
40
36
 
41
37
  # Identity
42
- mailer: payload[:mailer], # The class name of the mailer
38
+ mailer: payload[:mailer],
43
39
  message_id: payload[:message_id],
44
40
 
45
- # Content (Be careful with PII here)
41
+ # Content
46
42
  subject: payload[:subject],
47
43
  from: Array(payload[:from]).join(", "),
48
44
  to: Array(payload[:to]).join(", "),
49
45
 
50
46
  # Metrics
51
47
  recipient_count: total_recipients,
52
- duration_ms: event.duration.round(2)
48
+ duration_ms: event.duration.round(2),
49
+
50
+ # Correlation
51
+ request_id: request_id
53
52
  }
54
53
 
55
- # Error handling
56
54
  if payload[:exception]
57
55
  data[:error_class] = payload[:exception].first.to_s
58
56
  data[:error_message] = payload[:exception].last.to_s
@@ -10,8 +10,6 @@ module RailswatchGem
10
10
 
11
11
  def start
12
12
  return unless defined?(::ActiveRecord::Base)
13
-
14
- # Inject our tracking module into ActiveRecord
15
13
  ::ActiveRecord::Base.include(Tracker)
16
14
  end
17
15
 
@@ -19,8 +17,6 @@ module RailswatchGem
19
17
  extend ActiveSupport::Concern
20
18
 
21
19
  included do
22
- # We use after_commit to ensure we only report data that actually persisted.
23
- # We pass 'self' to the callbacks.
24
20
  after_commit :_railswatch_handle_create, on: :create
25
21
  after_commit :_railswatch_handle_update, on: :update
26
22
  after_commit :_railswatch_handle_destroy, on: :destroy
@@ -41,29 +37,25 @@ module RailswatchGem
41
37
  private
42
38
 
43
39
  def _railswatch_record_event(type)
44
- # Guard: Avoid tracking internal Rails models or strict excludes
45
- return if self.class.name.start_with?("ActiveRecord::", "RailswatchGem::")
40
+ # Guard: Avoid tracking internal Rails models, gem models, or schema migrations
41
+ return if self.class.name.start_with?("ActiveRecord::", "RailswatchGem::", "Ahoy::", "PaperTrail::")
46
42
 
47
- # Retrieve Request ID if available for correlation
48
43
  request_id = Thread.current[:railswatch_request_id]
49
-
50
- # Calculate changes.
51
- # Note: In after_commit, previous_changes is needed because changes is empty.
52
44
  changes_hash = previous_changes.dup
53
45
 
54
- # Filter sensitive attributes (naive approach, can be improved with Rails.application.config.filter_parameters)
46
+ # Use Rails standard parameter filtering for maximum safety
55
47
  _railswatch_filter_attributes!(changes_hash)
56
48
 
57
49
  data = {
58
50
  event_type: "model",
59
- action: type, # model_create, etc.
60
- timestamp: Time.now.utc.iso8601,
51
+ action: type,
52
+ timestamp: Time.now.utc.iso8601(6),
61
53
 
62
54
  # Model Identity
63
55
  model: self.class.name,
64
56
  key: self.id,
65
57
 
66
- # The actual data changed
58
+ # Data
67
59
  changes: changes_hash,
68
60
 
69
61
  # Correlation
@@ -72,17 +64,24 @@ module RailswatchGem
72
64
 
73
65
  RailswatchGem.record(data)
74
66
  rescue => e
75
- # Swallow errors to prevent model callbacks from aborting transactions
76
67
  warn "RailswatchGem: Failed to record model event: #{e.message}"
77
68
  end
78
69
 
79
70
  def _railswatch_filter_attributes!(hash)
80
- # Basic filtration of common sensitive fields
81
- sensitive = %w[password password_digest token secret credit_card cc_number]
82
- hash.each_key do |key|
83
- if sensitive.any? { |s| key.to_s.include?(s) }
84
- hash[key] = ["[FILTERED]", "[FILTERED]"]
85
- end
71
+ if defined?(::Rails.application) && ::Rails.application
72
+ # Use the app's real configuration
73
+ filter = ActiveSupport::ParameterFilter.new(::Rails.application.config.filter_parameters)
74
+ # Filter keys (parameter filter expects a hash, usually params, but works on attributes too)
75
+ filtered = filter.filter(hash)
76
+ hash.replace(filtered)
77
+ else
78
+ # Fallback
79
+ sensitive = %w[password password_digest token secret credit_card cc_number]
80
+ hash.each_key do |key|
81
+ if sensitive.any? { |s| key.to_s.include?(s) }
82
+ hash[key] = ["[FILTERED]", "[FILTERED]"]
83
+ end
84
+ end
86
85
  end
87
86
  end
88
87
  end
@@ -11,20 +11,12 @@ module RailswatchGem
11
11
  end
12
12
 
13
13
  def start
14
- # This instrumenter allows users to track arbitrary ActiveSupport::Notifications.
15
- # It relies on the user defining 'tracked_notifications' in the configuration.
16
- #
17
- # Example Config usage:
18
- # config.tracked_notifications = ["billing.checkout", /users\..*/]
19
-
20
14
  patterns = if @config.respond_to?(:tracked_notifications)
21
15
  @config.tracked_notifications
22
16
  else
23
17
  []
24
18
  end
25
19
 
26
- # FIX: Use .map instead of .each to return the array of subscribers.
27
- # This allows tests (and apps) to keep track of subscriptions and unsubscribe if needed.
28
20
  Array(patterns).map do |pattern|
29
21
  ActiveSupport::Notifications.subscribe(pattern) do |*args|
30
22
  event = ActiveSupport::Notifications::Event.new(*args)
@@ -36,27 +28,27 @@ module RailswatchGem
36
28
  private
37
29
 
38
30
  def process_event(event)
39
- # Prevent infinite loops if the user accidentally subscribes to our own internal events
31
+ # Prevent infinite loops
40
32
  return if event.name.start_with?("railswatch_gem")
41
33
 
42
34
  payload = event.payload
35
+ request_id = Thread.current[:railswatch_request_id]
43
36
 
44
37
  data = {
45
38
  event_type: "notification",
46
39
  name: event.name,
47
- # FIX: Handle Float timestamps safely
48
- timestamp: Time.at(event.end).utc.iso8601,
49
- duration_ms: event.duration.round(2)
40
+ timestamp: Time.at(event.end).utc.iso8601(6),
41
+ duration_ms: event.duration.round(2),
42
+ request_id: request_id
50
43
  }
51
44
 
52
- # Safely merge primitive payload values to avoid serialization issues with complex objects
45
+ # Safe Payload Merge
53
46
  safe_payload = payload.select do |_, v|
54
47
  v.is_a?(String) || v.is_a?(Numeric) || v.is_a?(TrueClass) || v.is_a?(FalseClass) || v.nil?
55
48
  end
56
49
 
57
50
  data[:payload] = safe_payload
58
51
 
59
- # Error handling
60
52
  if payload[:exception]
61
53
  data[:error_class] = payload[:exception].first.to_s
62
54
  data[:error_message] = payload[:exception].last.to_s
@@ -10,13 +10,10 @@ module RailswatchGem
10
10
  def initialize(client, config)
11
11
  @client = client
12
12
  @config = config
13
- # Cache ingestion URI components for fast comparison
14
13
  @ingest_uri = URI(@config.ingest_url) rescue nil
15
14
  end
16
15
 
17
16
  def start
18
- # Monkey-patch Net::HTTP if we haven't already.
19
- # This wraps the low-level request method to emit an ActiveSupport notification.
20
17
  unless Net::HTTP.include?(NetHttpPatch)
21
18
  Net::HTTP.prepend(NetHttpPatch)
22
19
  end
@@ -33,17 +30,17 @@ module RailswatchGem
33
30
  payload = event.payload
34
31
 
35
32
  # CRITICAL: Ignore requests to the ingestion endpoint.
36
- # This prevents an infinite loop where reporting metrics generates more metrics.
37
33
  if @ingest_uri &&
38
34
  payload[:host] == @ingest_uri.host &&
39
35
  payload[:port] == @ingest_uri.port
40
36
  return
41
37
  end
42
38
 
39
+ request_id = Thread.current[:railswatch_request_id]
40
+
43
41
  data = {
44
42
  event_type: "outgoing_request",
45
- # FIX: Handle Float timestamps safely
46
- timestamp: Time.at(event.end).utc.iso8601,
43
+ timestamp: Time.at(event.end).utc.iso8601(6),
47
44
 
48
45
  # Request Details
49
46
  method: payload[:method],
@@ -53,11 +50,13 @@ module RailswatchGem
53
50
  scheme: payload[:scheme],
54
51
  path: payload[:path],
55
52
 
53
+ # Correlation
54
+ request_id: request_id,
55
+
56
56
  # Performance
57
57
  duration_ms: event.duration.round(2)
58
58
  }
59
59
 
60
- # Error handling
61
60
  if payload[:exception]
62
61
  data[:error_class] = payload[:exception].first.to_s
63
62
  data[:error_message] = payload[:exception].last.to_s
@@ -68,15 +67,9 @@ module RailswatchGem
68
67
  warn "RailswatchGem: Failed to process outgoing request event: #{e.message}"
69
68
  end
70
69
 
71
- # ----------------------------------------------------------------
72
- # Internal Patch for Net::HTTP
73
- # ----------------------------------------------------------------
74
70
  module NetHttpPatch
75
71
  def request(req, body = nil, &block)
76
72
  scheme = use_ssl? ? "https" : "http"
77
-
78
- # Reconstruct basic URI for context
79
- # Note: address and port are methods on the Net::HTTP instance
80
73
  uri = URI("#{scheme}://#{address}:#{port}#{req.path}")
81
74
 
82
75
  ActiveSupport::Notifications.instrument("request.net_http", {
@@ -28,20 +28,26 @@ module RailswatchGem
28
28
  name = payload[:name]
29
29
  return if name && IGNORED_NAMES.include?(name)
30
30
 
31
- # Basic PII safety: You might want to sanitize SQL here depending on your needs.
32
- # payload[:sql] usually contains the raw SQL with bound parameters injected
33
- # if config.filter_parameters is not applied manually.
31
+ # 1. Capture the Application Trace
32
+ # Rails.backtrace_cleaner removes framework noise (gems, rails internals)
33
+ # leaving only the lines relevant to the user's application code.
34
+ app_trace = ::Rails.backtrace_cleaner.clean(caller)
34
35
 
36
+ # Basic PII safety: You might want to sanitize SQL here depending on your needs.
35
37
  data = {
36
38
  event_type: "query",
37
- # FIX: Handle Float timestamps safely
38
- timestamp: Time.at(event.end).utc.iso8601,
39
+
40
+ # Without this, the query logs might look 0ms off from the request logs.
41
+ timestamp: Time.at(event.end).utc.iso8601(6),
39
42
 
40
43
  # Query Details
41
44
  name: name || "SQL",
42
45
  sql: payload[:sql],
43
46
  cached: payload[:cached] || false,
44
47
 
48
+ # The New Field
49
+ backtrace: app_trace,
50
+
45
51
  # Performance
46
52
  duration_ms: event.duration.round(2)
47
53
  }
@@ -49,6 +55,11 @@ module RailswatchGem
49
55
  # Add connection_id if useful for debugging connection pool issues
50
56
  data[:connection_id] = payload[:connection_id] if payload[:connection_id]
51
57
 
58
+ # Link to the parent request if we are inside one
59
+ if (req_id = Thread.current[:railswatch_request_id])
60
+ data[:request_id] = req_id
61
+ end
62
+
52
63
  @client.record(data)
53
64
  rescue => e
54
65
  warn "RailswatchGem: Failed to process query event: #{e.message}"
@@ -12,7 +12,6 @@ module RailswatchGem
12
12
 
13
13
  def start
14
14
  # Subscribe to the standard Rails controller event.
15
- # This event includes details like status, path, duration, and DB runtime.
16
15
  ActiveSupport::Notifications.subscribe("process_action.action_controller") do |*args|
17
16
  event = ActiveSupport::Notifications::Event.new(*args)
18
17
  process_event(event)
@@ -40,8 +39,7 @@ module RailswatchGem
40
39
 
41
40
  data = {
42
41
  event_type: "request",
43
- # Fix: Ensure timestamp is a Time object before calling utc
44
- timestamp: Time.at(event.end).utc.iso8601,
42
+ timestamp: Time.at(event.end).utc.iso8601(6),
45
43
  request_id: request_id,
46
44
 
47
45
  # HTTP Context
@@ -109,17 +107,33 @@ module RailswatchGem
109
107
  def extract_user(headers)
110
108
  return nil unless headers.respond_to?(:env)
111
109
 
112
- warden = headers.env["warden"]
113
- if warden && warden.user
114
- user = warden.user
115
- {
110
+ env = headers.env
111
+
112
+ # 1. Try Warden (Devise)
113
+ warden = env["warden"]
114
+ if warden
115
+ # Warden proxy might not have loaded the user yet
116
+ user = warden.user(run_callbacks: false) rescue nil
117
+ if user
118
+ return {
119
+ id: user.respond_to?(:id) ? user.id : nil,
120
+ email: user.respond_to?(:email) ? user.email : nil,
121
+ class: user.class.name
122
+ }
123
+ end
124
+ end
125
+
126
+ # 2. Try CurrentAttributes (Modern Rails pattern)
127
+ if defined?(::Current) && ::Current.respond_to?(:user) && ::Current.user
128
+ user = ::Current.user
129
+ return {
116
130
  id: user.respond_to?(:id) ? user.id : nil,
117
131
  email: user.respond_to?(:email) ? user.email : nil,
118
132
  class: user.class.name
119
133
  }
120
- else
121
- nil
122
134
  end
135
+
136
+ nil
123
137
  rescue
124
138
  nil
125
139
  end
@@ -12,7 +12,6 @@ module RailswatchGem
12
12
 
13
13
  def start
14
14
  # Only instrument Rake if it is loaded.
15
- # This prevents errors if the gem is used in a context where Rake isn't present.
16
15
  if defined?(::Rake::Task)
17
16
  patch_rake_tasks
18
17
 
@@ -36,20 +35,25 @@ module RailswatchGem
36
35
  payload = event.payload
37
36
  task_name = payload[:name]
38
37
 
39
- # Optional: Ignore internal Railswatch tasks to prevent noise
38
+ # Ignore internal Railswatch tasks to prevent noise/loops
40
39
  return if task_name.to_s.start_with?("railswatch:")
41
40
 
41
+ # Check for correlation (e.g. if task was triggered from a controller)
42
+ request_id = Thread.current[:railswatch_request_id]
43
+
42
44
  data = {
43
45
  event_type: "scheduled_task",
44
- # FIX: Handle Float timestamps safely
45
- timestamp: Time.at(event.end).utc.iso8601,
46
+ timestamp: Time.at(event.end).utc.iso8601(6),
46
47
 
47
48
  # Task Identity
48
49
  name: task_name,
49
- args: payload[:args].to_a, # Rake args come as a wrapper, convert to array
50
+ args: payload[:args].to_a,
50
51
 
51
52
  # Performance
52
- duration_ms: event.duration.round(2)
53
+ duration_ms: event.duration.round(2),
54
+
55
+ # Correlation
56
+ request_id: request_id
53
57
  }
54
58
 
55
59
  # Error handling
@@ -71,7 +75,6 @@ module RailswatchGem
71
75
  # ----------------------------------------------------------------
72
76
  module RakeTaskPatch
73
77
  def execute(args = nil)
74
- # We explicitly capture 'name' here because it's available on the task instance
75
78
  ActiveSupport::Notifications.instrument("task.rake", { name: name, args: args }) do
76
79
  super
77
80
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailswatchGem
4
- VERSION = "0.1.1"
4
+ VERSION = "0.1.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: railswatch_gem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tyler Hammett