Skip to content

Commit 0e30c71

Browse files
authored
Add application info support (#124)
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 c35a830 commit 0e30c71

File tree

9 files changed

+359
-41
lines changed

9 files changed

+359
-41
lines changed

src/LaunchDarkly/Impl/Integrations/CurlEventPublisher.php

Lines changed: 15 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace LaunchDarkly\Impl\Integrations;
44

55
use LaunchDarkly\EventPublisher;
6+
use LaunchDarkly\Impl\Util;
67
use LaunchDarkly\LDClient;
78

89
/**
@@ -13,9 +14,6 @@
1314
*/
1415
class CurlEventPublisher implements EventPublisher
1516
{
16-
/** @var string */
17-
private $_sdkKey;
18-
1917
/** @var string */
2018
private $_host;
2119

@@ -37,10 +35,11 @@ class CurlEventPublisher implements EventPublisher
3735
/** @var bool */
3836
private $_isWindows;
3937

38+
/** @var array<string, string> */
39+
private $_eventHeaders;
40+
4041
public function __construct(string $sdkKey, array $options = [])
4142
{
42-
$this->_sdkKey = $sdkKey;
43-
4443
$baseUri = $options['events_uri'] ?? null;
4544
if (!$baseUri) {
4645
$baseUri = LDClient::DEFAULT_EVENTS_URI;
@@ -61,6 +60,7 @@ public function __construct(string $sdkKey, array $options = [])
6160
$this->_curl = $options['curl'];
6261
}
6362

63+
$this->_eventHeaders = Util::eventHeaders($sdkKey, $options['application_info'] ?? null);
6464
$this->_connectTimeout = $options['connect_timeout'];
6565
$this->_isWindows = PHP_OS_FAMILY == 'Windows';
6666
}
@@ -92,11 +92,15 @@ private function createCurlArgs(string $payload): string
9292
$scheme = $this->_ssl ? "https://" : "http://";
9393
$args = " -X POST";
9494
$args.= " --connect-timeout " . $this->_connectTimeout;
95-
$args.= " -H 'Content-Type: application/json'";
96-
$args.= " -H " . escapeshellarg("Authorization: " . $this->_sdkKey);
97-
$args.= " -H 'User-Agent: PHPClient/" . LDClient::VERSION . "'";
98-
$args.= " -H 'X-LaunchDarkly-Event-Schema: " . EventPublisher::CURRENT_SCHEMA_VERSION . "'";
99-
$args.= " -H 'Accept: application/json'";
95+
96+
foreach ($this->_eventHeaders as $key => $value) {
97+
if ($key == 'Authorization') {
98+
$args.= " -H " . escapeshellarg("Authorization: " . $value);
99+
} else {
100+
$args.= " -H '$key: $value'";
101+
}
102+
}
103+
100104
$args.= " -d " . escapeshellarg($payload);
101105
$args.= " " . escapeshellarg($scheme . $this->_host . ":" . $this->_port . $this->_path . "/bulk");
102106
return $args;
@@ -114,16 +118,8 @@ private function makeCurlRequest(string $args): bool
114118

115119
private function createPowershellArgs(string $payloadFile): string
116120
{
117-
$headers = [
118-
'Content-Type' => 'application/json',
119-
'Authorization' => $this->_sdkKey,
120-
'User-Agent' => 'PHPClient/' . LDClient::VERSION,
121-
'X-LaunchDarkly-Event-Schema' => EventPublisher::CURRENT_SCHEMA_VERSION,
122-
'Accept' => 'application/json',
123-
];
124-
125121
$headerString = "";
126-
foreach ($headers as $key => $value) {
122+
foreach ($this->_eventHeaders as $key => $value) {
127123
$headerString .= sprintf("'%s'='%s';", $key, $value);
128124
}
129125

src/LaunchDarkly/Impl/Integrations/GuzzleEventPublisher.php

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

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

src/LaunchDarkly/Impl/Integrations/GuzzleFeatureRequester.php

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
use LaunchDarkly\Impl\Model\Segment;
1313
use LaunchDarkly\Impl\UnrecoverableHTTPStatusException;
1414
use LaunchDarkly\Impl\Util;
15-
use LaunchDarkly\LDClient;
1615
use Psr\Log\LoggerInterface;
1716

1817
/**
@@ -49,11 +48,7 @@ public function __construct(string $baseUri, string $sdkKey, array $options)
4948
}
5049

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

src/LaunchDarkly/Impl/Util.php

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

55
use DateTime;
66
use DateTimeZone;
7+
use LaunchDarkly\EventPublisher;
8+
use LaunchDarkly\LDClient;
9+
use LaunchDarkly\Types\ApplicationInfo;
710

811
/**
912
* Internal class containing helper methods.
@@ -48,4 +51,47 @@ public static function httpErrorMessage(int $status, string $context, string $re
4851
. ' for ' . $context . ' - '
4952
. (Util::isHttpErrorRecoverable($status) ? $retryMessage : 'giving up permanently');
5053
}
54+
55+
/**
56+
* An array of header name and values that should be used for any request
57+
* made to LaunchDarkly servers.
58+
*
59+
* @param string $sdkKey
60+
* @param ApplicationInfo|null $applicationInfo
61+
* @return array<string, string>
62+
*/
63+
public static function defaultHeaders(string $sdkKey, $applicationInfo): array
64+
{
65+
$headers = [
66+
'Content-Type' => 'application/json',
67+
'Accept' => 'application/json',
68+
'Authorization' => $sdkKey,
69+
'User-Agent' => 'PHPClient/' . LDClient::VERSION,
70+
];
71+
72+
if ($applicationInfo instanceof ApplicationInfo) {
73+
$headerValue = (string) $applicationInfo;
74+
if ($headerValue) {
75+
$headers['X-LaunchDarkly-Tags'] = $headerValue;
76+
}
77+
}
78+
79+
return $headers;
80+
}
81+
82+
/**
83+
* An array of header name and values that should be used for any request
84+
* made to the LaunchDarkly Events API.
85+
*
86+
* @param string $sdkKey
87+
* @param ApplicationInfo|null $applicationInfo
88+
* @return array
89+
*/
90+
public static function eventHeaders(string $sdkKey, $applicationInfo): array
91+
{
92+
$headers = Util::defaultHeaders($sdkKey, $applicationInfo);
93+
$headers['X-LaunchDarkly-Event-Schema'] = EventPublisher::CURRENT_SCHEMA_VERSION;
94+
95+
return $headers;
96+
}
5197
}

src/LaunchDarkly/LDClient.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use LaunchDarkly\Impl\PreloadedFeatureRequester;
1010
use LaunchDarkly\Impl\UnrecoverableHTTPStatusException;
1111
use LaunchDarkly\Integrations\Guzzle;
12+
use LaunchDarkly\Types\ApplicationInfo;
1213
use Monolog\Handler\ErrorLogHandler;
1314
use Monolog\Logger;
1415
use Psr\Log\LoggerInterface;
@@ -81,6 +82,7 @@ class LDClient
8182
* - `private_attribute_names`: An optional array of user attribute names to be marked private. Any users sent to LaunchDarkly
8283
* with this configuration active will have attributes with these names removed. You can also set private attributes on a
8384
* per-user basis in LDUserBuilder.
85+
* - `application_info`: An optional {@see \LaunchDarkly\Types\ApplicationInfo} instance.
8486
* - Other options may be available depending on any features you are using from the `LaunchDarkly\Integrations` namespace.
8587
*
8688
* @return LDClient
@@ -125,8 +127,17 @@ public function __construct(string $sdkKey, array $options = [])
125127
$logger = new Logger("LaunchDarkly", [new ErrorLogHandler()]);
126128
$options['logger'] = $logger;
127129
}
130+
131+
/** @var LoggerInterface */
128132
$this->_logger = $options['logger'];
129133

134+
$applicationInfo = $options['application_info'] ?? null;
135+
if ($applicationInfo instanceof ApplicationInfo) {
136+
foreach ($applicationInfo->errors() as $error) {
137+
$this->_logger->warning($error);
138+
}
139+
}
140+
130141
$this->_eventFactoryDefault = new EventFactory(false);
131142
$this->_eventFactoryWithReasons = new EventFactory(true);
132143

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\EventPublisher;
77
use LaunchDarkly\Impl\Integrations;
88
use LaunchDarkly\LDClient;
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)