From 69b004d9a8940b113d6951654e4f80e3723dfc8c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 3 Sep 2026 09:35:26 +0200 Subject: [PATCH 1/2] fix(rails): attribute events to posthog-rails --- .changeset/warm-rails-metadata.md | 6 ++++++ lib/posthog/client.rb | 10 ++++++++++ lib/posthog/field_parser.rb | 4 ++-- posthog-rails/lib/posthog/rails/facade.rb | 6 +++++- spec/posthog/rails/railtie_spec.rb | 17 +++++++++++++++++ 5 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 .changeset/warm-rails-metadata.md diff --git a/.changeset/warm-rails-metadata.md b/.changeset/warm-rails-metadata.md new file mode 100644 index 0000000..9ddb13a --- /dev/null +++ b/.changeset/warm-rails-metadata.md @@ -0,0 +1,6 @@ +--- +'posthog-ruby': patch +'posthog-rails': patch +--- + +Attribute events captured through the Rails integration to `posthog-rails` with the Rails integration version. diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index 7d8cd54..a5ce48f 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -209,6 +209,8 @@ def initialize(opts = {}) @before_send = opts[:before_send] @is_server = opts.fetch(:is_server, true) != false + @lib = opts[:_lib] || 'posthog-ruby' + @lib_version = (opts[:_lib_version] || PostHog::VERSION).to_s @deprecation_emitted_for = Concurrent::Set.new end @@ -362,6 +364,8 @@ def capture(attrs) end attrs[:is_server] = @is_server + attrs[:lib] = @lib + attrs[:lib_version] = @lib_version message = FieldParser.parse_for_capture(attrs) # Minimal events are built from the allowlist after full assembly so # context properties and parser-added metadata can never leak in. @@ -417,6 +421,8 @@ def identify(attrs) symbolize_keys! attrs attrs[:is_server] = @is_server + attrs[:lib] = @lib + attrs[:lib_version] = @lib_version enqueue(FieldParser.parse_for_identify(attrs)) end @@ -435,6 +441,8 @@ def group_identify(attrs) symbolize_keys! attrs attrs[:is_server] = @is_server + attrs[:lib] = @lib + attrs[:lib_version] = @lib_version enqueue(FieldParser.parse_for_group_identify(attrs)) end @@ -450,6 +458,8 @@ def alias(attrs) symbolize_keys! attrs attrs[:is_server] = @is_server + attrs[:lib] = @lib + attrs[:lib_version] = @lib_version enqueue(FieldParser.parse_for_alias(attrs)) end diff --git a/lib/posthog/field_parser.rb b/lib/posthog/field_parser.rb index ecc3b5d..81c4636 100644 --- a/lib/posthog/field_parser.rb +++ b/lib/posthog/field_parser.rb @@ -144,8 +144,8 @@ def parse_common_fields(fields) is_server = fields.fetch(:is_server, true) != false properties = { - '$lib' => 'posthog-ruby', - '$lib_version' => PostHog::VERSION.to_s + '$lib' => fields.fetch(:lib, 'posthog-ruby'), + '$lib_version' => fields.fetch(:lib_version, PostHog::VERSION.to_s) } properties['$is_server'] = true if is_server diff --git a/posthog-rails/lib/posthog/rails/facade.rb b/posthog-rails/lib/posthog/rails/facade.rb index 5d9c5e8..5c66b99 100644 --- a/posthog-rails/lib/posthog/rails/facade.rb +++ b/posthog-rails/lib/posthog/rails/facade.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'posthog/rails/version' + module PostHog module Rails # Install the Rails singleton-style PostHog facade at load time so Rails app @@ -35,7 +37,9 @@ def init(options = {}) # after replacement so repeated init calls do not leave background # resources from the previous instance running. previous_client = @client - @client = PostHog::Client.new(options) + @client = PostHog::Client.new( + options.merge(_lib: 'posthog-rails', _lib_version: PostHog::Rails::VERSION) + ) begin previous_client&.shutdown rescue StandardError => e diff --git a/spec/posthog/rails/railtie_spec.rb b/spec/posthog/rails/railtie_spec.rb index 4389bf9..19c6357 100644 --- a/spec/posthog/rails/railtie_spec.rb +++ b/spec/posthog/rails/railtie_spec.rb @@ -228,6 +228,23 @@ expect(PostHog::Rails::Logs::Setup).to have_received(:remember_client_options) .with(hash_including(api_key: 'phc_test', host: 'https://eu.i.posthog.com')) end + + it 'attributes every event type to the Rails integration' do + PostHog.init(api_key: 'phc_test', test_mode: true) + + PostHog.capture(event: 'event', distinct_id: 'user') + PostHog.identify(distinct_id: 'user') + PostHog.alias(alias: 'anonymous', distinct_id: 'user') + PostHog.group_identify(group_type: 'organization', group_key: '5') + + properties = 4.times.map { PostHog.client.dequeue_last_message[:properties] } + expect(properties).to all( + include( + '$lib' => 'posthog-rails', + '$lib_version' => PostHog::Rails::VERSION + ) + ) + end end describe '.install_posthog_logs' do From 2e203cfc10e8e34c2a002e51e1f15b188de62215 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Thu, 3 Sep 2026 12:11:36 +0200 Subject: [PATCH 2/2] fix(rails): use rails user agent for requests --- .changeset/warm-rails-metadata.md | 2 +- lib/posthog/client.rb | 11 +++++++---- lib/posthog/feature_flags.rb | 7 +++++-- lib/posthog/send_worker.rb | 1 + spec/posthog/client_spec.rb | 14 ++++++++++---- spec/posthog/rails/railtie_spec.rb | 14 ++++++++++++++ 6 files changed, 38 insertions(+), 11 deletions(-) diff --git a/.changeset/warm-rails-metadata.md b/.changeset/warm-rails-metadata.md index 9ddb13a..5b9bbd4 100644 --- a/.changeset/warm-rails-metadata.md +++ b/.changeset/warm-rails-metadata.md @@ -3,4 +3,4 @@ 'posthog-rails': patch --- -Attribute events captured through the Rails integration to `posthog-rails` with the Rails integration version. +Attribute events and HTTP requests from the Rails integration to `posthog-rails` with the Rails integration version. diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index a5ce48f..96ea536 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -141,6 +141,9 @@ def initialize(opts = {}) @api_key = opts[:api_key] @disabled = @api_key.nil? || @api_key.empty? @max_queue_size = opts[:max_queue_size] || Defaults::Queue::MAX_SIZE + @lib = opts[:_lib] || 'posthog-ruby' + @lib_version = (opts[:_lib_version] || PostHog::VERSION).to_s + @headers = Defaults::Request::HEADERS.merge('User-Agent' => "#{@lib}/#{@lib_version}") @worker_mutex = Mutex.new @shutdown_mutex = Mutex.new @shutdown_condition = ConditionVariable.new @@ -154,11 +157,12 @@ def initialize(opts = {}) elsif @sync_mode nil else - SendWorker.new(@queue, @api_key, opts) + SendWorker.new(@queue, @api_key, opts.merge(headers: @headers)) end if @sync_mode @transport = Transport.new( api_host: opts[:host], + headers: @headers, skip_ssl_verification: opts[:skip_ssl_verification], retries: opts.key?(:max_retries) ? opts[:max_retries].to_i + 1 : 3, compress_request: opts[:compress_request] @@ -198,7 +202,8 @@ def initialize(opts = {}) opts[:on_error], flag_definition_cache_provider: opts[:flag_definition_cache_provider], feature_flag_request_max_retries: opts[:feature_flag_request_max_retries], - async_load: opts[:feature_flags_async_load] == true + async_load: opts[:feature_flags_async_load] == true, + user_agent: @headers['User-Agent'] ) end @@ -209,8 +214,6 @@ def initialize(opts = {}) @before_send = opts[:before_send] @is_server = opts.fetch(:is_server, true) != false - @lib = opts[:_lib] || 'posthog-ruby' - @lib_version = (opts[:_lib_version] || PostHog::VERSION).to_s @deprecation_emitted_for = Concurrent::Set.new end diff --git a/lib/posthog/feature_flags.rb b/lib/posthog/feature_flags.rb index 7c6f12e..bae1698 100644 --- a/lib/posthog/feature_flags.rb +++ b/lib/posthog/feature_flags.rb @@ -44,6 +44,7 @@ class FeatureFlagsPoller # @param async_load [Boolean] When true, flag definitions are fetched only on the poller thread: an # immediate first tick at construction, then the regular polling cadence, which keeps retrying until a # load succeeds. + # @param user_agent [String] User-Agent header sent with feature flag requests. def initialize( polling_interval, secret_key, @@ -53,7 +54,8 @@ def initialize( on_error = nil, flag_definition_cache_provider: nil, feature_flag_request_max_retries: nil, - async_load: false + async_load: false, + user_agent: "posthog-ruby/#{PostHog::VERSION}" ) @polling_interval = polling_interval || Defaults::FeatureFlags::POLLING_INTERVAL_SECONDS @secret_key = secret_key @@ -72,6 +74,7 @@ def initialize( @flags_etag = Concurrent::AtomicReference.new(nil) @flag_definitions_loaded_at = Concurrent::AtomicReference.new(nil) @async_load = async_load + @user_agent = user_agent # Server-controlled gate for minimal `$feature_flag_called` events, read # from the top-level `minimal_flag_called_events` key of the local # evaluation definitions payload. false when the server does not send it. @@ -1326,7 +1329,7 @@ def _request_remote_config_payload(flag_key) RETRYABLE_FLAGS_REQUEST_STATUS_CODES = [502, 504].freeze def _request(uri, request_object, timeout = nil, include_etag: false, retry_status_codes: []) - request_object['User-Agent'] = "posthog-ruby/#{PostHog::VERSION}" + request_object['User-Agent'] = @user_agent request_timeout = timeout || 10 backoff_policy = nil attempts = 0 diff --git a/lib/posthog/send_worker.rb b/lib/posthog/send_worker.rb index f004371..ccb1ad6 100644 --- a/lib/posthog/send_worker.rb +++ b/lib/posthog/send_worker.rb @@ -45,6 +45,7 @@ def initialize(queue, api_key, options = {}) @pid = Process.pid @transport_options = { api_host: options[:host], + headers: options[:headers], skip_ssl_verification: options[:skip_ssl_verification], compress_request: options[:compress_request] } diff --git a/spec/posthog/client_spec.rb b/spec/posthog/client_spec.rb index 040f7ff..105f54e 100644 --- a/spec/posthog/client_spec.rb +++ b/spec/posthog/client_spec.rb @@ -126,16 +126,22 @@ def shutdown end it 'handles skip_ssl_verification' do - expect(PostHog::Transport).to receive(:new).with({ api_host: 'https://us.i.posthog.com', + expect(PostHog::Transport).to receive(:new).with({ + api_host: 'https://us.i.posthog.com', + headers: PostHog::Defaults::Request::HEADERS, skip_ssl_verification: true, - compress_request: nil }) + compress_request: nil + }) expect { Client.new api_key: API_KEY, skip_ssl_verification: true }.to_not raise_error end it 'passes compress_request false to the transport' do - expect(PostHog::Transport).to receive(:new).with({ api_host: 'https://us.i.posthog.com', + expect(PostHog::Transport).to receive(:new).with({ + api_host: 'https://us.i.posthog.com', + headers: PostHog::Defaults::Request::HEADERS, skip_ssl_verification: nil, - compress_request: false }) + compress_request: false + }) expect { Client.new api_key: API_KEY, compress_request: false }.to_not raise_error end diff --git a/spec/posthog/rails/railtie_spec.rb b/spec/posthog/rails/railtie_spec.rb index 19c6357..0e602f7 100644 --- a/spec/posthog/rails/railtie_spec.rb +++ b/spec/posthog/rails/railtie_spec.rb @@ -245,6 +245,20 @@ ) ) end + + it 'uses the Rails user agent for event and feature flag requests' do + batch_request = stub_request(:post, 'https://us.i.posthog.com/batch/').to_return(status: 200, body: '{}') + flags_request = stub_request(:post, 'https://us.i.posthog.com/flags/?v=2') + .to_return(status: 200, body: { flags: {} }.to_json) + + PostHog.init(api_key: 'phc_test', sync_mode: true) + PostHog.capture(event: 'event', distinct_id: 'user') + PostHog.get_all_flags('user') + + expected_user_agent = "posthog-rails/#{PostHog::Rails::VERSION}" + expect(batch_request.with(headers: { 'User-Agent' => expected_user_agent })).to have_been_requested + expect(flags_request.with(headers: { 'User-Agent' => expected_user_agent })).to have_been_requested + end end describe '.install_posthog_logs' do