diff --git a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php index 69a39280..0ea2961e 100644 --- a/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php +++ b/src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php @@ -49,7 +49,7 @@ public function __construct(string $sdkKey, array $options = []) $this->_curl = $options['curl']; } - $this->_eventHeaders = Util::eventHeaders($sdkKey, $options['application_info'] ?? null); + $this->_eventHeaders = Util::eventHeaders($sdkKey, $options); $this->_connectTimeout = $options['connect_timeout']; $this->_isWindows = PHP_OS_FAMILY == 'Windows'; } diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php index 2128ec84..fb07b581 100644 --- a/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php @@ -34,7 +34,7 @@ public function __construct(string $sdkKey, array $options = []) $this->_eventsUri = \LaunchDarkly\Impl\Util::adjustBaseUri($baseUri); $this->_requestOptions = [ - 'headers' => Util::eventHeaders($this->_sdkKey, $options['application_info'] ?? null), + 'headers' => Util::eventHeaders($this->_sdkKey, $options), 'timeout' => $options['timeout'], 'connect_timeout' => $options['connect_timeout'] ]; diff --git a/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php index 5c90373e..65f5d93d 100644 --- a/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php +++ b/src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php @@ -43,7 +43,7 @@ public function __construct(string $baseUri, string $sdkKey, array $options) } $defaults = [ - 'headers' => Util::defaultHeaders($sdkKey, $options['application_info'] ?? null), + 'headers' => Util::defaultHeaders($sdkKey, $options), 'timeout' => $options['timeout'], 'connect_timeout' => $options['connect_timeout'], 'handler' => $stack, diff --git a/src/LaunchDarkly/Impl/Util.php b/src/LaunchDarkly/Impl/Util.php index aa4efc72..2f73ab51 100644 --- a/src/LaunchDarkly/Impl/Util.php +++ b/src/LaunchDarkly/Impl/Util.php @@ -92,10 +92,10 @@ public static function makeNullLogger(): LoggerInterface * made to LaunchDarkly servers. * * @param string $sdkKey - * @param ApplicationInfo|null $applicationInfo + * @params array $options * @return array */ - public static function defaultHeaders(string $sdkKey, $applicationInfo): array + public static function defaultHeaders(string $sdkKey, array $options): array { $headers = [ 'Content-Type' => 'application/json', @@ -104,6 +104,7 @@ public static function defaultHeaders(string $sdkKey, $applicationInfo): array 'User-Agent' => 'PHPClient/' . LDClient::VERSION, ]; + $applicationInfo = $options['application_info'] ?? null; if ($applicationInfo instanceof ApplicationInfo) { $headerValue = (string) $applicationInfo; if ($headerValue) { @@ -111,6 +112,14 @@ public static function defaultHeaders(string $sdkKey, $applicationInfo): array } } + if (!empty($options['wrapper_name'])) { + $headers['X-LaunchDarkly-Wrapper'] = $options['wrapper_name']; + + if (!empty($options['wrapper_version'])) { + $headers['X-LaunchDarkly-Wrapper'] .= '/' . $options['wrapper_version']; + } + } + return $headers; } @@ -119,12 +128,12 @@ public static function defaultHeaders(string $sdkKey, $applicationInfo): array * made to the LaunchDarkly Events API. * * @param string $sdkKey - * @param ApplicationInfo|null $applicationInfo + * @param array $options * @return array */ - public static function eventHeaders(string $sdkKey, $applicationInfo): array + public static function eventHeaders(string $sdkKey, array $options): array { - $headers = Util::defaultHeaders($sdkKey, $applicationInfo); + $headers = Util::defaultHeaders($sdkKey, $options); $headers['X-LaunchDarkly-Event-Schema'] = EventPublisher::CURRENT_SCHEMA_VERSION; // Only the presence of this header is important. We encode a string // value of 'true' to ensure it isn't dropped along the way. diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 0054b4a8..7e1846b4 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -78,6 +78,8 @@ class LDClient * with this configuration active will have attributes with these names removed. You can also set private attributes on a * - `application_info`: An optional {@see \LaunchDarkly\Types\ApplicationInfo} instance. * per-user basis in the LDContext builder. + * - `wrapper_name`: For use by wrapper libraries to set an identifying name for the wrapper being used. This will be sent in User-Agent headers during requests to the LaunchDarkly servers to allow recording metrics on the usage of these wrapper libraries. + * - `wrapper_version`: For use by wrapper libraries to report the version of the library in use. If `wrapper_name` is not set, this field will be ignored. Otherwise the version string will be included in the User-Agent headers along with the `wrapper_name` during requests to the LaunchDarkly servers. * - Other options may be available depending on any features you are using from the `LaunchDarkly\Integrations` namespace. * * @return LDClient diff --git a/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php b/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php index 4c2badf5..ec74046a 100644 --- a/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php +++ b/tests/Impl/Integrations/GuzzleFeatureRequesterTest.php @@ -72,4 +72,74 @@ public function testSendsCorrectHeaders(): void $this->assertEquals('PHPClient/' . LDClient::VERSION, $headers['User-Agent']); $this->assertEquals('application-id/my-id application-version/my-version', $headers['X-LaunchDarkly-Tags']); } + + public function wrapperProvider(): array + { + return [ + [null, null, null], + ['my-wrapper', null, 'my-wrapper'], + ['my-wrapper', '1.0.0', 'my-wrapper/1.0.0'], + [null, '1.0.0', null], + ]; + } + + /** + * @dataProvider wrapperProvider + */ + public function testSendsCorrectWrapperNameHeaders(?string $wrapper_name, ?string $wrapper_version, ?string $expected_header): void + { + /** @var LoggerInterface **/ + $logger = $this->getMockBuilder(LoggerInterface::class)->getMock(); + + $config = [ + 'logger' => $logger, + 'timeout' => 3, + 'connect_timeout' => 3, + ]; + + if ($wrapper_name) { + $config['wrapper_name'] = $wrapper_name; + } + if ($wrapper_version) { + $config['wrapper_version'] = $wrapper_version; + } + + $requester = new GuzzleFeatureRequester('http://localhost:8080', 'sdk-key', $config); + $requester->getFeature("flag-key"); + + $requests = []; + $client = new Client(); + + // Provide time for the curl to execute + $start = time(); + while (time() - $start < 5) { + $response = $client->request('GET', 'http://localhost:8080/__admin/requests'); + $body = json_decode($response->getBody()->getContents(), true); + $requests = $body['requests']; + + if ($requests) { + break; + } + usleep(100); + } + + if (!$requests) { + $this->fail("Unable to connect to endpoint within specified timeout"); + } + + $this->assertCount(1, $requests); + + $request = $requests[0]['request']; + + // Validate that we hit the right endpoint + $this->assertEquals('/sdk/flags/flag-key', $request['url']); + + // And validate that we provided all the correct headers + $headers = $request['headers']; + if ($expected_header) { + $this->assertEquals($expected_header, $headers['X-LaunchDarkly-Wrapper']); + } else { + $this->assertNotContains('X-LaunchDarkly-Wrapper', $headers); + } + } }