Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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))
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
>
Expand Down
45 changes: 45 additions & 0 deletions lib/posthog/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)

Expand All @@ -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],
Expand Down Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions posthog-rails/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions posthog-rails/lib/generators/posthog/templates/posthog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
# ============================================================================
Expand Down
55 changes: 55 additions & 0 deletions spec/posthog/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions spec/spec_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading