From e352f59a9d1e7610b8c27b9d3833fab4bead2a29 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Sun, 31 May 2026 16:02:45 +0200 Subject: [PATCH 1/2] fix: no-op when disabled or uninitialized --- .changeset/safe-otters-noop.md | 5 +++++ lib/posthog/client.rb | 8 +++++++ posthog-rails/lib/posthog/rails/railtie.rb | 12 +++++------ spec/posthog/client_spec.rb | 13 +++++++++++ spec/posthog/rails/railtie_spec.rb | 25 ++++++++++++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) create mode 100644 .changeset/safe-otters-noop.md diff --git a/.changeset/safe-otters-noop.md b/.changeset/safe-otters-noop.md new file mode 100644 index 0000000..7e36f20 --- /dev/null +++ b/.changeset/safe-otters-noop.md @@ -0,0 +1,5 @@ +--- +'posthog-ruby': patch +--- + +No-op when the SDK is disabled or the Rails facade is used before initialization. diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index 92e4904..90734d0 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -284,6 +284,8 @@ def capture(attrs) # same `$feature/` and `$active_feature_flags` properties as the snapshot. # @return [Boolean, nil] Whether the exception event was queued or sent, or nil if the input could not be parsed. def capture_exception(exception, distinct_id = nil, additional_properties = {}, flags: nil) + return false if @disabled + exception_info = ExceptionCapture.build_parsed_exception(exception) return if exception_info.nil? @@ -310,6 +312,8 @@ def capture_exception(exception, distinct_id = nil, additional_properties = {}, # @return [Boolean] Whether the identify event was queued or sent. # @macro common_attrs def identify(attrs) + return false if @disabled + symbolize_keys! attrs enqueue(FieldParser.parse_for_identify(attrs)) end @@ -325,6 +329,8 @@ def identify(attrs) # @return [Boolean] Whether the group identify event was queued or sent. # @macro common_attrs def group_identify(attrs) + return false if @disabled + symbolize_keys! attrs enqueue(FieldParser.parse_for_group_identify(attrs)) end @@ -337,6 +343,8 @@ def group_identify(attrs) # @return [Boolean] Whether the alias event was queued or sent. # @macro common_attrs def alias(attrs) + return false if @disabled + symbolize_keys! attrs enqueue(FieldParser.parse_for_alias(attrs)) end diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index ef2d0d2..4e08881 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -54,10 +54,10 @@ def initialized? # Fallback for any client methods not explicitly defined. # # @api private - # rubocop:disable Lint/RedundantSafeNavigation def method_missing(method_name, ...) - if client&.respond_to?(method_name) - ensure_initialized! + ensure_initialized! + + if client.respond_to?(method_name) client.public_send(method_name, ...) else super @@ -66,16 +66,16 @@ def method_missing(method_name, ...) # @api private def respond_to_missing?(method_name, include_private = false) - client&.respond_to?(method_name) || super + ensure_initialized! + client.respond_to?(method_name, include_private) || super end - # rubocop:enable Lint/RedundantSafeNavigation private def ensure_initialized! return if initialized? - raise 'PostHog is not initialized. Call PostHog.init in an initializer.' + @client = PostHog::Client.new(api_key: nil) end end end diff --git a/spec/posthog/client_spec.rb b/spec/posthog/client_spec.rb index d4f2237..9a6a36b 100644 --- a/spec/posthog/client_spec.rb +++ b/spec/posthog/client_spec.rb @@ -55,6 +55,19 @@ module PostHog .with(include('api_key is missing or empty after trimming whitespace')) .once end + + it 'no-ops event methods before validating payloads' do + client = Client.new(api_key: api_key) + + expect { client.identify({}) }.not_to raise_error + expect { client.group_identify({}) }.not_to raise_error + expect { client.alias({}) }.not_to raise_error + expect { client.capture_exception(StandardError.new('boom'), nil, 'not a hash') }.not_to raise_error + expect(client.identify({})).to eq(false) + expect(client.group_identify({})).to eq(false) + expect(client.alias({})).to eq(false) + expect(client.capture_exception(StandardError.new('boom'), nil, 'not a hash')).to eq(false) + end end context 'when api_key is nil' do diff --git a/spec/posthog/rails/railtie_spec.rb b/spec/posthog/rails/railtie_spec.rb index 85edd54..0f20b2a 100644 --- a/spec/posthog/rails/railtie_spec.rb +++ b/spec/posthog/rails/railtie_spec.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true # Minimal requires for testing the Railtie in isolation +require 'logger' require 'posthog' require 'rails/railtie' @@ -15,6 +16,30 @@ require 'posthog/rails/railtie' RSpec.describe PostHog::Rails::Railtie do + describe 'posthog.set_configs initializer' do + before do + initializer = PostHog::Rails::Railtie.initializers.find { |i| i.name == 'posthog.set_configs' } + PostHog::Rails::Railtie.instance.instance_exec(double('app'), &initializer.block) + PostHog::Logging.logger = Logger.new(File::NULL) + PostHog.client = nil + end + + after do + PostHog.client = nil + end + + it 'no-ops delegated calls before explicit init' do + expect { PostHog.capture(event: 'event', distinct_id: 'user') }.not_to raise_error + expect(PostHog.capture(event: 'event', distinct_id: 'user')).to eq(false) + expect(PostHog.identify(distinct_id: 'user')).to eq(false) + expect(PostHog.alias(alias: 'anon', distinct_id: 'user')).to eq(false) + expect(PostHog.group_identify(group_type: 'organization', group_key: 'id:5')).to eq(false) + expect(PostHog.get_feature_flag('flag', 'user')).to be_nil + expect(PostHog.get_all_flags('user')).to eq({}) + expect(PostHog.evaluate_flags('user').keys).to eq([]) + end + end + 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 From 07f6d2627081665a20593ed1d2dac5099eb641c2 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 1 Jun 2026 08:49:21 +0200 Subject: [PATCH 2/2] address pr review feedback --- lib/posthog/client.rb | 4 +++- posthog-rails/lib/posthog/rails/railtie.rb | 2 +- spec/posthog/client_spec.rb | 20 ++++++++++++-------- spec/posthog/rails/railtie_spec.rb | 6 +++++- 4 files changed, 21 insertions(+), 11 deletions(-) diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index 90734d0..f551c48 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -106,7 +106,9 @@ def initialize(opts = {}) @feature_flags_poller = nil @personal_api_key = opts[:personal_api_key] - logger.error('api_key is missing or empty after trimming whitespace; check your project API key') if @disabled + if @disabled && !opts[:silence_disabled_client_error] + logger.error('api_key is missing or empty after trimming whitespace; check your project API key') + end # Warn when multiple clients are created with the same API key (can cause dropped events) unless @disabled || opts[:test_mode] || opts[:disable_singleton_warning] diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index 4e08881..e1cf6e0 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -75,7 +75,7 @@ def respond_to_missing?(method_name, include_private = false) def ensure_initialized! return if initialized? - @client = PostHog::Client.new(api_key: nil) + @client = PostHog::Client.new(api_key: nil, silence_disabled_client_error: true) end end end diff --git a/spec/posthog/client_spec.rb b/spec/posthog/client_spec.rb index 9a6a36b..deea587 100644 --- a/spec/posthog/client_spec.rb +++ b/spec/posthog/client_spec.rb @@ -58,15 +58,19 @@ module PostHog it 'no-ops event methods before validating payloads' do client = Client.new(api_key: api_key) + event_methods = [ + [:identify, [{}]], + [:group_identify, [{}]], + [:alias, [{}]], + [:capture_exception, [StandardError.new('boom'), nil, 'not a hash']] + ] - expect { client.identify({}) }.not_to raise_error - expect { client.group_identify({}) }.not_to raise_error - expect { client.alias({}) }.not_to raise_error - expect { client.capture_exception(StandardError.new('boom'), nil, 'not a hash') }.not_to raise_error - expect(client.identify({})).to eq(false) - expect(client.group_identify({})).to eq(false) - expect(client.alias({})).to eq(false) - expect(client.capture_exception(StandardError.new('boom'), nil, 'not a hash')).to eq(false) + event_methods.each do |method_name, args| + aggregate_failures(method_name) do + expect { client.public_send(method_name, *args) }.not_to raise_error + expect(client.public_send(method_name, *args)).to eq(false) + end + end end end diff --git a/spec/posthog/rails/railtie_spec.rb b/spec/posthog/rails/railtie_spec.rb index 0f20b2a..0c5677a 100644 --- a/spec/posthog/rails/railtie_spec.rb +++ b/spec/posthog/rails/railtie_spec.rb @@ -28,7 +28,10 @@ PostHog.client = nil end - it 'no-ops delegated calls before explicit init' do + it 'no-ops delegated calls before explicit init without logging a missing api_key error' do + logger = instance_spy(Logger) + PostHog::Logging.logger = logger + expect { PostHog.capture(event: 'event', distinct_id: 'user') }.not_to raise_error expect(PostHog.capture(event: 'event', distinct_id: 'user')).to eq(false) expect(PostHog.identify(distinct_id: 'user')).to eq(false) @@ -37,6 +40,7 @@ expect(PostHog.get_feature_flag('flag', 'user')).to be_nil expect(PostHog.get_all_flags('user')).to eq({}) expect(PostHog.evaluate_flags('user').keys).to eq([]) + expect(logger).not_to have_received(:error) end end