Skip to content
Merged
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
4 changes: 2 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
20 changes: 20 additions & 0 deletions api/public-api.json
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
]
},
Expand Down Expand Up @@ -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
}
]
},
Expand Down
24 changes: 12 additions & 12 deletions example.php
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand All @@ -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)
Expand All @@ -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);
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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";
Expand Down
38 changes: 29 additions & 9 deletions lib/Client.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -184,19 +198,25 @@ 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,
array $options = [],
?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(
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1273,15 +1293,15 @@ 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()
);
if ($this->hasFlagDefinitionsLoaded()) {
return;
}
$shouldFetch = !is_null($this->personalAPIKey);
$shouldFetch = !is_null($this->secretKey);
}
}

Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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
Expand Down
13 changes: 9 additions & 4 deletions lib/PostHog.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ?? [];
Expand Down Expand Up @@ -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;
}
Expand Down
28 changes: 28 additions & 0 deletions test/PostHogTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,34 @@ public function testClientTrimsWhitespaceSensitiveConfig(): void
$this->assertEquals('https://app.posthog.com/', $optionsProp->getValue($client)['host']);
}

public static function secretKeyCredentialCases(): array
{
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'],
];
}

/**
* @dataProvider secretKeyCredentialCases
*/
public function testSecretKeyResolution(?string $secretKey, ?string $personalApiKey, string $expected): void
{
$client = new Client(self::FAKE_API_KEY, [], $this->http_client, $personalApiKey, false, secretKey: $secretKey);

$this->assertEquals($expected, $this->readPrivate($client, 'secretKey'));
$this->assertEquals($expected, $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
{
Comment thread
greptile-apps[bot] marked this conversation as resolved.
new Client(" \n\t ", ["debug" => true], $this->http_client);
Expand Down
Loading