From b177b5fead5814f725d695acc893bd27020e85d3 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Thu, 18 Dec 2025 16:38:24 -0500 Subject: [PATCH 1/3] feat: Set `$feature_flag_error` on `$feature_flag_called` --- lib/Client.php | 134 +++++++++-- lib/FeatureFlagError.php | 47 ++++ lib/HttpClient.php | 5 +- lib/HttpException.php | 74 ++++++ lib/HttpResponse.php | 14 +- test/FeatureFlagErrorTest.php | 418 ++++++++++++++++++++++++++++++++++ test/MockedHttpClient.php | 19 +- 7 files changed, 690 insertions(+), 21 deletions(-) create mode 100644 lib/FeatureFlagError.php create mode 100644 lib/HttpException.php create mode 100644 test/FeatureFlagErrorTest.php diff --git a/lib/Client.php b/lib/Client.php index 4dc1599..7aa271c 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -260,6 +260,7 @@ public function getFeatureFlag( $groupProperties ); $result = null; + $featureFlagError = null; foreach ($this->featureFlags as $flag) { if ($flag["key"] == $key) { @@ -290,6 +291,12 @@ public function getFeatureFlag( if (!$flagWasEvaluatedLocally && !$onlyEvaluateLocally) { try { $response = $this->fetchFlagsResponse($distinctId, $groups, $personProperties, $groupProperties); + $errors = []; + + if (isset($response['errorsWhileComputingFlags']) && $response['errorsWhileComputingFlags']) { + $errors[] = FeatureFlagError::ERRORS_WHILE_COMPUTING_FLAGS; + } + $requestId = isset($response['requestId']) ? $response['requestId'] : null; $evaluatedAt = isset($response['evaluatedAt']) ? $response['evaluatedAt'] : null; $flagDetail = isset($response['flags'][$key]) ? $response['flags'][$key] : null; @@ -297,10 +304,35 @@ public function getFeatureFlag( if (array_key_exists($key, $featureFlags)) { $result = $featureFlags[$key]; } else { + $errors[] = FeatureFlagError::FLAG_MISSING; $result = null; } + + if (!empty($errors)) { + $featureFlagError = implode(',', $errors); + } + } catch (HttpException $e) { + error_log("[PostHog][Client] Unable to get feature variants: " . $e->getMessage()); + switch ($e->getErrorType()) { + case HttpException::QUOTA_LIMITED: + $featureFlagError = FeatureFlagError::QUOTA_LIMITED; + break; + case HttpException::TIMEOUT: + $featureFlagError = FeatureFlagError::TIMEOUT; + break; + case HttpException::CONNECTION_ERROR: + $featureFlagError = FeatureFlagError::CONNECTION_ERROR; + break; + case HttpException::API_ERROR: + $featureFlagError = FeatureFlagError::apiError($e->getStatusCode()); + break; + default: + $featureFlagError = FeatureFlagError::UNKNOWN_ERROR; + } + $result = null; } catch (Exception $e) { - error_log("[PostHog][Client] Unable to get feature variants:" . $e->getMessage()); + error_log("[PostHog][Client] Unable to get feature variants: " . $e->getMessage()); + $featureFlagError = FeatureFlagError::UNKNOWN_ERROR; $result = null; } } @@ -325,6 +357,10 @@ public function getFeatureFlag( $properties['$feature_flag_reason'] = $flagDetail['reason']['description']; } + if (!is_null($featureFlagError)) { + $properties['$feature_flag_error'] = $featureFlagError; + } + $this->capture([ "properties" => $properties, "distinct_id" => $distinctId, @@ -355,10 +391,7 @@ public function getFeatureFlagPayload( array $personProperties = array(), array $groupProperties = array(), ): mixed { - $results = json_decode( - $this->flags($distinctId, $groups, $personProperties, $groupProperties), - true - ); + $results = $this->flags($distinctId, $groups, $personProperties, $groupProperties); if (isset($results['featureFlags'][$key]) === false || $results['featureFlags'][$key] !== true) { return null; @@ -517,10 +550,7 @@ private function fetchFlagsResponse( array $personProperties = [], array $groupProperties = [] ): ?array { - return json_decode( - $this->flags($distinctId, $groups, $personProperties, $groupProperties), - true - ); + return $this->flags($distinctId, $groups, $personProperties, $groupProperties); } /** @@ -600,9 +630,39 @@ public function getFlagsEtag(): ?string return $this->flagsEtag; } - private function normalizeFeatureFlags(string $response): string + /** + * Normalize feature flags response to ensure consistent format. + * Decodes JSON, checks for quota limits, and transforms v4 to v3 format. + * + * @param string $response The raw JSON response + * @return array The normalized response + * @throws HttpException On invalid JSON or quota limit + */ + private function normalizeFeatureFlags(string $response): array { $decoded = json_decode($response, true); + + if (!is_array($decoded)) { + throw new HttpException( + HttpException::API_ERROR, + 0, + "Invalid JSON response" + ); + } + + // Check for quota limit in response body + if ( + isset($decoded['quotaLimited']) + && is_array($decoded['quotaLimited']) + && in_array('feature_flags', $decoded['quotaLimited']) + ) { + throw new HttpException( + HttpException::QUOTA_LIMITED, + 0, + "Feature flags quota limited" + ); + } + if (isset($decoded['flags']) && !empty($decoded['flags'])) { // This is a v4 response, we need to transform it to a v3 response for backwards compatibility $transformedFlags = []; @@ -619,18 +679,27 @@ private function normalizeFeatureFlags(string $response): string } $decoded['featureFlags'] = $transformedFlags; $decoded['featureFlagPayloads'] = $transformedPayloads; - return json_encode($decoded); } - return $response; + return $decoded; } + /** + * Fetch feature flags from the PostHog API. + * + * @param string $distinctId The user's distinct ID + * @param array $groups Group identifiers + * @param array $personProperties Person properties for flag evaluation + * @param array $groupProperties Group properties for flag evaluation + * @return array The normalized feature flags response + * @throws HttpException On network errors, API errors, or quota limits + */ public function flags( string $distinctId, array $groups = array(), array $personProperties = [], array $groupProperties = [] - ) { + ): array { $payload = array( 'api_key' => $this->apiKey, 'distinct_id' => $distinctId, @@ -648,7 +717,7 @@ public function flags( $payload["group_properties"] = $groupProperties; } - $response = $this->httpClient->sendRequest( + $httpResponse = $this->httpClient->sendRequest( '/flags/?v=2', json_encode($payload), [ @@ -659,9 +728,42 @@ public function flags( "shouldRetry" => false, "timeout" => $this->featureFlagsRequestTimeout ] - )->getResponse(); + ); + + $responseCode = $httpResponse->getResponseCode(); + $curlErrno = $httpResponse->getCurlErrno(); + + if ($responseCode === 0) { + // CURLE_OPERATION_TIMEDOUT (28) + // https://curl.se/libcurl/c/libcurl-errors.html + if ($curlErrno === 28) { + throw new HttpException( + HttpException::TIMEOUT, + 0, + "Request timed out" + ); + } + // Consider everything else a connection error + // CURLE_COULDNT_RESOLVE_HOST (6) + // CURLE_COULDNT_CONNECT (7) + // CURLE_WEIRD_SERVER_REPLY (8) + // etc. + throw new HttpException( + HttpException::CONNECTION_ERROR, + 0, + "Connection error (curl errno: {$curlErrno})" + ); + } + + if ($responseCode >= 400) { + throw new HttpException( + HttpException::API_ERROR, + $responseCode, + "API error: HTTP {$responseCode}" + ); + } - return $this->normalizeFeatureFlags($response); + return $this->normalizeFeatureFlags($httpResponse->getResponse()); } /** diff --git a/lib/FeatureFlagError.php b/lib/FeatureFlagError.php new file mode 100644 index 0000000..19c7e9c --- /dev/null +++ b/lib/FeatureFlagError.php @@ -0,0 +1,47 @@ +errorType = $errorType; + $this->statusCode = $statusCode; + parent::__construct($message); + } + + /** + * Get the error type constant + * + * @return string One of TIMEOUT, CONNECTION_ERROR, QUOTA_LIMITED, API_ERROR + */ + public function getErrorType(): string + { + return $this->errorType; + } + + /** + * Get the HTTP status code + * + * @return int HTTP status code (0 for connection/timeout errors) + */ + public function getStatusCode(): int + { + return $this->statusCode; + } +} diff --git a/lib/HttpResponse.php b/lib/HttpResponse.php index 26c2eed..bd061fe 100644 --- a/lib/HttpResponse.php +++ b/lib/HttpResponse.php @@ -7,12 +7,14 @@ class HttpResponse private $response; private $responseCode; private $etag; + private $curlErrno; - public function __construct($response, $responseCode, ?string $etag = null) + public function __construct($response, $responseCode, ?string $etag = null, int $curlErrno = 0) { $this->response = $response; $this->responseCode = $responseCode; $this->etag = $etag; + $this->curlErrno = $curlErrno; } /** @@ -48,4 +50,14 @@ public function isNotModified(): bool { return $this->responseCode === 304; } + + /** + * Get the curl error number (0 if no error) + * + * @return int + */ + public function getCurlErrno(): int + { + return $this->curlErrno; + } } diff --git a/test/FeatureFlagErrorTest.php b/test/FeatureFlagErrorTest.php new file mode 100644 index 0000000..2d265a8 --- /dev/null +++ b/test/FeatureFlagErrorTest.php @@ -0,0 +1,418 @@ +http_client = new MockedHttpClient("app.posthog.com", flagsEndpointResponse: $flagsEndpointResponse); + $this->client = new Client( + self::FAKE_API_KEY, + [ + "debug" => true, + ], + $this->http_client, + $personalApiKey + ); + PostHog::init(null, null, $this->client); + + // Reset the errorMessages array before each test + global $errorMessages; + $errorMessages = []; + } + + public function testFlagMissingError() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + $this->setUp(MockedResponses::FLAGS_RESPONSE, personalApiKey: null); + + // Request a flag that doesn't exist in the response + $result = PostHog::getFeatureFlag('non-existent-flag', 'user-id'); + $this->assertNull($result); + + PostHog::flush(); + + // Check that the $feature_flag_called event includes the flag_missing error + $calls = $this->http_client->calls; + $this->assertCount(2, $calls); + + // First call is to /flags/ + $this->assertStringStartsWith("/flags/", $calls[0]['path']); + + // Second call is to /batch/ with the $feature_flag_called event + $this->assertEquals("/batch/", $calls[1]['path']); + $payload = json_decode($calls[1]['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals('non-existent-flag', $event['properties']['$feature_flag']); + $this->assertNull($event['properties']['$feature_flag_response']); + $this->assertEquals(FeatureFlagError::FLAG_MISSING, $event['properties']['$feature_flag_error']); + }); + } + + public function testErrorsWhileComputingFlagsError() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + // Create a response with errorsWhileComputingFlags set to true + $responseWithErrors = array_merge(MockedResponses::FLAGS_RESPONSE, [ + 'errorsWhileComputingFlags' => true + ]); + + $this->setUp($responseWithErrors, personalApiKey: null); + + // Request a flag that exists in the response + $result = PostHog::getFeatureFlag('simple-test', 'user-id'); + $this->assertTrue($result); + + PostHog::flush(); + + // Check that the $feature_flag_called event includes the errors_while_computing_flags error + $calls = $this->http_client->calls; + $this->assertCount(2, $calls); + + $payload = json_decode($calls[1]['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals('simple-test', $event['properties']['$feature_flag']); + $this->assertTrue($event['properties']['$feature_flag_response']); + $this->assertEquals(FeatureFlagError::ERRORS_WHILE_COMPUTING_FLAGS, $event['properties']['$feature_flag_error']); + }); + } + + public function testMultipleErrors() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + // Create a response with errorsWhileComputingFlags set to true + // and request a flag that doesn't exist + $responseWithErrors = array_merge(MockedResponses::FLAGS_RESPONSE, [ + 'errorsWhileComputingFlags' => true + ]); + + $this->setUp($responseWithErrors, personalApiKey: null); + + // Request a flag that doesn't exist + $result = PostHog::getFeatureFlag('non-existent-flag', 'user-id'); + $this->assertNull($result); + + PostHog::flush(); + + // Check that the $feature_flag_called event includes both errors + $calls = $this->http_client->calls; + $this->assertCount(2, $calls); + + $payload = json_decode($calls[1]['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals('non-existent-flag', $event['properties']['$feature_flag']); + $this->assertNull($event['properties']['$feature_flag_response']); + + // Errors should be joined with commas + $expectedErrors = FeatureFlagError::ERRORS_WHILE_COMPUTING_FLAGS . ',' . FeatureFlagError::FLAG_MISSING; + $this->assertEquals($expectedErrors, $event['properties']['$feature_flag_error']); + }); + } + + public function testNoErrorWhenFlagEvaluatesSuccessfully() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + $this->setUp(MockedResponses::FLAGS_RESPONSE, personalApiKey: null); + + // Request a flag that exists in the response + $result = PostHog::getFeatureFlag('simple-test', 'user-id'); + $this->assertTrue($result); + + PostHog::flush(); + + // Check that the $feature_flag_called event does NOT include an error + $calls = $this->http_client->calls; + $this->assertCount(2, $calls); + + $payload = json_decode($calls[1]['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals('simple-test', $event['properties']['$feature_flag']); + $this->assertTrue($event['properties']['$feature_flag_response']); + + // Should not have $feature_flag_error property + $this->assertArrayNotHasKey('$feature_flag_error', $event['properties']); + }); + } + + public function testUnknownErrorWhenExceptionThrown() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + // Create a mocked client that will throw an exception + $this->http_client = new class ("app.posthog.com") extends MockedHttpClient { + public function sendRequest(string $path, ?string $payload, array $extraHeaders = [], array $requestOptions = []): \PostHog\HttpResponse + { + if (!isset($this->calls)) { + $this->calls = []; + } + array_push($this->calls, array("path" => $path, "payload" => $payload, "extraHeaders" => $extraHeaders, "requestOptions" => $requestOptions)); + + if (str_starts_with($path, "/flags/")) { + throw new \Exception("Network error"); + } + + return parent::sendRequest($path, $payload, $extraHeaders, $requestOptions); + } + }; + + $this->client = new Client( + self::FAKE_API_KEY, + [ + "debug" => true, + ], + $this->http_client, + null + ); + PostHog::init(null, null, $this->client); + + // Reset error messages + global $errorMessages; + $errorMessages = []; + + // Request a flag - this should trigger an exception + $result = PostHog::getFeatureFlag('simple-test', 'user-id'); + $this->assertNull($result); + + PostHog::flush(); + + // Check that the $feature_flag_called event includes the unknown_error + $calls = $this->http_client->calls; + + // Find the batch call (there might be multiple calls) + $batchCall = null; + foreach ($calls as $call) { + if ($call['path'] === '/batch/') { + $batchCall = $call; + break; + } + } + + $this->assertNotNull($batchCall, "Expected to find a /batch/ call"); + + $payload = json_decode($batchCall['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals('simple-test', $event['properties']['$feature_flag']); + $this->assertNull($event['properties']['$feature_flag_response']); + $this->assertEquals(FeatureFlagError::UNKNOWN_ERROR, $event['properties']['$feature_flag_error']); + }); + } + + public function testApiErrorMethod() + { + // Test the apiError static method + $this->assertEquals('api_error_500', FeatureFlagError::apiError(500)); + $this->assertEquals('api_error_404', FeatureFlagError::apiError(404)); + $this->assertEquals('api_error_429', FeatureFlagError::apiError(429)); + } + + public function testTimeoutError() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + // Create a mocked client that simulates a timeout (responseCode=0, curlErrno=28) + $this->http_client = new MockedHttpClient( + "app.posthog.com", + flagsEndpointResponse: MockedResponses::FLAGS_RESPONSE, + flagsEndpointResponseCode: 0, + flagsEndpointCurlErrno: 28 // CURLE_OPERATION_TIMEDOUT + ); + + $this->client = new Client( + self::FAKE_API_KEY, + ["debug" => true], + $this->http_client, + null + ); + PostHog::init(null, null, $this->client); + + global $errorMessages; + $errorMessages = []; + + $result = PostHog::getFeatureFlag('simple-test', 'user-id'); + $this->assertNull($result); + + PostHog::flush(); + + $calls = $this->http_client->calls; + $batchCall = null; + foreach ($calls as $call) { + if ($call['path'] === '/batch/') { + $batchCall = $call; + break; + } + } + + $this->assertNotNull($batchCall, "Expected to find a /batch/ call"); + + $payload = json_decode($batchCall['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals(FeatureFlagError::TIMEOUT, $event['properties']['$feature_flag_error']); + }); + } + + public function testConnectionError() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + // Create a mocked client that simulates a connection error (responseCode=0, curlErrno=6) + $this->http_client = new MockedHttpClient( + "app.posthog.com", + flagsEndpointResponse: MockedResponses::FLAGS_RESPONSE, + flagsEndpointResponseCode: 0, + flagsEndpointCurlErrno: 6 // CURLE_COULDNT_RESOLVE_HOST + ); + + $this->client = new Client( + self::FAKE_API_KEY, + ["debug" => true], + $this->http_client, + null + ); + PostHog::init(null, null, $this->client); + + global $errorMessages; + $errorMessages = []; + + $result = PostHog::getFeatureFlag('simple-test', 'user-id'); + $this->assertNull($result); + + PostHog::flush(); + + $calls = $this->http_client->calls; + $batchCall = null; + foreach ($calls as $call) { + if ($call['path'] === '/batch/') { + $batchCall = $call; + break; + } + } + + $this->assertNotNull($batchCall, "Expected to find a /batch/ call"); + + $payload = json_decode($batchCall['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals(FeatureFlagError::CONNECTION_ERROR, $event['properties']['$feature_flag_error']); + }); + } + + public function testApiError500() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + // Create a mocked client that simulates a 500 error + $this->http_client = new MockedHttpClient( + "app.posthog.com", + flagsEndpointResponse: MockedResponses::FLAGS_RESPONSE, + flagsEndpointResponseCode: 500 + ); + + $this->client = new Client( + self::FAKE_API_KEY, + ["debug" => true], + $this->http_client, + null + ); + PostHog::init(null, null, $this->client); + + global $errorMessages; + $errorMessages = []; + + $result = PostHog::getFeatureFlag('simple-test', 'user-id'); + $this->assertNull($result); + + PostHog::flush(); + + $calls = $this->http_client->calls; + $batchCall = null; + foreach ($calls as $call) { + if ($call['path'] === '/batch/') { + $batchCall = $call; + break; + } + } + + $this->assertNotNull($batchCall, "Expected to find a /batch/ call"); + + $payload = json_decode($batchCall['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals('api_error_500', $event['properties']['$feature_flag_error']); + }); + } + + public function testQuotaLimitedError() + { + ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { + // Create a response with quotaLimited containing feature_flags + $quotaLimitedResponse = array_merge(MockedResponses::FLAGS_RESPONSE, [ + 'quotaLimited' => ['feature_flags'] + ]); + + $this->http_client = new MockedHttpClient( + "app.posthog.com", + flagsEndpointResponse: $quotaLimitedResponse + ); + + $this->client = new Client( + self::FAKE_API_KEY, + ["debug" => true], + $this->http_client, + null + ); + PostHog::init(null, null, $this->client); + + global $errorMessages; + $errorMessages = []; + + $result = PostHog::getFeatureFlag('simple-test', 'user-id'); + $this->assertNull($result); + + PostHog::flush(); + + $calls = $this->http_client->calls; + $batchCall = null; + foreach ($calls as $call) { + if ($call['path'] === '/batch/') { + $batchCall = $call; + break; + } + } + + $this->assertNotNull($batchCall, "Expected to find a /batch/ call"); + + $payload = json_decode($batchCall['payload'], true); + $event = $payload['batch'][0]; + + $this->assertEquals('$feature_flag_called', $event['event']); + $this->assertEquals(FeatureFlagError::QUOTA_LIMITED, $event['properties']['$feature_flag_error']); + }); + } +} diff --git a/test/MockedHttpClient.php b/test/MockedHttpClient.php index c49f138..28b5b7b 100644 --- a/test/MockedHttpClient.php +++ b/test/MockedHttpClient.php @@ -18,6 +18,12 @@ class MockedHttpClient extends \PostHog\HttpClient /** @var array|null Queue of responses for sequential calls */ private $flagEndpointResponseQueue; + /** @var int Response code for /flags/ endpoint (for error simulation) */ + private $flagsEndpointResponseCode; + + /** @var int Curl error number for /flags/ endpoint (for error simulation) */ + private $flagsEndpointCurlErrno; + public function __construct( string $host, bool $useSsl = true, @@ -29,7 +35,9 @@ public function __construct( array $flagEndpointResponse = [], array $flagsEndpointResponse = [], ?string $flagEndpointEtag = null, - int $flagEndpointResponseCode = 200 + int $flagEndpointResponseCode = 200, + int $flagsEndpointResponseCode = 200, + int $flagsEndpointCurlErrno = 0 ) { parent::__construct( $host, @@ -45,6 +53,8 @@ public function __construct( $this->flagEndpointEtag = $flagEndpointEtag; $this->flagEndpointResponseCode = $flagEndpointResponseCode; $this->flagEndpointResponseQueue = null; + $this->flagsEndpointResponseCode = $flagsEndpointResponseCode; + $this->flagsEndpointCurlErrno = $flagsEndpointCurlErrno; } /** @@ -66,7 +76,12 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders array_push($this->calls, array("path" => $path, "payload" => $payload, "extraHeaders" => $extraHeaders, "requestOptions" => $requestOptions)); if (str_starts_with($path, "/flags/")) { - return new HttpResponse(json_encode($this->flagsEndpointResponse), 200); + return new HttpResponse( + json_encode($this->flagsEndpointResponse), + $this->flagsEndpointResponseCode, + null, + $this->flagsEndpointCurlErrno + ); } if (str_starts_with($path, "/api/feature_flag/local_evaluation")) { From c91082cf88d13095a1381177c5fc3358a0272f45 Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Fri, 19 Dec 2025 15:13:46 -0500 Subject: [PATCH 2/3] chore: phpcs lint warnings --- test/FeatureFlagErrorTest.php | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/test/FeatureFlagErrorTest.php b/test/FeatureFlagErrorTest.php index 2d265a8..0154644 100644 --- a/test/FeatureFlagErrorTest.php +++ b/test/FeatureFlagErrorTest.php @@ -14,7 +14,7 @@ class FeatureFlagErrorTest extends TestCase { - const FAKE_API_KEY = "random_key"; + public const FAKE_API_KEY = "random_key"; private $http_client; private $client; @@ -94,7 +94,10 @@ public function testErrorsWhileComputingFlagsError() $this->assertEquals('$feature_flag_called', $event['event']); $this->assertEquals('simple-test', $event['properties']['$feature_flag']); $this->assertTrue($event['properties']['$feature_flag_response']); - $this->assertEquals(FeatureFlagError::ERRORS_WHILE_COMPUTING_FLAGS, $event['properties']['$feature_flag_error']); + $this->assertEquals( + FeatureFlagError::ERRORS_WHILE_COMPUTING_FLAGS, + $event['properties']['$feature_flag_error'] + ); }); } @@ -164,12 +167,21 @@ public function testUnknownErrorWhenExceptionThrown() ClockMock::executeAtFrozenDateTime(new \DateTime('2022-05-01'), function () { // Create a mocked client that will throw an exception $this->http_client = new class ("app.posthog.com") extends MockedHttpClient { - public function sendRequest(string $path, ?string $payload, array $extraHeaders = [], array $requestOptions = []): \PostHog\HttpResponse - { + public function sendRequest( + string $path, + ?string $payload, + array $extraHeaders = [], + array $requestOptions = [] + ): \PostHog\HttpResponse { if (!isset($this->calls)) { $this->calls = []; } - array_push($this->calls, array("path" => $path, "payload" => $payload, "extraHeaders" => $extraHeaders, "requestOptions" => $requestOptions)); + array_push($this->calls, array( + "path" => $path, + "payload" => $payload, + "extraHeaders" => $extraHeaders, + "requestOptions" => $requestOptions + )); if (str_starts_with($path, "/flags/")) { throw new \Exception("Network error"); From 0500365b1b9b38a61a8a1a14f7655b89e997f5ac Mon Sep 17 00:00:00 2001 From: Dustin Byrne Date: Fri, 19 Dec 2025 15:20:20 -0500 Subject: [PATCH 3/3] chore: Ignore side effect warning It's due to us requiring error_log_mock --- test/FeatureFlagErrorTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/FeatureFlagErrorTest.php b/test/FeatureFlagErrorTest.php index 0154644..118c8a0 100644 --- a/test/FeatureFlagErrorTest.php +++ b/test/FeatureFlagErrorTest.php @@ -1,5 +1,5 @@