posthog-rails 3.8.1 → 3.9.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5f7082d07840d3127b46409cfaa97b2d169cb0520d1589c9c5fdf7cf875a403b
4
- data.tar.gz: 2371da2d51b6977fd8a2544270fae72c8c6e5e9465783ca9fd32bf118b85ae8c
3
+ metadata.gz: e9d0502771ef899181fbcf4acbbaf7b3c72000e5082ef69e98163bf43ae57002
4
+ data.tar.gz: 6002687536b139e97f4e4afc361f17ec519fc0fa54b16416bf888ba55e9b37f7
5
5
  SHA512:
6
- metadata.gz: 1eb75d92349a07684187e06367675d7e4a844f7c9fb04616e9c85a0c88e94f33a8adce88f092c1a476813523ebfb0b5ee317cb5013b17b9eb15b31209218fed1
7
- data.tar.gz: '0338af574847d6c9f52e1c3f43b7f7b7401ee9966a1d0816d3b1a5967b3a4689d584e1801cfa50101e0106f4692239eb0ac2a9be5e7f7533099bca7266730705'
6
+ metadata.gz: 339d21bdceb568ff493da11539db72fb0ca32d61cc50e18d12facc6a7920761554cc15a2cc85a45adf60a9b8fa324ad6129fb09ca176e180b3c7a4002f08e2ec
7
+ data.tar.gz: b6f2417f98317d42a5cdad3f15e7b2ae1d00254fd63571768a01956ffbb4cc0e398892409f65eb69167941a19392fc549c33406723b3cc36cef38b8f0f49017c
@@ -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
- # Capture user context with exceptions (default: true)
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)
@@ -18,6 +18,7 @@ module PostHog
18
18
  PostHog::Rails.enter_web_request
19
19
 
20
20
  response = @app.call(env)
21
+ env['posthog.response_status_code'] = response_status(response)
21
22
 
22
23
  # Check if there was an exception that Rails handled
23
24
  exception = collect_exception(env)
@@ -51,7 +52,7 @@ module PostHog
51
52
 
52
53
  def capture_exception(exception, env)
53
54
  request = ActionDispatch::Request.new(env)
54
- distinct_id = extract_distinct_id(env, request)
55
+ distinct_id = extract_distinct_id(env)
55
56
  additional_properties = build_properties(request, env)
56
57
 
57
58
  PostHog.capture_exception(exception, distinct_id, additional_properties)
@@ -60,20 +61,21 @@ module PostHog
60
61
  PostHog::Logging.logger.error("Backtrace: #{e.backtrace&.first(5)&.join("\n")}")
61
62
  end
62
63
 
63
- def extract_distinct_id(env, request)
64
- # Try to get user from controller if capture_user_context is enabled
64
+ def extract_distinct_id(env)
65
+ # Prefer authenticated Rails user context. Request/tracing context is
66
+ # applied later by the core capture path if this returns nil.
65
67
  if PostHog::Rails.config&.capture_user_context && env['action_controller.instance']
66
68
  controller = env['action_controller.instance']
67
69
  method_name = PostHog::Rails.config&.current_user_method || :current_user
68
70
 
69
71
  if controller.respond_to?(method_name, true)
70
72
  user = controller.send(method_name)
71
- return extract_user_id(user) if user
73
+ user_id = extract_user_id(user) if user
74
+ return user_id if present?(user_id)
72
75
  end
73
76
  end
74
77
 
75
- # Fallback to session ID or nil
76
- request.session&.id&.to_s
78
+ nil
77
79
  end
78
80
 
79
81
  def extract_user_id(user)
@@ -98,10 +100,7 @@ module PostHog
98
100
 
99
101
  def build_properties(request, env)
100
102
  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)
103
+ '$exception_source' => 'rails'
105
104
  }
106
105
 
107
106
  # Add controller and action if available
@@ -118,14 +117,23 @@ module PostHog
118
117
  properties['$request_params'] = safe_serialize(filtered_params) unless filtered_params.empty?
119
118
  end
120
119
 
121
- # Add user agent
122
- properties['$user_agent'] = safe_serialize(request.user_agent) if request.user_agent
120
+ response_status_code = env['posthog.response_status_code']
121
+ properties['$response_status_code'] = response_status_code if response_status_code
123
122
 
124
123
  # Add referrer
125
124
  properties['$referrer'] = safe_serialize(request.referrer) if request.referrer
126
125
 
127
126
  properties
128
127
  end
128
+
129
+ def response_status(response)
130
+ status = response.respond_to?(:[]) ? response[0] : nil
131
+ status if status.is_a?(Integer)
132
+ end
133
+
134
+ def present?(value)
135
+ !(value.nil? || (value.respond_to?(:empty?) && value.empty?))
136
+ end
129
137
  end
130
138
  end
131
139
  end
@@ -15,6 +15,9 @@ module PostHog
15
15
  # List of exception classes to ignore (in addition to default)
16
16
  attr_accessor :excluded_exceptions
17
17
 
18
+ # Whether to use PostHog tracing headers for request-scoped identity/session context
19
+ attr_accessor :use_tracing_headers
20
+
18
21
  # Whether to capture the current user context in exceptions
19
22
  attr_accessor :capture_user_context
20
23
 
@@ -30,6 +33,7 @@ module PostHog
30
33
  @report_rescued_exceptions = false
31
34
  @auto_instrument_active_job = false
32
35
  @excluded_exceptions = []
36
+ @use_tracing_headers = true
33
37
  @capture_user_context = true
34
38
  @current_user_method = :current_user
35
39
  @user_id_method = nil
@@ -73,8 +73,15 @@ module PostHog
73
73
  end
74
74
  end
75
75
 
76
- # Insert middleware for exception capturing
76
+ # Insert middleware for request context and exception capturing
77
77
  initializer 'posthog.insert_middlewares' do |app|
78
+ # Wrap the Rails exception middleware so request context is active for
79
+ # downstream handlers and exception capture.
80
+ insert_middleware_before(
81
+ app, ActionDispatch::ShowExceptions,
82
+ PostHog::Rails::RequestContext
83
+ )
84
+
78
85
  # Insert after DebugExceptions to catch rescued exceptions
79
86
  insert_middleware_after(
80
87
  app, ActionDispatch::DebugExceptions,
@@ -118,6 +125,13 @@ module PostHog
118
125
  app.config.middleware.insert_after(target, middleware)
119
126
  end
120
127
 
128
+ def insert_middleware_before(app, target, middleware)
129
+ # During initialization, app.config.middleware is a MiddlewareStackProxy
130
+ # which only supports recording operations (insert_before, use, etc.)
131
+ # and does NOT support query methods like include?.
132
+ app.config.middleware.insert_before(target, middleware)
133
+ end
134
+
121
135
  def self.register_error_subscriber
122
136
  return unless PostHog::Rails.config&.auto_capture_exceptions
123
137
 
@@ -0,0 +1,58 @@
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
+ class RequestContext
11
+ def initialize(app)
12
+ @app = app
13
+ end
14
+
15
+ def call(env)
16
+ request = build_request(env)
17
+
18
+ Internal::Context.with_context(context_data(request), fresh: true) do
19
+ @app.call(env)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def context_data(request)
26
+ data = { properties: request_properties(request) }
27
+ return data unless use_tracing_headers?
28
+
29
+ data.merge(
30
+ distinct_id: tracing_header(request, 'X-POSTHOG-DISTINCT-ID'),
31
+ session_id: tracing_header(request, 'X-POSTHOG-SESSION-ID')
32
+ )
33
+ end
34
+
35
+ def use_tracing_headers?
36
+ PostHog::Rails.config&.use_tracing_headers != false
37
+ end
38
+
39
+ def request_properties(request)
40
+ RequestMetadata.extract(request)
41
+ end
42
+
43
+ def tracing_header(request, header_name)
44
+ TracingHeaders.extract_header(request, header_name)
45
+ end
46
+
47
+ def build_request(env)
48
+ if defined?(ActionDispatch::Request)
49
+ ActionDispatch::Request.new(env)
50
+ elsif defined?(Rack::Request)
51
+ Rack::Request.new(env)
52
+ else
53
+ env
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,76 @@
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
+ module RequestMetadata
9
+ module_function
10
+
11
+ def extract(request)
12
+ properties = {}
13
+ add_property(properties, '$current_url', current_url(request))
14
+ request_method = request_value(request, :request_method) || request_value(request, :method)
15
+ add_property(properties, '$request_method', request_method)
16
+ add_property(properties, '$request_path', request_value(request, :path) || request_value(request, :path_info))
17
+ add_property(properties, '$user_agent', TracingHeaders.extract_header(request, 'User-Agent'))
18
+ add_property(properties, '$ip', client_ip(request))
19
+ properties
20
+ end
21
+
22
+ def current_url(request)
23
+ url = request_value(request, :url)
24
+ return if url.nil?
25
+
26
+ url.to_s.split('?', 2).first
27
+ end
28
+ private_class_method :current_url
29
+
30
+ def client_ip(request)
31
+ trusted_ip = request_value(request, :remote_ip) || request_value(request, :ip)
32
+ return trusted_ip if present?(trusted_ip)
33
+
34
+ forwarded_for = TracingHeaders.extract_header(request, 'X-Forwarded-For')
35
+ forwarded_ip = forwarded_for.split(',').first&.strip if forwarded_for
36
+ return forwarded_ip if present?(forwarded_ip)
37
+
38
+ env_value(request, 'REMOTE_ADDR')
39
+ end
40
+ private_class_method :client_ip
41
+
42
+ def present?(value)
43
+ !(value.nil? || (value.respond_to?(:empty?) && value.empty?))
44
+ end
45
+ private_class_method :present?
46
+
47
+ def add_property(properties, key, value)
48
+ return if value.nil?
49
+
50
+ serialized = value.to_s
51
+ return if serialized.empty?
52
+
53
+ properties[key] = serialized
54
+ end
55
+ private_class_method :add_property
56
+
57
+ def request_value(request, method_name)
58
+ return unless request.respond_to?(method_name)
59
+
60
+ request.public_send(method_name)
61
+ rescue StandardError
62
+ nil
63
+ end
64
+ private_class_method :request_value
65
+
66
+ def env_value(request, key)
67
+ request.respond_to?(:get_header) ? request.get_header(key) : request.env[key]
68
+ rescue StandardError
69
+ nil
70
+ end
71
+ private_class_method :env_value
72
+ end
73
+
74
+ private_constant :RequestMetadata
75
+ end
76
+ end
@@ -0,0 +1,77 @@
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
+ module TracingHeaders
7
+ MAX_HEADER_VALUE_LENGTH = 1000
8
+ CONTROL_CHARACTERS = /[[:cntrl:]]/
9
+
10
+ module_function
11
+
12
+ def sanitize_header_value(value)
13
+ return nil unless value.is_a?(String)
14
+
15
+ sanitized = value.strip.gsub(CONTROL_CHARACTERS, '').strip
16
+ return nil if sanitized.empty?
17
+
18
+ sanitized[0, MAX_HEADER_VALUE_LENGTH]
19
+ end
20
+
21
+ def extract_header(request_or_env, header_name)
22
+ candidates = header_candidates(header_name)
23
+
24
+ candidates.each do |candidate|
25
+ value = header_value(request_or_env, candidate)
26
+ sanitized = sanitize_header_value(value)
27
+ return sanitized if sanitized
28
+ end
29
+
30
+ env = request_env(request_or_env)
31
+ return nil unless env.respond_to?(:each)
32
+
33
+ target_names = candidates.map { |candidate| normalize_header_name(candidate) }
34
+ env.each do |key, value|
35
+ next unless target_names.include?(normalize_header_name(key))
36
+
37
+ sanitized = sanitize_header_value(value)
38
+ return sanitized if sanitized
39
+ end
40
+
41
+ nil
42
+ end
43
+
44
+ def header_candidates(header_name)
45
+ canonical = header_name.to_s
46
+ rack = "HTTP_#{canonical.upcase.tr('-', '_')}"
47
+ [canonical, canonical.downcase, rack]
48
+ end
49
+ private_class_method :header_candidates
50
+
51
+ def header_value(request_or_env, header_name)
52
+ if request_or_env.respond_to?(:headers)
53
+ value = request_or_env.headers[header_name]
54
+ return value unless value.nil?
55
+ end
56
+
57
+ env = request_env(request_or_env)
58
+ return nil unless env.respond_to?(:[])
59
+
60
+ env[header_name]
61
+ end
62
+ private_class_method :header_value
63
+
64
+ def request_env(request_or_env)
65
+ request_or_env.respond_to?(:env) ? request_or_env.env : request_or_env
66
+ end
67
+ private_class_method :request_env
68
+
69
+ def normalize_header_name(header_name)
70
+ header_name.to_s.upcase.tr('-', '_')
71
+ end
72
+ private_class_method :normalize_header_name
73
+ end
74
+
75
+ private_constant :TracingHeaders
76
+ end
77
+ 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'
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.8.1
4
+ version: 3.9.0
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.8'
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.8'
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