diff --git a/History.md b/History.md index 8c07e47..580cc90 100644 --- a/History.md +++ b/History.md @@ -1,4 +1,10 @@ +3.2.2 / 2024-03-11 +================== + + * feat(flags): Add specific timeout for feature flags (#62) + * Adds a new `feature_flag_request_timeout_ms` timeout parameter for feature flags which defaults to 3 seconds, updated from the default 10s for all other API calls. + 3.2.1 / 2024-01-26 ================== diff --git a/composer.json b/composer.json index 606c1d9..9e0ba30 100644 --- a/composer.json +++ b/composer.json @@ -1,6 +1,6 @@ { "name": "posthog/posthog-php", - "version": "3.2.1", + "version": "3.2.2", "description": "PostHog PHP Library", "keywords": [ "posthog" diff --git a/lib/Client.php b/lib/Client.php index cdf1b0a..096f14d 100644 --- a/lib/Client.php +++ b/lib/Client.php @@ -30,6 +30,11 @@ class Client */ private $personalAPIKey; + /** + * @var integer + */ + private $featureFlagsRequestTimeout; + /** * Consumer object handles queueing and bundling requests to PostHog. * @@ -90,6 +95,7 @@ public function __construct( null, (int) ($options['timeout'] ?? 10000) ); + $this->featureFlagsRequestTimeout = (int) ($options['feature_flag_request_timeout_ms'] ?? 3000); $this->featureFlags = []; $this->groupTypeMapping = []; $this->cohorts = []; @@ -461,6 +467,10 @@ public function decide( [ // Send user agent in the form of {library_name}/{library_version} as per RFC 7231. "User-Agent: posthog-php/" . PostHog::VERSION, + ], + [ + "shouldRetry" => false, + "timeout" => $this->featureFlagsRequestTimeout ] )->getResponse(); } diff --git a/lib/HttpClient.php b/lib/HttpClient.php index 4fc28e5..8d8c773 100644 --- a/lib/HttpClient.php +++ b/lib/HttpClient.php @@ -62,14 +62,17 @@ public function __construct( * @param string $path * @param string|null $payload * @param array $extraHeaders + * @param array $requestOptions * @return HttpResponse */ - public function sendRequest(string $path, ?string $payload, array $extraHeaders = []): HttpResponse + public function sendRequest(string $path, ?string $payload, array $extraHeaders = [], array $requestOptions = []): HttpResponse { $protocol = $this->useSsl ? "https://" : "http://"; $backoff = 100; // Set initial waiting time to 100ms + $shouldRetry = $requestOptions['shouldRetry'] ?? true; + do { // open connection $ch = curl_init(); @@ -84,11 +87,17 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders $headers[] = 'Content-Encoding: gzip'; } + // check if timeout exists in request options, if not use default + $timeout = $this->curlTimeoutMilliseconds; + if (isset($requestOptions['timeout'])) { + $timeout = $requestOptions['timeout']; + } + curl_setopt($ch, CURLOPT_HTTPHEADER, array_merge($headers, $extraHeaders)); curl_setopt($ch, CURLOPT_URL, $protocol . $this->host . $path); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT_MS, $this->curlTimeoutMilliseconds); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $this->curlTimeoutMilliseconds); + curl_setopt($ch, CURLOPT_TIMEOUT_MS, $timeout); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT_MS, $timeout); // retry failed requests just once to diminish impact on performance $httpResponse = $this->executePost($ch); @@ -101,7 +110,9 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders // log error $this->handleError($ch, $responseCode); - if (($responseCode >= 500 && $responseCode <= 600) || 429 == $responseCode) { + if ($shouldRetry === false) { + break; + } elseif (($responseCode >= 500 && $responseCode <= 600) || 429 == $responseCode) { // If status code is greater than 500 and less than 600, it indicates server error // Error code 429 indicates rate limited. // Retry uploading in these cases. @@ -115,7 +126,7 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders } else { break; // no error } - } while ($backoff < $this->maximumBackoffDuration); + } while ($shouldRetry && $backoff < $this->maximumBackoffDuration); return $httpResponse; } diff --git a/test/MockedHttpClient.php b/test/MockedHttpClient.php index 7ac4ab7..26979a4 100644 --- a/test/MockedHttpClient.php +++ b/test/MockedHttpClient.php @@ -34,12 +34,12 @@ public function __construct( $this->flagEndpointResponse = $flagEndpointResponse; } - public function sendRequest(string $path, ?string $payload, array $extraHeaders = []): HttpResponse + public function sendRequest(string $path, ?string $payload, array $extraHeaders = [], array $requestOptions = []): HttpResponse { if (!isset($this->calls)) { $this->calls = []; } - array_push($this->calls, array("path" => $path, "payload" => $payload)); + array_push($this->calls, array("path" => $path, "payload" => $payload, "extraHeaders" => $extraHeaders, "requestOptions" => $requestOptions)); if (str_starts_with($path, "/decide/")) { return new HttpResponse(json_encode(MockedResponses::DECIDE_REQUEST), 200); @@ -49,6 +49,6 @@ public function sendRequest(string $path, ?string $payload, array $extraHeaders return new HttpResponse(json_encode($this->flagEndpointResponse), 200); } - return parent::sendRequest($path, $payload, $extraHeaders); + return parent::sendRequest($path, $payload, $extraHeaders, $requestOptions); } } diff --git a/test/PostHogTest.php b/test/PostHogTest.php index a9fe472..d0a5d9d 100644 --- a/test/PostHogTest.php +++ b/test/PostHogTest.php @@ -89,6 +89,7 @@ public function testCaptureWithSendFeatureFlagsOption(): void self::FAKE_API_KEY, [ "debug" => true, + "feature_flag_request_timeout_ms" => 1234, ], $this->http_client, "test" @@ -112,14 +113,20 @@ public function testCaptureWithSendFeatureFlagsOption(): void 0 => array ( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), ), 1 => array ( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"john"}', self::FAKE_API_KEY), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 1234, "shouldRetry" => false), ), 2 => array ( "path" => "/batch/", "payload" => '{"batch":[{"event":"Module PHP Event","send_feature_flags":true,"properties":{"$feature\/simpleFlag":true,"$feature\/having_fun":false,"$feature\/enabled-flag":true,"$feature\/disabled-flag":false,"$feature\/multivariate-simple-test":"variant-simple-value","$feature\/simple-test":true,"$feature\/multivariate-test":"variant-value","$feature\/group-flag":"decide-fallback-value","$feature\/complex-flag":"decide-fallback-value","$feature\/beta-feature":"decide-fallback-value","$feature\/beta-feature2":"alakazam","$feature\/feature-1":"decide-fallback-value","$feature\/feature-2":"decide-fallback-value","$feature\/variant-1":"variant-1","$feature\/variant-3":"variant-3","$active_feature_flags":["simpleFlag","enabled-flag","multivariate-simple-test","simple-test","multivariate-test","group-flag","complex-flag","beta-feature","beta-feature2","feature-1","feature-2","variant-1","variant-3"],"$lib":"posthog-php","$lib_version":"3.0.3","$lib_consumer":"LibCurl"},"library":"posthog-php","library_version":"3.0.3","library_consumer":"LibCurl","distinct_id":"john","groups":[],"timestamp":"2022-05-01T00:00:00+00:00","type":"capture"}],"api_key":"random_key"}', + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array(), ), ) ); @@ -167,10 +174,14 @@ public function testCaptureWithLocalSendFlags(): void 0 => array ( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), ), 1 => array ( "path" => "/batch/", "payload" => '{"batch":[{"event":"Module PHP Event","properties":{"$feature\/true-flag":true,"$active_feature_flags":["true-flag"],"$lib":"posthog-php","$lib_version":"3.0.3","$lib_consumer":"LibCurl"},"library":"posthog-php","library_version":"3.0.3","library_consumer":"LibCurl","distinct_id":"john","groups":[],"timestamp":"2022-05-01T00:00:00+00:00","type":"capture"}],"api_key":"random_key"}', + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array(), ), ) ); @@ -211,10 +222,15 @@ public function testCaptureWithLocalSendFlagsNoOverrides(): void 0 => array ( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), + ), 1 => array ( "path" => "/batch/", "payload" => '{"batch":[{"event":"Module PHP Event","properties":{"$feature\/true-flag":"random-override","$active_feature_flags":["true-flag"],"$lib":"posthog-php","$lib_version":"3.0.3","$lib_consumer":"LibCurl"},"library":"posthog-php","library_version":"3.0.3","library_consumer":"LibCurl","distinct_id":"john","groups":[],"timestamp":"2022-05-01T00:00:00+00:00","type":"capture"}],"api_key":"random_key"}', + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array(), ), ) ); @@ -245,10 +261,14 @@ public function testIsFeatureEnabled() 0 => array( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), ), 1 => array( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"user-id","person_properties":{"distinct_id":"user-id"}}', self::FAKE_API_KEY), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) ); @@ -264,6 +284,8 @@ public function testIsFeatureEnabledGroups() 0 => array( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), ), 1 => array( "path" => "/decide/?v=2", @@ -271,6 +293,8 @@ public function testIsFeatureEnabledGroups() '{"api_key":"%s","distinct_id":"user-id","groups":{"company":"id:5"},"person_properties":{"distinct_id":"user-id"},"group_properties":{"company":{"$group_key":"id:5"}}}', self::FAKE_API_KEY ), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) ); @@ -285,10 +309,14 @@ public function testGetFeatureFlag() 0 => array( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), ), 1 => array( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"user-id","person_properties":{"distinct_id":"user-id"}}', self::FAKE_API_KEY), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) ); @@ -314,6 +342,8 @@ public function testGetFeatureFlagGroups() 0 => array( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), ), 1 => array( "path" => "/decide/?v=2", @@ -321,6 +351,8 @@ public function testGetFeatureFlagGroups() '{"api_key":"%s","distinct_id":"user-id","groups":{"company":"id:5"},"person_properties":{"distinct_id":"user-id"},"group_properties":{"company":{"$group_key":"id:5"}}}', self::FAKE_API_KEY ), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) ); @@ -480,10 +512,14 @@ public function testDefaultPropertiesGetAddedProperly(): void 0 => array( "path" => "/api/feature_flag/local_evaluation?send_cohorts&token=random_key", "payload" => null, + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3', 1 => 'Authorization: Bearer test'), + "requestOptions" => array(), ), 1 => array( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"some_id","groups":{"company":"id:5","instance":"app.posthog.com"},"person_properties":{"distinct_id":"some_id","x1":"y1"},"group_properties":{"company":{"$group_key":"id:5","x":"y"},"instance":{"$group_key":"app.posthog.com"}}}', self::FAKE_API_KEY), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) ); @@ -504,6 +540,8 @@ public function testDefaultPropertiesGetAddedProperly(): void 0 => array( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"some_id","groups":{"company":"id:5","instance":"app.posthog.com"},"person_properties":{"distinct_id":"override"},"group_properties":{"company":{"$group_key":"group_override"},"instance":{"$group_key":"app.posthog.com"}}}', self::FAKE_API_KEY), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) ); @@ -518,6 +556,8 @@ public function testDefaultPropertiesGetAddedProperly(): void 0 => array( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"some_id","groups":{"company":"id:5"},"person_properties":{"distinct_id":"some_id"},"group_properties":{"company":{"$group_key":"id:5"}}}', self::FAKE_API_KEY), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) ); @@ -532,6 +572,8 @@ public function testDefaultPropertiesGetAddedProperly(): void 0 => array( "path" => "/decide/?v=2", "payload" => sprintf('{"api_key":"%s","distinct_id":"some_id","groups":{"company":"id:5","instance":"app.posthog.com"},"person_properties":{"distinct_id":"some_id","x1":"y1"},"group_properties":{"company":{"$group_key":"id:5","x":"y"},"instance":{"$group_key":"app.posthog.com"}}}', self::FAKE_API_KEY), + "extraHeaders" => array(0 => 'User-Agent: posthog-php/3.0.3'), + "requestOptions" => array("timeout" => 3000, "shouldRetry" => false), ), ) );