diff --git a/.changeset/tame-keys-rename.md b/.changeset/tame-keys-rename.md new file mode 100644 index 0000000..8f03026 --- /dev/null +++ b/.changeset/tame-keys-rename.md @@ -0,0 +1,8 @@ +--- +"posthog-ruby": minor +"posthog-rails": minor +--- + +Add `secret_key` config option and deprecate `personal_api_key`. + +`secret_key` is the new canonical credential for local feature flag evaluation and remote config. It accepts either a Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). `personal_api_key` still works as a deprecated alias; when both are supplied, `secret_key` wins. diff --git a/example.rb b/example.rb index f745546..226f65b 100644 --- a/example.rb +++ b/example.rb @@ -54,7 +54,7 @@ # Create a minimal client for testing test_client = PostHog::Client.new( api_key: api_key, - personal_api_key: personal_api_key, + secret_key: personal_api_key, host: host, on_error: proc { |_status, _msg| }, # Suppress error output during test feature_flags_polling_interval: 60 # Longer interval for test @@ -82,7 +82,7 @@ posthog = PostHog::Client.new( api_key: api_key, # You can find this key on the /setup page in PostHog - personal_api_key: personal_api_key, # Required for local feature flag evaluation + secret_key: personal_api_key, # Required for local feature flag evaluation (Personal or Project Secret API Key) host: host, # Where you host PostHog. You can remove this line if using app.posthog.com on_error: proc { |_status, msg| print msg }, feature_flags_polling_interval: 10 # How often to poll for feature flags diff --git a/lib/posthog/client.rb b/lib/posthog/client.rb index 8f49169..adb2d7e 100644 --- a/lib/posthog/client.rb +++ b/lib/posthog/client.rb @@ -53,7 +53,11 @@ def _decrement_instance_count(api_key) # @param opts [Hash] Client configuration. # @option opts [String, nil] :api_key Your project's API key. Missing or blank values disable the client. - # @option opts [String, nil] :personal_api_key Your personal API key. Required for local feature flag evaluation. + # @option opts [String, nil] :secret_key The credential used for local feature flag evaluation and remote + # config. Accepts either a Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). Required + # for local feature flag evaluation. + # @option opts [String, nil] :personal_api_key + # @deprecated Use +:secret_key+ instead. Retained as an alias; when both are supplied, +:secret_key+ wins. # @option opts [String] :host Fully qualified hostname of the PostHog server. Defaults to `https://us.i.posthog.com`. # @option opts [Integer] :max_queue_size Maximum number of calls to remain queued. Defaults to 10_000. # @option opts [Integer] :batch_size Maximum number of events to send in one async batch. @@ -88,9 +92,18 @@ def initialize(opts = {}) symbolize_keys!(opts) opts[:api_key] = normalize_string_option(opts[:api_key]) + opts[:secret_key] = normalize_string_option(opts[:secret_key], blank_as_nil: true) opts[:personal_api_key] = normalize_string_option(opts[:personal_api_key], blank_as_nil: true) opts[:host] = normalize_host_option(opts[:host]) + if opts[:secret_key].nil? && !opts[:personal_api_key].nil? + logger.warn( + 'The :personal_api_key option is deprecated; use :secret_key instead. It accepts either a ' \ + 'Personal API Key (phx_...) or a Project Secret API Key (phs_...).' + ) + end + secret_key = opts[:secret_key] || opts[:personal_api_key] + @queue = Queue.new @queue_mutex = Mutex.new @api_key = opts[:api_key] @@ -119,7 +132,8 @@ def initialize(opts = {}) end @worker_thread = nil @feature_flags_poller = nil - @personal_api_key = opts[:personal_api_key] + @secret_key = secret_key + @personal_api_key = secret_key if @disabled && !opts[:silence_disabled_client_error] logger.error('api_key is missing or empty after trimming whitespace; check your project API key') @@ -142,7 +156,7 @@ def initialize(opts = {}) @feature_flags_poller = FeatureFlagsPoller.new( opts[:feature_flags_polling_interval], - opts[:personal_api_key], + secret_key, @api_key, opts[:host], opts[:feature_flag_request_timeout_seconds] || Defaults::FeatureFlags::FLAG_REQUEST_TIMEOUT_SECONDS, @@ -758,9 +772,9 @@ def get_all_flags_and_payloads( def reload_feature_flags return if @disabled - unless @personal_api_key + unless @secret_key logger.error( - 'You need to specify a personal_api_key to locally evaluate feature flags' + 'You need to specify a secret_key to locally evaluate feature flags' ) return end diff --git a/lib/posthog/feature_flags.rb b/lib/posthog/feature_flags.rb index 5d9a356..fb34212 100644 --- a/lib/posthog/feature_flags.rb +++ b/lib/posthog/feature_flags.rb @@ -30,7 +30,8 @@ class FeatureFlagsPoller include PostHog::Utils # @param polling_interval [Integer, nil] Seconds between local feature flag definition polls. - # @param personal_api_key [String, nil] Personal API key used to fetch local evaluation definitions. + # @param secret_key [String, nil] Credential used to fetch local evaluation definitions. Accepts either a + # Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). # @param project_api_key [String] Project API key. # @param host [String] PostHog API host URL. # @param feature_flag_request_timeout_seconds [Integer] Timeout for feature flag requests. @@ -41,7 +42,7 @@ class FeatureFlagsPoller # Set to 0 to disable retrying. def initialize( polling_interval, - personal_api_key, + secret_key, project_api_key, host, feature_flag_request_timeout_seconds, @@ -50,7 +51,7 @@ def initialize( feature_flag_request_max_retries: nil ) @polling_interval = polling_interval || 30 - @personal_api_key = personal_api_key + @secret_key = secret_key @project_api_key = project_api_key @host = host @feature_flags = Concurrent::Array.new @@ -74,9 +75,9 @@ def initialize( execution_interval: polling_interval ) { _load_feature_flags } - # If no personal API key, disable local evaluation & thus polling for definitions - if @personal_api_key.nil? - logger.info 'No personal API key provided, disabling local evaluation' + # If no secret_key, disable local evaluation & thus polling for definitions + if @secret_key.nil? + logger.info 'No secret_key provided, disabling local evaluation' @loaded_flags_successfully_once.make_true else # load once before timer @@ -1227,7 +1228,7 @@ def _request_feature_flag_definitions(etag: nil) uri = URI("#{@host}/flags/definitions") uri.query = URI.encode_www_form([['token', @project_api_key], %w[send_cohorts true]]) req = Net::HTTP::Get.new(uri) - req['Authorization'] = "Bearer #{@personal_api_key}" + req['Authorization'] = "Bearer #{@secret_key}" req['If-None-Match'] = etag if etag _request(uri, req, nil, include_etag: true) @@ -1253,7 +1254,7 @@ def _request_remote_config_payload(flag_key) uri.query = URI.encode_www_form([['token', @project_api_key]]) req = Net::HTTP::Get.new(uri) req['Content-Type'] = 'application/json' - req['Authorization'] = "Bearer #{@personal_api_key}" + req['Authorization'] = "Bearer #{@secret_key}" _request(uri, req, @feature_flag_request_timeout_seconds) end diff --git a/lib/posthog/flag_definition_cache.rb b/lib/posthog/flag_definition_cache.rb index 6b8c4f2..d2dfa67 100644 --- a/lib/posthog/flag_definition_cache.rb +++ b/lib/posthog/flag_definition_cache.rb @@ -52,7 +52,7 @@ module PostHog # cache = RedisFlagCache.new(redis, service_key: 'my-service') # client = PostHog::Client.new( # api_key: '', - # personal_api_key: '', + # secret_key: '', # flag_definition_cache_provider: cache # ) # diff --git a/posthog-rails/examples/posthog.rb b/posthog-rails/examples/posthog.rb index e8a2eb7..eacf08e 100644 --- a/posthog-rails/examples/posthog.rb +++ b/posthog-rails/examples/posthog.rb @@ -118,10 +118,10 @@ # For PostHog Cloud, use: https://us.i.posthog.com or https://eu.i.posthog.com config.host = ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com') - # Personal API key (optional, but required for local feature flag evaluation) - # Get this from: PostHog Settings > Personal API Keys - # https://app.posthog.com/settings/user-api-keys - config.personal_api_key = ENV.fetch('POSTHOG_PERSONAL_API_KEY', nil) + # Secret key (optional, but required for local feature flag evaluation). + # Accepts a Personal API Key (phx_...) or a Project Secret API Key (phs_...). + # Get this from: PostHog Settings > Personal API Keys / Project API Keys + config.secret_key = ENV.fetch('POSTHOG_SECRET_API_KEY', ENV.fetch('POSTHOG_PERSONAL_API_KEY', nil)) # Maximum number of events to queue before dropping (default: 10000) config.max_queue_size = 10_000 diff --git a/posthog-rails/lib/generators/posthog/templates/posthog.rb b/posthog-rails/lib/generators/posthog/templates/posthog.rb index 64b1e38..95b7b66 100644 --- a/posthog-rails/lib/generators/posthog/templates/posthog.rb +++ b/posthog-rails/lib/generators/posthog/templates/posthog.rb @@ -120,10 +120,10 @@ # For PostHog Cloud, use: https://us.i.posthog.com or https://eu.i.posthog.com config.host = ENV.fetch('POSTHOG_HOST', 'https://us.i.posthog.com') - # Personal API key (optional, but required for local feature flag evaluation) - # Get this from: PostHog Settings > Personal API Keys - # https://app.posthog.com/settings/user-api-keys - config.personal_api_key = ENV.fetch('POSTHOG_PERSONAL_API_KEY', nil) + # Secret key (optional, but required for local feature flag evaluation). + # Accepts a Personal API Key (phx_...) or a Project Secret API Key (phs_...). + # Get this from: PostHog Settings > Personal API Keys / Project API Keys + config.secret_key = ENV.fetch('POSTHOG_SECRET_API_KEY', ENV.fetch('POSTHOG_PERSONAL_API_KEY', nil)) # Maximum number of events to queue before dropping (default: 10000) config.max_queue_size = 10_000 diff --git a/posthog-rails/lib/posthog/rails/railtie.rb b/posthog-rails/lib/posthog/rails/railtie.rb index e06dbc4..dfa4bcd 100644 --- a/posthog-rails/lib/posthog/rails/railtie.rb +++ b/posthog-rails/lib/posthog/rails/railtie.rb @@ -277,6 +277,16 @@ def api_key=(value) @base_options[:api_key] = value end + # The credential used for local feature flag evaluation and remote config. Accepts either a + # Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). + # + # @param value [String, nil] + # @return [String, nil] + def secret_key=(value) + @base_options[:secret_key] = value + end + + # @deprecated Use {#secret_key=} instead. Retained as an alias. # @param value [String, nil] # @return [String, nil] def personal_api_key=(value) diff --git a/public_api_snapshot.txt b/public_api_snapshot.txt index 2d65f9d..e4b361c 100644 --- a/public_api_snapshot.txt +++ b/public_api_snapshot.txt @@ -191,6 +191,7 @@ instance_method PostHog::Rails::InitConfig#host=(value) instance_method PostHog::Rails::InitConfig#max_queue_size=(value) instance_method PostHog::Rails::InitConfig#on_error=(value) instance_method PostHog::Rails::InitConfig#personal_api_key=(value) +instance_method PostHog::Rails::InitConfig#secret_key=(value) instance_method PostHog::Rails::InitConfig#sync_mode=(value) instance_method PostHog::Rails::InitConfig#test_mode=(value) instance_method PostHog::Rails::InitConfig#to_client_options() diff --git a/spec/posthog/client_spec.rb b/spec/posthog/client_spec.rb index 77a5d2e..d7062a9 100644 --- a/spec/posthog/client_spec.rb +++ b/spec/posthog/client_spec.rb @@ -134,6 +134,33 @@ module PostHog expect(client.instance_variable_get(:@feature_flags_poller).instance_variable_get(:@host)).to eq('https://us.i.posthog.com') end + context 'secret_key' do + before do + stub_request(:get, %r{https://us\.i\.posthog\.com/flags/definitions}) + .to_return(status: 200, body: '{"flags":[]}') + end + + [ + ['resolves the credential from :secret_key', { secret_key: 'phs_secret' }, 'phs_secret', false], + ['accepts the deprecated :personal_api_key alias and warns', + { personal_api_key: 'phx_personal' }, 'phx_personal', true], + ['prefers :secret_key when both are supplied', + { secret_key: 'phs_secret', personal_api_key: 'phx_personal' }, 'phs_secret', false] + ].each do |description, opts, expected, warns| + it description do + client = Client.new(api_key: API_KEY, test_mode: true, **opts) + + expect(client.instance_variable_get(:@secret_key)).to eq(expected) + expect(client.instance_variable_get(:@personal_api_key)).to eq(expected) + if warns + expect(logger).to have_received(:warn).with(include(':personal_api_key option is deprecated')).once + else + expect(logger).not_to have_received(:warn) + end + end + end + end + context 'singleton warning' do before do # Stub HTTP to allow creating clients without test_mode (which triggers the warning)