From 125ac4c0c2d97eb3ec10a597736ecf5b947405cd Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 14:41:17 -0800 Subject: [PATCH 01/12] First cut at event processing --- src/LaunchDarkly/EventProcessor.php | 148 ++++++++++++++++++++++++++++ src/LaunchDarkly/LDClient.php | 47 ++++++++- src/LaunchDarkly/LDUser.php | 19 ++++ 3 files changed, 212 insertions(+), 2 deletions(-) create mode 100644 src/LaunchDarkly/EventProcessor.php diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php new file mode 100644 index 000000000..06d3226d2 --- /dev/null +++ b/src/LaunchDarkly/EventProcessor.php @@ -0,0 +1,148 @@ +_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->_port = $url['port']; + $this->_ssl = $url['scheme'] === 'https'; + } + + $this->_capacity = $options['capacity']; + $this->_timeout = $options['timeout']; + + $this->_queue = array(); + } + + public function __destruct() { + $this->flush(); + } + + public function sendEvent($event) { + return $this->enqueue($event); + } + + protected 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) { + 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) { + $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 " . base64_encode($this->_apiKey) . "\r\n"; + $req.= "User-Agent: PHPClient/" . LaunchDarkly\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; + # Write the request + while (!$closed && $bytes_written < $bytes_total) { + try { + # Since we're try catch'ing prevent PHP logs. + $written = @fwrite($socket, substr($req, $bytes_written)); + } catch (Exception $e) { + // $this->handleError($e->getCode(), $e->getMessage()); + $closed = true; + } + if (!isset($written) || !$written) { + $closed = true; + } else { + $bytes_written += $written; + } + } + # If the socket has been closed, attempt to retry a single time. + if ($closed) { + fclose($socket); + if ($retry) { + $socket = $this->createSocket(); + if ($socket) return $this->makeRequest($socket, $req, false); + } + return false; + } + /* + $success = true; + if ($this->debug()) { + $res = $this->parseResponse(fread($socket, 2048)); + if ($res["status"] != "200") { + $this->handleError($res["status"], $res["message"]); + $success = false; + } + } + */ + return true; + } + + +} \ No newline at end of file diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 77c3c43ac..332bb4b2c 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,48 @@ 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)) { + _sendFlagRequestEvent($key, $user, $default); + return $default; + } + else { + _sendFlagRequestEvent($key, $user, $flag); + return $flag; + } } catch (Exception $e) { error_log("LaunchDarkly caught $e"); + _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) { + $event = array(); + $event['user'] = $user.toJSON(); + $event['kind'] = "custom"; + $event['creationDate'] = round(microtime(1) * 1000); + $event['key'] = $eventName; + $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..7962fdf3e 100644 --- a/src/LaunchDarkly/LDUser.php +++ b/src/LaunchDarkly/LDUser.php @@ -47,4 +47,23 @@ 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_encode($json); + } } From 6915e75d435296763fea0c7bd7c1bc4edbf7d647 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 14:59:20 -0800 Subject: [PATCH 02/12] Fix typo --- src/LaunchDarkly/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index 06d3226d2..3d4312a23 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -23,7 +23,7 @@ public function __construct($apiKey, $options = []) { $this->_ssl = true; } else { - $url = parse_url(options['base_uri']); + $url = parse_url($options['base_uri']); $this->_host = $url['host']; $this->_port = $url['port']; $this->_ssl = $url['scheme'] === 'https'; From ebda6d0db25ba2ea6df2033019b01390a56d846a Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 15:06:50 -0800 Subject: [PATCH 03/12] Fix logic for port lookup --- src/LaunchDarkly/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index 3d4312a23..3630d63dd 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -25,8 +25,8 @@ public function __construct($apiKey, $options = []) { else { $url = parse_url($options['base_uri']); $this->_host = $url['host']; - $this->_port = $url['port']; $this->_ssl = $url['scheme'] === 'https'; + $this->_port = $this->_ssl ? 443 : 80; } $this->_capacity = $options['capacity']; From 89fd253a9187b5b087957ff9750cc87b52f61d87 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 15:16:46 -0800 Subject: [PATCH 04/12] Add support for $data. Fix calls to send flag events. --- src/LaunchDarkly/LDClient.php | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 332bb4b2c..3153b5ead 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -64,16 +64,16 @@ public function getFlag($key, $user, $default = false) { $flag = $this->_getFlag($key, $user, $default); if (is_null($flag)) { - _sendFlagRequestEvent($key, $user, $default); + $this->_sendFlagRequestEvent($key, $user, $default); return $default; } else { - _sendFlagRequestEvent($key, $user, $flag); + $this->_sendFlagRequestEvent($key, $user, $flag); return $flag; } } catch (Exception $e) { error_log("LaunchDarkly caught $e"); - _sendFlagRequestEvent($key, $user, $default); + $this->_sendFlagRequestEvent($key, $user, $default); return $default; } } @@ -85,12 +85,15 @@ public function getFlag($key, $user, $default = false) { * @param LDUser $user The user that performed the event * */ - public function sendEvent($eventName, $user) { + 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); } From 06d576b86354c0b88050247c7fad79d01b92c357 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 15:28:41 -0800 Subject: [PATCH 05/12] Fix typo. --- src/LaunchDarkly/LDClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index 3153b5ead..afe117b5d 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -87,7 +87,7 @@ public function getFlag($key, $user, $default = false) { */ public function sendEvent($eventName, $user, $data) { $event = array(); - $event['user'] = $user.toJSON(); + $event['user'] = $user->toJSON(); $event['kind'] = "custom"; $event['creationDate'] = round(microtime(1) * 1000); $event['key'] = $eventName; @@ -99,7 +99,7 @@ public function sendEvent($eventName, $user, $data) { protected function _sendFlagRequestEvent($key, $user, $value) { $event = array(); - $event['user'] = $user.toJSON(); + $event['user'] = $user->toJSON(); $event['value'] = $value; $event['kind'] = "feature"; $event['creationDate'] = round(microtime(1) * 1000); From 6cd5053ebd3a39b272a836f0a0113b0e4e9c1b87 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 15:36:39 -0800 Subject: [PATCH 06/12] Fix typo --- src/LaunchDarkly/LDClient.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/LDClient.php b/src/LaunchDarkly/LDClient.php index afe117b5d..b1a6178d6 100644 --- a/src/LaunchDarkly/LDClient.php +++ b/src/LaunchDarkly/LDClient.php @@ -94,7 +94,7 @@ public function sendEvent($eventName, $user, $data) { if (isset($data)) { $event['data'] = $data; } - $this->_eventProcessor.enqueue($event); + $this->_eventProcessor->enqueue($event); } protected function _sendFlagRequestEvent($key, $user, $value) { @@ -104,7 +104,7 @@ protected function _sendFlagRequestEvent($key, $user, $value) { $event['kind'] = "feature"; $event['creationDate'] = round(microtime(1) * 1000); $event['key'] = $key; - $this->_eventProcessor.enqueue($event); + $this->_eventProcessor->enqueue($event); } protected function _getFlag($key, $user, $default) { From 715d83866bade47db01416949d2fbb2a111e755b Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 15:42:56 -0800 Subject: [PATCH 07/12] Make enqueue public --- src/LaunchDarkly/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index 3630d63dd..9551f1237 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -43,7 +43,7 @@ public function sendEvent($event) { return $this->enqueue($event); } - protected function enqueue($event) { + public function enqueue($event) { if (count($this->_queue) > $this->_capacity) { return false; } From d91f9c067771fbde184f17ba8430fd5d456dfac3 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 15:48:56 -0800 Subject: [PATCH 08/12] _host, not host() --- src/LaunchDarkly/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index 9551f1237..6b32cab8b 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -92,7 +92,7 @@ private function createSocket() { private function createBody($content) { $req = ""; $req.= "POST /api/events/bulk HTTP/1.1\r\n"; - $req.= "Host: " . $this->host() . "\r\n"; + $req.= "Host: " . $this->_host . "\r\n"; $req.= "Content-Type: application/json\r\n"; $req.= "Authorization: api_key " . base64_encode($this->_apiKey) . "\r\n"; $req.= "User-Agent: PHPClient/" . LaunchDarkly\LDClient::VERSION . "\r\n"; From 5e985db2507c61721817ac4c776265d318b6023b Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 15:54:58 -0800 Subject: [PATCH 09/12] Figure out the port from the URL if specified --- src/LaunchDarkly/EventProcessor.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index 6b32cab8b..ccb54268a 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -26,7 +26,12 @@ public function __construct($apiKey, $options = []) { $url = parse_url($options['base_uri']); $this->_host = $url['host']; $this->_ssl = $url['scheme'] === 'https'; - $this->_port = $this->_ssl ? 443 : 80; + if (isset($url['port'])) { + $this->_port = $url['port'] + } + else { + $this->_port = $this->_ssl ? 443 : 80; + } } $this->_capacity = $options['capacity']; @@ -95,7 +100,7 @@ private function createBody($content) { $req.= "Host: " . $this->_host . "\r\n"; $req.= "Content-Type: application/json\r\n"; $req.= "Authorization: api_key " . base64_encode($this->_apiKey) . "\r\n"; - $req.= "User-Agent: PHPClient/" . LaunchDarkly\LDClient::VERSION . "\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"; From ead53dacfc17eced21c17fc835982b29ff8751f0 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 16:00:54 -0800 Subject: [PATCH 10/12] Fix another typo --- src/LaunchDarkly/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index ccb54268a..d44b6a8c9 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -27,7 +27,7 @@ public function __construct($apiKey, $options = []) { $this->_host = $url['host']; $this->_ssl = $url['scheme'] === 'https'; if (isset($url['port'])) { - $this->_port = $url['port'] + $this->_port = $url['port']; } else { $this->_port = $this->_ssl ? 443 : 80; From 2a7515858551974872f0499713d164b58d3d29ab Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 16:09:17 -0800 Subject: [PATCH 11/12] Typo --- src/LaunchDarkly/EventProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index d44b6a8c9..2c28447be 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -53,7 +53,7 @@ public function enqueue($event) { return false; } - array_push($this->queue, $event); + array_push($this->_queue, $event); return true; } From 84c39663ff3bef807c69f853ca2091ce69d9c8b2 Mon Sep 17 00:00:00 2001 From: John Kodumal Date: Mon, 12 Jan 2015 17:43:37 -0800 Subject: [PATCH 12/12] Don't double-encode the LDUser, and don't base64 encode the API key --- src/LaunchDarkly/EventProcessor.php | 25 +++++++++---------------- src/LaunchDarkly/LDUser.php | 3 +-- 2 files changed, 10 insertions(+), 18 deletions(-) diff --git a/src/LaunchDarkly/EventProcessor.php b/src/LaunchDarkly/EventProcessor.php index 2c28447be..823865c1e 100644 --- a/src/LaunchDarkly/EventProcessor.php +++ b/src/LaunchDarkly/EventProcessor.php @@ -62,6 +62,7 @@ protected function flush() { $socket = $this->createSocket(); if (!$socket) { + error_log("LaunchDarkly unable to open socket"); return; } $payload = json_encode($this->_queue); @@ -79,6 +80,7 @@ private function createSocket() { $protocol = $this->_ssl ? "ssl" : "tcp"; try { + $socket = @pfsockopen($protocol . "://" . $this->_host, $this->_port, $errno, $errstr, $this->_timeout); if ($errno != 0) { @@ -87,8 +89,8 @@ private function createSocket() { } return $socket; - } catch (Exception $e) { + error_log("LaunchDarkly caught $e"); $this->socket_failed = true; return false; } @@ -99,7 +101,7 @@ private function createBody($content) { $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 " . base64_encode($this->_apiKey) . "\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"; @@ -112,13 +114,12 @@ private function makeRequest($socket, $req, $retry = true) { $bytes_written = 0; $bytes_total = strlen($req); $closed = false; - # Write the request + while (!$closed && $bytes_written < $bytes_total) { try { - # Since we're try catch'ing prevent PHP logs. $written = @fwrite($socket, substr($req, $bytes_written)); } catch (Exception $e) { - // $this->handleError($e->getCode(), $e->getMessage()); + error_log("LaunchDarkly caught $e"); $closed = true; } if (!isset($written) || !$written) { @@ -127,25 +128,17 @@ private function makeRequest($socket, $req, $retry = true) { $bytes_written += $written; } } - # If the socket has been closed, attempt to retry a single time. + if ($closed) { fclose($socket); if ($retry) { + error_log("LaunchDarkly retrying send"); $socket = $this->createSocket(); if ($socket) return $this->makeRequest($socket, $req, false); } return false; } - /* - $success = true; - if ($this->debug()) { - $res = $this->parseResponse(fread($socket, 2048)); - if ($res["status"] != "200") { - $this->handleError($res["status"], $res["message"]); - $success = false; - } - } - */ + return true; } diff --git a/src/LaunchDarkly/LDUser.php b/src/LaunchDarkly/LDUser.php index 7962fdf3e..85a3cd379 100644 --- a/src/LaunchDarkly/LDUser.php +++ b/src/LaunchDarkly/LDUser.php @@ -63,7 +63,6 @@ public function toJSON() { if (isset($this->custom)) { $json['custom'] = $this->custom; } - - return json_encode($json); + return $json; } }