From ec2dee818f967417a3afc75b4d838155e4de9869 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 20 Apr 2026 15:10:34 +0200 Subject: [PATCH 1/5] fix: trim whitespace from API keys and host config --- lib/Client.php | 28 ++++++++++++++++++---- lib/PostHog.php | 5 ++-- test/FeatureFlagTest.php | 17 ++++++++++++++ test/PostHogTest.php | 50 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 6 deletions(-) diff --git a/lib/Client.php b/lib/Client.php index 020579f..2900912 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -103,14 +103,18 @@ public function __construct( ?string $personalAPIKey = null, bool $loadFeatureFlags = true, ) { - $this->apiKey = $apiKey; - $this->personalAPIKey = $personalAPIKey; + $this->apiKey = trim($apiKey); + $this->personalAPIKey = self::normalizeOptionalString($personalAPIKey); $this->options = $options; $this->debug = $options["debug"] ?? false; + $this->options['host'] = self::normalizeHost($options['host'] ?? null); + if ($this->apiKey === '') { + error_log('[PostHog][Client] apiKey is empty after trimming whitespace; check your project API key'); + } $Consumer = self::CONSUMERS[$options["consumer"] ?? "lib_curl"]; - $this->consumer = new $Consumer($apiKey, $options, $httpClient); + $this->consumer = new $Consumer($this->apiKey, $this->options, $httpClient); $this->httpClient = $httpClient !== null ? $httpClient : new HttpClient( - $options['host'] ?? "app.posthog.com", + $this->options['host'], $options['ssl'] ?? true, (int) ($options['maximum_backoff_duration'] ?? 10000), false, @@ -143,6 +147,22 @@ public function __destruct() $this->consumer->__destruct(); } + private static function normalizeOptionalString(?string $value): ?string + { + if ($value === null) { + return null; + } + + $normalized = trim($value); + return $normalized === '' ? null : $normalized; + } + + private static function normalizeHost(?string $host): string + { + $normalized = self::normalizeOptionalString($host); + return $normalized ?? 'app.posthog.com'; + } + /** * Captures a user action * diff --git a/lib/PostHog.php b/lib/PostHog.php index e207eba..935999a 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -373,8 +373,9 @@ public static function getClient(): Client private static function cleanHost(?string $host): string { - if (!isset($host)) { - return $host; + $host = trim((string) $host); + if ($host === '') { + return 'app.posthog.com'; } // remove protocol if (substr($host, 0, 8) === "https://") { diff --git a/test/FeatureFlagTest.php b/test/FeatureFlagTest.php index ee2d699..e8e433a 100644 --- a/test/FeatureFlagTest.php +++ b/test/FeatureFlagTest.php @@ -106,6 +106,23 @@ public function testIsFeatureEnabledCapturesFeatureFlagCalledEventWithAdditional }); } + public function testWhitespacePersonalApiKeyFallsBackToFlagsEndpoint() + { + $this->setUp(MockedResponses::FLAGS_V2_RESPONSE, personalApiKey: " \n\t "); + $this->assertTrue(PostHog::isFeatureEnabled('simple-test', 'user-id')); + $this->assertEquals( + [ + [ + "path" => "/flags/?v=2", + "payload" => sprintf('{"api_key":"%s","distinct_id":"user-id","person_properties":{"distinct_id":"user-id"}}', self::FAKE_API_KEY), + "extraHeaders" => [0 => 'User-Agent: posthog-php/' . PostHog::VERSION], + "requestOptions" => ["timeout" => 3000, "shouldRetry" => false], + ], + ], + $this->http_client->calls + ); + } + /** * @dataProvider decideResponseCases */ diff --git a/test/PostHogTest.php b/test/PostHogTest.php index 98f6379..828ebfc 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -63,6 +63,39 @@ public function testInitWithEnvApiKey(): void putenv(PostHog::ENV_API_KEY); } + public function testClientTrimsWhitespaceSensitiveConfig(): void + { + $client = new Client( + " \nrandom_key\t ", + ["host" => " \nhttps://app.posthog.com/\t ", "debug" => true], + $this->http_client, + " \n\t " + ); + + $ref = new \ReflectionClass($client); + $apiKeyProp = $ref->getProperty('apiKey'); + $apiKeyProp->setAccessible(true); + $personalApiKeyProp = $ref->getProperty('personalAPIKey'); + $personalApiKeyProp->setAccessible(true); + $optionsProp = $ref->getProperty('options'); + $optionsProp->setAccessible(true); + + $this->assertEquals('random_key', $apiKeyProp->getValue($client)); + $this->assertNull($personalApiKeyProp->getValue($client)); + $this->assertEquals('https://app.posthog.com/', $optionsProp->getValue($client)['host']); + } + + public function testClientLogsWhenApiKeyIsEmptyAfterTrimmingWhitespace(): void + { + new Client(" \n\t ", ["debug" => true], $this->http_client); + + global $errorMessages; + $this->assertContains( + '[PostHog][Client] apiKey is empty after trimming whitespace; check your project API key', + $errorMessages + ); + } + public function testInitWithHttpHostSetsSslFalse(): void { PostHog::init("random_key", ["host" => "http://localhost:8010"]); @@ -85,6 +118,23 @@ public function testInitWithHttpHostSetsSslFalse(): void $this->assertFalse($sslProp->getValue($httpClient), 'HttpClient should use ssl=false for http:// hosts'); } + public function testInitDefaultsBlankHostAfterTrimmingWhitespace(): void + { + PostHog::init("random_key", ["host" => " \n\t "]); + + $client = PostHog::getClient(); + $ref = new \ReflectionClass($client); + $consumerProp = $ref->getProperty('consumer'); + $consumerProp->setAccessible(true); + $consumer = $consumerProp->getValue($client); + + $cRef = new \ReflectionClass($consumer); + $hostProp = $cRef->getProperty('host'); + $hostProp->setAccessible(true); + + $this->assertEquals('app.posthog.com', $hostProp->getValue($consumer)); + } + public function testInitWithHttpsHostSetsSslTrue(): void { PostHog::init("random_key", ["host" => "https://app.posthog.com"]); From 73bb5a6fab10680de20228a46e25fdfbbffa7c07 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Mon, 20 Apr 2026 17:35:55 +0200 Subject: [PATCH 2/5] fix: default host to us.i.posthog.com Co-Authored-By: Claude Opus 4.7 (1M context) --- lib/Client.php | 2 +- lib/PostHog.php | 2 +- lib/QueueConsumer.php | 2 +- test/PostHogTest.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/Client.php b/lib/Client.php index 2900912..172aec2 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -160,7 +160,7 @@ private static function normalizeOptionalString(?string $value): ?string private static function normalizeHost(?string $host): string { $normalized = self::normalizeOptionalString($host); - return $normalized ?? 'app.posthog.com'; + return $normalized ?? 'us.i.posthog.com'; } /** diff --git a/lib/PostHog.php b/lib/PostHog.php index 935999a..e1271fd 100644 --- a/lib/PostHog.php +++ b/lib/PostHog.php @@ -375,7 +375,7 @@ private static function cleanHost(?string $host): string { $host = trim((string) $host); if ($host === '') { - return 'app.posthog.com'; + return 'us.i.posthog.com'; } // remove protocol if (substr($host, 0, 8) === "https://") { diff --git a/lib/QueueConsumer.php b/lib/QueueConsumer.php index 0eaec96..1f22ce3 100644 --- a/lib/QueueConsumer.php +++ b/lib/QueueConsumer.php @@ -13,7 +13,7 @@ abstract class QueueConsumer extends Consumer protected $max_queue_size = 1000; protected $batch_size = 100; protected $maximum_backoff_duration = 10000; // Set maximum waiting limit to 10s - protected $host = "app.posthog.com"; + protected $host = "us.i.posthog.com"; protected $compress_request = false; /** diff --git a/test/PostHogTest.php b/test/PostHogTest.php index 828ebfc..0898b67 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -132,7 +132,7 @@ public function testInitDefaultsBlankHostAfterTrimmingWhitespace(): void $hostProp = $cRef->getProperty('host'); $hostProp->setAccessible(true); - $this->assertEquals('app.posthog.com', $hostProp->getValue($consumer)); + $this->assertEquals('us.i.posthog.com', $hostProp->getValue($consumer)); } public function testInitWithHttpsHostSetsSslTrue(): void From 761e440923da6ea4756b8ba078c956e17c2dbab4 Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 21 Apr 2026 11:23:16 +0200 Subject: [PATCH 3/5] refactor: share host normalization helper --- lib/Client.php | 3 +-- lib/HostNormalizer.php | 14 ++++++++++++++ lib/PostHog.php | 11 +++++------ test/PostHogTest.php | 22 ++++++++++++++++++++++ 4 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 lib/HostNormalizer.php diff --git a/lib/Client.php b/lib/Client.php index 172aec2..1efb8b5 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -159,8 +159,7 @@ private static function normalizeOptionalString(?string $value): ?string private static function normalizeHost(?string $host): string { - $normalized = self::normalizeOptionalString($host); - return $normalized ?? 'us.i.posthog.com'; + return HostNormalizer::normalize($host); } /** diff --git a/lib/HostNormalizer.php b/lib/HostNormalizer.php new file mode 100644 index 0000000..7fef0fa --- /dev/null +++ b/lib/HostNormalizer.php @@ -0,0 +1,14 @@ +assertEquals('us.i.posthog.com', $hostProp->getValue($consumer)); } + public function testInitWithWhitespacePaddedHttpHostSetsSslFalse(): void + { + PostHog::init("random_key", ["host" => " \nhttp://localhost:8010\t "]); + + $client = PostHog::getClient(); + $ref = new \ReflectionClass($client); + $consumerProp = $ref->getProperty('consumer'); + $consumerProp->setAccessible(true); + $consumer = $consumerProp->getValue($client); + + $cRef = new \ReflectionClass($consumer); + $httpProp = $cRef->getProperty('httpClient'); + $httpProp->setAccessible(true); + $httpClient = $httpProp->getValue($consumer); + + $hRef = new \ReflectionClass($httpClient); + $sslProp = $hRef->getProperty('useSsl'); + $sslProp->setAccessible(true); + + $this->assertFalse($sslProp->getValue($httpClient), 'HttpClient should use ssl=false for whitespace-padded http:// hosts'); + } + public function testInitWithHttpsHostSetsSslTrue(): void { PostHog::init("random_key", ["host" => "https://app.posthog.com"]); From 85d6691783f78cd3bbb37fb5149f09bad281610a Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 21 Apr 2026 11:24:30 +0200 Subject: [PATCH 4/5] refactor: call shared host normalizer directly --- lib/Client.php | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/lib/Client.php b/lib/Client.php index 1efb8b5..f0d108b 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -107,7 +107,7 @@ public function __construct( $this->personalAPIKey = self::normalizeOptionalString($personalAPIKey); $this->options = $options; $this->debug = $options["debug"] ?? false; - $this->options['host'] = self::normalizeHost($options['host'] ?? null); + $this->options['host'] = HostNormalizer::normalize($options['host'] ?? null); if ($this->apiKey === '') { error_log('[PostHog][Client] apiKey is empty after trimming whitespace; check your project API key'); } @@ -157,11 +157,6 @@ private static function normalizeOptionalString(?string $value): ?string return $normalized === '' ? null : $normalized; } - private static function normalizeHost(?string $host): string - { - return HostNormalizer::normalize($host); - } - /** * Captures a user action * From f0a43ed1453e5dde11eef12faa750119e417c80c Mon Sep 17 00:00:00 2001 From: Manoel Aranda Neto Date: Tue, 21 Apr 2026 11:25:59 +0200 Subject: [PATCH 5/5] refactor: share string normalization helpers --- lib/Client.php | 14 ++------------ lib/HostNormalizer.php | 14 -------------- lib/PostHog.php | 4 ++-- lib/StringNormalizer.php | 23 +++++++++++++++++++++++ 4 files changed, 27 insertions(+), 28 deletions(-) delete mode 100644 lib/HostNormalizer.php create mode 100644 lib/StringNormalizer.php diff --git a/lib/Client.php b/lib/Client.php index f0d108b..4f41443 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -104,10 +104,10 @@ public function __construct( bool $loadFeatureFlags = true, ) { $this->apiKey = trim($apiKey); - $this->personalAPIKey = self::normalizeOptionalString($personalAPIKey); + $this->personalAPIKey = StringNormalizer::normalizeOptional($personalAPIKey); $this->options = $options; $this->debug = $options["debug"] ?? false; - $this->options['host'] = HostNormalizer::normalize($options['host'] ?? null); + $this->options['host'] = StringNormalizer::normalizeHost($options['host'] ?? null); if ($this->apiKey === '') { error_log('[PostHog][Client] apiKey is empty after trimming whitespace; check your project API key'); } @@ -147,16 +147,6 @@ public function __destruct() $this->consumer->__destruct(); } - private static function normalizeOptionalString(?string $value): ?string - { - if ($value === null) { - return null; - } - - $normalized = trim($value); - return $normalized === '' ? null : $normalized; - } - /** * Captures a user action * diff --git a/lib/HostNormalizer.php b/lib/HostNormalizer.php deleted file mode 100644 index 7fef0fa..0000000 --- a/lib/HostNormalizer.php +++ /dev/null @@ -1,14 +0,0 @@ -