Skip to content
Open
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
8 changes: 8 additions & 0 deletions .changeset/tame-keys-rename.md
Original file line number Diff line number Diff line change
@@ -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.
4 changes: 2 additions & 2 deletions example.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
24 changes: 19 additions & 5 deletions lib/posthog/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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')
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
17 changes: 9 additions & 8 deletions lib/posthog/feature_flags.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion lib/posthog/flag_definition_cache.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ module PostHog
# cache = RedisFlagCache.new(redis, service_key: 'my-service')
# client = PostHog::Client.new(
# api_key: '<project_api_key>',
# personal_api_key: '<personal_api_key>',
# secret_key: '<secret_key>',
# flag_definition_cache_provider: cache
# )
#
Expand Down
8 changes: 4 additions & 4 deletions posthog-rails/examples/posthog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions posthog-rails/lib/generators/posthog/templates/posthog.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions posthog-rails/lib/posthog/rails/railtie.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions public_api_snapshot.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
27 changes: 27 additions & 0 deletions spec/posthog/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
greptile-apps[bot] marked this conversation as resolved.
before do
# Stub HTTP to allow creating clients without test_mode (which triggers the warning)
Expand Down
Loading