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
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'txlemetry/rails/facade'
|
|
4
|
+
|
|
5
|
+
module Txlemetry
|
|
6
|
+
module Rails
|
|
7
|
+
class Railtie < ::Rails::Railtie
|
|
8
|
+
# Keep the historical Railtie initializer name, but install the facade at
|
|
9
|
+
# load time so application initializers can call Txlemetry.init.
|
|
10
|
+
initializer 'txlemetry.set_configs' do |_app|
|
|
11
|
+
Txlemetry::Rails.install_txlemetry_facade!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Insert middleware for request context and exception capturing
|
|
15
|
+
initializer 'txlemetry.insert_middlewares' do |app|
|
|
16
|
+
# Wrap the Rails exception middleware so request context is active for
|
|
17
|
+
# downstream handlers and exception capture.
|
|
18
|
+
insert_middleware_before(
|
|
19
|
+
app, ActionDispatch::ShowExceptions,
|
|
20
|
+
Txlemetry::Rails::RequestContext
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
# Insert after DebugExceptions to catch rescued exceptions
|
|
24
|
+
insert_middleware_after(
|
|
25
|
+
app, ActionDispatch::DebugExceptions,
|
|
26
|
+
Txlemetry::Rails::RescuedExceptionInterceptor
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
# Insert after ShowExceptions to capture all exceptions
|
|
30
|
+
insert_middleware_after(
|
|
31
|
+
app, ActionDispatch::ShowExceptions,
|
|
32
|
+
Txlemetry::Rails::CaptureExceptions
|
|
33
|
+
)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# After initialization, set up remaining integrations
|
|
37
|
+
config.after_initialize do |_app|
|
|
38
|
+
# Hook into ActiveJob only if enabled
|
|
39
|
+
if Txlemetry::Rails.config&.auto_instrument_active_job
|
|
40
|
+
ActiveSupport.on_load(:active_job) do
|
|
41
|
+
prepend Txlemetry::Rails::ActiveJobExtensions
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
next unless Txlemetry.initialized?
|
|
46
|
+
|
|
47
|
+
# Register with Rails error reporter (Rails 7.0+)
|
|
48
|
+
register_error_subscriber if rails_version_above_7?
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Opt-in: forward logs to Txlemetry Logs over OTLP
|
|
52
|
+
config.after_initialize do
|
|
53
|
+
install_txlemetry_logs if Txlemetry::Rails.config&.logs_enabled
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Ensure Txlemetry shuts down gracefully (register only once)
|
|
57
|
+
config.after_initialize do
|
|
58
|
+
next if @txlemetry_at_exit_registered
|
|
59
|
+
|
|
60
|
+
@txlemetry_at_exit_registered = true
|
|
61
|
+
at_exit do
|
|
62
|
+
Txlemetry::Rails::Logs::Setup.shutdown
|
|
63
|
+
Txlemetry.client&.shutdown if Txlemetry.initialized?
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
MISSING_MIDDLEWARE_MESSAGE = 'No such middleware to insert'
|
|
68
|
+
MIDDLEWARE_FALLBACK_OPERATION = :txlemetry_insert_middleware_with_fallback
|
|
69
|
+
|
|
70
|
+
module MiddlewareStackFallback
|
|
71
|
+
def txlemetry_insert_middleware_with_fallback(location, target, middleware)
|
|
72
|
+
Txlemetry::Rails::Railtie.instance.send(
|
|
73
|
+
:insert_middleware_with_fallback,
|
|
74
|
+
self,
|
|
75
|
+
location,
|
|
76
|
+
target,
|
|
77
|
+
middleware
|
|
78
|
+
)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
private :txlemetry_insert_middleware_with_fallback
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
private_constant :MISSING_MIDDLEWARE_MESSAGE,
|
|
85
|
+
:MIDDLEWARE_FALLBACK_OPERATION,
|
|
86
|
+
:MiddlewareStackFallback
|
|
87
|
+
|
|
88
|
+
# @api private
|
|
89
|
+
# @return [void]
|
|
90
|
+
def insert_middleware_after(app, target, middleware)
|
|
91
|
+
insert_middleware(app.config.middleware, :after, target, middleware)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# @api private
|
|
95
|
+
# @return [void]
|
|
96
|
+
def insert_middleware_before(app, target, middleware)
|
|
97
|
+
insert_middleware(app.config.middleware, :before, target, middleware)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# During initialization, app.config.middleware is usually a
|
|
101
|
+
# Rails::Configuration::MiddlewareStackProxy. The proxy only records
|
|
102
|
+
# operations, so missing-target errors happen later when Rails builds the
|
|
103
|
+
# real ActionDispatch::MiddlewareStack. Add our own deferred operation so
|
|
104
|
+
# we can fall back at build time instead of crashing or silently skipping.
|
|
105
|
+
def insert_middleware(middleware_stack, location, target, middleware)
|
|
106
|
+
if middleware_stack_proxy?(middleware_stack)
|
|
107
|
+
append_middleware_operation(middleware_stack, location, target, middleware)
|
|
108
|
+
else
|
|
109
|
+
insert_middleware_with_fallback(middleware_stack, location, target, middleware)
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def middleware_stack_proxy?(middleware_stack)
|
|
114
|
+
defined?(::Rails::Configuration::MiddlewareStackProxy) &&
|
|
115
|
+
middleware_stack.instance_of?(::Rails::Configuration::MiddlewareStackProxy) &&
|
|
116
|
+
middleware_stack.instance_variable_defined?(:@operations)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def append_middleware_operation(middleware_stack, location, target, middleware)
|
|
120
|
+
operations = middleware_stack.instance_variable_get(:@operations)
|
|
121
|
+
|
|
122
|
+
if callable_middleware_operations?(middleware_stack, operations)
|
|
123
|
+
operations << lambda do |resolved_stack|
|
|
124
|
+
insert_middleware_with_fallback(resolved_stack, location, target, middleware)
|
|
125
|
+
end
|
|
126
|
+
else
|
|
127
|
+
ensure_middleware_stack_fallback_operation!
|
|
128
|
+
operations << [MIDDLEWARE_FALLBACK_OPERATION, [location, target, middleware], nil]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def callable_middleware_operations?(middleware_stack, operations)
|
|
133
|
+
operation = operations.first || probe_middleware_operation(middleware_stack)
|
|
134
|
+
|
|
135
|
+
operation.respond_to?(:call)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def probe_middleware_operation(middleware_stack)
|
|
139
|
+
probe = middleware_stack.class.new
|
|
140
|
+
probe.use(Object)
|
|
141
|
+
probe.instance_variable_get(:@operations).first
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def ensure_middleware_stack_fallback_operation!
|
|
145
|
+
require 'action_dispatch/middleware/stack' unless defined?(::ActionDispatch::MiddlewareStack)
|
|
146
|
+
return if ::ActionDispatch::MiddlewareStack < MiddlewareStackFallback
|
|
147
|
+
|
|
148
|
+
::ActionDispatch::MiddlewareStack.include(MiddlewareStackFallback)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def insert_middleware_with_fallback(middleware_stack, location, target, middleware)
|
|
152
|
+
perform_middleware_insert(middleware_stack, location, target, middleware)
|
|
153
|
+
rescue RuntimeError => e
|
|
154
|
+
raise unless missing_middleware_error?(e)
|
|
155
|
+
|
|
156
|
+
fallback_insert_middleware(middleware_stack, location, target, middleware)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def perform_middleware_insert(middleware_stack, location, target, middleware)
|
|
160
|
+
if location == :after
|
|
161
|
+
middleware_stack.insert_after(target, middleware)
|
|
162
|
+
else
|
|
163
|
+
middleware_stack.insert_before(target, middleware)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def fallback_insert_middleware(middleware_stack, location, target, middleware)
|
|
168
|
+
fallback_position = if location == :before && middleware_stack.respond_to?(:unshift)
|
|
169
|
+
middleware_stack.unshift(middleware)
|
|
170
|
+
'beginning'
|
|
171
|
+
else
|
|
172
|
+
middleware_stack.use(middleware)
|
|
173
|
+
'end'
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
Txlemetry::Logging.logger.warn(
|
|
177
|
+
"Could not find #{target.inspect} in the Rails middleware stack; " \
|
|
178
|
+
"inserted #{middleware.inspect} at the #{fallback_position} " \
|
|
179
|
+
'of the stack instead.'
|
|
180
|
+
)
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def missing_middleware_error?(error)
|
|
184
|
+
error.message.start_with?(MISSING_MIDDLEWARE_MESSAGE)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private :insert_middleware,
|
|
188
|
+
:middleware_stack_proxy?,
|
|
189
|
+
:append_middleware_operation,
|
|
190
|
+
:callable_middleware_operations?,
|
|
191
|
+
:probe_middleware_operation,
|
|
192
|
+
:ensure_middleware_stack_fallback_operation!,
|
|
193
|
+
:insert_middleware_with_fallback,
|
|
194
|
+
:perform_middleware_insert,
|
|
195
|
+
:fallback_insert_middleware,
|
|
196
|
+
:missing_middleware_error?
|
|
197
|
+
|
|
198
|
+
# Build the Txlemetry Logs pipeline and broadcast Rails.logger into it.
|
|
199
|
+
#
|
|
200
|
+
# @api private
|
|
201
|
+
# @return [void]
|
|
202
|
+
def self.install_txlemetry_logs
|
|
203
|
+
unless Txlemetry.initialized?
|
|
204
|
+
# logs_enabled is an explicit opt-in, so leave a breadcrumb instead
|
|
205
|
+
# of silently skipping when Txlemetry.init never ran.
|
|
206
|
+
Txlemetry::Logging.logger.warn(
|
|
207
|
+
'Txlemetry Logs is enabled but Txlemetry.init has not been called; ' \
|
|
208
|
+
'skipping log forwarding. Call Txlemetry.init in your initializer.'
|
|
209
|
+
)
|
|
210
|
+
return
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# Mirror the core client: when it is disabled (missing/blank api_key)
|
|
214
|
+
# every capture no-ops, so log forwarding should stay off too. The
|
|
215
|
+
# client already logs its own missing-api_key error, so skip quietly.
|
|
216
|
+
return unless Txlemetry.client.enabled?
|
|
217
|
+
|
|
218
|
+
appender = Txlemetry::Rails::Logs::Setup.install
|
|
219
|
+
return if appender.nil?
|
|
220
|
+
|
|
221
|
+
broadcast_rails_logger(appender) if Txlemetry::Rails.config&.logs_forward_rails_logger
|
|
222
|
+
rescue StandardError => e
|
|
223
|
+
Txlemetry::Logging.logger.warn("Failed to set up Txlemetry Logs: #{e.message}")
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Attach the appender to Rails.logger, supporting both the Rails 7.1+
|
|
227
|
+
# BroadcastLogger and the older ActiveSupport::Logger.broadcast mechanism.
|
|
228
|
+
#
|
|
229
|
+
# @api private
|
|
230
|
+
# @return [void]
|
|
231
|
+
def self.broadcast_rails_logger(appender)
|
|
232
|
+
logger = ::Rails.logger
|
|
233
|
+
return unless logger
|
|
234
|
+
|
|
235
|
+
if logger.respond_to?(:broadcast_to)
|
|
236
|
+
logger.broadcast_to(appender)
|
|
237
|
+
elsif defined?(ActiveSupport::Logger) && ActiveSupport::Logger.respond_to?(:broadcast)
|
|
238
|
+
logger.extend(ActiveSupport::Logger.broadcast(appender))
|
|
239
|
+
else
|
|
240
|
+
Txlemetry::Logging.logger.warn(
|
|
241
|
+
'Txlemetry Logs could not broadcast Rails.logger; no compatible broadcast mechanism found.'
|
|
242
|
+
)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# @api private
|
|
247
|
+
# @return [void]
|
|
248
|
+
def self.register_error_subscriber
|
|
249
|
+
return unless Txlemetry::Rails.config&.auto_capture_exceptions
|
|
250
|
+
|
|
251
|
+
subscriber = Txlemetry::Rails::ErrorSubscriber.new
|
|
252
|
+
::Rails.error.subscribe(subscriber)
|
|
253
|
+
rescue StandardError => e
|
|
254
|
+
Txlemetry::Logging.logger.warn("Failed to register error subscriber: #{e.message}")
|
|
255
|
+
Txlemetry::Logging.logger.warn("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# @api private
|
|
259
|
+
# @return [Boolean]
|
|
260
|
+
def self.rails_version_above_7?
|
|
261
|
+
::Rails.version.to_f >= 7.0
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# Configuration wrapper for the init block
|
|
266
|
+
class InitConfig
|
|
267
|
+
# @param base_options [Hash] Initial core SDK options.
|
|
268
|
+
def initialize(base_options = {})
|
|
269
|
+
@base_options = base_options
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
# Core Txlemetry options
|
|
273
|
+
#
|
|
274
|
+
# @param value [String]
|
|
275
|
+
# @return [String]
|
|
276
|
+
def api_key=(value)
|
|
277
|
+
@base_options[:api_key] = value
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# The credential used for local feature flag evaluation and remote config. Accepts either a
|
|
281
|
+
# Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`).
|
|
282
|
+
#
|
|
283
|
+
# @param value [String, nil]
|
|
284
|
+
# @return [String, nil]
|
|
285
|
+
def secret_key=(value)
|
|
286
|
+
@base_options[:secret_key] = value
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# @deprecated Use {#secret_key=} instead. Retained as an alias.
|
|
290
|
+
# @param value [String, nil]
|
|
291
|
+
# @return [String, nil]
|
|
292
|
+
def personal_api_key=(value)
|
|
293
|
+
@base_options[:personal_api_key] = value
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
# @param value [String]
|
|
297
|
+
# @return [String]
|
|
298
|
+
def host=(value)
|
|
299
|
+
@base_options[:host] = value
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
# @param value [Integer]
|
|
303
|
+
# @return [Integer]
|
|
304
|
+
def max_queue_size=(value)
|
|
305
|
+
@base_options[:max_queue_size] = value
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# @param value [Boolean]
|
|
309
|
+
# @return [Boolean]
|
|
310
|
+
def test_mode=(value)
|
|
311
|
+
@base_options[:test_mode] = value
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# @param value [Boolean]
|
|
315
|
+
# @return [Boolean]
|
|
316
|
+
def sync_mode=(value)
|
|
317
|
+
@base_options[:sync_mode] = value
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# @param value [Boolean]
|
|
321
|
+
# @return [Boolean]
|
|
322
|
+
def compress_request=(value)
|
|
323
|
+
@base_options[:compress_request] = value
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# @param value [Proc]
|
|
327
|
+
# @return [Proc]
|
|
328
|
+
def on_error=(value)
|
|
329
|
+
@base_options[:on_error] = value
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# @param value [Integer]
|
|
333
|
+
# @return [Integer]
|
|
334
|
+
def feature_flags_polling_interval=(value)
|
|
335
|
+
@base_options[:feature_flags_polling_interval] = value
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# @param value [Integer]
|
|
339
|
+
# @return [Integer]
|
|
340
|
+
def feature_flag_request_timeout_seconds=(value)
|
|
341
|
+
@base_options[:feature_flag_request_timeout_seconds] = value
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
# @param value [Proc]
|
|
345
|
+
# @return [Proc]
|
|
346
|
+
def before_send=(value)
|
|
347
|
+
@base_options[:before_send] = value
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
# @return [Hash] Core SDK options suitable for {Txlemetry::Client.new}.
|
|
351
|
+
def to_client_options
|
|
352
|
+
@base_options
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'txlemetry/internal/context'
|
|
4
|
+
require 'txlemetry/rails/tracing_headers'
|
|
5
|
+
require 'txlemetry/rails/request_metadata'
|
|
6
|
+
|
|
7
|
+
module Txlemetry
|
|
8
|
+
module Rails
|
|
9
|
+
# Rack middleware that creates a request-local Txlemetry context from tracing headers.
|
|
10
|
+
#
|
|
11
|
+
# @api private
|
|
12
|
+
class RequestContext
|
|
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
|
+
request = build_request(env)
|
|
22
|
+
|
|
23
|
+
Internal::Context.with_context(context_data(request), fresh: true) do
|
|
24
|
+
@app.call(env)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def context_data(request)
|
|
31
|
+
data = { properties: request_properties(request) }
|
|
32
|
+
return data unless use_tracing_headers?
|
|
33
|
+
|
|
34
|
+
data.merge(
|
|
35
|
+
distinct_id: tracing_header(request, 'X-POSTHOG-DISTINCT-ID'),
|
|
36
|
+
session_id: tracing_header(request, 'X-POSTHOG-SESSION-ID')
|
|
37
|
+
)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def use_tracing_headers?
|
|
41
|
+
Txlemetry::Rails.config&.use_tracing_headers != false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def request_properties(request)
|
|
45
|
+
RequestMetadata.extract(request)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def tracing_header(request, header_name)
|
|
49
|
+
TracingHeaders.extract_header(request, header_name)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_request(env)
|
|
53
|
+
if defined?(ActionDispatch::Request)
|
|
54
|
+
ActionDispatch::Request.new(env)
|
|
55
|
+
elsif defined?(Rack::Request)
|
|
56
|
+
Rack::Request.new(env)
|
|
57
|
+
else
|
|
58
|
+
env
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'txlemetry/rails/tracing_headers'
|
|
4
|
+
|
|
5
|
+
module Txlemetry
|
|
6
|
+
module Rails
|
|
7
|
+
# Internal helpers for extracting request metadata owned by RequestContext.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
module RequestMetadata
|
|
11
|
+
module_function
|
|
12
|
+
|
|
13
|
+
# @param request [Object] Rack or Rails request object.
|
|
14
|
+
# @return [Hash] Event properties extracted from the request.
|
|
15
|
+
def extract(request)
|
|
16
|
+
properties = {}
|
|
17
|
+
add_property(properties, '$current_url', current_url(request))
|
|
18
|
+
request_method = request_value(request, :request_method) || request_value(request, :method)
|
|
19
|
+
add_property(properties, '$request_method', request_method)
|
|
20
|
+
add_property(properties, '$request_path', request_value(request, :path) || request_value(request, :path_info))
|
|
21
|
+
user_agent = TracingHeaders.extract_header(request, 'User-Agent')
|
|
22
|
+
add_property(properties, '$user_agent', user_agent)
|
|
23
|
+
# Mirrored into $raw_user_agent, the standardized property Txlemetry's
|
|
24
|
+
# server-side classification (e.g. bot detection) reads
|
|
25
|
+
add_property(properties, '$raw_user_agent', user_agent)
|
|
26
|
+
add_property(properties, '$ip', client_ip(request))
|
|
27
|
+
properties
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def current_url(request)
|
|
31
|
+
url = request_value(request, :url)
|
|
32
|
+
return if url.nil?
|
|
33
|
+
|
|
34
|
+
url.to_s.split('?', 2).first
|
|
35
|
+
end
|
|
36
|
+
private_class_method :current_url
|
|
37
|
+
|
|
38
|
+
def client_ip(request)
|
|
39
|
+
trusted_ip = request_value(request, :remote_ip) || request_value(request, :ip)
|
|
40
|
+
return trusted_ip if present?(trusted_ip)
|
|
41
|
+
|
|
42
|
+
forwarded_for = TracingHeaders.extract_header(request, 'X-Forwarded-For')
|
|
43
|
+
forwarded_ip = forwarded_for.split(',').first&.strip if forwarded_for
|
|
44
|
+
return forwarded_ip if present?(forwarded_ip)
|
|
45
|
+
|
|
46
|
+
env_value(request, 'REMOTE_ADDR')
|
|
47
|
+
end
|
|
48
|
+
private_class_method :client_ip
|
|
49
|
+
|
|
50
|
+
def present?(value)
|
|
51
|
+
!(value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
52
|
+
end
|
|
53
|
+
private_class_method :present?
|
|
54
|
+
|
|
55
|
+
def add_property(properties, key, value)
|
|
56
|
+
return if value.nil?
|
|
57
|
+
|
|
58
|
+
serialized = value.to_s
|
|
59
|
+
return if serialized.empty?
|
|
60
|
+
|
|
61
|
+
properties[key] = serialized
|
|
62
|
+
end
|
|
63
|
+
private_class_method :add_property
|
|
64
|
+
|
|
65
|
+
def request_value(request, method_name)
|
|
66
|
+
return unless request.respond_to?(method_name)
|
|
67
|
+
|
|
68
|
+
request.public_send(method_name)
|
|
69
|
+
rescue StandardError
|
|
70
|
+
nil
|
|
71
|
+
end
|
|
72
|
+
private_class_method :request_value
|
|
73
|
+
|
|
74
|
+
def env_value(request, key)
|
|
75
|
+
request.respond_to?(:get_header) ? request.get_header(key) : request.env[key]
|
|
76
|
+
rescue StandardError
|
|
77
|
+
nil
|
|
78
|
+
end
|
|
79
|
+
private_class_method :env_value
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private_constant :RequestMetadata
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Portions of this file are derived from getsentry/sentry-ruby
|
|
4
|
+
# Copyright (c) 2020 Sentry
|
|
5
|
+
# Licensed under the MIT License: https://github.com/getsentry/sentry-ruby/blob/master/LICENSE
|
|
6
|
+
|
|
7
|
+
module Txlemetry
|
|
8
|
+
module Rails
|
|
9
|
+
# Middleware that intercepts exceptions that are rescued by Rails.
|
|
10
|
+
# This middleware runs before ShowExceptions and captures the exception
|
|
11
|
+
# so we can report it even if Rails rescues it.
|
|
12
|
+
#
|
|
13
|
+
# @api private
|
|
14
|
+
class RescuedExceptionInterceptor
|
|
15
|
+
# @param app [#call] Rack application.
|
|
16
|
+
def initialize(app)
|
|
17
|
+
@app = app
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param env [Hash] Rack environment.
|
|
21
|
+
# @return [Array] Rack response.
|
|
22
|
+
def call(env)
|
|
23
|
+
@app.call(env)
|
|
24
|
+
rescue StandardError => e
|
|
25
|
+
# Store the exception so CaptureExceptions middleware can report it
|
|
26
|
+
env['txlemetry.rescued_exception'] = e if should_intercept?
|
|
27
|
+
raise e
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def should_intercept?
|
|
33
|
+
Txlemetry::Rails.config&.report_rescued_exceptions
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Txlemetry
|
|
4
|
+
module Rails
|
|
5
|
+
# Helpers for extracting and sanitizing Txlemetry tracing headers from Rack/Rails requests.
|
|
6
|
+
#
|
|
7
|
+
# @api private
|
|
8
|
+
module TracingHeaders
|
|
9
|
+
MAX_HEADER_VALUE_LENGTH = 1000
|
|
10
|
+
CONTROL_CHARACTERS = /[[:cntrl:]]/
|
|
11
|
+
|
|
12
|
+
module_function
|
|
13
|
+
|
|
14
|
+
# @param value [Object]
|
|
15
|
+
# @return [String, nil]
|
|
16
|
+
def sanitize_header_value(value)
|
|
17
|
+
return nil unless value.is_a?(String)
|
|
18
|
+
|
|
19
|
+
sanitized = value.strip.gsub(CONTROL_CHARACTERS, '').strip
|
|
20
|
+
return nil if sanitized.empty?
|
|
21
|
+
|
|
22
|
+
sanitized[0, MAX_HEADER_VALUE_LENGTH]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# @param request_or_env [Object, Hash] Rack request, Rails request, or Rack env hash.
|
|
26
|
+
# @param header_name [String]
|
|
27
|
+
# @return [String, nil]
|
|
28
|
+
def extract_header(request_or_env, header_name)
|
|
29
|
+
candidates = header_candidates(header_name)
|
|
30
|
+
|
|
31
|
+
candidates.each do |candidate|
|
|
32
|
+
value = header_value(request_or_env, candidate)
|
|
33
|
+
sanitized = sanitize_header_value(value)
|
|
34
|
+
return sanitized if sanitized
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
env = request_env(request_or_env)
|
|
38
|
+
return nil unless env.respond_to?(:each)
|
|
39
|
+
|
|
40
|
+
target_names = candidates.map { |candidate| normalize_header_name(candidate) }
|
|
41
|
+
env.each do |key, value|
|
|
42
|
+
next unless target_names.include?(normalize_header_name(key))
|
|
43
|
+
|
|
44
|
+
sanitized = sanitize_header_value(value)
|
|
45
|
+
return sanitized if sanitized
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def header_candidates(header_name)
|
|
52
|
+
canonical = header_name.to_s
|
|
53
|
+
rack = "HTTP_#{canonical.upcase.tr('-', '_')}"
|
|
54
|
+
[canonical, canonical.downcase, rack]
|
|
55
|
+
end
|
|
56
|
+
private_class_method :header_candidates
|
|
57
|
+
|
|
58
|
+
def header_value(request_or_env, header_name)
|
|
59
|
+
if request_or_env.respond_to?(:headers)
|
|
60
|
+
value = request_or_env.headers[header_name]
|
|
61
|
+
return value unless value.nil?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
env = request_env(request_or_env)
|
|
65
|
+
return nil unless env.respond_to?(:[])
|
|
66
|
+
|
|
67
|
+
env[header_name]
|
|
68
|
+
end
|
|
69
|
+
private_class_method :header_value
|
|
70
|
+
|
|
71
|
+
def request_env(request_or_env)
|
|
72
|
+
request_or_env.respond_to?(:env) ? request_or_env.env : request_or_env
|
|
73
|
+
end
|
|
74
|
+
private_class_method :request_env
|
|
75
|
+
|
|
76
|
+
def normalize_header_name(header_name)
|
|
77
|
+
header_name.to_s.upcase.tr('-', '_')
|
|
78
|
+
end
|
|
79
|
+
private_class_method :normalize_header_name
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private_constant :TracingHeaders
|
|
83
|
+
end
|
|
84
|
+
end
|