Skip to content

Commit f0db959

Browse files
authored
Add application info support (#124) (#125)
In the 5.x branch, we introduce the Types namespace. To avoid shuffling this file around between versions, we are creating it now in the 4.x branch.
1 parent 0cbf8c5 commit f0db959

File tree

9 files changed

+359
-39
lines changed

9 files changed

+359
-39
lines changed

src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php

Lines changed: 15 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace LaunchDarkly\Impl\Integrations;
66

7+
use LaunchDarkly\Impl\Util;
78
use LaunchDarkly\LDClient;
89
use LaunchDarkly\Subsystems\EventPublisher;
910

@@ -15,7 +16,6 @@
1516
*/
1617
class CurlEventPublisher implements EventPublisher
1718
{
18-
private string $_sdkKey;
1919
private string $_host;
2020
private int $_port;
2121
private string $_path;
@@ -24,10 +24,11 @@ class CurlEventPublisher implements EventPublisher
2424
private int $_connectTimeout;
2525
private bool $_isWindows;
2626

27+
/** @var array<string, string> */
28+
private array $_eventHeaders;
29+
2730
public function __construct(string $sdkKey, array $options = [])
2831
{
29-
$this->_sdkKey = $sdkKey;
30-
3132
$baseUri = $options['events_uri'] ?? null;
3233
if (!$baseUri) {
3334
$baseUri = LDClient::DEFAULT_EVENTS_URI;
@@ -48,6 +49,7 @@ public function __construct(string $sdkKey, array $options = [])
4849
$this->_curl = $options['curl'];
4950
}
5051

52+
$this->_eventHeaders = Util::eventHeaders($sdkKey, $options['application_info'] ?? null);
5153
$this->_connectTimeout = $options['connect_timeout'];
5254
$this->_isWindows = PHP_OS_FAMILY == 'Windows';
5355
}
@@ -79,11 +81,15 @@ private function createCurlArgs(string $payload): string
7981
$scheme = $this->_ssl ? "https://" : "http://";
8082
$args = " -X POST";
8183
$args.= " --connect-timeout " . $this->_connectTimeout;
82-
$args.= " -H 'Content-Type: application/json'";
83-
$args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey);
84-
$args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'";
85-
$args.= " -H 'X-LaunchDarkly-Event-Schema: " . EventPublisher::CURRENT_SCHEMA_VERSION . "'";
86-
$args.= " -H 'Accept: application/json'";
84+
85+
foreach ($this->_eventHeaders as $key => $value) {
86+
if ($key == 'Authorization') {
87+
$args.= " -H " . escapeshellarg("Authorization: " . $value);
88+
} else {
89+
$args.= " -H '$key: $value'";
90+
}
91+
}
92+
8793
$args.= " -d " . escapeshellarg($payload);
8894
$args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk");
8995
return $args;
@@ -101,16 +107,8 @@ private function makeCurlRequest(string $args): bool
101107

102108
private function createPowershellArgs(string $payloadFile): string
103109
{
104-
$headers = [
105-
'Content-Type' => 'application/json',
106-
'Authorization' => $this->_sdkKey,
107-
'User-Agent' => 'PHPClient/' . LDClient::VERSION,
108-
'X-LaunchDarkly-Event-Schema' => EventPublisher::CURRENT_SCHEMA_VERSION,
109-
'Accept' => 'application/json',
110-
];
111-
112110
$headerString = "";
113-
foreach ($headers as $key => $value) {
111+
foreach ($this->_eventHeaders as $key => $value) {
114112
$headerString .= sprintf("'%s'='%s';", $key, $value);
115113
}
116114

src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,7 @@ public function __construct(string $sdkKey, array $options = [])
3434
$this->_eventsUri = \LaunchDarkly\Impl\Util::adjustBaseUri($baseUri);
3535

3636
$this->_requestOptions = [
37-
'headers' => [
38-
'Content-Type' => 'application/json',
39-
'Authorization' => $this->_sdkKey,
40-
'User-Agent' => 'PHPClient/' . LDClient::VERSION,
41-
'Accept' => 'application/json',
42-
'X-LaunchDarkly-Event-Schema' => strval(EventPublisher::CURRENT_SCHEMA_VERSION)
43-
],
37+
'headers' => Util::eventHeaders($this->_sdkKey, $options['application_info'] ?? null),
4438
'timeout' => $options['timeout'],
4539
'connect_timeout' => $options['connect_timeout']
4640
];

src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use LaunchDarkly\Impl\Model\Segment;
1414
use LaunchDarkly\Impl\UnrecoverableHTTPStatusException;
1515
use LaunchDarkly\Impl\Util;
16-
use LaunchDarkly\LDClient;
1716
use LaunchDarkly\Subsystems\FeatureRequester;
1817
use Psr\Log\LoggerInterface;
1918

@@ -48,11 +47,7 @@ public function __construct(string $baseUri, string $sdkKey, array $options)
4847
}
4948

5049
$defaults = [
51-
'headers' => [
52-
'Authorization' => $sdkKey,
53-
'Content-Type' => 'application/json',
54-
'User-Agent' => 'PHPClient/' . LDClient::VERSION
55-
],
50+
'headers' => Util::defaultHeaders($sdkKey, $options['application_info'] ?? null),
5651
'timeout' => $options['timeout'],
5752
'connect_timeout' => $options['connect_timeout'],
5853
'handler' => $stack,

src/LaunchDarkly/Impl/Util.php

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66

77
use DateTime;
88
use DateTimeZone;
9+
use LaunchDarkly\LDClient;
10+
use LaunchDarkly\Subsystems\EventPublisher;
11+
use LaunchDarkly\Types\ApplicationInfo;
912
use Monolog\Handler\NullHandler;
1013
use Monolog\Logger;
1114
use Psr\Log\LoggerInterface;
@@ -64,4 +67,47 @@ public static function makeNullLogger(): LoggerInterface
6467
{
6568
return new Logger('', [new NullHandler()]);
6669
}
70+
71+
/**
72+
* An array of header name and values that should be used for any request
73+
* made to LaunchDarkly servers.
74+
*
75+
* @param string $sdkKey
76+
* @param ApplicationInfo|null $applicationInfo
77+
* @return array<string, string>
78+
*/
79+
public static function defaultHeaders(string $sdkKey, $applicationInfo): array
80+
{
81+
$headers = [
82+
'Content-Type' => 'application/json',
83+
'Accept' => 'application/json',
84+
'Authorization' => $sdkKey,
85+
'User-Agent' => 'PHPClient/' . LDClient::VERSION,
86+
];
87+
88+
if ($applicationInfo instanceof ApplicationInfo) {
89+
$headerValue = (string) $applicationInfo;
90+
if ($headerValue) {
91+
$headers['X-LaunchDarkly-Tags'] = $headerValue;
92+
}
93+
}
94+
95+
return $headers;
96+
}
97+
98+
/**
99+
* An array of header name and values that should be used for any request
100+
* made to the LaunchDarkly Events API.
101+
*
102+
* @param string $sdkKey
103+
* @param ApplicationInfo|null $applicationInfo
104+
* @return array
105+
*/
106+
public static function eventHeaders(string $sdkKey, $applicationInfo): array
107+
{
108+
$headers = Util::defaultHeaders($sdkKey, $applicationInfo);
109+
$headers['X-LaunchDarkly-Event-Schema'] = EventPublisher::CURRENT_SCHEMA_VERSION;
110+
111+
return $headers;
112+
}
67113
}

src/LaunchDarkly/LDClient.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
use LaunchDarkly\Impl\Util;
1717
use LaunchDarkly\Integrations\Guzzle;
1818
use LaunchDarkly\Subsystems\FeatureRequester;
19+
use LaunchDarkly\Types\ApplicationInfo;
1920
use Monolog\Handler\ErrorLogHandler;
2021
use Monolog\Logger;
2122
use Psr\Log\LoggerInterface;
@@ -75,6 +76,7 @@ class LDClient
7576
* Defaults to false.
7677
* - `private_attribute_names`: An optional array of user attribute names to be marked private. Any users sent to LaunchDarkly
7778
* with this configuration active will have attributes with these names removed. You can also set private attributes on a
79+
* - `application_info`: An optional {@see \LaunchDarkly\Types\ApplicationInfo} instance.
7880
* per-user basis in the LDContext builder.
7981
* - Other options may be available depending on any features you are using from the `LaunchDarkly\Integrations` namespace.
8082
*
@@ -120,8 +122,17 @@ public function __construct(string $sdkKey, array $options = [])
120122
$logger = new Logger("LaunchDarkly", [new ErrorLogHandler()]);
121123
$options['logger'] = $logger;
122124
}
125+
126+
/** @var LoggerInterface */
123127
$this->_logger = $options['logger'];
124128

129+
$applicationInfo = $options['application_info'] ?? null;
130+
if ($applicationInfo instanceof ApplicationInfo) {
131+
foreach ($applicationInfo->errors() as $error) {
132+
$this->_logger->warning($error);
133+
}
134+
}
135+
125136
$this->_eventFactoryDefault = new EventFactory(false);
126137
$this->_eventFactoryWithReasons = new EventFactory(true);
127138

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
namespace LaunchDarkly\Types;
4+
5+
/**
6+
* An object that allows configuration of application metadata.
7+
*
8+
* Application metadata may be used in LaunchDarkly analytics or other product
9+
* features, but does not affect feature flag evaluations.
10+
*
11+
* To use these properties, provide an instance of ApplicationInfo in the config
12+
* parameter of the LDClient.
13+
*
14+
* Application values have the following restrictions:
15+
* - Cannot be empty
16+
* - Cannot exceed 64 characters in length
17+
* - Can only contain a-z, A-Z, 0-9, period (.), dash (-), and underscore (_).
18+
*/
19+
final class ApplicationInfo
20+
{
21+
/** @var string|null **/
22+
private $id;
23+
24+
/** @var string|null **/
25+
private $version;
26+
27+
/** @var array **/
28+
private $errors;
29+
30+
public function __construct()
31+
{
32+
$this->id = null;
33+
$this->version = null;
34+
$this->errors = [];
35+
}
36+
37+
/**
38+
* Set the application id metadata identifier.
39+
*/
40+
public function withId(string $id): ApplicationInfo
41+
{
42+
$this->id = $this->validateValue($id, 'id');
43+
44+
return $this;
45+
}
46+
47+
/**
48+
* Set the application version metadata identifier.
49+
*/
50+
public function withVersion(string $version): ApplicationInfo
51+
{
52+
$this->version = $this->validateValue($version, 'version');
53+
54+
return $this;
55+
}
56+
57+
/**
58+
* Retrieve any validation errors that have accumulated as a result of creating this instance.
59+
*/
60+
public function errors(): array
61+
{
62+
return array_values($this->errors);
63+
}
64+
65+
public function __toString(): string
66+
{
67+
$parts = [];
68+
69+
if ($this->id !== null) {
70+
$parts[] = "application-id/{$this->id}";
71+
}
72+
73+
if ($this->version !== null) {
74+
$parts[] = "application-version/{$this->version}";
75+
}
76+
77+
return join(" ", $parts);
78+
}
79+
80+
private function validateValue(string $value, string $label): ?string
81+
{
82+
$value = strval($value);
83+
84+
if ($value === '') {
85+
return null;
86+
}
87+
88+
if (strlen($value) > 64) {
89+
$this->errors[$label] = "Application value for $label was longer than 64 characters and was discarded";
90+
return null;
91+
}
92+
93+
if (preg_match('/[^a-zA-Z0-9._-]/', $value)) {
94+
$this->errors[$label] = "Application value for $label contained invalid characters and was discarded";
95+
return null;
96+
}
97+
98+
return $value;
99+
}
100+
}

tests/Impl/Integrations/CurlEventPublisherTest.php renamed to tests/Impl/Integrations/EventPublisherTest.php

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
use LaunchDarkly\Impl\Integrations;
77
use LaunchDarkly\LDClient;
88
use LaunchDarkly\Subsystems\EventPublisher;
9+
use LaunchDarkly\Types\ApplicationInfo;
910
use PHPUnit\Framework\TestCase;
11+
use Psr\Log\LoggerInterface;
1012

11-
class CurlEventPublisherTest extends TestCase
13+
class EventPublisherTest extends TestCase
1214
{
1315
public function setUp(): void
1416
{
@@ -20,16 +22,35 @@ public function setUp(): void
2022
$client->request('DELETE', 'http://localhost:8080/__admin/requests');
2123
}
2224

23-
public function testSendsCorrectBodyAndHeaders()
25+
public function getEventPublisher(): array
26+
{
27+
/** @var LoggerInterface **/
28+
$logger = $this->getMockBuilder(LoggerInterface::class)->getMock();
29+
$appInfo = (new ApplicationInfo())->withId('my-id')->withVersion('my-version');
30+
31+
$config = [
32+
'events_uri' => 'http://localhost:8080',
33+
'timeout' => 3,
34+
'connect_timeout' => 3,
35+
'application_info' => $appInfo,
36+
'logger' => $logger,
37+
];
38+
39+
$curlPublisher = new Integrations\CurlEventPublisher('sdk-key', $config);
40+
$guzzlePublisher = new Integrations\GuzzleEventPublisher('sdk-key', $config);
41+
42+
return [
43+
[$curlPublisher],
44+
[$guzzlePublisher],
45+
];
46+
}
47+
48+
/**
49+
* @dataProvider getEventPublisher
50+
*/
51+
public function testSendsCorrectBodyAndHeaders($publisher)
2452
{
2553
$event = json_encode(["key" => "user-key"]);
26-
$publisher = new Integrations\CurlEventPublisher(
27-
'sdk-key',
28-
[
29-
'events_uri' => 'http://localhost:8080',
30-
'connect_timeout' => 3,
31-
]
32-
);
3354
$publisher->publish($event);
3455

3556
$requests = [];
@@ -67,5 +88,6 @@ public function testSendsCorrectBodyAndHeaders()
6788
$this->assertEquals('sdk-key', $headers['Authorization']);
6889
$this->assertEquals('PHPClient/' . LDClient::VERSION, $headers['User-Agent']);
6990
$this->assertEquals(EventPublisher::CURRENT_SCHEMA_VERSION, $headers['X-LaunchDarkly-Event-Schema']);
91+
$this->assertEquals('application-id/my-id application-version/my-version', $headers['X-LaunchDarkly-Tags']);
7092
}
7193
}

0 commit comments

Comments
 (0)