From a961dc4b6991efd1961c3d31d1a71f96a04e6aea Mon Sep 17 00:00:00 2001 From: John Nagro Date: Fri, 6 Feb 2026 14:36:47 -0500 Subject: [PATCH 1/7] fix: Fix Railtie middleware insertion crashing on Rails initialization The initializer block called insert_middleware_after as a class method, but Rails runs initializer blocks via instance_exec on the Railtie instance. Also removed the include? check on MiddlewareStackProxy which doesn't support query methods during initialization. --- Gemfile | 1 + posthog-rails/lib/posthog/rails/railtie.rb | 12 ++--- spec/posthog/rails/railtie_spec.rb | 56 ++++++++++++++++++++++ 3 files changed, 62 insertions(+), 7 deletions(-) create mode 100644 spec/posthog/rails/railtie_spec.rb diff --git a/Gemfile b/Gemfile index 9291bc8..72d5bbb 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,7 @@ gem 'irb' group :development, :test do gem 'activesupport', '~> 7.1' + gem 'railties', '~> 7.1' gem 'commander', '~> 5.0' gem 'oj', '~> 3.16.10' gem 'prettier' diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index 292731d..62bb72a 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -111,13 +111,11 @@ def ensure_initialized! at_exit { PostHog.client&.shutdown if PostHog.initialized? } end - def self.insert_middleware_after(app, target, middleware) - if app.config.middleware.include?(target) - app.config.middleware.insert_after(target, middleware) - else - # Fallback: append to stack if target middleware is missing (e.g., API-only apps) - app.config.middleware.use(middleware) - end + def insert_middleware_after(app, target, middleware) + # During initialization, app.config.middleware is a MiddlewareStackProxy + # which only supports recording operations (insert_after, use, etc.) + # and does NOT support query methods like include?. + app.config.middleware.insert_after(target, middleware) end def self.register_error_subscriber diff --git a/spec/posthog/rails/railtie_spec.rb b/spec/posthog/rails/railtie_spec.rb new file mode 100644 index 0000000..f873195 --- /dev/null +++ b/spec/posthog/rails/railtie_spec.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +# Minimal requires for testing the Railtie in isolation +require 'posthog' +require 'rails/railtie' + +# The posthog-rails lib has its own gemspec and isn't in the default load path, +# so we add it manually for testing. +$LOAD_PATH.unshift File.expand_path('../../../posthog-rails/lib', __dir__) + +# Load just enough of posthog-rails to define the Railtie. +# Middleware classes (CaptureExceptions, etc.) are only referenced inside +# initializer blocks, not at file-load time, so we don't need them here. +require 'posthog/rails/configuration' +require 'posthog/rails/railtie' + +RSpec.describe PostHog::Rails::Railtie do + describe 'posthog.insert_middlewares initializer' do + it 'has insert_middleware_after accessible from initializer context' do + # Rails initializer blocks are executed via instance_exec on the Railtie + # instance (see railties/lib/rails/initializable.rb). This means `self` + # inside the block is the Railtie INSTANCE, not the class. + # + # Any method called without an explicit receiver in the block must be + # defined as an instance method (or delegated to one). + railtie = PostHog::Rails::Railtie.instance + expect(railtie).to respond_to(:insert_middleware_after) + end + + it 'successfully calls insert_middleware_after when the initializer runs' do + # Stub the middleware constants referenced in the initializer block + stub_const('ActionDispatch::DebugExceptions', Class.new) + stub_const('ActionDispatch::ShowExceptions', Class.new) + stub_const('PostHog::Rails::RescuedExceptionInterceptor', Class.new) + stub_const('PostHog::Rails::CaptureExceptions', Class.new) + + # Find the initializer by name + initializer = PostHog::Rails::Railtie.initializers.find { |i| i.name == 'posthog.insert_middlewares' } + expect(initializer).not_to be_nil + + # During initialization, app.config.middleware is a MiddlewareStackProxy + # which only supports recording operations — NOT query methods like include?. + # The mock must reflect this accurately. + middleware_proxy = double('MiddlewareStackProxy', insert_after: true) + app = double('app', config: double('config', middleware: middleware_proxy)) + + # Reproduce the exact execution context: the block is run via instance_exec + # on the Railtie instance, with the app passed as the block argument. + # This is how Rails runs initializer blocks internally. + railtie = PostHog::Rails::Railtie.instance + expect { + railtie.instance_exec(app, &initializer.block) + }.not_to raise_error + end + end +end From 83ecaa09e0b8c870ba9320b869142fe00734ded0 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Fri, 6 Feb 2026 15:40:23 -0500 Subject: [PATCH 2/7] fix: Prevent sending empty batches and handle non-JSON response bodies When a message exceeds the 32KB size limit, it is silently dropped from the batch. Previously the worker would still send the empty batch to the API, which returned a non-JSON response, causing a JSON::ParserError that triggered 10 pointless retries. - Skip transport request when the batch is empty after consuming the queue - Rescue JSON::ParserError in Transport#send and fall back to the raw response body instead of crashing --- lib/posthog/send_worker.rb | 6 ++++-- lib/posthog/transport.rb | 7 ++++++- spec/posthog/transport_spec.rb | 12 ++++++------ 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/posthog/send_worker.rb b/lib/posthog/send_worker.rb index 589f5b9..5e1fa8f 100644 --- a/lib/posthog/send_worker.rb +++ b/lib/posthog/send_worker.rb @@ -43,8 +43,10 @@ def run consume_message_from_queue! until @batch.full? || @queue.empty? end - res = @transport.send @api_key, @batch - @on_error.call(res.status, res.error) unless res.status == 200 + unless @batch.empty? + res = @transport.send @api_key, @batch + @on_error.call(res.status, res.error) unless res.status == 200 + end @lock.synchronize { @batch.clear } end diff --git a/lib/posthog/transport.rb b/lib/posthog/transport.rb index cc9bf52..77ea1ec 100644 --- a/lib/posthog/transport.rb +++ b/lib/posthog/transport.rb @@ -50,7 +50,12 @@ def send(api_key, batch) last_response, exception = retry_with_backoff(@retries) do status_code, body = send_request(api_key, batch) - error = JSON.parse(body)['error'] + error = + begin + JSON.parse(body)['error'] + rescue JSON::ParserError + body + end should_retry = should_retry_request?(status_code, body) logger.debug("Response status code: #{status_code}") logger.debug("Response error: #{error}") if error diff --git a/spec/posthog/transport_spec.rb b/spec/posthog/transport_spec.rb index 7dcaa48..7e7b18a 100644 --- a/spec/posthog/transport_spec.rb +++ b/spec/posthog/transport_spec.rb @@ -225,21 +225,21 @@ module PostHog it_behaves_like('non-retried request', 400, '{}') end - context 'request or parsing of response results in an exception' do + context 'response body is malformed JSON' do let(:response_body) { 'Malformed JSON ---' } subject { described_class.new(retries: 0) } - it 'returns a -1 for status' do - expect(subject.send(api_key, batch).status).to eq(-1) + it 'returns the HTTP status code' do + expect(subject.send(api_key, batch).status).to eq(200) end - it 'has a connection error' do + it 'uses the raw body as the error' do error = subject.send(api_key, batch).error - expect(error).to match(/unexpected character.*Malformed/) + expect(error).to eq('Malformed JSON ---') end - it_behaves_like('retried request', 200, 'Malformed JSON ---') + it_behaves_like('non-retried request', 200, 'Malformed JSON ---') end end end From 0965e2d7e9cdfb95885f419909af4eac3f344028 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Fri, 6 Feb 2026 15:46:36 -0500 Subject: [PATCH 3/7] fix: Use $current_url property so exception URL appears in PostHog UI The CaptureExceptions middleware was setting $request_url, which is not recognized by PostHog for the "URL / Screen" column. Renamed to $current_url to match the standard property used across PostHog SDKs. --- posthog-rails/lib/posthog/rails/capture_exceptions.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/posthog-rails/lib/posthog/rails/capture_exceptions.rb b/posthog-rails/lib/posthog/rails/capture_exceptions.rb index c17534e..657ab0e 100644 --- a/posthog-rails/lib/posthog/rails/capture_exceptions.rb +++ b/posthog-rails/lib/posthog/rails/capture_exceptions.rb @@ -99,7 +99,7 @@ def extract_user_id(user) def build_properties(request, env) properties = { '$exception_source' => 'rails', - '$request_url' => safe_serialize(request.url), + '$current_url' => safe_serialize(request.url), '$request_method' => safe_serialize(request.method), '$request_path' => safe_serialize(request.path) } From ddeb7dfb559efa0da2a201406ccbb5b92b0b12dc Mon Sep 17 00:00:00 2001 From: John Nagro Date: Fri, 6 Feb 2026 15:59:03 -0500 Subject: [PATCH 4/7] fix: Only include source context lines for in-app exception frames Exception payloads frequently exceeded the 32KB message limit because context lines (11 lines of source code) were read for every stack frame, including gem and framework frames. A typical Rails backtrace has ~50 frames, most from Rails internals, easily blowing past the limit and causing the event to be silently dropped. Now context lines are only added for in_app frames. Gem/framework frames still include file, line number, and function name. Fixes #88 --- lib/posthog/exception_capture.rb | 2 +- spec/posthog/exception_capture_spec.rb | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/lib/posthog/exception_capture.rb b/lib/posthog/exception_capture.rb index 3bccfef..73976de 100644 --- a/lib/posthog/exception_capture.rb +++ b/lib/posthog/exception_capture.rb @@ -67,7 +67,7 @@ def self.parse_backtrace_line(line) 'platform' => 'ruby' } - add_context_lines(frame, file, lineno) if File.exist?(file) + add_context_lines(frame, file, lineno) if frame['in_app'] && File.exist?(file) frame end diff --git a/spec/posthog/exception_capture_spec.rb b/spec/posthog/exception_capture_spec.rb index daef028..320a9fa 100644 --- a/spec/posthog/exception_capture_spec.rb +++ b/spec/posthog/exception_capture_spec.rb @@ -25,6 +25,28 @@ module PostHog expect(frame['in_app']).to be false end + + it 'does not add context lines for non-in_app frames' do + # Use a gem-style path that points to this real file so File.exist? would be true + # but in_app should be false, so context lines should not be added + gem_line = "#{__FILE__}:10:in `gem_method'" + .gsub(%r{/spec/}, '/gems/ruby/spec/') + frame = described_class.parse_backtrace_line(gem_line) + + expect(frame['in_app']).to be false + expect(frame['context_line']).to be_nil + expect(frame['pre_context']).to be_nil + expect(frame['post_context']).to be_nil + end + + it 'adds context lines for in_app frames' do + # Use a real in_app path so File.exist? is true and in_app is true + app_line = "#{__FILE__}:10:in `app_method'" + frame = described_class.parse_backtrace_line(app_line) + + expect(frame['in_app']).to be true + expect(frame['context_line']).not_to be_nil + end end describe '#add_context_lines' do From 21f7bd749afbad9f68756d40a413b5a49d7feb77 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sun, 8 Feb 2026 10:12:03 -0500 Subject: [PATCH 5/7] rubocop autocorrect applied --- Gemfile | 2 +- spec/posthog/exception_capture_spec.rb | 2 +- spec/posthog/rails/railtie_spec.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Gemfile b/Gemfile index 72d5bbb..4aa32b8 100644 --- a/Gemfile +++ b/Gemfile @@ -8,10 +8,10 @@ gem 'irb' group :development, :test do gem 'activesupport', '~> 7.1' - gem 'railties', '~> 7.1' gem 'commander', '~> 5.0' gem 'oj', '~> 3.16.10' gem 'prettier' + gem 'railties', '~> 7.1' gem 'rake', '~> 13.2.1' gem 'rspec', '~> 3.13' gem 'rubocop', '~> 1.75.6' diff --git a/spec/posthog/exception_capture_spec.rb b/spec/posthog/exception_capture_spec.rb index 320a9fa..8136fac 100644 --- a/spec/posthog/exception_capture_spec.rb +++ b/spec/posthog/exception_capture_spec.rb @@ -30,7 +30,7 @@ module PostHog # Use a gem-style path that points to this real file so File.exist? would be true # but in_app should be false, so context lines should not be added gem_line = "#{__FILE__}:10:in `gem_method'" - .gsub(%r{/spec/}, '/gems/ruby/spec/') + .gsub(%r{/spec/}, '/gems/ruby/spec/') frame = described_class.parse_backtrace_line(gem_line) expect(frame['in_app']).to be false diff --git a/spec/posthog/rails/railtie_spec.rb b/spec/posthog/rails/railtie_spec.rb index f873195..340a344 100644 --- a/spec/posthog/rails/railtie_spec.rb +++ b/spec/posthog/rails/railtie_spec.rb @@ -48,9 +48,9 @@ # on the Railtie instance, with the app passed as the block argument. # This is how Rails runs initializer blocks internally. railtie = PostHog::Rails::Railtie.instance - expect { + expect do railtie.instance_exec(app, &initializer.block) - }.not_to raise_error + end.not_to raise_error end end end From 5b3eee83021e270704d1e52a90601c21016f2eee Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sun, 8 Feb 2026 10:15:16 -0500 Subject: [PATCH 6/7] chore: bump version to 3.5.2 and update CHANGELOG --- CHANGELOG.md | 7 +++++++ lib/posthog/version.rb | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 86e1e68..2194e18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## 3.5.2 - 2026-02-08 + +1. fix: Fix Railtie middleware insertion crashing on Rails initialization — changed `insert_middleware_after` from a class method to an instance method (matching how Rails executes initializer blocks via `instance_exec`), and removed the unsupported `include?` query on `MiddlewareStackProxy` +2. fix: Prevent sending empty batches and handle non-JSON response bodies gracefully in transport layer +3. fix: Use `$current_url` property (instead of `$request_url`) so exception URLs appear correctly in the PostHog UI +4. fix: Only include source context lines for in-app exception frames, avoiding unnecessary reads of gem source files + ## 3.5.1 - 2026-02-06 1. Fix `posthog-rails` deployment diff --git a/lib/posthog/version.rb b/lib/posthog/version.rb index 8f1d8d1..0a5f628 100644 --- a/lib/posthog/version.rb +++ b/lib/posthog/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module PostHog - VERSION = '3.5.1' + VERSION = '3.5.2' end From fe09e48519a1dbb8fe612a67b12fcc10b6c00904 Mon Sep 17 00:00:00 2001 From: John Nagro Date: Sun, 8 Feb 2026 10:27:27 -0500 Subject: [PATCH 7/7] adding GH issues to CHANGELOG --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a3aef8d..1cbf611 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,9 @@ ## 3.5.3 - 2026-02-08 -1. fix: Fix Railtie middleware insertion crashing on Rails initialization — changed `insert_middleware_after` from a class method to an instance method (matching how Rails executes initializer blocks via `instance_exec`), and removed the unsupported `include?` query on `MiddlewareStackProxy` -2. fix: Prevent sending empty batches and handle non-JSON response bodies gracefully in transport layer +1. fix: Fix Railtie middleware insertion crashing on Rails initialization — changed `insert_middleware_after` from a class method to an instance method (matching how Rails executes initializer blocks via `instance_exec`), and removed the unsupported `include?` query on `MiddlewareStackProxy` ([#97](https://github.com/PostHog/posthog-ruby/issues/97)) +2. fix: Prevent sending empty batches and handle non-JSON response bodies gracefully in transport layer ([#87](https://github.com/PostHog/posthog-ruby/issues/87)) 3. fix: Use `$current_url` property (instead of `$request_url`) so exception URLs appear correctly in the PostHog UI -4. fix: Only include source context lines for in-app exception frames, avoiding unnecessary reads of gem source files +4. fix: Only include source context lines for in-app exception frames, avoiding unnecessary reads of gem source files ([#88](https://github.com/PostHog/posthog-ruby/issues/88)) ## 3.5.2 - 2026-02-06