diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php new file mode 100644 index 000000000..823865c1e --- /dev/null +++ b/src/LaunchDarkly/EventProcessor.php @@ -0,0 +1,146 @@ +_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; + } + + +} \ No newline at end of file diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 77c3c43ac..b1a6178d6 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -14,6 +14,7 @@ class LDClient { protected $_apiKey; protected $_baseUri; protected $_client; + protected $_eventProcessor; /** * Creates a new client instance that connects to LaunchDarkly. @@ -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'])) { @@ -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); } @@ -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"); diff --git a/src/LaunchDarkly/LDUser.php b/src/LaunchDarkly/LDUser.php index c014a334b..85a3cd379 100644 --- a/src/LaunchDarkly/LDUser.php +++ b/src/LaunchDarkly/LDUser.php @@ -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; + } }