posthog-rails 3.8.1 → 3.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/generators/posthog/templates/posthog.rb +7 -1
- data/lib/posthog/rails/active_job.rb +7 -1
- data/lib/posthog/rails/capture_exceptions.rb +26 -13
- data/lib/posthog/rails/configuration.rb +19 -9
- data/lib/posthog/rails/error_subscriber.rb +10 -2
- data/lib/posthog/rails/parameter_filter.rb +4 -0
- data/lib/posthog/rails/railtie.rb +56 -3
- data/lib/posthog/rails/request_context.rb +63 -0
- data/lib/posthog/rails/request_metadata.rb +80 -0
- data/lib/posthog/rails/rescued_exception_interceptor.rb +7 -2
- data/lib/posthog/rails/tracing_headers.rb +84 -0
- data/lib/posthog/rails.rb +15 -0
- metadata +6 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 792f6e0ca6b259928ec0704d585e4887fbd4839837b7d49c08fc47b7eeeeeaaf
|
|
4
|
+
data.tar.gz: bc72fab471755cd32695bfdc919aa5cc99559c626504af061e89b80ad98dfd33
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f860543aeca28abfd50a110fa76fd2eed17a86e8c933cbbb0a0fdcdf4002d11bf400d2fb41d5680fa7587de749cad9514e71ad2a0855f1050827fb3da5d7a71b
|
|
7
|
+
data.tar.gz: afb7cb589db961516dea558424cdbe71ff9e20a004a0b944eba6f0aa54fdcb5a4be92acd0dfe6ce88abc84e35a63ca6567ecea4b5fd662018940b05b595aa27b
|
|
@@ -22,7 +22,13 @@ PostHog::Rails.configure do |config|
|
|
|
22
22
|
# Set to true to enable automatic ActiveJob exception tracking
|
|
23
23
|
# config.auto_instrument_active_job = true
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Use PostHog 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
|
|
26
32
|
# config.capture_user_context = true
|
|
27
33
|
|
|
28
34
|
# Controller method name to get current user (default: :current_user)
|
|
@@ -13,7 +13,12 @@ module PostHog
|
|
|
13
13
|
end
|
|
14
14
|
|
|
15
15
|
module ClassMethods
|
|
16
|
-
# DSL for defining how to extract distinct_id from job arguments
|
|
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
|
+
#
|
|
17
22
|
# Example:
|
|
18
23
|
# class MyJob < ApplicationJob
|
|
19
24
|
# posthog_distinct_id ->(user, arg1, arg2) { user.id }
|
|
@@ -25,6 +30,7 @@ module PostHog
|
|
|
25
30
|
@posthog_distinct_id_proc = proc || block
|
|
26
31
|
end
|
|
27
32
|
|
|
33
|
+
# @return [Proc, nil] The configured distinct_id extractor.
|
|
28
34
|
def posthog_distinct_id_proc
|
|
29
35
|
@posthog_distinct_id_proc
|
|
30
36
|
end
|
|
@@ -4,20 +4,26 @@ require 'posthog/rails/parameter_filter'
|
|
|
4
4
|
|
|
5
5
|
module PostHog
|
|
6
6
|
module Rails
|
|
7
|
-
# Middleware that captures exceptions and sends them to PostHog
|
|
7
|
+
# Middleware that captures exceptions and sends them to PostHog.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
8
10
|
class CaptureExceptions
|
|
9
11
|
include ParameterFilter
|
|
10
12
|
|
|
13
|
+
# @param app [#call] Rack application.
|
|
11
14
|
def initialize(app)
|
|
12
15
|
@app = app
|
|
13
16
|
end
|
|
14
17
|
|
|
18
|
+
# @param env [Hash] Rack environment.
|
|
19
|
+
# @return [Array] Rack response.
|
|
15
20
|
def call(env)
|
|
16
21
|
# Signal that we're in a web request context
|
|
17
22
|
# ErrorSubscriber will skip capture for web requests to avoid duplicates
|
|
18
23
|
PostHog::Rails.enter_web_request
|
|
19
24
|
|
|
20
25
|
response = @app.call(env)
|
|
26
|
+
env['posthog.response_status_code'] = response_status(response)
|
|
21
27
|
|
|
22
28
|
# Check if there was an exception that Rails handled
|
|
23
29
|
exception = collect_exception(env)
|
|
@@ -51,7 +57,7 @@ module PostHog
|
|
|
51
57
|
|
|
52
58
|
def capture_exception(exception, env)
|
|
53
59
|
request = ActionDispatch::Request.new(env)
|
|
54
|
-
distinct_id = extract_distinct_id(env
|
|
60
|
+
distinct_id = extract_distinct_id(env)
|
|
55
61
|
additional_properties = build_properties(request, env)
|
|
56
62
|
|
|
57
63
|
PostHog.capture_exception(exception, distinct_id, additional_properties)
|
|
@@ -60,20 +66,21 @@ module PostHog
|
|
|
60
66
|
PostHog::Logging.logger.error("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
|
|
61
67
|
end
|
|
62
68
|
|
|
63
|
-
def extract_distinct_id(env
|
|
64
|
-
#
|
|
69
|
+
def extract_distinct_id(env)
|
|
70
|
+
# Prefer authenticated Rails user context. Request/tracing context is
|
|
71
|
+
# applied later by the core capture path if this returns nil.
|
|
65
72
|
if PostHog::Rails.config&.capture_user_context && env['action_controller.instance']
|
|
66
73
|
controller = env['action_controller.instance']
|
|
67
74
|
method_name = PostHog::Rails.config&.current_user_method || :current_user
|
|
68
75
|
|
|
69
76
|
if controller.respond_to?(method_name, true)
|
|
70
77
|
user = controller.send(method_name)
|
|
71
|
-
|
|
78
|
+
user_id = extract_user_id(user) if user
|
|
79
|
+
return user_id if present?(user_id)
|
|
72
80
|
end
|
|
73
81
|
end
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
request.session&.id&.to_s
|
|
83
|
+
nil
|
|
77
84
|
end
|
|
78
85
|
|
|
79
86
|
def extract_user_id(user)
|
|
@@ -98,10 +105,7 @@ module PostHog
|
|
|
98
105
|
|
|
99
106
|
def build_properties(request, env)
|
|
100
107
|
properties = {
|
|
101
|
-
'$exception_source' => 'rails'
|
|
102
|
-
'$current_url' => safe_serialize(request.url),
|
|
103
|
-
'$request_method' => safe_serialize(request.method),
|
|
104
|
-
'$request_path' => safe_serialize(request.path)
|
|
108
|
+
'$exception_source' => 'rails'
|
|
105
109
|
}
|
|
106
110
|
|
|
107
111
|
# Add controller and action if available
|
|
@@ -118,14 +122,23 @@ module PostHog
|
|
|
118
122
|
properties['$request_params'] = safe_serialize(filtered_params) unless filtered_params.empty?
|
|
119
123
|
end
|
|
120
124
|
|
|
121
|
-
|
|
122
|
-
properties['$
|
|
125
|
+
response_status_code = env['posthog.response_status_code']
|
|
126
|
+
properties['$response_status_code'] = response_status_code if response_status_code
|
|
123
127
|
|
|
124
128
|
# Add referrer
|
|
125
129
|
properties['$referrer'] = safe_serialize(request.referrer) if request.referrer
|
|
126
130
|
|
|
127
131
|
properties
|
|
128
132
|
end
|
|
133
|
+
|
|
134
|
+
def response_status(response)
|
|
135
|
+
status = response.respond_to?(:[]) ? response[0] : nil
|
|
136
|
+
status if status.is_a?(Integer)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def present?(value)
|
|
140
|
+
!(value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
141
|
+
end
|
|
129
142
|
end
|
|
130
143
|
end
|
|
131
144
|
end
|
|
@@ -3,39 +3,47 @@
|
|
|
3
3
|
module PostHog
|
|
4
4
|
module Rails
|
|
5
5
|
class Configuration
|
|
6
|
-
# Whether to automatically capture exceptions from Rails
|
|
6
|
+
# @return [Boolean] Whether to automatically capture exceptions from Rails. Defaults to false.
|
|
7
7
|
attr_accessor :auto_capture_exceptions
|
|
8
8
|
|
|
9
|
-
# Whether to capture exceptions that Rails rescues (e.g., with rescue_from)
|
|
9
|
+
# @return [Boolean] Whether to capture exceptions that Rails rescues (e.g., with rescue_from). Defaults to false.
|
|
10
10
|
attr_accessor :report_rescued_exceptions
|
|
11
11
|
|
|
12
|
-
# Whether to automatically instrument ActiveJob
|
|
12
|
+
# @return [Boolean] Whether to automatically instrument ActiveJob. Defaults to false.
|
|
13
13
|
attr_accessor :auto_instrument_active_job
|
|
14
14
|
|
|
15
|
-
#
|
|
15
|
+
# @return [Array<String>] Exception class names to ignore in addition to the defaults.
|
|
16
16
|
attr_accessor :excluded_exceptions
|
|
17
17
|
|
|
18
|
-
# Whether to
|
|
18
|
+
# @return [Boolean] Whether to use PostHog tracing headers for request-scoped identity/session context.
|
|
19
|
+
# Defaults to true.
|
|
20
|
+
attr_accessor :use_tracing_headers
|
|
21
|
+
|
|
22
|
+
# @return [Boolean] Whether to capture the current user context in exceptions. Defaults to true.
|
|
19
23
|
attr_accessor :capture_user_context
|
|
20
24
|
|
|
21
|
-
# Method name to call on controller to get user
|
|
25
|
+
# @return [Symbol] Method name to call on controller to get the current user. Defaults to :current_user.
|
|
22
26
|
attr_accessor :current_user_method
|
|
23
27
|
|
|
24
|
-
# Method name to call on user object to get distinct_id
|
|
25
|
-
#
|
|
28
|
+
# @return [Symbol, nil] Method name to call on the user object to get distinct_id. When nil, tries:
|
|
29
|
+
# posthog_distinct_id, distinct_id, id, pk, uuid in order.
|
|
26
30
|
attr_accessor :user_id_method
|
|
27
31
|
|
|
32
|
+
# @return [PostHog::Rails::Configuration]
|
|
28
33
|
def initialize
|
|
29
34
|
@auto_capture_exceptions = false
|
|
30
35
|
@report_rescued_exceptions = false
|
|
31
36
|
@auto_instrument_active_job = false
|
|
32
37
|
@excluded_exceptions = []
|
|
38
|
+
@use_tracing_headers = true
|
|
33
39
|
@capture_user_context = true
|
|
34
40
|
@current_user_method = :current_user
|
|
35
41
|
@user_id_method = nil
|
|
36
42
|
end
|
|
37
43
|
|
|
38
|
-
# Default exceptions that Rails apps typically don't want to track
|
|
44
|
+
# Default exceptions that Rails apps typically don't want to track.
|
|
45
|
+
#
|
|
46
|
+
# @return [Array<String>]
|
|
39
47
|
def default_excluded_exceptions
|
|
40
48
|
[
|
|
41
49
|
'AbstractController::ActionNotFound',
|
|
@@ -54,6 +62,8 @@ module PostHog
|
|
|
54
62
|
]
|
|
55
63
|
end
|
|
56
64
|
|
|
65
|
+
# @param exception [Exception] The exception to check.
|
|
66
|
+
# @return [Boolean] Whether the exception should be captured.
|
|
57
67
|
def should_capture_exception?(exception)
|
|
58
68
|
exception_name = exception.class.name
|
|
59
69
|
!all_excluded_exceptions.include?(exception_name)
|
|
@@ -4,11 +4,19 @@ require 'posthog/rails/parameter_filter'
|
|
|
4
4
|
|
|
5
5
|
module PostHog
|
|
6
6
|
module Rails
|
|
7
|
-
# Rails 7.0+ error reporter integration
|
|
8
|
-
# This integrates with Rails.error.handle and Rails.error.record
|
|
7
|
+
# Rails 7.0+ error reporter integration.
|
|
8
|
+
# This integrates with Rails.error.handle and Rails.error.record.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
9
11
|
class ErrorSubscriber
|
|
10
12
|
include ParameterFilter
|
|
11
13
|
|
|
14
|
+
# @param error [Exception] Error reported by Rails.
|
|
15
|
+
# @param handled [Boolean]
|
|
16
|
+
# @param severity [Symbol, String]
|
|
17
|
+
# @param context [Hash]
|
|
18
|
+
# @param source [String, nil]
|
|
19
|
+
# @return [void]
|
|
12
20
|
def report(error, handled:, severity:, context:, source: nil)
|
|
13
21
|
return unless PostHog::Rails.config&.auto_capture_exceptions
|
|
14
22
|
return unless PostHog::Rails.config&.should_capture_exception?(error)
|
|
@@ -9,6 +9,8 @@ module PostHog
|
|
|
9
9
|
# It automatically detects the correct Rails parameter filtering API based on
|
|
10
10
|
# the Rails version.
|
|
11
11
|
#
|
|
12
|
+
# @api private
|
|
13
|
+
#
|
|
12
14
|
# @example Usage in a class
|
|
13
15
|
# class MyClass
|
|
14
16
|
# include PostHog::Rails::ParameterFilter
|
|
@@ -24,10 +26,12 @@ module PostHog
|
|
|
24
26
|
MAX_DEPTH = 10
|
|
25
27
|
|
|
26
28
|
if ::Rails.version.to_f >= 6.0
|
|
29
|
+
# @return [Class] Rails parameter filter backend.
|
|
27
30
|
def self.backend
|
|
28
31
|
ActiveSupport::ParameterFilter
|
|
29
32
|
end
|
|
30
33
|
else
|
|
34
|
+
# @return [Class] Rails parameter filter backend.
|
|
31
35
|
def self.backend
|
|
32
36
|
ActionDispatch::Http::ParameterFilter
|
|
33
37
|
end
|
|
@@ -21,7 +21,11 @@ module PostHog
|
|
|
21
21
|
get_all_flags
|
|
22
22
|
].freeze
|
|
23
23
|
|
|
24
|
-
# Initialize PostHog client
|
|
24
|
+
# Initialize the singleton PostHog client used by Rails delegators.
|
|
25
|
+
#
|
|
26
|
+
# @param options [Hash] Core {PostHog::Client} options.
|
|
27
|
+
# @yieldparam config [PostHog::Rails::InitConfig] Block-based core SDK configuration.
|
|
28
|
+
# @return [PostHog::Client]
|
|
25
29
|
def init(options = {})
|
|
26
30
|
# If block given, yield to configuration
|
|
27
31
|
if block_given?
|
|
@@ -42,11 +46,14 @@ module PostHog
|
|
|
42
46
|
end
|
|
43
47
|
end
|
|
44
48
|
|
|
49
|
+
# @return [Boolean] Whether {PostHog.init} has created a client.
|
|
45
50
|
def initialized?
|
|
46
51
|
!@client.nil?
|
|
47
52
|
end
|
|
48
53
|
|
|
49
|
-
# Fallback for any client methods not explicitly defined
|
|
54
|
+
# Fallback for any client methods not explicitly defined.
|
|
55
|
+
#
|
|
56
|
+
# @api private
|
|
50
57
|
# rubocop:disable Lint/RedundantSafeNavigation
|
|
51
58
|
def method_missing(method_name, ...)
|
|
52
59
|
if client&.respond_to?(method_name)
|
|
@@ -57,6 +64,7 @@ module PostHog
|
|
|
57
64
|
end
|
|
58
65
|
end
|
|
59
66
|
|
|
67
|
+
# @api private
|
|
60
68
|
def respond_to_missing?(method_name, include_private = false)
|
|
61
69
|
client&.respond_to?(method_name) || super
|
|
62
70
|
end
|
|
@@ -73,8 +81,15 @@ module PostHog
|
|
|
73
81
|
end
|
|
74
82
|
end
|
|
75
83
|
|
|
76
|
-
# Insert middleware for exception capturing
|
|
84
|
+
# Insert middleware for request context and exception capturing
|
|
77
85
|
initializer 'posthog.insert_middlewares' do |app|
|
|
86
|
+
# Wrap the Rails exception middleware so request context is active for
|
|
87
|
+
# downstream handlers and exception capture.
|
|
88
|
+
insert_middleware_before(
|
|
89
|
+
app, ActionDispatch::ShowExceptions,
|
|
90
|
+
PostHog::Rails::RequestContext
|
|
91
|
+
)
|
|
92
|
+
|
|
78
93
|
# Insert after DebugExceptions to catch rescued exceptions
|
|
79
94
|
insert_middleware_after(
|
|
80
95
|
app, ActionDispatch::DebugExceptions,
|
|
@@ -111,6 +126,8 @@ module PostHog
|
|
|
111
126
|
at_exit { PostHog.client&.shutdown if PostHog.initialized? }
|
|
112
127
|
end
|
|
113
128
|
|
|
129
|
+
# @api private
|
|
130
|
+
# @return [void]
|
|
114
131
|
def insert_middleware_after(app, target, middleware)
|
|
115
132
|
# During initialization, app.config.middleware is a MiddlewareStackProxy
|
|
116
133
|
# which only supports recording operations (insert_after, use, etc.)
|
|
@@ -118,6 +135,17 @@ module PostHog
|
|
|
118
135
|
app.config.middleware.insert_after(target, middleware)
|
|
119
136
|
end
|
|
120
137
|
|
|
138
|
+
# @api private
|
|
139
|
+
# @return [void]
|
|
140
|
+
def insert_middleware_before(app, target, middleware)
|
|
141
|
+
# During initialization, app.config.middleware is a MiddlewareStackProxy
|
|
142
|
+
# which only supports recording operations (insert_before, use, etc.)
|
|
143
|
+
# and does NOT support query methods like include?.
|
|
144
|
+
app.config.middleware.insert_before(target, middleware)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# @api private
|
|
148
|
+
# @return [void]
|
|
121
149
|
def self.register_error_subscriber
|
|
122
150
|
return unless PostHog::Rails.config&.auto_capture_exceptions
|
|
123
151
|
|
|
@@ -128,6 +156,8 @@ module PostHog
|
|
|
128
156
|
PostHog::Logging.logger.warn("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
|
|
129
157
|
end
|
|
130
158
|
|
|
159
|
+
# @api private
|
|
160
|
+
# @return [Boolean]
|
|
131
161
|
def self.rails_version_above_7?
|
|
132
162
|
::Rails.version.to_f >= 7.0
|
|
133
163
|
end
|
|
@@ -135,51 +165,74 @@ module PostHog
|
|
|
135
165
|
|
|
136
166
|
# Configuration wrapper for the init block
|
|
137
167
|
class InitConfig
|
|
168
|
+
# @param base_options [Hash] Initial core SDK options.
|
|
138
169
|
def initialize(base_options = {})
|
|
139
170
|
@base_options = base_options
|
|
140
171
|
end
|
|
141
172
|
|
|
142
173
|
# Core PostHog options
|
|
174
|
+
#
|
|
175
|
+
# @param value [String]
|
|
176
|
+
# @return [String]
|
|
143
177
|
def api_key=(value)
|
|
144
178
|
@base_options[:api_key] = value
|
|
145
179
|
end
|
|
146
180
|
|
|
181
|
+
# @param value [String, nil]
|
|
182
|
+
# @return [String, nil]
|
|
147
183
|
def personal_api_key=(value)
|
|
148
184
|
@base_options[:personal_api_key] = value
|
|
149
185
|
end
|
|
150
186
|
|
|
187
|
+
# @param value [String]
|
|
188
|
+
# @return [String]
|
|
151
189
|
def host=(value)
|
|
152
190
|
@base_options[:host] = value
|
|
153
191
|
end
|
|
154
192
|
|
|
193
|
+
# @param value [Integer]
|
|
194
|
+
# @return [Integer]
|
|
155
195
|
def max_queue_size=(value)
|
|
156
196
|
@base_options[:max_queue_size] = value
|
|
157
197
|
end
|
|
158
198
|
|
|
199
|
+
# @param value [Boolean]
|
|
200
|
+
# @return [Boolean]
|
|
159
201
|
def test_mode=(value)
|
|
160
202
|
@base_options[:test_mode] = value
|
|
161
203
|
end
|
|
162
204
|
|
|
205
|
+
# @param value [Boolean]
|
|
206
|
+
# @return [Boolean]
|
|
163
207
|
def sync_mode=(value)
|
|
164
208
|
@base_options[:sync_mode] = value
|
|
165
209
|
end
|
|
166
210
|
|
|
211
|
+
# @param value [Proc]
|
|
212
|
+
# @return [Proc]
|
|
167
213
|
def on_error=(value)
|
|
168
214
|
@base_options[:on_error] = value
|
|
169
215
|
end
|
|
170
216
|
|
|
217
|
+
# @param value [Integer]
|
|
218
|
+
# @return [Integer]
|
|
171
219
|
def feature_flags_polling_interval=(value)
|
|
172
220
|
@base_options[:feature_flags_polling_interval] = value
|
|
173
221
|
end
|
|
174
222
|
|
|
223
|
+
# @param value [Integer]
|
|
224
|
+
# @return [Integer]
|
|
175
225
|
def feature_flag_request_timeout_seconds=(value)
|
|
176
226
|
@base_options[:feature_flag_request_timeout_seconds] = value
|
|
177
227
|
end
|
|
178
228
|
|
|
229
|
+
# @param value [Proc]
|
|
230
|
+
# @return [Proc]
|
|
179
231
|
def before_send=(value)
|
|
180
232
|
@base_options[:before_send] = value
|
|
181
233
|
end
|
|
182
234
|
|
|
235
|
+
# @return [Hash] Core SDK options suitable for {PostHog::Client.new}.
|
|
183
236
|
def to_client_options
|
|
184
237
|
@base_options
|
|
185
238
|
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'posthog/internal/context'
|
|
4
|
+
require 'posthog/rails/tracing_headers'
|
|
5
|
+
require 'posthog/rails/request_metadata'
|
|
6
|
+
|
|
7
|
+
module PostHog
|
|
8
|
+
module Rails
|
|
9
|
+
# Rack middleware that creates a request-local PostHog 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
|
+
PostHog::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,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'posthog/rails/tracing_headers'
|
|
4
|
+
|
|
5
|
+
module PostHog
|
|
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
|
+
add_property(properties, '$user_agent', TracingHeaders.extract_header(request, 'User-Agent'))
|
|
22
|
+
add_property(properties, '$ip', client_ip(request))
|
|
23
|
+
properties
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def current_url(request)
|
|
27
|
+
url = request_value(request, :url)
|
|
28
|
+
return if url.nil?
|
|
29
|
+
|
|
30
|
+
url.to_s.split('?', 2).first
|
|
31
|
+
end
|
|
32
|
+
private_class_method :current_url
|
|
33
|
+
|
|
34
|
+
def client_ip(request)
|
|
35
|
+
trusted_ip = request_value(request, :remote_ip) || request_value(request, :ip)
|
|
36
|
+
return trusted_ip if present?(trusted_ip)
|
|
37
|
+
|
|
38
|
+
forwarded_for = TracingHeaders.extract_header(request, 'X-Forwarded-For')
|
|
39
|
+
forwarded_ip = forwarded_for.split(',').first&.strip if forwarded_for
|
|
40
|
+
return forwarded_ip if present?(forwarded_ip)
|
|
41
|
+
|
|
42
|
+
env_value(request, 'REMOTE_ADDR')
|
|
43
|
+
end
|
|
44
|
+
private_class_method :client_ip
|
|
45
|
+
|
|
46
|
+
def present?(value)
|
|
47
|
+
!(value.nil? || (value.respond_to?(:empty?) && value.empty?))
|
|
48
|
+
end
|
|
49
|
+
private_class_method :present?
|
|
50
|
+
|
|
51
|
+
def add_property(properties, key, value)
|
|
52
|
+
return if value.nil?
|
|
53
|
+
|
|
54
|
+
serialized = value.to_s
|
|
55
|
+
return if serialized.empty?
|
|
56
|
+
|
|
57
|
+
properties[key] = serialized
|
|
58
|
+
end
|
|
59
|
+
private_class_method :add_property
|
|
60
|
+
|
|
61
|
+
def request_value(request, method_name)
|
|
62
|
+
return unless request.respond_to?(method_name)
|
|
63
|
+
|
|
64
|
+
request.public_send(method_name)
|
|
65
|
+
rescue StandardError
|
|
66
|
+
nil
|
|
67
|
+
end
|
|
68
|
+
private_class_method :request_value
|
|
69
|
+
|
|
70
|
+
def env_value(request, key)
|
|
71
|
+
request.respond_to?(:get_header) ? request.get_header(key) : request.env[key]
|
|
72
|
+
rescue StandardError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
private_class_method :env_value
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
private_constant :RequestMetadata
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -2,14 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
module PostHog
|
|
4
4
|
module Rails
|
|
5
|
-
# Middleware that intercepts exceptions that are rescued by Rails
|
|
5
|
+
# Middleware that intercepts exceptions that are rescued by Rails.
|
|
6
6
|
# This middleware runs before ShowExceptions and captures the exception
|
|
7
|
-
# so we can report it even if Rails rescues it
|
|
7
|
+
# so we can report it even if Rails rescues it.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
8
10
|
class RescuedExceptionInterceptor
|
|
11
|
+
# @param app [#call] Rack application.
|
|
9
12
|
def initialize(app)
|
|
10
13
|
@app = app
|
|
11
14
|
end
|
|
12
15
|
|
|
16
|
+
# @param env [Hash] Rack environment.
|
|
17
|
+
# @return [Array] Rack response.
|
|
13
18
|
def call(env)
|
|
14
19
|
@app.call(env)
|
|
15
20
|
rescue StandardError => e
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PostHog
|
|
4
|
+
module Rails
|
|
5
|
+
# Helpers for extracting and sanitizing PostHog 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
|
data/lib/posthog/rails.rb
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'posthog/rails/configuration'
|
|
4
|
+
require 'posthog/rails/tracing_headers'
|
|
5
|
+
require 'posthog/rails/request_metadata'
|
|
6
|
+
require 'posthog/rails/request_context'
|
|
4
7
|
require 'posthog/rails/capture_exceptions'
|
|
5
8
|
require 'posthog/rails/rescued_exception_interceptor'
|
|
6
9
|
require 'posthog/rails/active_job'
|
|
@@ -15,29 +18,41 @@ module PostHog
|
|
|
15
18
|
IN_WEB_REQUEST_KEY = :posthog_in_web_request
|
|
16
19
|
|
|
17
20
|
class << self
|
|
21
|
+
# @return [PostHog::Rails::Configuration] Rails integration configuration.
|
|
18
22
|
def config
|
|
19
23
|
@config ||= Configuration.new
|
|
20
24
|
end
|
|
21
25
|
|
|
26
|
+
# @param config [PostHog::Rails::Configuration] Rails integration configuration.
|
|
22
27
|
attr_writer :config
|
|
23
28
|
|
|
29
|
+
# Configure Rails integration options.
|
|
30
|
+
#
|
|
31
|
+
# @yieldparam config [PostHog::Rails::Configuration]
|
|
32
|
+
# @return [void]
|
|
24
33
|
def configure
|
|
25
34
|
yield config if block_given?
|
|
26
35
|
end
|
|
27
36
|
|
|
28
37
|
# Mark that we're in a web request context
|
|
29
38
|
# CaptureExceptions middleware will handle exception capture
|
|
39
|
+
# @api private
|
|
40
|
+
# @return [void]
|
|
30
41
|
def enter_web_request
|
|
31
42
|
Thread.current[IN_WEB_REQUEST_KEY] = true
|
|
32
43
|
end
|
|
33
44
|
|
|
34
45
|
# Clear web request context (called at end of request)
|
|
46
|
+
# @api private
|
|
47
|
+
# @return [void]
|
|
35
48
|
def exit_web_request
|
|
36
49
|
Thread.current[IN_WEB_REQUEST_KEY] = false
|
|
37
50
|
end
|
|
38
51
|
|
|
39
52
|
# Check if we're currently in a web request context
|
|
40
53
|
# Used by ErrorSubscriber to avoid duplicate captures
|
|
54
|
+
# @api private
|
|
55
|
+
# @return [Boolean]
|
|
41
56
|
def in_web_request?
|
|
42
57
|
Thread.current[IN_WEB_REQUEST_KEY] == true
|
|
43
58
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: posthog-rails
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.9.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- PostHog
|
|
@@ -29,14 +29,14 @@ dependencies:
|
|
|
29
29
|
requirements:
|
|
30
30
|
- - "~>"
|
|
31
31
|
- !ruby/object:Gem::Version
|
|
32
|
-
version: '3.
|
|
32
|
+
version: '3.9'
|
|
33
33
|
type: :runtime
|
|
34
34
|
prerelease: false
|
|
35
35
|
version_requirements: !ruby/object:Gem::Requirement
|
|
36
36
|
requirements:
|
|
37
37
|
- - "~>"
|
|
38
38
|
- !ruby/object:Gem::Version
|
|
39
|
-
version: '3.
|
|
39
|
+
version: '3.9'
|
|
40
40
|
description: Automatic exception tracking and instrumentation for Ruby on Rails applications
|
|
41
41
|
using PostHog
|
|
42
42
|
email: engineering@posthog.com
|
|
@@ -54,7 +54,10 @@ files:
|
|
|
54
54
|
- lib/posthog/rails/error_subscriber.rb
|
|
55
55
|
- lib/posthog/rails/parameter_filter.rb
|
|
56
56
|
- lib/posthog/rails/railtie.rb
|
|
57
|
+
- lib/posthog/rails/request_context.rb
|
|
58
|
+
- lib/posthog/rails/request_metadata.rb
|
|
57
59
|
- lib/posthog/rails/rescued_exception_interceptor.rb
|
|
60
|
+
- lib/posthog/rails/tracing_headers.rb
|
|
58
61
|
homepage: https://github.com/PostHog/posthog-ruby
|
|
59
62
|
licenses:
|
|
60
63
|
- MIT
|