diff --git a/.changeset/warm-rails-metadata.md b/.changeset/warm-rails-metadata.md new file mode 100644 index 0000000..5b9bbd4 --- /dev/null +++ b/.changeset/warm-rails-metadata.md @@ -0,0 +1,6 @@ +--- +'posthog-ruby': patch +'posthog-rails': patch +--- + +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 7d8cd54..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 @@ -362,6 +367,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 +424,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 +444,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 +461,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/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/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/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/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/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 4389bf9..0e602f7 100644 --- a/spec/posthog/rails/railtie_spec.rb +++ b/spec/posthog/rails/railtie_spec.rb @@ -228,6 +228,37 @@ 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 + + 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