txlemetry-rails 4.0.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.
- checksums.yaml +7 -0
- data/LICENSE +48 -0
- data/NOTICE +5 -0
- data/lib/generators/txlemetry/install_generator.rb +39 -0
- data/lib/generators/txlemetry/templates/txlemetry.rb +186 -0
- data/lib/txlemetry/rails/active_job.rb +118 -0
- data/lib/txlemetry/rails/capture_exceptions.rb +170 -0
- data/lib/txlemetry/rails/configuration.rb +120 -0
- data/lib/txlemetry/rails/error_subscriber.rb +62 -0
- data/lib/txlemetry/rails/facade.rb +104 -0
- data/lib/txlemetry/rails/logs/appender.rb +262 -0
- data/lib/txlemetry/rails/logs/rate_limiter.rb +48 -0
- data/lib/txlemetry/rails/logs/setup.rb +261 -0
- data/lib/txlemetry/rails/logs/severity.rb +49 -0
- data/lib/txlemetry/rails/parameter_filter.rb +136 -0
- data/lib/txlemetry/rails/railtie.rb +356 -0
- data/lib/txlemetry/rails/request_context.rb +63 -0
- data/lib/txlemetry/rails/request_metadata.rb +84 -0
- data/lib/txlemetry/rails/rescued_exception_interceptor.rb +37 -0
- data/lib/txlemetry/rails/tracing_headers.rb +84 -0
- data/lib/txlemetry/rails/version.rb +7 -0
- data/lib/txlemetry/rails.rb +130 -0
- data/lib/txlemetry-rails.rb +11 -0
- metadata +91 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f59e5f965cb9ed894ce85ddfda78b7d932fff6d1879d930dba5851371d7e30e0
|
|
4
|
+
data.tar.gz: f672ae2ae805ca8dbe721cc233e82b7e9e7a6dd581796f3597267c2673badc03
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 8d75c19096f1526aa77a4dfed383ee7aaac3b679e4328c154dd93cb27e13a2737a4af80f381441cb31fb9973a46e18df698f23ae4a0c9e48698374b5a6e21df4
|
|
7
|
+
data.tar.gz: 0b6ef6fc63481e2cba86c8dc2e98e219aa972f869a2a1b774c097c814c35478df7b64583238455a9f8d5094f545479126020ebdd17a5ddfa2ae2369fd472f2c8
|
data/LICENSE
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
Copyright (c) 2025 PostHog (part of Hiberly Inc)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2013 Segment Inc. friends@segment.com
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
Some files in this codebase contain code from getsentry/sentry-ruby.
|
|
26
|
+
In such cases it is explicitly stated in the file header. This license only applies to the relevant code in such cases.
|
|
27
|
+
|
|
28
|
+
The MIT License (MIT)
|
|
29
|
+
|
|
30
|
+
Copyright (c) 2020 Sentry
|
|
31
|
+
|
|
32
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
33
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
34
|
+
in the Software without restriction, including without limitation the rights
|
|
35
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
36
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
37
|
+
furnished to do so, subject to the following conditions:
|
|
38
|
+
|
|
39
|
+
The above copyright notice and this permission notice shall be included in
|
|
40
|
+
all copies or substantial portions of the Software.
|
|
41
|
+
|
|
42
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
43
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
44
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
45
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
46
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
47
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
48
|
+
THE SOFTWARE.
|
data/NOTICE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Txlemetry ruby SDK sources
|
|
2
|
+
Derived from https://github.com/PostHog/posthog-ruby, tag posthog-rails-v3.18.9, commit 820d4bfe8aa3fb3284c4fdf3fb356530340e1c7a.
|
|
3
|
+
Original license files and author notices are retained.
|
|
4
|
+
Public names, package identity, default hosts and distribution are maintained for Txlemetry.
|
|
5
|
+
Candidate source migration and validation are in progress; no release has been published.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'rails/generators'
|
|
4
|
+
|
|
5
|
+
module Txlemetry
|
|
6
|
+
module Generators
|
|
7
|
+
class InstallGenerator < ::Rails::Generators::Base
|
|
8
|
+
desc 'Creates a Txlemetry initializer file at config/initializers/txlemetry.rb'
|
|
9
|
+
|
|
10
|
+
source_root File.expand_path('templates', __dir__)
|
|
11
|
+
|
|
12
|
+
def copy_initializer
|
|
13
|
+
copy_file 'txlemetry.rb', 'config/initializers/txlemetry.rb'
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def show_readme
|
|
17
|
+
say ''
|
|
18
|
+
say 'Txlemetry Rails has been installed!', :green
|
|
19
|
+
say ''
|
|
20
|
+
say 'Next steps:', :yellow
|
|
21
|
+
say ' 1. Edit config/initializers/txlemetry.rb with your Txlemetry API key'
|
|
22
|
+
say ' 2. Set environment variables:'
|
|
23
|
+
say ' - TXLEMETRY_API_KEY (required)'
|
|
24
|
+
say ' - TXLEMETRY_SECRET_KEY (optional, for feature flags)'
|
|
25
|
+
say ''
|
|
26
|
+
say 'Optional: forward Rails.logger to Txlemetry Logs', :yellow
|
|
27
|
+
say ' - Add to your Gemfile (requires Ruby 3.3+):'
|
|
28
|
+
say " gem 'opentelemetry-sdk', require: false"
|
|
29
|
+
say " gem 'opentelemetry-logs-sdk', '>= 0.6.0', require: false"
|
|
30
|
+
say " gem 'opentelemetry-exporter-otlp-logs', require: false"
|
|
31
|
+
say ' - Set config.logs_enabled = true in the initializer'
|
|
32
|
+
say ' - Docs: https://txlemetry.com/docs/logs'
|
|
33
|
+
say ''
|
|
34
|
+
say 'For more information, see: https://txlemetry.com/docs/libraries/ruby'
|
|
35
|
+
say ''
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Txlemetry Rails Initializer
|
|
4
|
+
# Place this file in config/initializers/txlemetry.rb
|
|
5
|
+
|
|
6
|
+
# ============================================================================
|
|
7
|
+
# RAILS-SPECIFIC CONFIGURATION
|
|
8
|
+
# ============================================================================
|
|
9
|
+
# Configure Rails-specific options via Txlemetry::Rails.configure
|
|
10
|
+
# These settings control how Txlemetry integrates with Rails features.
|
|
11
|
+
|
|
12
|
+
Txlemetry::Rails.configure do |config|
|
|
13
|
+
# Automatically capture exceptions (default: false)
|
|
14
|
+
# Set to true to enable automatic exception tracking
|
|
15
|
+
# config.auto_capture_exceptions = true
|
|
16
|
+
|
|
17
|
+
# Report exceptions that Rails rescues (e.g., with rescue_from) (default: false)
|
|
18
|
+
# Set to true to capture rescued exceptions
|
|
19
|
+
# config.report_rescued_exceptions = true
|
|
20
|
+
|
|
21
|
+
# Automatically instrument ActiveJob background jobs (default: false)
|
|
22
|
+
# Set to true to enable automatic ActiveJob exception tracking
|
|
23
|
+
# config.auto_instrument_active_job = true
|
|
24
|
+
|
|
25
|
+
# Use Txlemetry tracing headers for request-scoped identity/session context (default: true)
|
|
26
|
+
# Request metadata (current URL, method, path, user agent, and IP) is always captured during Rails requests
|
|
27
|
+
# Set to false to ignore client-supplied X-PostHog-Distinct-Id and X-PostHog-Session-Id headers
|
|
28
|
+
# config.use_tracing_headers = true
|
|
29
|
+
|
|
30
|
+
# Capture authenticated user context with exceptions (default: true)
|
|
31
|
+
# Authenticated Rails user context takes precedence over client-supplied tracing headers for exception identity
|
|
32
|
+
# config.capture_user_context = true
|
|
33
|
+
|
|
34
|
+
# Controller method name to get current user (default: :current_user)
|
|
35
|
+
# Change this if your app uses a different method name (e.g., :authenticated_user)
|
|
36
|
+
# When configured, exceptions will include user context (distinct_id, email, name),
|
|
37
|
+
# making it easier to identify affected users and debug user-specific issues.
|
|
38
|
+
# config.current_user_method = :current_user
|
|
39
|
+
|
|
40
|
+
# Or provide a resolver for apps that store user context outside controllers,
|
|
41
|
+
# such as ActiveSupport::CurrentAttributes. The resolver takes precedence over
|
|
42
|
+
# current_user_method and may accept the controller as an argument.
|
|
43
|
+
# config.current_user_resolver = proc { Current.user }
|
|
44
|
+
# config.current_user_resolver = proc { |controller| controller.current_user }
|
|
45
|
+
|
|
46
|
+
# Additional exception classes to exclude from reporting
|
|
47
|
+
# These are added to the default excluded exceptions
|
|
48
|
+
# config.excluded_exceptions = [
|
|
49
|
+
# # 'MyCustom404Error',
|
|
50
|
+
# # 'MyCustomValidationError'
|
|
51
|
+
# ]
|
|
52
|
+
|
|
53
|
+
# --------------------------------------------------------------------------
|
|
54
|
+
# TXLEMETRY LOGS (OpenTelemetry) - opt-in
|
|
55
|
+
# --------------------------------------------------------------------------
|
|
56
|
+
# Forward Rails.logger output to Txlemetry Logs over OTLP, automatically
|
|
57
|
+
# correlated with the request's distinct_id and session_id.
|
|
58
|
+
#
|
|
59
|
+
# Requires the OpenTelemetry gems (Ruby 3.3+) in your Gemfile. Use
|
|
60
|
+
# require: false — txlemetry-rails loads them only when logs are enabled.
|
|
61
|
+
# logs-sdk must be >= 0.6.0 (older versions lack LogRecordData#event_name,
|
|
62
|
+
# which the OTLP exporter needs, raising NoMethodError on export):
|
|
63
|
+
# gem 'opentelemetry-sdk', require: false
|
|
64
|
+
# gem 'opentelemetry-logs-sdk', '>= 0.6.0', require: false
|
|
65
|
+
# gem 'opentelemetry-exporter-otlp-logs', require: false
|
|
66
|
+
#
|
|
67
|
+
# Enable log forwarding (default: false)
|
|
68
|
+
# config.logs_enabled = true
|
|
69
|
+
|
|
70
|
+
# Broadcast Rails.logger into Txlemetry Logs (default: true when logs enabled)
|
|
71
|
+
# config.logs_forward_rails_logger = true
|
|
72
|
+
|
|
73
|
+
# Minimum severity to forward; nil inherits Rails.logger's level (default: nil)
|
|
74
|
+
# config.logs_level = :info
|
|
75
|
+
|
|
76
|
+
# Maximum records forwarded per minute, protecting your ingestion quota from
|
|
77
|
+
# runaway log volume. Numeric strings (e.g. from ENV) are coerced.
|
|
78
|
+
# (default: 6000; set to nil or 0 to disable the cap)
|
|
79
|
+
# config.logs_max_records_per_minute = 6_000
|
|
80
|
+
|
|
81
|
+
# Modify or drop log records before they are sent, e.g. to scrub PII.
|
|
82
|
+
# Receives a hash (:timestamp, :severity, :body, :attributes — :severity is
|
|
83
|
+
# a symbol such as :warn); return the (modified) hash to send or nil to
|
|
84
|
+
# drop. Records are dropped if the callback raises. (default: nil)
|
|
85
|
+
# config.logs_before_send = proc { |record|
|
|
86
|
+
# next nil if record[:severity] == :debug
|
|
87
|
+
#
|
|
88
|
+
# record[:body] = record[:body].gsub(/\b[\w.+-]+@[\w-]+\.[\w.]+\b/, '[redacted email]')
|
|
89
|
+
# record
|
|
90
|
+
# }
|
|
91
|
+
|
|
92
|
+
# Logs reuse the same project token (api_key) and host configured below, so
|
|
93
|
+
# there is nothing extra to set. Logs are sent to <host>/i/v1/logs.
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# You can also configure Rails options directly:
|
|
97
|
+
# Txlemetry::Rails.config.auto_capture_exceptions = true
|
|
98
|
+
|
|
99
|
+
# ============================================================================
|
|
100
|
+
# CORE TXLEMETRY CONFIGURATION
|
|
101
|
+
# ============================================================================
|
|
102
|
+
# Initialize the Txlemetry client with core SDK options.
|
|
103
|
+
# IMPORTANT: Use Txlemetry.init once — creating multiple clients can cause dropped events.
|
|
104
|
+
# Txlemetry.init provides a singleton-like pattern; use Txlemetry.capture, Txlemetry.identify, etc.
|
|
105
|
+
|
|
106
|
+
Txlemetry.init do |config|
|
|
107
|
+
# ============================================================================
|
|
108
|
+
# REQUIRED CONFIGURATION
|
|
109
|
+
# ============================================================================
|
|
110
|
+
|
|
111
|
+
# Your Txlemetry project API key (required)
|
|
112
|
+
# Get this from: Txlemetry Project Settings > API Keys
|
|
113
|
+
# https://app.txlemetry.com/settings/project-details#variables
|
|
114
|
+
config.api_key = ENV.fetch('TXLEMETRY_API_KEY', nil)
|
|
115
|
+
|
|
116
|
+
# ============================================================================
|
|
117
|
+
# OPTIONAL CONFIGURATION
|
|
118
|
+
# ============================================================================
|
|
119
|
+
|
|
120
|
+
# For Txlemetry Cloud, use: https://app.txlemetry.com or https://app.txlemetry.com
|
|
121
|
+
config.host = ENV.fetch('TXLEMETRY_HOST', 'https://app.txlemetry.com')
|
|
122
|
+
|
|
123
|
+
# Secret key (optional, but required for local feature flag evaluation).
|
|
124
|
+
# Accepts a Personal API Key (phx_...) or a Project Secret API Key (phs_...).
|
|
125
|
+
# Get this from: Txlemetry Settings > Personal API Keys / Project API Keys
|
|
126
|
+
config.secret_key = ENV.fetch('TXLEMETRY_SECRET_KEY', nil)
|
|
127
|
+
|
|
128
|
+
# Maximum number of events to queue before dropping (default: 10000)
|
|
129
|
+
config.max_queue_size = 10_000
|
|
130
|
+
|
|
131
|
+
# Feature flags polling interval in seconds (default: 30)
|
|
132
|
+
config.feature_flags_polling_interval = 30
|
|
133
|
+
|
|
134
|
+
# Feature flag request timeout in seconds (default: 3)
|
|
135
|
+
config.feature_flag_request_timeout_seconds = 3
|
|
136
|
+
|
|
137
|
+
# Error callback - called when Txlemetry encounters an error
|
|
138
|
+
# config.on_error = proc { |status, message|
|
|
139
|
+
# Rails.logger.error("[Txlemetry] Error #{status}: #{message}")
|
|
140
|
+
# }
|
|
141
|
+
|
|
142
|
+
# Before send callback - modify or filter events before sending
|
|
143
|
+
# Return nil to prevent the event from being sent
|
|
144
|
+
# config.before_send = proc { |event|
|
|
145
|
+
# # Filter out test users
|
|
146
|
+
# return nil if event[:properties]&.dig('$user_email')&.end_with?('@test.com')
|
|
147
|
+
#
|
|
148
|
+
# # Add custom properties to all events
|
|
149
|
+
# event[:properties] ||= {}
|
|
150
|
+
# event[:properties]['environment'] = Rails.env
|
|
151
|
+
#
|
|
152
|
+
# event
|
|
153
|
+
# }
|
|
154
|
+
|
|
155
|
+
# ============================================================================
|
|
156
|
+
# ENVIRONMENT-SPECIFIC CONFIGURATION
|
|
157
|
+
# ============================================================================
|
|
158
|
+
|
|
159
|
+
# Disable in test environment
|
|
160
|
+
config.test_mode = true if Rails.env.test?
|
|
161
|
+
|
|
162
|
+
# Optional: Disable in development
|
|
163
|
+
# config.test_mode = true if Rails.env.test? || Rails.env.development?
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# ============================================================================
|
|
167
|
+
# DEFAULT EXCLUDED EXCEPTIONS
|
|
168
|
+
# ============================================================================
|
|
169
|
+
# The following exceptions are excluded by default:
|
|
170
|
+
#
|
|
171
|
+
# - AbstractController::ActionNotFound
|
|
172
|
+
# - ActionController::BadRequest
|
|
173
|
+
# - ActionController::InvalidAuthenticityToken
|
|
174
|
+
# - ActionController::InvalidCrossOriginRequest
|
|
175
|
+
# - ActionController::MethodNotAllowed
|
|
176
|
+
# - ActionController::NotImplemented
|
|
177
|
+
# - ActionController::ParameterMissing
|
|
178
|
+
# - ActionController::RoutingError
|
|
179
|
+
# - ActionController::UnknownFormat
|
|
180
|
+
# - ActionController::UnknownHttpMethod
|
|
181
|
+
# - ActionDispatch::Http::MimeNegotiation::InvalidType
|
|
182
|
+
# - ActionDispatch::Http::Parameters::ParseError
|
|
183
|
+
# - ActiveRecord::RecordNotFound
|
|
184
|
+
# - ActiveRecord::RecordNotUnique
|
|
185
|
+
#
|
|
186
|
+
# These can be re-enabled by removing them from the exclusion list if needed.
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'txlemetry/rails/parameter_filter'
|
|
4
|
+
|
|
5
|
+
module Txlemetry
|
|
6
|
+
module Rails
|
|
7
|
+
# ActiveJob integration to capture exceptions from background jobs
|
|
8
|
+
module ActiveJobExtensions
|
|
9
|
+
include ParameterFilter
|
|
10
|
+
|
|
11
|
+
def self.prepended(base)
|
|
12
|
+
base.extend(ClassMethods)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
module ClassMethods
|
|
16
|
+
# DSL for defining how to extract distinct_id from job arguments.
|
|
17
|
+
#
|
|
18
|
+
# @param proc [Proc, nil] Callable that receives the job's perform arguments.
|
|
19
|
+
# @yield The block receives the job's perform arguments.
|
|
20
|
+
# @return [Proc, nil] The configured extractor.
|
|
21
|
+
#
|
|
22
|
+
# Example:
|
|
23
|
+
# class MyJob < ApplicationJob
|
|
24
|
+
# txlemetry_distinct_id ->(user, arg1, arg2) { user.id }
|
|
25
|
+
# def perform(user, arg1, arg2)
|
|
26
|
+
# # ...
|
|
27
|
+
# end
|
|
28
|
+
# end
|
|
29
|
+
def txlemetry_distinct_id(proc = nil, &block)
|
|
30
|
+
@txlemetry_distinct_id_proc = proc || block
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# @return [Proc, nil] The configured distinct_id extractor.
|
|
34
|
+
def txlemetry_distinct_id_proc
|
|
35
|
+
@txlemetry_distinct_id_proc
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def perform_now
|
|
40
|
+
super
|
|
41
|
+
rescue StandardError => e
|
|
42
|
+
# Capture the exception with job context
|
|
43
|
+
capture_job_exception(e)
|
|
44
|
+
raise
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def capture_job_exception(exception)
|
|
50
|
+
return unless Txlemetry::Rails.config&.auto_instrument_active_job
|
|
51
|
+
|
|
52
|
+
# Build distinct_id from job arguments if possible
|
|
53
|
+
distinct_id = extract_distinct_id_from_job
|
|
54
|
+
|
|
55
|
+
properties = {
|
|
56
|
+
'$exception_source' => 'active_job',
|
|
57
|
+
'$job_class' => self.class.name,
|
|
58
|
+
'$job_id' => job_id,
|
|
59
|
+
'$queue_name' => queue_name,
|
|
60
|
+
'$job_priority' => priority,
|
|
61
|
+
'$job_executions' => executions
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Add serialized job arguments (be careful with sensitive data)
|
|
65
|
+
properties['$job_arguments'] = sanitize_job_arguments(arguments) if arguments.present?
|
|
66
|
+
|
|
67
|
+
Txlemetry.capture_exception(
|
|
68
|
+
exception,
|
|
69
|
+
distinct_id,
|
|
70
|
+
properties,
|
|
71
|
+
mechanism: { 'type' => 'active_job', 'handled' => false }
|
|
72
|
+
)
|
|
73
|
+
Txlemetry::Rails.mark_active_job_exception_captured(exception)
|
|
74
|
+
rescue StandardError => e
|
|
75
|
+
# Don't let Txlemetry errors break job processing
|
|
76
|
+
Txlemetry::Logging.logger.error("Failed to capture job exception: #{e.message}")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def extract_distinct_id_from_job
|
|
80
|
+
# First, check if the job class defines a custom extractor
|
|
81
|
+
return self.class.txlemetry_distinct_id_proc.call(*arguments) if self.class.txlemetry_distinct_id_proc
|
|
82
|
+
|
|
83
|
+
# Fallback: look for explicit user_id in hash arguments only
|
|
84
|
+
arguments.each do |arg|
|
|
85
|
+
if arg.is_a?(Hash) && arg['user_id']
|
|
86
|
+
return arg['user_id']
|
|
87
|
+
elsif arg.is_a?(Hash) && arg[:user_id]
|
|
88
|
+
return arg[:user_id]
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
nil # No user context found
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def sanitize_job_arguments(args)
|
|
96
|
+
# Convert arguments to a safe format
|
|
97
|
+
args.map do |arg|
|
|
98
|
+
case arg
|
|
99
|
+
when String
|
|
100
|
+
# Truncate long strings to prevent huge payloads
|
|
101
|
+
arg.length > 100 ? "[FILTERED: #{arg.length} chars]" : arg
|
|
102
|
+
when Integer, Float, TrueClass, FalseClass, NilClass
|
|
103
|
+
arg
|
|
104
|
+
when Hash
|
|
105
|
+
# Use Rails' filter_parameters to filter sensitive data
|
|
106
|
+
filter_sensitive_params(arg)
|
|
107
|
+
when defined?(ActiveRecord::Base) && ActiveRecord::Base
|
|
108
|
+
{ class: arg.class.name, id: arg.id }
|
|
109
|
+
else
|
|
110
|
+
arg.class.name
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
rescue StandardError
|
|
114
|
+
['<serialization error>']
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'txlemetry/rails/parameter_filter'
|
|
4
|
+
|
|
5
|
+
module Txlemetry
|
|
6
|
+
module Rails
|
|
7
|
+
# Middleware that captures exceptions and sends them to Txlemetry.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
class CaptureExceptions
|
|
11
|
+
include ParameterFilter
|
|
12
|
+
|
|
13
|
+
# @param app [#call] Rack application.
|
|
14
|
+
def initialize(app)
|
|
15
|
+
@app = app
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# @param env [Hash] Rack environment.
|
|
19
|
+
# @return [Array] Rack response.
|
|
20
|
+
def call(env)
|
|
21
|
+
# Signal that we're in a web request context
|
|
22
|
+
# ErrorSubscriber will skip capture for web requests to avoid duplicates
|
|
23
|
+
Txlemetry::Rails.enter_web_request
|
|
24
|
+
|
|
25
|
+
response = @app.call(env)
|
|
26
|
+
env['txlemetry.response_status_code'] = response_status(response)
|
|
27
|
+
|
|
28
|
+
# Check if there was an exception that Rails handled
|
|
29
|
+
exception = collect_exception(env)
|
|
30
|
+
|
|
31
|
+
capture_exception(exception, env) if exception && should_capture?(exception)
|
|
32
|
+
|
|
33
|
+
response
|
|
34
|
+
rescue StandardError => e
|
|
35
|
+
# Capture unhandled exceptions
|
|
36
|
+
capture_exception(e, env) if should_capture?(e)
|
|
37
|
+
raise
|
|
38
|
+
ensure
|
|
39
|
+
Txlemetry::Rails.exit_web_request
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def collect_exception(env)
|
|
45
|
+
# Rails stores exceptions in these env keys
|
|
46
|
+
env['action_dispatch.exception'] ||
|
|
47
|
+
env['rack.exception'] ||
|
|
48
|
+
env['txlemetry.rescued_exception']
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def should_capture?(exception)
|
|
52
|
+
return false unless Txlemetry::Rails.config&.auto_capture_exceptions
|
|
53
|
+
return false unless Txlemetry::Rails.config&.should_capture_exception?(exception)
|
|
54
|
+
|
|
55
|
+
true
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def capture_exception(exception, env)
|
|
59
|
+
request = ActionDispatch::Request.new(env)
|
|
60
|
+
distinct_id = extract_distinct_id(env)
|
|
61
|
+
additional_properties = build_properties(request, env)
|
|
62
|
+
|
|
63
|
+
captured = Txlemetry.capture_exception(
|
|
64
|
+
exception,
|
|
65
|
+
distinct_id,
|
|
66
|
+
additional_properties,
|
|
67
|
+
mechanism: { 'type' => 'rails', 'handled' => false }
|
|
68
|
+
)
|
|
69
|
+
Txlemetry::Rails.mark_web_exception_captured(exception) if captured
|
|
70
|
+
rescue StandardError => e
|
|
71
|
+
Txlemetry::Logging.logger.error("Failed to capture exception: #{e.message}")
|
|
72
|
+
Txlemetry::Logging.logger.error("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def extract_distinct_id(env)
|
|
76
|
+
# Prefer authenticated Rails user context. Request/tracing context is
|
|
77
|
+
# applied later by the core capture path if this returns nil.
|
|
78
|
+
return nil unless Txlemetry::Rails.config&.capture_user_context
|
|
79
|
+
|
|
80
|
+
user = extract_current_user(env['action_controller.instance'])
|
|
81
|
+
user_id = extract_user_id(user) if user
|
|
82
|
+
return user_id if present?(user_id)
|
|
83
|
+
|
|
84
|
+
nil
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def extract_current_user(controller)
|
|
88
|
+
resolver = Txlemetry::Rails.config&.current_user_resolver
|
|
89
|
+
return resolve_current_user(resolver, controller) if resolver
|
|
90
|
+
|
|
91
|
+
method_name = Txlemetry::Rails.config&.current_user_method || :current_user
|
|
92
|
+
return unless controller.respond_to?(method_name, true)
|
|
93
|
+
|
|
94
|
+
controller.send(method_name)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def resolve_current_user(resolver, controller)
|
|
98
|
+
call_current_user_resolver(resolver, controller)
|
|
99
|
+
rescue StandardError => e
|
|
100
|
+
Txlemetry::Logging.logger.warn("current_user_resolver failed: #{e.message}")
|
|
101
|
+
nil
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def call_current_user_resolver(resolver, controller)
|
|
105
|
+
if resolver.arity.zero?
|
|
106
|
+
controller ? controller.instance_exec(&resolver) : resolver.call
|
|
107
|
+
elsif controller
|
|
108
|
+
resolver.call(controller)
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def extract_user_id(user)
|
|
113
|
+
# Use configured method if specified
|
|
114
|
+
method_name = Txlemetry::Rails.config&.user_id_method
|
|
115
|
+
return user.send(method_name) if method_name && user.respond_to?(method_name)
|
|
116
|
+
|
|
117
|
+
# Try explicit Txlemetry method (allows users to customize without config)
|
|
118
|
+
return user.txlemetry_distinct_id if user.respond_to?(:txlemetry_distinct_id)
|
|
119
|
+
return user.distinct_id if user.respond_to?(:distinct_id)
|
|
120
|
+
|
|
121
|
+
# Try common ID methods
|
|
122
|
+
return user.id if user.respond_to?(:id)
|
|
123
|
+
return user['id'] if user.respond_to?(:[]) && user['id']
|
|
124
|
+
return user.pk if user.respond_to?(:pk)
|
|
125
|
+
return user['pk'] if user.respond_to?(:[]) && user['pk']
|
|
126
|
+
return user.uuid if user.respond_to?(:uuid)
|
|
127
|
+
return user['uuid'] if user.respond_to?(:[]) && user['uuid']
|
|
128
|
+
|
|
129
|
+
user.to_s
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_properties(request, env)
|
|
133
|
+
properties = {
|
|
134
|
+
'$exception_source' => 'rails'
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
# Add controller and action if available
|
|
138
|
+
if env['action_controller.instance']
|
|
139
|
+
controller = env['action_controller.instance']
|
|
140
|
+
properties['$controller'] = safe_serialize(controller.controller_name)
|
|
141
|
+
properties['$action'] = safe_serialize(controller.action_name)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Add request parameters (be careful with sensitive data)
|
|
145
|
+
if request.params.present?
|
|
146
|
+
filtered_params = filter_sensitive_params(request.params)
|
|
147
|
+
# Safe serialize to handle any complex objects in params
|
|
148
|
+
properties['$request_params'] = safe_serialize(filtered_params) unless filtered_params.empty?
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
response_status_code = env['txlemetry.response_status_code']
|
|
152
|
+
properties['$response_status_code'] = response_status_code if response_status_code
|
|
153
|
+
|
|
154
|
+
# Add referrer
|
|
155
|
+
properties['$referrer'] = safe_serialize(request.referrer) if request.referrer
|
|
156
|
+
|
|
157
|
+
properties
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def response_status(response)
|
|
161
|
+
status = response.respond_to?(:[]) ? response[0] : nil
|
|
162
|
+
status if status.is_a?(Integer)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def present?(value)
|
|
166
|
+
!(value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|