Skip to content
Merged
146 changes: 146 additions & 0 deletions src/LaunchDarkly/EventProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
<?php
namespace LaunchDarkly;

/**
* @internal
*/
class EventProcessor {

private $_apiKey;
private $_queue;
private $_capacity;
private $_timeout;
private $_socket_failed;
private $_host;
private $_port;
private $_ssl;

public function __construct($apiKey, $options = []) {
$this->_apiKey = $apiKey;
if (!isset($options['base_uri'])) {
$this->_host = 'app.launchdarkly.com';
$this->_port = 443;
$this->_ssl = true;
}
else {
$url = parse_url($options['base_uri']);
$this->_host = $url['host'];
$this->_ssl = $url['scheme'] === 'https';
if (isset($url['port'])) {
$this->_port = $url['port'];
}
else {
$this->_port = $this->_ssl ? 443 : 80;
}
}

$this->_capacity = $options['capacity'];
$this->_timeout = $options['timeout'];

$this->_queue = array();
}

public function __destruct() {
$this->flush();
}

public function sendEvent($event) {
return $this->enqueue($event);
}

public function enqueue($event) {
if (count($this->_queue) > $this->_capacity) {
return false;
}

array_push($this->_queue, $event);

return true;
}

protected function flush() {
$socket = $this->createSocket();

if (!$socket) {
error_log("LaunchDarkly unable to open socket");
return;
}
$payload = json_encode($this->_queue);

$body = $this->createBody($payload);

return $this->makeRequest($socket, $body);
}

private function createSocket() {
if ($this->_socket_failed) {
return false;
}

$protocol = $this->_ssl ? "ssl" : "tcp";

try {

$socket = @pfsockopen($protocol . "://" . $this->_host, $this->_port, $errno, $errstr, $this->_timeout);

if ($errno != 0) {
$this->_socket_failed = true;
return false;
}

return $socket;
} catch (Exception $e) {
error_log("LaunchDarkly caught $e");
$this->socket_failed = true;
return false;
}
}

private function createBody($content) {
$req = "";
$req.= "POST /api/events/bulk HTTP/1.1\r\n";
$req.= "Host: " . $this->_host . "\r\n";
$req.= "Content-Type: application/json\r\n";
$req.= "Authorization: api_key " . $this->_apiKey . "\r\n";
$req.= "User-Agent: PHPClient/" . LDClient::VERSION . "\r\n";
$req.= "Accept: application/json\r\n";
$req.= "Content-length: " . strlen($content) . "\r\n";
$req.= "\r\n";
$req.= $content;
return $req;
}

private function makeRequest($socket, $req, $retry = true) {
$bytes_written = 0;
$bytes_total = strlen($req);
$closed = false;

while (!$closed && $bytes_written < $bytes_total) {
try {
$written = @fwrite($socket, substr($req, $bytes_written));
} catch (Exception $e) {
error_log("LaunchDarkly caught $e");
$closed = true;
}
if (!isset($written) || !$written) {
$closed = true;
} else {
$bytes_written += $written;
}
}

if ($closed) {
fclose($socket);
if ($retry) {
error_log("LaunchDarkly retrying send");
$socket = $this->createSocket();
if ($socket) return $this->makeRequest($socket, $req, false);
}
return false;
}

return true;
}


}
50 changes: 48 additions & 2 deletions src/LaunchDarkly/LDClient.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ class LDClient {
protected $_apiKey;
protected $_baseUri;
protected $_client;
protected $_eventProcessor;

/**
* Creates a new client instance that connects to LaunchDarkly.
Expand All @@ -29,7 +30,8 @@ public function __construct($apiKey, $options = []) {
$this->_apiKey = $apiKey;
if (!isset($options['base_uri'])) {
$this->_baseUri = self::DEFAULT_BASE_URI;
} else {
}
else {
$this->_baseUri = rtrim($options['base_uri'], '/');
}
if (!isset($options['timeout'])) {
Expand All @@ -39,6 +41,12 @@ public function __construct($apiKey, $options = []) {
$options['connect_timeout'] = 3;
}

if (!isset($options['capacity'])) {
$options['capacity'] = 1000;
}

$this->_eventProcessor = new \LaunchDarkly\EventProcessor($apiKey, $options);

$this->_client = $this->_make_client($options);
}

Expand All @@ -54,13 +62,51 @@ public function __construct($apiKey, $options = []) {
public function getFlag($key, $user, $default = false) {
try {
$flag = $this->_getFlag($key, $user, $default);
return is_null($flag) ? $default : $flag;

if (is_null($flag)) {
$this->_sendFlagRequestEvent($key, $user, $default);
return $default;
}
else {
$this->_sendFlagRequestEvent($key, $user, $flag);
return $flag;
}
} catch (Exception $e) {
error_log("LaunchDarkly caught $e");
$this->_sendFlagRequestEvent($key, $user, $default);
return $default;
}
}

/**
* Tracks that a user performed an event.
*
* @param string $eventName The name of the event
* @param LDUser $user The user that performed the event
*
*/
public function sendEvent($eventName, $user, $data) {
$event = array();
$event['user'] = $user->toJSON();
$event['kind'] = "custom";
$event['creationDate'] = round(microtime(1) * 1000);
$event['key'] = $eventName;
if (isset($data)) {
$event['data'] = $data;
}
$this->_eventProcessor->enqueue($event);
}

protected function _sendFlagRequestEvent($key, $user, $value) {
$event = array();
$event['user'] = $user->toJSON();
$event['value'] = $value;
$event['kind'] = "feature";
$event['creationDate'] = round(microtime(1) * 1000);
$event['key'] = $key;
$this->_eventProcessor->enqueue($event);
}

protected function _getFlag($key, $user, $default) {
try {
$response = $this->_client->get("/api/eval/features/$key");
Expand Down
18 changes: 18 additions & 0 deletions src/LaunchDarkly/LDUser.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,4 +47,22 @@ public function getKey() {
public function getSecondary() {
return $this->_secondary;
}

public function toJSON() {
$json = ["key" => $this->_key];

if (isset($this->_secondary)) {
$json['secondary'] = $this->_secondary;
}
if (isset($this->ip)) {
$json['ip'] = $this->ip;
}
if (isset($this->country)) {
$json['country'] = $this->country;
}
if (isset($this->custom)) {
$json['custom'] = $this->custom;
}
return $json;
}
}