From fbfb27fe729690341a3f6b652e5498e452e3f875 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Thu, 2 Jul 2026 11:57:22 -0400 Subject: [PATCH 1/3] feat: add secretKey config, deprecate personalAPIKey Add a canonical `secretKey` argument to `PostHog::init()` and `Client::__construct()` for local feature flag evaluation and remote config. It accepts either a Personal API Key (phx_...) or a Project Secret API Key (phs_...). `personalAPIKey` is kept as a deprecated, backwards-compatible alias; when both are provided, `secretKey` wins. The resolved value mirrors onto the existing `personalAPIKey` field so all internal reads keep working. Non-breaking. --- api/public-api.json | 20 ++++++++++++++++++++ lib/Client.php | 38 +++++++++++++++++++++++++++++--------- lib/PostHog.php | 13 +++++++++---- test/PostHogTest.php | 32 ++++++++++++++++++++++++++++++++ 4 files changed, 90 insertions(+), 13 deletions(-) diff --git a/api/public-api.json b/api/public-api.json index 7d515c4..979073c 100644 --- a/api/public-api.json +++ b/api/public-api.json @@ -112,6 +112,16 @@ "default": true, "defaultConstant": null, "hasDefault": true + }, + { + "name": "secretKey", + "type": "?string", + "byReference": false, + "variadic": false, + "optional": true, + "default": null, + "defaultConstant": null, + "hasDefault": true } ] }, @@ -3888,6 +3898,16 @@ "default": null, "defaultConstant": null, "hasDefault": true + }, + { + "name": "secretKey", + "type": "?string", + "byReference": false, + "variadic": false, + "optional": true, + "default": null, + "defaultConstant": null, + "hasDefault": true } ] }, diff --git a/lib/Client.php b/lib/Client.php index 2701ac0..f47f60b 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -33,7 +33,21 @@ class Client implements FeatureFlagEvaluationsHost private $apiKey; /** - * @var string + * Credential for local feature flag evaluation and remote config. + * + * Holds the resolved credential from the $secretKey constructor argument, falling back to the + * deprecated $personalAPIKey argument. Accepts a Personal API Key (`phx_...`) or a Project + * Secret API Key (`phs_...`). Null when neither is provided. + * + * @var string|null + */ + private $secretKey; + + /** + * @deprecated Use {@see $secretKey} instead. Kept as a mirror of the resolved credential so + * existing reads keep working. + * + * @var string|null */ private $personalAPIKey; @@ -184,8 +198,12 @@ class Client implements FeatureFlagEvaluationsHost * } * } $options Client and consumer configuration options. * @param HttpClient|null $httpClient Custom HTTP client, primarily for tests and advanced integrations. - * @param string|null $personalAPIKey Personal API key used to load local feature flag definitions. + * @param string|null $personalAPIKey Deprecated: use $secretKey instead. Kept as a + * backwards-compatible alias for $secretKey. * @param bool $loadFeatureFlags Whether to load local feature flag definitions during construction. + * @param string|null $secretKey Credential for local feature flag evaluation and remote config. + * Accepts either a Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). + * Defaults to null. When both $secretKey and $personalAPIKey are provided, $secretKey wins. */ public function __construct( ?string $apiKey = null, @@ -193,10 +211,12 @@ public function __construct( ?HttpClient $httpClient = null, ?string $personalAPIKey = null, bool $loadFeatureFlags = true, + ?string $secretKey = null, ) { $this->apiKey = StringNormalizer::normalizeOptional($apiKey) ?? ''; $this->enabled = $this->apiKey !== ''; - $this->personalAPIKey = StringNormalizer::normalizeOptional($personalAPIKey); + $this->secretKey = StringNormalizer::normalizeOptional($secretKey ?? $personalAPIKey); + $this->personalAPIKey = $this->secretKey; $this->options = $options; $this->debug = $options["debug"] ?? false; $this->flagDefinitionCacheProvider = self::normalizeFlagDefinitionCacheProvider( @@ -241,7 +261,7 @@ public function __construct( if ( $this->enabled && count($this->featureFlags) == 0 - && !is_null($this->personalAPIKey) + && !is_null($this->secretKey) && $loadFeatureFlags ) { $this->loadFlags(); @@ -1239,7 +1259,7 @@ private function fetchFlagsResponse( } /** - * Load local feature flag definitions using the configured personal API key. + * Load local feature flag definitions using the configured secret key. * * @return void * @throws Exception @@ -1273,7 +1293,7 @@ public function loadFlags() return; } - $shouldFetch = !is_null($this->personalAPIKey); + $shouldFetch = !is_null($this->secretKey); } catch (Throwable $throwable) { $this->logFlagDefinitionCacheWarning( 'Cache provider read error: ' . $throwable->getMessage() @@ -1281,7 +1301,7 @@ public function loadFlags() if ($this->hasFlagDefinitionsLoaded()) { return; } - $shouldFetch = !is_null($this->personalAPIKey); + $shouldFetch = !is_null($this->secretKey); } } @@ -1307,7 +1327,7 @@ private function shouldFetchFlagDefinitionsFromApi(): bool $this->logFlagDefinitionCacheWarning( 'Cache provider fetch-decision error: ' . $throwable->getMessage() ); - return !is_null($this->personalAPIKey); + return !is_null($this->secretKey); } } @@ -1525,7 +1545,7 @@ public function localFlags(): HttpResponse $headers = [ // Send user agent in the form of {library_name}/{library_version} as per RFC 7231. "User-Agent: " . PostHog::LIBRARY . "/" . PostHog::VERSION, - "Authorization: Bearer " . $this->personalAPIKey + "Authorization: Bearer " . $this->secretKey ]; // Add If-None-Match header if we have a cached ETag diff --git a/lib/PostHog.php b/lib/PostHog.php index f961ec4..432deb4 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -60,15 +60,20 @@ class PostHog * } * }|null $options Client and consumer configuration options. * @param Client|null $client Preconfigured client instance. When provided, $apiKey, $options, - * and $personalAPIKey are ignored. - * @param string|null $personalAPIKey Personal API key used to load local feature flag definitions. + * $personalAPIKey, and $secretKey are ignored. + * @param string|null $personalAPIKey Deprecated: use $secretKey instead. Kept as a + * backwards-compatible alias for $secretKey. + * @param string|null $secretKey Credential for local feature flag evaluation and remote config. + * Accepts either a Personal API Key (`phx_...`) or a Project Secret API Key (`phs_...`). + * Defaults to null. When both $secretKey and $personalAPIKey are provided, $secretKey wins. * @return void */ public static function init( ?string $apiKey = null, ?array $options = [], ?Client $client = null, - ?string $personalAPIKey = null + ?string $personalAPIKey = null, + ?string $secretKey = null ): void { if (null === $client) { $options = $options ?? []; @@ -100,7 +105,7 @@ public static function init( } } - self::$client = new Client($apiKey, $options, null, $personalAPIKey); + self::$client = new Client($apiKey, $options, null, $personalAPIKey, secretKey: $secretKey); } else { self::$client = $client; } diff --git a/test/PostHogTest.php b/test/PostHogTest.php index fcb0e07..9c0d3da 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -267,6 +267,38 @@ public function testClientTrimsWhitespaceSensitiveConfig(): void $this->assertEquals('https://app.posthog.com/', $optionsProp->getValue($client)['host']); } + public function testSecretKeySetsCredential(): void + { + $client = new Client(self::FAKE_API_KEY, [], $this->http_client, loadFeatureFlags: false, secretKey: "phs_secret"); + + $this->assertEquals("phs_secret", $this->readPrivate($client, 'secretKey')); + $this->assertEquals("phs_secret", $this->readPrivate($client, 'personalAPIKey')); + } + + public function testPersonalApiKeyStillWorksAsAlias(): void + { + $client = new Client(self::FAKE_API_KEY, [], $this->http_client, "phx_personal", false); + + $this->assertEquals("phx_personal", $this->readPrivate($client, 'secretKey')); + $this->assertEquals("phx_personal", $this->readPrivate($client, 'personalAPIKey')); + } + + public function testSecretKeyWinsWhenBothProvided(): void + { + $client = new Client(self::FAKE_API_KEY, [], $this->http_client, "phx_personal", false, secretKey: "phs_secret"); + + $this->assertEquals("phs_secret", $this->readPrivate($client, 'secretKey')); + $this->assertEquals("phs_secret", $this->readPrivate($client, 'personalAPIKey')); + } + + private function readPrivate(Client $client, string $property): mixed + { + $prop = (new \ReflectionClass($client))->getProperty($property); + $prop->setAccessible(true); + + return $prop->getValue($client); + } + public function testClientLogsWhenApiKeyIsEmptyAfterTrimmingWhitespace(): void { new Client(" \n\t ", ["debug" => true], $this->http_client); From ec61bdab43e8590ac5646324fb0bd3ac70a9ac38 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Thu, 2 Jul 2026 12:12:32 -0400 Subject: [PATCH 2/3] test: collapse secret key credential tests into dataProvider --- test/PostHogTest.php | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/test/PostHogTest.php b/test/PostHogTest.php index 9c0d3da..4ce064a 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -267,28 +267,24 @@ public function testClientTrimsWhitespaceSensitiveConfig(): void $this->assertEquals('https://app.posthog.com/', $optionsProp->getValue($client)['host']); } - public function testSecretKeySetsCredential(): void + public static function secretKeyCredentialCases(): array { - $client = new Client(self::FAKE_API_KEY, [], $this->http_client, loadFeatureFlags: false, secretKey: "phs_secret"); - - $this->assertEquals("phs_secret", $this->readPrivate($client, 'secretKey')); - $this->assertEquals("phs_secret", $this->readPrivate($client, 'personalAPIKey')); - } - - public function testPersonalApiKeyStillWorksAsAlias(): void - { - $client = new Client(self::FAKE_API_KEY, [], $this->http_client, "phx_personal", false); - - $this->assertEquals("phx_personal", $this->readPrivate($client, 'secretKey')); - $this->assertEquals("phx_personal", $this->readPrivate($client, 'personalAPIKey')); + return [ + 'secret key only' => ['phs_secret', null, 'phs_secret'], + 'personal api key alias' => [null, 'phx_personal', 'phx_personal'], + 'secret key wins over personal api key' => ['phs_secret', 'phx_personal', 'phs_secret'], + ]; } - public function testSecretKeyWinsWhenBothProvided(): void + /** + * @dataProvider secretKeyCredentialCases + */ + public function testSecretKeyResolution(?string $secretKey, ?string $personalApiKey, string $expected): void { - $client = new Client(self::FAKE_API_KEY, [], $this->http_client, "phx_personal", false, secretKey: "phs_secret"); + $client = new Client(self::FAKE_API_KEY, [], $this->http_client, $personalApiKey, false, secretKey: $secretKey); - $this->assertEquals("phs_secret", $this->readPrivate($client, 'secretKey')); - $this->assertEquals("phs_secret", $this->readPrivate($client, 'personalAPIKey')); + $this->assertEquals($expected, $this->readPrivate($client, 'secretKey')); + $this->assertEquals($expected, $this->readPrivate($client, 'personalAPIKey')); } private function readPrivate(Client $client, string $property): mixed From d4289a65623e35b20c9ab84a95a9ca429ad8a0a4 Mon Sep 17 00:00:00 2001 From: Anna Garcia Date: Tue, 7 Jul 2026 10:08:11 -0400 Subject: [PATCH 3/3] docs(php): use secretKey in example and .env.example --- .env.example | 4 ++-- example.php | 24 ++++++++++++------------ 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.env.example b/.env.example index 01d4fc6..09f01a5 100644 --- a/.env.example +++ b/.env.example @@ -4,8 +4,8 @@ # Your project API key (found on the /setup page in PostHog) POSTHOG_PROJECT_API_KEY=phc_your_project_api_key_here -# Your personal API key (for local evaluation and other advanced features) -POSTHOG_PERSONAL_API_KEY=phx_your_personal_api_key_here +# Your secret API key (for local evaluation and other advanced features) +POSTHOG_SECRET_KEY=phx_your_secret_api_key_here # PostHog host URL (remove this line if using posthog.com) POSTHOG_HOST=https://app.posthog.com diff --git a/example.php b/example.php index eb6cb21..9e40f3a 100644 --- a/example.php +++ b/example.php @@ -39,13 +39,13 @@ function loadEnvFile() // Get configuration $projectKey = $_ENV['POSTHOG_PROJECT_API_KEY'] ?? getenv('POSTHOG_PROJECT_API_KEY') ?: ''; -$personalApiKey = $_ENV['POSTHOG_PERSONAL_API_KEY'] ?? getenv('POSTHOG_PERSONAL_API_KEY') ?: ''; +$secretKey = $_ENV['POSTHOG_SECRET_KEY'] ?? getenv('POSTHOG_SECRET_KEY') ?: ''; $host = $_ENV['POSTHOG_HOST'] ?? getenv('POSTHOG_HOST') ?: 'https://app.posthog.com'; // Check if credentials are provided -if (!$projectKey || !$personalApiKey) { +if (!$projectKey || !$secretKey) { echo "❌ Missing PostHog credentials!\n"; - echo " Please set POSTHOG_PROJECT_API_KEY and POSTHOG_PERSONAL_API_KEY environment variables\n"; + echo " Please set POSTHOG_PROJECT_API_KEY and POSTHOG_SECRET_KEY environment variables\n"; echo " or copy .env.example to .env and fill in your values\n"; exit(1); } @@ -63,7 +63,7 @@ function loadEnvFile() 'ssl' => !(substr($host, 0, 7) === 'http://') // Use SSL unless explicitly http:// ], null, - $personalApiKey + secretKey: $secretKey ); // Test by attempting to get feature flags (this validates both keys) @@ -72,14 +72,14 @@ function loadEnvFile() // If we get here without exception, credentials work echo "✅ Authentication successful!\n"; echo " Project API Key: " . substr($projectKey, 0, 9) . "...\n"; - echo " Personal API Key: [REDACTED]\n"; + echo " Secret Key: [REDACTED]\n"; echo " Host: $host\n\n\n"; } catch (Exception $e) { echo "❌ Authentication failed!\n"; echo " Error: " . $e->getMessage() . "\n"; echo "\n Please check your credentials:\n"; echo " - POSTHOG_PROJECT_API_KEY: Project API key from PostHog settings\n"; - echo " - POSTHOG_PERSONAL_API_KEY: Personal API key (required for local evaluation)\n"; + echo " - POSTHOG_SECRET_KEY: Secret key (required for local evaluation)\n"; echo " - POSTHOG_HOST: Your PostHog instance URL\n"; exit(1); } @@ -111,7 +111,7 @@ function identifyAndCaptureExamples() 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], null, - $_ENV['POSTHOG_PERSONAL_API_KEY'] + secretKey: $_ENV['POSTHOG_SECRET_KEY'] ); // Capture an event @@ -177,7 +177,7 @@ function featureFlagExamples() 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], null, - $_ENV['POSTHOG_PERSONAL_API_KEY'] + secretKey: $_ENV['POSTHOG_SECRET_KEY'] ); echo "🚩 Getting individual feature flags...\n"; @@ -227,7 +227,7 @@ function flagDependencyExamples() 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], null, - $_ENV['POSTHOG_PERSONAL_API_KEY'] + secretKey: $_ENV['POSTHOG_SECRET_KEY'] ); // Test @example.com user (should satisfy dependency if flags exist) @@ -400,7 +400,7 @@ function contextManagementExamples() 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], null, - $_ENV['POSTHOG_PERSONAL_API_KEY'] + secretKey: $_ENV['POSTHOG_SECRET_KEY'] ); echo "🏷️ Testing groups and properties...\n"; @@ -452,7 +452,7 @@ function etagPollingExamples() 'ssl' => !str_starts_with($_ENV['POSTHOG_HOST'] ?? 'https://app.posthog.com', 'http://') ], null, - $_ENV['POSTHOG_PERSONAL_API_KEY'] + secretKey: $_ENV['POSTHOG_SECRET_KEY'] ); $client = PostHog::getClient(); @@ -538,7 +538,7 @@ function errorTrackingExamples() ], ], null, - $_ENV['POSTHOG_PERSONAL_API_KEY'] + secretKey: $_ENV['POSTHOG_SECRET_KEY'] ); echo "Auto capture enabled for uncaught exceptions, PHP errors, and fatal shutdown errors.\n";