diff --git a/CHANGELOG.md b/CHANGELOG.md index 88894b8..882bc70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,8 @@ +1. fix: Warn when multiple PostHog client instances are created with the same API key ([#57](https://github.com/PostHog/posthog-ruby/issues/57)) + - Multiple instances can cause dropped events and inconsistent behavior + - Use `disable_singleton_warning: true` when intentionally creating multiple clients (e.g., for different projects) + - Documentation updated with singleton best practices + ## 3.5.5 - 2026-03-04 1. feat: Add semver comparison operators for local feature flag evaluation ([#107](https://github.com/PostHog/posthog-ruby/pull/107)) diff --git a/README.md b/README.md index 430ff63..18ba223 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,9 @@ Please see the main [PostHog docs](https://posthog.com/docs). Specifically, the [Ruby integration](https://posthog.com/docs/integrations/ruby-integration) details. +> [!IMPORTANT] +> **Use a single client instance (singleton)** — Create the PostHog client once and reuse it throughout your application. Multiple client instances with the same API key can cause dropped events and inconsistent behavior. The SDK will log a warning if it detects multiple instances. For Rails apps, use `PostHog.init` in an initializer (see [posthog-rails](posthog-rails/README.md)). + > [!IMPORTANT] > Supports Ruby 3.2 and above > diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index 8e9355c..c9fc619 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -17,6 +17,35 @@ class Client include PostHog::Utils include PostHog::Logging + # Thread-safe tracking of client instances per API key for singleton warnings + @instances_by_api_key = {} + @instances_mutex = Mutex.new + + class << self + # Resets instance tracking. Used primarily for testing. + # In production, instance counts persist for the lifetime of the process. + def reset_instance_tracking! + @instances_mutex.synchronize do + @instances_by_api_key = {} + end + end + + def _increment_instance_count(api_key) + @instances_mutex.synchronize do + count = @instances_by_api_key[api_key] || 0 + @instances_by_api_key[api_key] = count + 1 + count + end + end + + def _decrement_instance_count(api_key) + @instances_mutex.synchronize do + count = (@instances_by_api_key[api_key] || 1) - 1 + @instances_by_api_key[api_key] = [count, 0].max + end + end + end + # @param [Hash] opts # @option opts [String] :api_key Your project's api_key # @option opts [String] :personal_api_key Your personal API key @@ -32,6 +61,8 @@ class Client # Measured in seconds, defaults to 3. # @option opts [Proc] :before_send A block that receives the event hash and should return either a modified hash # to be sent to PostHog or nil to prevent the event from being sent. e.g. `before_send: ->(event) { event }` + # @option opts [Bool] :disable_singleton_warning +true+ to suppress the warning when multiple clients + # share the same API key. Use only when you intentionally need multiple clients. Defaults to +false+. def initialize(opts = {}) symbolize_keys!(opts) @@ -52,6 +83,19 @@ def initialize(opts = {}) check_api_key! + # Warn when multiple clients are created with the same API key (can cause dropped events) + unless opts[:test_mode] || opts[:disable_singleton_warning] + previous_count = self.class._increment_instance_count(@api_key) + if previous_count >= 1 + logger.warn( + 'Multiple PostHog client instances detected for the same API key. ' \ + 'This can cause dropped events and inconsistent behavior. ' \ + 'Use a singleton pattern: instantiate once and reuse the client. ' \ + 'See https://posthog.com/docs/libraries/ruby' + ) + end + end + @feature_flags_poller = FeatureFlagsPoller.new( opts[:feature_flags_polling_interval], @@ -444,6 +488,7 @@ def reload_feature_flags end def shutdown + self.class._decrement_instance_count(@api_key) if @api_key @feature_flags_poller.shutdown_poller flush end diff --git a/posthog-rails/README.md b/posthog-rails/README.md index 78c1292..2686984 100644 --- a/posthog-rails/README.md +++ b/posthog-rails/README.md @@ -40,6 +40,8 @@ This will create `config/initializers/posthog.rb` with sensible defaults and doc ## Configuration +PostHog.init creates a single client instance used across your app. Avoid creating multiple `PostHog::Client` instances with the same API key — it can cause dropped events. + The generated initializer at `config/initializers/posthog.rb` includes all available options: ```ruby diff --git a/posthog-rails/lib/generators/posthog/templates/posthog.rb b/posthog-rails/lib/generators/posthog/templates/posthog.rb index 3b6fc81..2f0ca2d 100644 --- a/posthog-rails/lib/generators/posthog/templates/posthog.rb +++ b/posthog-rails/lib/generators/posthog/templates/posthog.rb @@ -46,6 +46,8 @@ # CORE POSTHOG CONFIGURATION # ============================================================================ # Initialize the PostHog client with core SDK options. +# IMPORTANT: Use PostHog.init once — creating multiple clients can cause dropped events. +# PostHog.init provides a singleton-like pattern; use PostHog.capture, PostHog.identify, etc. PostHog.init do |config| # ============================================================================ diff --git a/spec/posthog/client_spec.rb b/spec/posthog/client_spec.rb index c01b19a..002486f 100644 --- a/spec/posthog/client_spec.rb +++ b/spec/posthog/client_spec.rb @@ -36,6 +36,61 @@ module PostHog skip_ssl_verification: true }) expect { Client.new api_key: API_KEY, skip_ssl_verification: true }.to_not raise_error end + + context 'singleton warning' do + before do + # Stub HTTP to allow creating clients without test_mode (which triggers the warning) + stub_request(:post, 'https://app.posthog.com/batch/').to_return(status: 200, body: '{}') + stub_request(:get, %r{https://app\.posthog\.com/api/feature_flag/}).to_return(status: 200, body: '{}') + end + + it 'warns when multiple clients are created with the same API key' do + Client.new(api_key: API_KEY, test_mode: false) + Client.new(api_key: API_KEY, test_mode: false) + + expect(logger).to have_received(:warn).with( + include('Multiple PostHog client instances detected') + ) + expect(logger).to have_received(:warn).with( + include('singleton pattern') + ) + end + + it 'does not warn when only one client exists' do + Client.new(api_key: API_KEY, test_mode: false) + + expect(logger).not_to have_received(:warn).with( + include('Multiple PostHog client instances detected') + ) + end + + it 'does not warn when disable_singleton_warning is true' do + Client.new(api_key: API_KEY, test_mode: false) + Client.new(api_key: API_KEY, test_mode: false, disable_singleton_warning: true) + + expect(logger).not_to have_received(:warn).with( + include('Multiple PostHog client instances detected') + ) + end + + it 'does not warn when clients use different API keys' do + Client.new(api_key: API_KEY, test_mode: false) + Client.new(api_key: 'different_key', test_mode: false) + + expect(logger).not_to have_received(:warn).with( + include('Multiple PostHog client instances detected') + ) + end + + it 'does not warn when test_mode is true' do + Client.new(api_key: API_KEY, test_mode: true) + Client.new(api_key: API_KEY, test_mode: true) + + expect(logger).not_to have_received(:warn).with( + include('Multiple PostHog client instances detected') + ) + end + end end describe '#capture' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index cdc3288..c227ba0 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -7,6 +7,7 @@ RSpec.configure do |config| config.before(:each) do PostHog::Logging.logger = Logger.new(File::NULL) # Suppress all logging + PostHog::Client.reset_instance_tracking! end end