diff --git a/.gitignore b/.gitignore index 1803b562..1047e4f1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *.swo vendor composer.lock +.idea diff --git a/.travis.yml b/.travis.yml index acb69d0c..2e4cf3be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,4 +12,4 @@ sudo: false install: composer install --no-interaction -script: vendor/bin/phpunit +script: composer test diff --git a/POST_FORMAT.md b/POST_FORMAT.md new file mode 100644 index 00000000..56ff5b47 --- /dev/null +++ b/POST_FORMAT.md @@ -0,0 +1,346 @@ +``` +{ + // Required: access_token + // An access token with scope "post_server_item" or "post_client_item". + // A post_client_item token must be used if the "platform" is "browser", "android", "ios", "flash", or "client" + // A post_server_item token should be used for other platforms. + "access_token": "aaaabbbbccccddddeeeeffff00001111", + + // Required: data + "data": { + + // Required: environment + // The name of the environment in which this occurrence was seen. + // A string up to 255 characters. For best results, use "production" or "prod" for your + // production environment. + // You don't need to configure anything in the Rollbar UI for new environment names; + // we'll detect them automatically. + "environment": "production", + + // Required: body + // The main data being sent. It can either be a message, an exception, or a crash report. + "body": { + + // Required: "trace", "trace_chain", "message", or "crash_report" (exactly one) + // If this payload is a single exception, use "trace" + // If a chain of exceptions (for languages that support inner exceptions), use "trace_chain" + // If a message with no stack trace, use "message" + // If an iOS crash report, use "crash_report" + + // Option 1: "trace" + "trace": { + + // Required: frames + // A list of stack frames, ordered such that the most recent call is last in the list. + "frames": [ + // Each frame is an object. + { + // Required: filename + // The filename including its full path. + "filename": "/Users/brian/www/mox/mox/views/project.py", + + // Optional: lineno + // The line number as an integer + "lineno": 26, + + // Optional: colno + // The column number as an integer + "colno": 8, + + // Optional: method + // The method or function name + "method": "index", + + // Optional: code + // The line of code + "code": "_save_last_project(request, project_id, force=True)", + + // Optional: context + // Additional code before and after the "code" line + "context": { + // Optional: pre + // List of lines of code before the "code" line + "pre": [ + "project = request.context", + "project_id = project.id" + ], + + // Optional: post + // List of lines of code after the "code" line + "post": [] + }, + + // Optional: args + // List of values of positional arguments to the method/function call + // NOTE: as this can contain sensitive data, you may want to scrub the values + "args": ["", 25], + + // Optional: kwargs + // Object of keyword arguments (name => value) to the method/function call + // NOTE: as this can contain sensitive data, you may want to scrub the values + "kwargs": { + "force": true + } + }, + { + "filename": "/Users/brian/www/mox/mox/views/project.py", + "lineno": 497, + "method": "_save_last_project", + "code": "user = foo" + } + ], + + // Required: exception + // An object describing the exception instance. + "exception": { + // Required: class + // The exception class name. + "class": "NameError", + + // Optional: message + // The exception message, as a string + "message": "global name 'foo' is not defined", + + // Optional: description + // An alternate human-readable string describing the exception + // Usually the original exception message will have been machine-generated; + // you can use this to send something custom + "description": "Something went wrong while trying to save the user object" + } + + }, + + // Option 2: "trace_chain" + // Used for exceptions with inner exceptions or causes + "trace_chain": [ + // Each element in the list should be a "trace" object, as shown above + // Must contain at least one element. + ], + + // Option 3: "message" + // Only one of "trace", "trace_chain", "message", or "crash_report" should be present. + // Presence of a "message" key means that this payload is a log message. + "message": { + + // Required: body + // The primary message text, as a string + "body": "Request over threshold of 10 seconds", + + // Can also contain any arbitrary keys of metadata. Their values can be any valid JSON. + // For example: + + "route": "home#index", + "time_elapsed": 15.23 + + }, + + // Option 4: "crash_report" + // Only one of "trace", "trace_chain", "message", or "crash_report" should be present. + "crash_report": { + // Required: raw + // The raw crash report, as a string + // Rollbar expects the format generated by rollbar-ios + "raw": "" + } + + }, + + // Optional: level + // The severity level. One of: "critical", "error", "warning", "info", "debug" + // Defaults to "error" for exceptions and "info" for messages. + // The level of the *first* occurrence of an item is used as the item's level. + "level": "error", + + // Optional: timestamp + // When this occurred, as a unix timestamp. + "timestamp": 1369188822, + + // Optional: code_version + // A string, up to 40 characters, describing the version of the application code + // Rollbar understands these formats: + // - semantic version (i.e. "2.1.12") + // - integer (i.e. "45") + // - git SHA (i.e. "3da541559918a808c2402bba5012f6c60b27661c") + // If you have multiple code versions that are relevant, those can be sent inside "client" and "server" + // (see those sections below) + // For most cases, just send it here. + "code_version": "3da541559918a808c2402bba5012f6c60b27661c" + + // Optional: platform + // The platform on which this occurred. Meaningful platform names: + // "browser", "android", "ios", "flash", "client", "heroku", "google-app-engine" + // If this is a client-side event, be sure to specify the platform and use a post_client_item access token. + "platform": "linux", + + // Optional: language + // The name of the language your code is written in. + "language": "python", + + // Optional: framework + // The name of the framework your code uses + "framework": "pyramid", + + // Optional: context + // An identifier for which part of your application this event came from. + // Items can be searched by context (prefix search) + // For example, in a Rails app, this could be `controller#action`. + // In a single-page javascript app, it could be the name of the current screen or route. + "context": "project#index", + + // Optional: request + // Data about the request this event occurred in. + "request": { + // Can contain any arbitrary keys. Rollbar understands the following: + + // url: full URL where this event occurred + "url": "https://rollbar.com/project/1", + + // method: the request method + "method": "GET", + + // headers: object containing the request headers. + "headers": { + // Header names should be formatted like they are in HTTP. + "Accept": "text/html", + "Referer": "https://rollbar.com/" + }, + + // params: any routing parameters (i.e. for use with Rails Routes) + "params": { + "controller": "project", + "action": "index" + }, + + // GET: query string params + "GET": {}, + + // query_string: the raw query string + "query_string": "", + + // POST: POST params + "POST": {}, + + // body: the raw POST body + "body": "", + + // user_ip: the user's IP address as a string. + // Can also be the special value "$remote_ip", which will be replaced with the source IP of the API request. + // Will be indexed, as long as it is a valid IPv4 address. + "user_ip": "100.51.43.14" + + }, + + // Optional: person + // The user affected by this event. Will be indexed by ID, username, and email. + // People are stored in Rollbar keyed by ID. If you send a multiple different usernames/emails for the + // same ID, the last received values will overwrite earlier ones. + "person": { + // Required: id + // A string up to 40 characters identifying this user in your system. + "id": "12345", + + // Optional: username + // A string up to 255 characters + "username": "brianr", + + // Optional: email + // A string up to 255 characters + "email": "brian@rollbar.com" + }, + + // Optional: server + // Data about the server related to this event. + "server": { + // Can contain any arbitrary keys. Rollbar understands the following: + + // host: The server hostname. Will be indexed. + "host": "web4", + + // root: Path to the application code root, not including the final slash. + // Used to collapse non-project code when displaying tracebacks. + "root": "/Users/brian/www/mox", + + // branch: Name of the checked-out source control branch. Defaults to "master" + "branch": "master", + + // Optiona: code_version + // String describing the running code version on the server + // See note about "code_version" above + "code_version": "b6437f45b7bbbb15f5eddc2eace4c71a8625da8c", + + // (Deprecated) sha: Git SHA of the running code revision. Use the full sha. + "sha": "b6437f45b7bbbb15f5eddc2eace4c71a8625da8c" + }, + + // Optional: client + // Data about the client device this event occurred on. + // As there can be multiple client environments for a given event (i.e. Flash running inside + // an HTML page), data should be namespaced by platform. + "client": { + // Can contain any arbitrary keys. Rollbar understands the following: + + "javascript": { + + // Optional: browser + // The user agent string + "browser": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3)", + + // Optional: code_version + // String describing the running code version in javascript + // See note about "code_version" above + "code_version": "b6437f45b7bbbb15f5eddc2eace4c71a8625da8c" + + // Optional: source_map_enabled + // Set to true to enable source map deobfuscation + // See the "Source Maps" guide for more details. + "source_map_enabled": false, + + // Optional: guess_uncaught_frames + // Set to true to enable frame guessing + // See the "Source Maps" guide for more details. + "guess_uncaught_frames": false + + } + }, + + // Optional: custom + // Any arbitrary metadata you want to send. "custom" itself should be an object. + "custom": {}, + + // Optional: fingerprint + // A string controlling how this occurrence should be grouped. Occurrences with the same + // fingerprint are grouped together. See the "Grouping" guide for more information. + // Should be a string up to 40 characters long; if longer than 40 characters, we'll use its SHA1 hash. + // If omitted, we'll determine this on the backend. + "fingerprint": "50a5ef9dbcf9d0e0af2d4e25338da0d430f20e52", + + // Optional: title + // A string that will be used as the title of the Item occurrences will be grouped into. + // Max length 255 characters. + // If omitted, we'll determine this on the backend. + "title": "NameError when setting last project in views/project.py", + + // Optional: uuid + // A string, up to 36 characters, that uniquely identifies this occurrence. + // While it can now be any latin1 string, this may change to be a 16 byte field in the future. + // We recommend using a UUID4 (16 random bytes). + // The UUID space is unique to each project, and can be used to look up an occurrence later. + // It is also used to detect duplicate requests. If you send the same UUID in two payloads, the second + // one will be discarded. + "uuid": "d4c7acef55bf4c9ea95e4fe9428a8287", + + // Optional: notifier + // Describes the library used to send this event. + "notifier": { + // Optional: name + // Name of the library + "name": "pyrollbar", + + // Optional: version + // Library version string + "version": "0.5.5" + } + + } +} +``` diff --git a/TODO.md b/TODO.md new file mode 100644 index 00000000..ed6d9b03 --- /dev/null +++ b/TODO.md @@ -0,0 +1,6 @@ + 1. Include Code Context + 1. Implement Agent Sender + 1. Get and sanitize function arguments from backtrace: + * You can get argument names like so: http://stackoverflow.com/a/2692514/456188 + * You can use `array_combine` to get a kwargs version of the arguments + * You can then sanitize based on argument name diff --git a/UPGRADE_FROM_RATCHET.md b/UPGRADE_FROM_RATCHET.md deleted file mode 100644 index 76bb7e68..00000000 --- a/UPGRADE_FROM_RATCHET.md +++ /dev/null @@ -1,5 +0,0 @@ -# Upgrading from ratchetio-php - -Replace your existing `ratchetio.php` with the latest [rollbar.php](https://github.com/rollbar/rollbar-php/raw/master/rollbar.php). - -Optionally, search your app for references to `Ratchetio` and replace them with `Rollbar`. `rollbar.php` sets up a class alias from `Ratchetio` to `Rollbar` which is why this step is optional. \ No newline at end of file diff --git a/composer.json b/composer.json index c7147577..81052080 100644 --- a/composer.json +++ b/composer.json @@ -5,6 +5,7 @@ "keywords": ["logging", "debugging", "monitoring", "errors", "exceptions"], "license": "MIT", "homepage": "http://github.com/rollbar/rollbar-php", + "authors": [ { "name": "Rollbar, Inc.", @@ -12,20 +13,41 @@ "role": "Developer" } ], + "support": { "email": "support@rollbar.com" }, + "autoload": { - "classmap": [ - "src/Level.php", - "src/rollbar.php" - ] + "psr-4": { + "Rollbar\\": "src/" + } + }, + + "autoload-dev": { + "psr-4": { + "Rollbar\\": "tests/" + } }, + "require": { - "ext-curl": "*" + "ext-curl": "*", + "psr/log": "dev-master", + "packfire/php5.3-compat": "*" }, + "require-dev": { - "phpunit/phpunit": "4.5.*", - "mockery/mockery": "0.9.*" + "phpunit/phpunit": "4.8.*", + "mockery/mockery": "0.9.*", + "squizlabs/php_codesniffer": "2.*" + }, + + "scripts": { + "test": [ + "phpunit", + "phpcs --standard=PSR1,PSR2 src tests" + ], + "fix": "phpcbf --standard=PSR1,PSR2 src tests" + } } diff --git a/phpunit.xml b/phpunit.xml index 1bb384ff..fa5f59c5 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,22 +1,22 @@ - - - ./tests/ - - - - - ./src - - + backupStaticAttributes="false" + bootstrap="./vendor/autoload.php" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" + syntaxCheck="false"> + + + ./tests/ + + + + + ./src + + diff --git a/src/Config.php b/src/Config.php new file mode 100644 index 00000000..cdfaea9f --- /dev/null +++ b/src/Config.php @@ -0,0 +1,343 @@ +updateConfig($configArray); + + if (isset($configArray['error_sample_rates'])) { + $this->error_sample_rates = $configArray['error_sample_rates']; + } + + $levels = array(E_WARNING, E_NOTICE, E_USER_ERROR, E_USER_WARNING, + E_USER_NOTICE, E_STRICT, E_RECOVERABLE_ERROR); + // PHP 5.3.0 + if (defined('E_DEPRECATED')) { + $levels = array_merge($levels, array(E_DEPRECATED, E_USER_DEPRECATED)); + } + $curr = 1; + for ($i = 0, $num = count($levels); $i < $num; $i++) { + $level = $levels[$i]; + if (!isset($this->error_sample_rates[$level])) { + $this->error_sample_rates[$level] = $curr; + } + } + $this->mt_randmax = mt_getrandmax(); + } + + public function configure($config) + { + $this->updateConfig($this->extend($config)); + } + + public function extend($config) + { + return array_replace_recursive(array(), $this->configArray, $config); + } + + public function getConfigArray() + { + return $this->configArray; + } + + protected function updateConfig($c) + { + $this->configArray = $c; + + $this->setAccessToken($c); + $this->setDataBuilder($c); + $this->setTransformer($c); + $this->setMinimumLevel($c); + $this->setReportSuppressed($c); + $this->setFilters($c); + $this->setSender($c); + $this->setResponseHandler($c); + $this->setCheckIgnoreFunction($c); + + if (isset($c['included_errno'])) { + $this->included_errno = $c['included_errno']; + } + } + + private function setAccessToken($c) + { + if (isset($_ENV['ROLLBAR_ACCESS_TOKEN']) && !isset($c['access_token'])) { + $c['access_token'] = $_ENV['ROLLBAR_ACCESS_TOKEN']; + } + Utilities::validateString($c['access_token'], "config['access_token']", 32, false); + $this->accessToken = $c['access_token']; + } + + private function setDataBuilder($c) + { + $exp = "Rollbar\DataBuilderInterface"; + $def = "Rollbar\DataBuilder"; + $this->setupWithOptions($c, "dataBuilder", $exp, $def, true); + } + + private function setTransformer($c) + { + $expected = "Rollbar\TransformerInterface"; + $this->setupWithOptions($c, "transformer", $expected); + } + + private function setMinimumLevel($c) + { + if (empty($c['minimumLevel'])) { + $this->minimumLevel = 0; + } elseif ($c['minimumLevel'] instanceof Level) { + $this->minimumLevel = $c['minimumLevel']->toInt(); + } elseif (is_string($c['minimumLevel'])) { + $level = Level::fromName($c['minimumLevel']); + if ($level !== null) { + $this->minimumLevel = $level->toInt(); + } + } elseif (is_int($c['minimumLevel'])) { + $this->minimumLevel = $c['minimumLevel']; + } else { + $this->minimumLevel = 0; + } + } + + private function setReportSuppressed($c) + { + $this->reportSuppressed = isset($c['reportSuppressed']) && $c['reportSuppressed']; + if (!isset($this->reportSuppressed)) { + $this->reportSuppressed = isset($c['report_suppressed']) && $c['report_suppressed']; + } + } + + private function setFilters($c) + { + $this->setupWithOptions($c, "filter", "Rollbar\FilterInterface"); + } + + private function setSender($c) + { + $expected = "Rollbar\Senders\SenderInterface"; + $default = "Rollbar\Senders\CurlSender"; + + if (array_key_exists('base_api_url', $c)) { + $c['senderOptions']['endpoint'] = $c['base_api_url']; + } + + if (array_key_exists('timeout', $c)) { + $c['senderOptions']['timeout'] = $c['timeout']; + } + + if (array_key_exists('proxy', $c)) { + $c['senderOptions']['proxy'] = $c['proxy']; + } + + if (array_key_exists('handler', $c) && $c['handler'] == 'agent') { + $default = "Rollbar\Senders\AgentSender"; + if (array_key_exists('agent_log_location', $c)) { + $c['senderOptions'] = array( + 'agentLogLocation' => $c['agent_log_location'] + ); + } + } + $this->setupWithOptions($c, "sender", $expected, $default); + } + + private function setResponseHandler($c) + { + $this->setupWithOptions($c, "responseHandler", "Rollbar\ResponseHandlerInterface"); + } + + private function setCheckIgnoreFunction($c) + { + if (!isset($c['checkIgnore'])) { + return; + } + + $this->checkIgnore = $c['checkIgnore']; + } + + /** + * Allows setting up configuration options that might be specified by class + * name. Any interface used with `setupWithOptions` should be constructed + * with a single parameter: an associative array with the config options. + * It is assumed that it will be in the configuration as a sibling to the + * key the class is named in. The options should have the same key as the + * classname, but with 'Options' appended. E.g: + * ```array( + * "sender" => "MySender", + * "senderOptions" => array( + * "speed" => 11, + * "protocol" => "First Contact" + * ) + * );``` + * Will be initialized as if you'd used: + * `new MySender(array("speed"=>11,"protocol"=>"First Contact"));` + * You can also just pass an instance in directly. (In which case options + * are ignored) + * @param $c + * @param $keyName + * @param $expectedType + * @param mixed $defaultClass + * @param bool $passWholeConfig + */ + protected function setupWithOptions( + $c, + $keyName, + $expectedType, + $defaultClass = null, + $passWholeConfig = false + ) { + $$keyName = isset($c[$keyName]) ? $c[$keyName] : null; + + if (is_null($defaultClass) && is_null($$keyName)) { + return; + } + + if (is_null($$keyName)) { + $$keyName = $defaultClass; + } + if (is_string($$keyName)) { + if ($passWholeConfig) { + $options = $c; + } else { + $options = isset($c[$keyName . "Options"]) ? $c[$keyName . "Options"] : array(); + } + $this->$keyName = new $$keyName($options); + } else { + $this->$keyName = $$keyName; + } + + if (!$this->$keyName instanceof $expectedType) { + throw new \InvalidArgumentException("$keyName must be a $expectedType"); + } + } + + public function getRollbarData($level, $toLog, $context) + { + return $this->dataBuilder->makeData($level, $toLog, $context); + } + + /** + * @param Payload $payload + * @param Level $level + * @param \Exception | \Throwable $toLog + * @param array $context + * @return Payload + */ + public function transform($payload, $level, $toLog, $context) + { + if (is_null($this->transformer)) { + return $payload; + } + return $this->transformer->transform($payload, $level, $toLog, $context); + } + + public function getAccessToken() + { + return $this->accessToken; + } + + public function checkIgnored($payload, $accessToken, $toLog) + { + if ($this->shouldSuppress()) { + return true; + } + if (isset($this->checkIgnore) && call_user_func($this->checkIgnore)) { + return true; + } + if ($this->levelTooLow($payload)) { + return true; + } + if (!is_null($this->filter)) { + return $this->filter->shouldSend($payload, $accessToken); + } + + if ($toLog instanceof ErrorWrapper) { + $errno = $toLog->errorLevel; + + if ($this->included_errno != -1 && ($errno & $this->included_errno) != $errno) { + // ignore + return true; + } + + if (isset($this->error_sample_rates[$errno])) { + // get a float in the range [0, 1) + // mt_rand() is inclusive, so add 1 to mt_randmax + $float_rand = mt_rand() / ($this->mt_randmax + 1); + if ($float_rand > $this->error_sample_rates[$errno]) { + // skip + return true; + } + } + } + return false; + } + + /** + * @param Payload $payload + * @return bool + */ + private function levelTooLow($payload) + { + return $payload->getData()->getLevel()->toInt() < $this->minimumLevel; + } + + private function shouldSuppress() + { + return error_reporting() === 0 && !$this->reportSuppressed; + } + + public function send($payload, $accessToken) + { + return $this->sender->send($payload, $accessToken); + } + + public function handleResponse($payload, $response) + { + if (!is_null($this->responseHandler)) { + $this->responseHandler->handleResponse($payload, $response); + } + } +} diff --git a/src/DataBuilder.php b/src/DataBuilder.php new file mode 100644 index 00000000..18ccee0d --- /dev/null +++ b/src/DataBuilder.php @@ -0,0 +1,803 @@ +setEnvironment($config); + + $this->setDefaultMessageLevel($config); + $this->setDefaultExceptionLevel($config); + $this->setDefaultPsrLevels($config); + $this->setScrubFields($config); + $this->setErrorLevels($config); + $this->setCodeVersion($config); + $this->setPlatform($config); + $this->setFramework($config); + $this->setContext($config); + $this->setRequestParams($config); + $this->setRequestBody($config); + $this->setRequestExtras($config); + $this->setHost($config); + $this->setPerson($config); + $this->setPersonFunc($config); + $this->setServerRoot($config); + $this->setServerBranch($config); + $this->setServerCodeVersion($config); + $this->setServerExtras($config); + $this->setCustom($config); + $this->setFingerprint($config); + $this->setTitle($config); + $this->setNotifier($config); + $this->setBaseException($config); + $this->setIncludeCodeContext($config); + + $this->shiftFunction = $this->tryGet($config, 'shift_function'); + if (!isset($this->shiftFunction)) { + $this->shiftFunction = true; + } + } + + protected function getOrCall($name, $level, $toLog, $context) + { + if (is_callable($this->$name)) { + try { + return $this->$name($level, $toLog, $context); + } catch (\Exception $e) { + // TODO Report the configuration error. + return null; + } + } + return $this->$name; + } + + protected function tryGet($array, $key) + { + return isset($array[$key]) ? $array[$key] : null; + } + + protected function setEnvironment($config) + { + $fromConfig = $this->tryGet($config, 'environment'); + Utilities::validateString($fromConfig, "config['environment']", null, false); + $this->environment = $fromConfig; + } + + protected function setDefaultMessageLevel($config) + { + $fromConfig = $this->tryGet($config, 'messageLevel'); + $this->messageLevel = self::$defaults->messageLevel($fromConfig); + } + + protected function setDefaultExceptionLevel($config) + { + $fromConfig = $this->tryGet($config, 'exceptionLevel'); + $this->exceptionLevel = self::$defaults->exceptionLevel($fromConfig); + } + + protected function setDefaultPsrLevels($config) + { + $fromConfig = $this->tryGet($config, 'psrLevels'); + $this->psrLevels = self::$defaults->psrLevels($fromConfig); + } + + protected function setScrubFields($config) + { + $fromConfig = $this->tryGet($config, 'scrubFields'); + if (!isset($fromConfig)) { + $fromConfig = $this->tryGet($config, 'scrub_fields'); + } + $this->scrubFields = self::$defaults->scrubFields($fromConfig); + } + + protected function setErrorLevels($config) + { + $fromConfig = $this->tryGet($config, 'errorLevels'); + $this->errorLevels = self::$defaults->errorLevels($fromConfig); + } + + protected function setCodeVersion($c) + { + $fromConfig = $this->tryGet($c, 'codeVersion'); + if (!isset($fromConfig)) { + $fromConfig = $this->tryGet($c, 'code_version'); + } + $this->codeVersion = self::$defaults->codeVersion($fromConfig); + } + + protected function setPlatform($c) + { + $fromConfig = $this->tryGet($c, 'platform'); + $this->platform = self::$defaults->platform($fromConfig); + } + + protected function setFramework($c) + { + $this->framework = $this->tryGet($c, 'framework'); + } + + protected function setContext($c) + { + $this->context = $this->tryGet($c, 'context'); + } + + protected function setRequestParams($c) + { + $this->requestParams = $this->tryGet($c, 'requestParams'); + } + + protected function setRequestBody($c) + { + $this->requestBody = $this->tryGet($c, 'requestBody'); + } + + protected function setRequestExtras($c) + { + $this->requestExtras = $this->tryGet($c, "requestExtras"); + } + + protected function setPerson($c) + { + $this->person = $this->tryGet($c, 'person'); + } + + protected function setPersonFunc($c) + { + $this->personFunc = $this->tryGet($c, 'person_fn'); + } + + protected function setServerRoot($c) + { + $fromConfig = $this->tryGet($c, 'serverRoot'); + if (!isset($fromConfig)) { + $fromConfig = $this->tryGet($c, 'root'); + } + $this->serverRoot = self::$defaults->serverRoot($fromConfig); + } + + protected function setServerBranch($c) + { + $fromConfig = $this->tryGet($c, 'serverBranch'); + if (!isset($fromConfig)) { + $fromConfig = $this->tryGet($c, 'branch'); + } + $this->serverBranch = self::$defaults->gitBranch($fromConfig); + } + + protected function setServerCodeVersion($c) + { + $this->serverCodeVersion = $this->tryGet($c, 'serverCodeVersion'); + } + + protected function setServerExtras($c) + { + $this->serverExtras = $this->tryGet($c, 'serverExtras'); + } + + protected function setCustom($c) + { + $this->custom = $this->tryGet($c, 'custom'); + } + + protected function setFingerprint($c) + { + $this->fingerprint = $this->tryGet($c, 'fingerprint'); + if (!is_null($this->fingerprint) && !is_callable($this->fingerprint)) { + $msg = "If set, config['fingerprint'] must be a callable that returns a uuid string"; + throw new \InvalidArgumentException($msg); + } + } + + protected function setTitle($c) + { + $this->title = $this->tryGet($c, 'title'); + if (!is_null($this->title) && !is_callable($this->title)) { + $msg = "If set, config['title'] must be a callable that returns a string"; + throw new \InvalidArgumentException($msg); + } + } + + protected function setNotifier($c) + { + $fromConfig = $this->tryGet($c, 'notifier'); + $this->notifier = self::$defaults->notifier($fromConfig); + } + + protected function setBaseException($c) + { + $fromConfig = $this->tryGet($c, 'baseException'); + $this->baseException = self::$defaults->baseException($fromConfig); + } + + protected function setIncludeCodeContext($c) + { + $fromConfig = $this->tryGet($c, 'include_error_code_context'); + $this->includeCodeContext = true; + if ($fromConfig != null) { + $this->includeCodeContext = $fromConfig; + } + } + + protected function setHost($c) + { + $this->host = $this->tryGet($c, 'host'); + } + + /** + * @param Level $level + * @param \Exception | \Throwable | string $toLog + * @param $context + * @return Data + */ + public function makeData($level, $toLog, $context) + { + $env = $this->getEnvironment(); + $body = $this->getBody($toLog, $context); + $data = new Data($env, $body); + $data->setLevel($this->getLevel($level, $toLog)) + ->setTimestamp($this->getTimestamp()) + ->setCodeVersion($this->getCodeVersion()) + ->setPlatform($this->getPlatform()) + ->setLanguage($this->getLanguage()) + ->setFramework($this->getFramework()) + ->setContext($this->getContext()) + ->setRequest($this->getRequest()) + ->setPerson($this->getPerson()) + ->setServer($this->getServer()) + ->setCustom($this->getCustom($toLog, $context)) + ->setFingerprint($this->getFingerprint()) + ->setTitle($this->getTitle()) + ->setUuid($this->getUuid()) + ->setNotifier($this->getNotifier()); + return $data; + } + + protected function getEnvironment() + { + return $this->environment; + } + + protected function getBody($toLog, $context) + { + $baseException = $this->getBaseException(); + if ($toLog instanceof ErrorWrapper) { + $content = $this->getErrorTrace($toLog); + } elseif ($toLog instanceof $baseException) { + $content = $this->getExceptionTrace($baseException); + } else { + $scrubFields = $this->getScrubFields(); + $content = $this->getMessage($toLog, self::scrub($context, $scrubFields)); + } + return new Body($content); + } + + protected function getErrorTrace(ErrorWrapper $error) + { + return $this->makeTrace($error, $error->getClassName()); + } + + /** + * @param \Throwable|\Exception $exc + * @return Trace|TraceChain + */ + protected function getExceptionTrace($exc) + { + $chain = array(); + $chain[] = $this->makeTrace($exc); + + $previous = $exc->getPrevious(); + + $baseException = $this->getBaseException(); + while ($previous instanceof $baseException) { + $chain[] = $this->makeTrace($previous); + $previous = $exc->getPrevious(); + } + + if (count($chain) > 1) { + return new TraceChain($chain); + } + return new Trace($chain[0], $chain[0]->getException()); + } + + /** + * @param \Throwable|\Exception $exception + * @param string $classOverride + * @return Trace + */ + public function makeTrace($exception, $classOverride = null) + { + $frames = $this->makeFrames($exception); + $excInfo = new ExceptionInfo( + Utilities::coalesce($classOverride, get_class($exception)), + $exception->getMessage() + ); + return new Trace($frames, $excInfo); + } + + public function makeFrames($exception) + { + $frames = array(); + foreach ($this->getTrace($exception) as $frameInfo) { + $filename = Utilities::coalesce($this->tryGet($frameInfo, 'file'), ''); + $lineno = Utilities::coalesce($this->tryGet($frameInfo, 'line'), 0); + $method = $frameInfo['function']; + // TODO 4 (arguments are in $frame) + + $frame = new Frame($filename); + $frame->setLineno($lineno) + ->setMethod($method); + + if ($this->includeCodeContext) { + $this->addCodeContextToFrame($frame, $filename, $lineno); + } + + $frames[] = $frame; + } + array_reverse($frames); + + if ($this->shiftFunction && count($frames) > 0) { + for ($i = count($frames) - 1; $i > 0; $i--) { + $frames[$i]->setMethod($frames[$i - 1]->getMethod()); + } + $frames[0]->setMethod('
'); + } + + return $frames; + } + + private function addCodeContextToFrame(Frame $frame, $filename, $line) + { + if (!file_exists($filename)) { + return; + } + + $source = explode(PHP_EOL, file_get_contents($filename)); + if (!is_array($source)) { + return; + } + + $source = str_replace(array("\n", "\t", "\r"), '', $source); + $total = count($source); + $line = $line - 1; + $frame->setCode($source[$line]); + $offset = 6; + $min = max($line - $offset, 0); + $pre = null; + $post = null; + if ($min !== $line) { + $pre = array_slice($source, $min, $line - $min); + } + $max = min($line + $offset, $total); + if ($max !== $line) { + $post = array_slice($source, $line + 1, $max - $line); + } + $frame->setContext(new Context($pre, $post)); + } + + private function getTrace($exc) + { + if ($exc instanceof ErrorWrapper) { + return $exc->getBacktrace(); + } else { + return $exc->getTrace(); + } + } + + protected function getMessage($toLog, $context) + { + return new Message((string)$toLog, $context); + } + + protected function getLevel($level, $toLog) + { + if (is_null($level)) { + if ($toLog instanceof ErrorWrapper) { + $level = $this->tryGet($this->errorLevels, $toLog->errorLevel); + } elseif ($toLog instanceof \Exception) { + $level = $this->exceptionLevel; + } else { + $level = $this->messageLevel; + } + } + $level = strtolower($level); + return Level::fromName($this->tryGet($this->psrLevels, $level)); + } + + protected function getTimestamp() + { + return time(); + } + + protected function getCodeVersion() + { + return $this->codeVersion; + } + + protected function getPlatform() + { + return $this->platform; + } + + protected function getLanguage() + { + return "PHP " . phpversion(); + } + + protected function getFramework() + { + return $this->framework; + } + + protected function getContext() + { + return $this->context; + } + + protected function getRequest() + { + $scrubFields = $this->getScrubFields(); + $request = new Request(); + $request->setUrl($this->getUrl($scrubFields)) + ->setMethod($this->tryGet($_SERVER, 'REQUEST_METHOD')) + ->setHeaders($this->getScrubbedHeaders($scrubFields)) + ->setParams($this->getRequestParams()) + ->setGet(self::scrub($_GET, $scrubFields)) + ->setQueryString(self::scrubUrl($this->tryGet($_SERVER, "QUERY_STRING"), $scrubFields)) + ->setPost(self::scrub($_POST, $scrubFields)) + ->setBody($this->getRequestBody()) + ->setUserIp($this->getUserIp()); + $extras = $this->getRequestExtras(); + if (!$extras) { + $extras = array(); + } + foreach ($extras as $key => $val) { + if (in_array($scrubFields, $key)) { + $request->$key = str_repeat("*", 8); + } else { + $request->$key = $val; + } + } + if (is_array($_SESSION) && count($_SESSION) > 0) { + $request->session = self::scrub($_SESSION, $scrubFields); + } + return $request; + } + + protected function getUrl($scrubFields) + { + if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { + $proto = strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']); + } elseif (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { + $proto = 'https'; + } else { + $proto = 'http'; + } + + if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { + $host = $_SERVER['HTTP_X_FORWARDED_HOST']; + } elseif (!empty($_SERVER['HTTP_HOST'])) { + $parts = explode(':', $_SERVER['HTTP_HOST']); + $host = $parts[0]; + } elseif (!empty($_SERVER['SERVER_NAME'])) { + $host = $_SERVER['SERVER_NAME']; + } else { + $host = 'unknown'; + } + + if (!empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { + $port = $_SERVER['HTTP_X_FORWARDED_PORT']; + } elseif (!empty($_SERVER['SERVER_PORT'])) { + $port = $_SERVER['SERVER_PORT']; + } elseif ($proto === 'https') { + $port = 443; + } else { + $port = 80; + } + + $path = Utilities::coalesce($this->tryGet($_SERVER, 'REQUEST_URI'), '/'); + $url = $proto . '://' . $host; + if (($proto == 'https' && $port != 443) || ($proto == 'http' && $port != 80)) { + $url .= ':' . $port; + } + + $url .= $path; + + if ($host == 'unknown') { + $url = null; + } + + return self::scrubUrl($url, $scrubFields); + } + + protected function getScrubbedHeaders($scrubFields) + { + $headers = $this->getHeaders(); + return self::scrub($headers, $scrubFields); + } + + protected function getHeaders() + { + $headers = array(); + foreach ($_SERVER as $key => $val) { + if (substr($key, 0, 5) == 'HTTP_') { + // convert HTTP_CONTENT_TYPE to Content-Type, HTTP_HOST to Host, etc. + $name = strtolower(substr($key, 5)); + if (strpos($name, '_') != -1) { + $name = preg_replace('/ /', '-', ucwords(preg_replace('/_/', ' ', $name))); + } else { + $name = ucfirst($name); + } + $headers[$name] = $val; + } + } + if (count($headers) > 0) { + return $headers; + } else { + return null; + } + } + + protected function getRequestParams() + { + return $this->requestParams; + } + + protected function getRequestBody() + { + return $this->requestBody; + } + + protected function getUserIp() + { + $forwardFor = $this->tryGet($_SERVER, 'HTTP_X_FORWARDED_FOR'); + if ($forwardFor) { + // return everything until the first comma + $parts = explode(',', $forwardFor); + return $parts[0]; + } + $realIp = $this->tryGet($_SERVER, 'HTTP_X_REAL_IP'); + if ($realIp) { + return $realIp; + } + return $this->tryGet($_SERVER, 'REMOTE_ADDR'); + } + + protected function getRequestExtras() + { + return $this->requestExtras; + } + + /** + * @return Person + */ + protected function getPerson() + { + $personData = $this->person; + if (!isset($personData) && is_callable($this->personFunc)) { + $personData = call_user_func($this->personFunc); + } + + if (!isset($personData['id'])) { + return null; + } + + $id = $personData['id']; + + $email = null; + if (isset($personData['email'])) { + $email = $personData['email']; + } + + $username = null; + if (isset($personData['username'])) { + $username = $personData['username']; + } + + unset($personData['id'], $personData['email'], $personData['username']); + return new Person($id, $username, $email, $personData); + } + + protected function getServer() + { + $server = new Server(); + $server->setHost($this->getHost()) + ->setRoot($this->getServerRoot()) + ->setBranch($this->getServerBranch()) + ->setCodeVersion($this->getServerCodeVersion()); + $scrubFields = $this->getScrubFields(); + $extras = $this->getServerExtras(); + if (!$extras) { + $extras = array(); + } + foreach ($extras as $key => $val) { + if (in_array($scrubFields, $key)) { + $server->$key = str_repeat("*", 8); + } { + $server->$key = $val; + } + } + if (array_key_exists('argv', $_SERVER)) { + $server->argv = $_SERVER['argv']; + } + return $server; + } + + protected function getHost() + { + if (isset($this->host)) { + return $this->host; + } + return gethostname(); + } + + protected function getServerRoot() + { + return $this->serverRoot; + } + + protected function getServerBranch() + { + return $this->serverBranch; + } + + protected function getServerCodeVersion() + { + return $this->serverCodeVersion; + } + + protected function getServerExtras() + { + return $this->serverExtras; + } + + protected function getCustom($toLog, $context) + { + $custom = $this->custom; + + // Make this an array if possible: + if ($custom instanceof \JsonSerializable) { + $custom = $custom->jsonSerialize(); + } elseif (is_null($custom)) { + return null; + } elseif (!is_array($custom)) { + $custom = get_object_vars($custom); + } + + $baseException = $this->getBaseException(); + if (!$toLog instanceof $baseException) { + return array_replace_recursive(array(), $custom); + } + + $scrubFields = $this->getScrubFields(); + $custom = self::scrub($custom, $scrubFields); + return array_replace_recursive(array(), $context, $custom); + } + + protected function getFingerprint() + { + return $this->fingerprint; + } + + protected function getTitle() + { + return $this->title; + } + + protected function getUuid() + { + return self::uuid4(); + } + + protected function getNotifier() + { + return $this->notifier; + } + + protected function getBaseException() + { + return $this->baseException; + } + + protected function getScrubFields() + { + return $this->scrubFields; + } + + protected function scrub($arr, $fields, $replacement = '*') + { + if (!$fields || !$arr) { + return null; + } + + $scrubber = function (&$val, $key) use ($fields, $replacement, $arr) { + + if (key_exists($key, $arr)) { + $val = str_repeat($replacement, 8); + } + }; + array_walk_recursive($arr, $scrubber); + return $arr; + } + + protected function scrubUrl($url, $fields) + { + $urlQuery = parse_url($url, PHP_URL_QUERY); + if (!$urlQuery) { + return $url; + } + + parse_str($urlQuery, $parsedOutput); + $scrubbedOutput = $this->scrub($parsedOutput, $fields, 'x'); + + return str_replace($urlQuery, http_build_query($scrubbedOutput), $url); + } + + // from http://www.php.net/manual/en/function.uniqid.php#94959 + protected static function uuid4() + { + mt_srand(); + return sprintf( + '%04x%04x-%04x-%04x-%04x-%04x%04x%04x', + // 32 bits for "time_low" + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + // 16 bits for "time_mid" + mt_rand(0, 0xffff), + // 16 bits for "time_hi_and_version", + // four most significant bits holds version number 4 + mt_rand(0, 0x0fff) | 0x4000, + // 16 bits, 8 bits for "clk_seq_hi_res", + // 8 bits for "clk_seq_low", + // two most significant bits holds zero and one for variant DCE1.1 + mt_rand(0, 0x3fff) | 0x8000, + // 48 bits for "node" + mt_rand(0, 0xffff), + mt_rand(0, 0xffff), + mt_rand(0, 0xffff) + ); + } +} diff --git a/src/DataBuilderInterface.php b/src/DataBuilderInterface.php new file mode 100644 index 00000000..36e4c962 --- /dev/null +++ b/src/DataBuilderInterface.php @@ -0,0 +1,6 @@ + /dev/null', $output); + return @$output[0]; + } catch (\Exception $e) { + return null; + } + } + + private static function getGitBranch() + { + try { + @exec('git rev-parse --abbrev-ref HEAD 2> /dev/null', $output); + return @$output[0]; + } catch (\Exception $e) { + return null; + } + } + + private static function getServerRoot() + { + return isset($_ENV["HEROKU_APP_DIR"]) ? $_ENV["HEROKU_APP_DIR"] : null; + } + + private static function getPlatform() + { + return php_uname('a'); + } + + private static function getNotifier() + { + return Notifier::defaultNotifier(); + } + + private static function getBaseException() + { + return version_compare(phpversion(), '7.0', '<') + ? '\Exception' + : '\Throwable'; + } + + private static function getScrubFields() + { + return array( + 'passwd', + 'password', + 'secret', + 'confirm_password', + 'password_confirmation', + 'auth_token', + 'csrf_token', + 'access_token' + ); + } + + private $defaultMessageLevel = "warning"; + private $defaultExceptionLevel = "error"; + private $defaultPsrLevels; + private $defaultCodeVersion; + private $defaultErrorLevels; + private $defaultGitHash; + private $defaultGitBranch; + private $defaultServerRoot; + private $defaultPlatform; + private $defaultNotifier; + private $defaultBaseException; + private $defaultScrubFields; + + public function __construct() + { + $this->defaultPsrLevels = array( + LogLevel::EMERGENCY => "critical", + "emergency" => "critical", + LogLevel::ALERT => "critical", + "alert" => "critical", + LogLevel::CRITICAL => "critical", + "critical" => "critical", + LogLevel::ERROR => "error", + "error" => "error", + LogLevel::WARNING => "warning", + "warning" => "warning", + LogLevel::NOTICE => "info", + "notice" => "info", + LogLevel::INFO => "info", + "info" => "info", + LogLevel::DEBUG => "debug", + "debug" => "debug" + ); + $this->defaultErrorLevels = array( + E_ERROR => "error", + E_WARNING => "warning", + E_PARSE => "critical", + E_NOTICE => "debug", + E_CORE_ERROR => "critical", + E_CORE_WARNING => "warning", + E_COMPILE_ERROR => "critical", + E_COMPILE_WARNING => "warning", + E_USER_ERROR => "error", + E_USER_WARNING => "warning", + E_USER_NOTICE => "debug", + E_STRICT => "info", + E_RECOVERABLE_ERROR => "error", + E_DEPRECATED => "info", + E_USER_DEPRECATED => "info" + ); + $this->defaultGitHash = self::getGitHash(); + $this->defaultGitBranch = self::getGitBranch(); + $this->defaultServerRoot = self::getServerRoot(); + $this->defaultPlatform = self::getPlatform(); + $this->defaultNotifier = self::getNotifier(); + $this->defaultBaseException = self::getBaseException(); + $this->defaultScrubFields = self::getScrubFields(); + $this->defaultCodeVersion = ""; + } + + public function messageLevel($level = null) + { + return Utilities::coalesce($level, $this->defaultMessageLevel); + } + + public function exceptionLevel($level = null) + { + return Utilities::coalesce($level, $this->defaultExceptionLevel); + } + + public function errorLevels($level = null) + { + return Utilities::coalesce($level, $this->defaultErrorLevels); + } + + public function psrLevels($level = null) + { + return Utilities::coalesce($level, $this->defaultPsrLevels); + } + + public function codeVersion($codeVersion = null) + { + return Utilities::coalesce($codeVersion, $this->defaultCodeVersion); + } + + public function gitHash($gitHash = null) + { + return Utilities::coalesce($gitHash, $this->defaultGitHash); + } + + public function gitBranch($gitBranch = null) + { + return Utilities::coalesce($gitBranch, $this->defaultGitBranch); + } + + public function serverRoot($serverRoot = null) + { + return Utilities::coalesce($serverRoot, $this->defaultServerRoot); + } + + public function platform($platform = null) + { + return Utilities::coalesce($platform, $this->defaultPlatform); + } + + public function notifier($notifier = null) + { + return Utilities::coalesce($notifier, $this->defaultNotifier); + } + + public function baseException($baseException = null) + { + return Utilities::coalesce($baseException, $this->defaultBaseException); + } + + public function scrubFields($scrubFields = null) + { + return Utilities::coalesce($scrubFields, $this->defaultScrubFields); + } +} diff --git a/src/ErrorWrapper.php b/src/ErrorWrapper.php new file mode 100644 index 00000000..c0884acf --- /dev/null +++ b/src/ErrorWrapper.php @@ -0,0 +1,57 @@ + "E_ERROR", + E_WARNING => "E_WARNING", + E_PARSE => "E_PARSE", + E_NOTICE => "E_NOTICE", + E_CORE_ERROR => "E_CORE_ERROR", + E_CORE_WARNING => "E_CORE_WARNING", + E_COMPILE_ERROR => "E_COMPILE_ERROR", + E_COMPILE_WARNING => "E_COMPILE_WARNING", + E_USER_ERROR => "E_USER_ERROR", + E_USER_WARNING => "E_USER_WARNING", + E_USER_NOTICE => "E_USER_NOTICE", + E_STRICT => "E_STRICT", + E_RECOVERABLE_ERROR => "E_RECOVERABLE_ERROR", + E_DEPRECATED => "E_DEPRECATED", + E_USER_DEPRECATED => "E_USER_DEPRECATED" + ); + } + return isset(self::$constName[$const]) ? self::$constName[$const] : null; + } + + public $errorLevel; + public $errorMessage; + public $errorFile; + public $errorLine; + public $backTrace; + + public function __construct($errorLevel, $errorMessage, $errorFile, $errorLine, $backTrace) + { + parent::__construct($errorMessage, $errorLevel); + $this->errorLevel = $errorLevel; + $this->errorMessage = $errorMessage; + $this->errorFile = $errorFile; + $this->errorLine = $errorLine; + $this->backTrace = $backTrace; + } + + public function getBacktrace() + { + return $this->backTrace; + } + + public function getClassName() + { + $constName = Utilities::coalesce(self::getConstName($this->errorLevel), "#$this->errorLevel"); + return "$constName: $this->errorMessage"; + } +} diff --git a/src/FilterInterface.php b/src/FilterInterface.php new file mode 100644 index 00000000..d501b3da --- /dev/null +++ b/src/FilterInterface.php @@ -0,0 +1,6 @@ +setValue($value); + } + + public function getValue() + { + return $this->value; + } + + public function setValue(ContentInterface $value) + { + $this->value = $value; + return $this; + } + + public function jsonSerialize() + { + $overrideNames = array( + "value" => $this->value->getKey() + ); + $obj = get_object_vars($this); + return Utilities::serializeForRollbar($obj, $overrideNames); + } +} diff --git a/src/Payload/ContentInterface.php b/src/Payload/ContentInterface.php new file mode 100644 index 00000000..462b4b63 --- /dev/null +++ b/src/Payload/ContentInterface.php @@ -0,0 +1,12 @@ +setPre($pre); + $this->setPost($post); + } + + public function getPre() + { + return $this->pre; + } + + public function setPre($pre) + { + $this->validateAllString($pre, "pre"); + $this->pre = $pre; + return $this; + } + + public function getPost() + { + return $this->post; + } + + public function setPost($post) + { + $this->validateAllString($post, "post"); + $this->post = $post; + return $this; + } + + public function jsonSerialize() + { + return Utilities::serializeForRollbar(get_object_vars($this)); + } + + private function validateAllString($arr, $arg) + { + foreach ($arr as $line) { + if (!is_string($line)) { + throw new \InvalidArgumentException("\$$arg must be all strings"); + } + } + } +} diff --git a/src/Payload/Data.php b/src/Payload/Data.php new file mode 100644 index 00000000..6f588c06 --- /dev/null +++ b/src/Payload/Data.php @@ -0,0 +1,245 @@ +setEnvironment($environment); + $this->setBody($body); + } + + public function getEnvironment() + { + return $this->environment; + } + + public function setEnvironment($environment) + { + Utilities::validateString($environment, "environment", null, false); + $this->environment = $environment; + return $this; + } + + public function getBody() + { + return $this->body; + } + + public function setBody(Body $body) + { + $this->body = $body; + return $this; + } + + /** + * @return Level + */ + public function getLevel() + { + return $this->level; + } + + public function setLevel(Level $level) + { + $this->level = $level; + return $this; + } + + public function getTimestamp() + { + return $this->timestamp; + } + + public function setTimestamp($timestamp) + { + Utilities::validateInteger($timestamp, "timestamp"); + $this->timestamp = $timestamp; + return $this; + } + + public function getCodeVersion() + { + return $this->codeVersion; + } + + public function setCodeVersion($codeVersion) + { + Utilities::validateString($codeVersion, "codeVersion"); + $this->codeVersion = $codeVersion; + return $this; + } + + public function getPlatform() + { + return $this->platform; + } + + public function setPlatform($platform) + { + Utilities::validateString($platform, "platform"); + $this->platform = $platform; + return $this; + } + + public function getLanguage() + { + return $this->language; + } + + public function setLanguage($language) + { + Utilities::validateString($language, "language"); + $this->language = $language; + return $this; + } + + public function getFramework() + { + return $this->framework; + } + + public function setFramework($framework) + { + Utilities::validateString($framework, "framework"); + $this->framework = $framework; + return $this; + } + + public function getContext() + { + return $this->context; + } + + public function setContext($context) + { + Utilities::validateString($context, "context"); + $this->context = $context; + return $this; + } + + /** + * @return Request + */ + public function getRequest() + { + return $this->request; + } + + public function setRequest(Request $request = null) + { + $this->request = $request; + return $this; + } + + /** + * @return Person + */ + public function getPerson() + { + return $this->person; + } + + public function setPerson(Person $person = null) + { + $this->person = $person; + return $this; + } + + /** + * @return Server + */ + public function getServer() + { + return $this->server; + } + + public function setServer(Server $server = null) + { + $this->server = $server; + return $this; + } + + public function getCustom() + { + return $this->custom; + } + + public function setCustom(array $custom = null) + { + $this->custom = $custom; + return $this; + } + + public function getFingerprint() + { + return $this->fingerprint; + } + + public function setFingerprint($fingerprint) + { + Utilities::validateString($fingerprint, "fingerprint"); + $this->fingerprint = $fingerprint; + return $this; + } + + public function getTitle() + { + return $this->title; + } + + public function setTitle($title) + { + Utilities::validateString($title, "title"); + $this->title = $title; + return $this; + } + + public function getUuid() + { + return $this->uuid; + } + + public function setUuid($uuid) + { + Utilities::validateString($uuid, "uuid"); + $this->uuid = $uuid; + return $this; + } + + public function getNotifier() + { + return $this->notifier; + } + + public function setNotifier(Notifier $notifier) + { + $this->notifier = $notifier; + return $this; + } + + public function jsonSerialize() + { + return Utilities::serializeForRollbar(get_object_vars($this)); + } +} diff --git a/src/Payload/ExceptionInfo.php b/src/Payload/ExceptionInfo.php new file mode 100644 index 00000000..4f808b56 --- /dev/null +++ b/src/Payload/ExceptionInfo.php @@ -0,0 +1,53 @@ +setClass($class); + $this->setMessage($message); + $this->setDescription($description); + } + + public function getClass() + { + return $this->class; + } + + public function setClass($class) + { + Utilities::validateString($class, "class", null, false); + $this->class = $class; + return $this; + } + + public function getMessage() + { + return $this->message; + } + + public function setMessage($message) + { + Utilities::validateString($message, "message", null, false); + $this->message = $message; + return $this; + } + + public function getDescription() + { + return $this->description; + } + + public function setDescription($description) + { + Utilities::validateString($description, "description"); + $this->description = $description; + return $this; + } +} diff --git a/src/Payload/Frame.php b/src/Payload/Frame.php new file mode 100644 index 00000000..5b8c2870 --- /dev/null +++ b/src/Payload/Frame.php @@ -0,0 +1,119 @@ +setFilename($filename); + } + + public function getFilename() + { + return $this->filename; + } + + public function setFilename($filename) + { + Utilities::validateString($filename, "filename", null, false); + $this->filename = $filename; + return $this; + } + + public function getLineno() + { + return $this->lineno; + } + + public function setLineno($lineno) + { + Utilities::validateInteger($lineno, "lineno"); + $this->lineno = $lineno; + return $this; + } + + public function getColno() + { + return $this->colno; + } + + public function setColno($colno) + { + Utilities::validateInteger($colno, "colno"); + $this->colno = $colno; + return $this; + } + + public function getMethod() + { + return $this->method; + } + + public function setMethod($method) + { + Utilities::validateString($method, "method"); + $this->method = $method; + return $this; + } + + public function getCode() + { + return $this->code; + } + + public function setCode($code) + { + Utilities::validateString($code, "code"); + $this->code = $code; + return $this; + } + + public function getContext() + { + return $this->context; + } + + public function setContext(Context $context) + { + $this->context = $context; + return $this; + } + + public function getArgs() + { + return $this->args; + } + + public function setArgs(array $args) + { + $this->args = $args; + return $this; + } + + public function getKwargs() + { + return $this->kwargs; + } + + public function setKwargs(array $kwargs) + { + $this->kwargs = $kwargs; + return $this; + } + + + public function jsonSerialize() + { + return Utilities::serializeForRollbar(get_object_vars($this)); + } +} diff --git a/src/Payload/Level.php b/src/Payload/Level.php new file mode 100644 index 00000000..86671437 --- /dev/null +++ b/src/Payload/Level.php @@ -0,0 +1,65 @@ + new Level("critical", 100000), + "error" => new Level("error", 10000), + "warning" => new Level("warning", 1000), + "info" => new Level("info", 100), + "debug" => new Level("debug", 10), + "ignored" => new Level("ignore", 0), + "ignore" => new Level("ignore", 0) + + ); + } + } + + public static function __callStatic($name, $args) + { + return self::fromName($name); + } + + /** + * @param string $name level name + * @return Level + */ + public static function fromName($name) + { + self::init(); + $name = strtolower($name); + return array_key_exists($name, self::$values) ? self::$values[$name] : null; + } + + /** + * @var string + */ + private $level; + private $val; + + private function __construct($level, $val) + { + $this->level = $level; + $this->val = $val; + } + + public function __toString() + { + return $this->level; + } + + public function toInt() + { + return $this->val; + } + + public function jsonSerialize() + { + return $this->level; + } +} diff --git a/src/Payload/Message.php b/src/Payload/Message.php new file mode 100644 index 00000000..a1d56094 --- /dev/null +++ b/src/Payload/Message.php @@ -0,0 +1,45 @@ +setBody($body); + $this->extra = $extra == null ? array() : $extra; + } + + public function getBody() + { + return $this->body; + } + + public function setBody($body) + { + $this->body = $body; + return $this; + } + + public function __set($key, $val) + { + $this->extra[$key] = $val; + } + + public function __get($key) + { + return isset($this->extra[$key]) ? $this->extra[$key] : null; + } + + public function jsonSerialize() + { + $toSerialize = array("body" => $this->getBody()); + foreach ($this->extra as $key => $value) { + $toSerialize[$key] = $value; + } + return Utilities::serializeForRollbar($toSerialize, null, array_keys($this->extra)); + } +} diff --git a/src/Payload/Notifier.php b/src/Payload/Notifier.php new file mode 100644 index 00000000..548913ee --- /dev/null +++ b/src/Payload/Notifier.php @@ -0,0 +1,52 @@ +setName($name); + $this->setVersion($version); + } + + public function getName() + { + return $this->name; + } + + public function setName($name) + { + Utilities::validateString($name, "name", null, false); + $this->name = $name; + return $this; + } + + public function getVersion() + { + return $this->version; + } + + public function setVersion($version) + { + Utilities::validateString($version, "version", null, false); + $this->version = $version; + return $this; + } + + public function jsonSerialize() + { + return Utilities::serializeForRollbar(get_object_vars($this)); + } +} diff --git a/src/Payload/Payload.php b/src/Payload/Payload.php new file mode 100644 index 00000000..e9edad83 --- /dev/null +++ b/src/Payload/Payload.php @@ -0,0 +1,46 @@ +setData($data); + $this->setAccessToken($accessToken); + } + + /** + * @return Data + */ + public function getData() + { + return $this->data; + } + + public function setData(Data $data) + { + $this->data = $data; + return $this; + } + + public function getAccessToken() + { + return $this->accessToken; + } + + public function setAccessToken($accessToken) + { + Utilities::validateString($accessToken, "accessToken", 32, false); + $this->accessToken = $accessToken; + return $this; + } + + public function jsonSerialize() + { + return Utilities::serializeForRollbar(get_object_vars($this)); + } +} diff --git a/src/Payload/Person.php b/src/Payload/Person.php new file mode 100644 index 00000000..1d0fb0c4 --- /dev/null +++ b/src/Payload/Person.php @@ -0,0 +1,75 @@ +setId($id); + $this->setUsername($username); + $this->setEmail($email); + $this->extra = $extra == null ? array() : $extra; + } + + public function getId() + { + return $this->id; + } + + public function setId($id) + { + Utilities::validateString($id, "id", null, false); + $this->id = $id; + return $this; + } + + public function getUsername() + { + return $this->username; + } + + public function setUsername($username) + { + Utilities::validateString($username, "username"); + $this->username = $username; + return $this; + } + + public function getEmail() + { + return $this->email; + } + + public function setEmail($email) + { + Utilities::validateString($email, "email"); + $this->email = $email; + return $this; + } + + public function __get($name) + { + return isset($this->extra[$name]) ? $this->extra[$name] : null; + } + + public function __set($name, $val) + { + $this->extra[$name] = $val; + } + + public function jsonSerialize() + { + $result = get_object_vars($this); + unset($result['extra']); + foreach ($this->extra as $key => $val) { + $result[$key] = $val; + } + return Utilities::serializeForRollbar($result, null, array_keys($this->extra)); + } +} diff --git a/src/Payload/Request.php b/src/Payload/Request.php new file mode 100644 index 00000000..b2ec776e --- /dev/null +++ b/src/Payload/Request.php @@ -0,0 +1,145 @@ +url; + } + + public function setUrl($url) + { + Utilities::validateString($url, "url"); + $this->url = $url; + return $this; + } + + public function getMethod() + { + return $this->method; + } + + public function setMethod($method) + { + Utilities::validateString($method, "method"); + $this->method = $method; + return $this; + } + + public function getHeaders() + { + return $this->headers; + } + + public function setHeaders(array $headers = null) + { + $this->headers = $headers; + return $this; + } + + public function getParams() + { + return $this->params; + } + + public function setParams(array $params = null) + { + $this->params = $params; + return $this; + } + + public function getGet() + { + return $this->get; + } + + public function setGet(array $get = null) + { + $this->get = $get; + return $this; + } + + public function getQueryString() + { + return $this->queryString; + } + + public function setQueryString($queryString) + { + Utilities::validateString($queryString, "queryString"); + $this->queryString = $queryString; + return $this; + } + + public function getPost() + { + return $this->post; + } + + public function setPost(array $post = null) + { + $this->post = $post; + return $this; + } + + public function getBody() + { + return $this->body; + } + + public function setBody($body) + { + Utilities::validateString($body, "body"); + $this->body = $body; + return $this; + } + + public function getUserIp() + { + return $this->userIp; + } + + public function setUserIp($userIp) + { + Utilities::validateString($userIp, "userIp"); + $this->userIp = $userIp; + return $this; + } + + public function __get($key) + { + return isset($this->extra[$key]) ? $this->extra[$key] : null; + } + + public function __set($key, $val) + { + $this->extra[$key] = $val; + } + + public function jsonSerialize() + { + $result = get_object_vars($this); + unset($result['extra']); + foreach ($this->extra as $key => $val) { + $result[$key] = $val; + } + $overrideNames = array( + "get" => "GET", + "post" => "POST" + ); + return Utilities::serializeForRollbar($result, $overrideNames, array_keys($this->extra)); + } +} diff --git a/src/Payload/Server.php b/src/Payload/Server.php new file mode 100644 index 00000000..dfa50b83 --- /dev/null +++ b/src/Payload/Server.php @@ -0,0 +1,80 @@ +host; + } + + public function setHost($host) + { + Utilities::validateString($host, "host"); + $this->host = $host; + return $this; + } + + public function getRoot() + { + return $this->root; + } + + public function setRoot($root) + { + Utilities::validateString($root, "root"); + $this->root = $root; + return $this; + } + + public function getBranch() + { + return $this->branch; + } + + public function setBranch($branch) + { + Utilities::validateString($branch, "branch"); + $this->branch = $branch; + return $this; + } + + public function getCodeVersion() + { + return $this->codeVersion; + } + + public function setCodeVersion($codeVersion) + { + Utilities::validateString($codeVersion, "codeVersion"); + $this->codeVersion = $codeVersion; + return $this; + } + + public function __get($key) + { + return isset($this->extra[$key]) ? $this->extra[$key] : null; + } + + public function __set($key, $val) + { + $this->extra[$key] = $val; + } + + public function jsonSerialize() + { + $result = get_object_vars($this); + unset($result['extra']); + foreach ($this->extra as $key => $val) { + $result[$key] = $val; + } + return Utilities::serializeForRollbar($result, null, array_keys($this->extra)); + } +} diff --git a/src/Payload/Trace.php b/src/Payload/Trace.php new file mode 100644 index 00000000..6d92fbd2 --- /dev/null +++ b/src/Payload/Trace.php @@ -0,0 +1,47 @@ +setFrames($frames); + $this->setException($exception); + } + + public function getFrames() + { + return $this->frames; + } + + public function setFrames(array $frames) + { + foreach ($frames as $frame) { + if (!$frame instanceof Frame) { + throw new \InvalidArgumentException("\$frames must all be Rollbar\Payload\Frames"); + } + } + $this->frames = $frames; + return $this; + } + + public function getException() + { + return $this->exception; + } + + public function setException(ExceptionInfo $exception) + { + $this->exception = $exception; + return $this; + } + + public function jsonSerialize() + { + return Utilities::serializeForRollbar(get_object_vars($this)); + } +} diff --git a/src/Payload/TraceChain.php b/src/Payload/TraceChain.php new file mode 100644 index 00000000..2cccdf3d --- /dev/null +++ b/src/Payload/TraceChain.php @@ -0,0 +1,41 @@ +setTraces($traces); + } + + public function getTraces() + { + return $this->traces; + } + + public function setTraces($traces) + { + if (count($traces) < 1) { + throw new \InvalidArgumentException('$traces must contain at least 1 Trace'); + } + foreach ($traces as $trace) { + if (!$trace instanceof Trace) { + throw new \InvalidArgumentException('$traces must all be Trace instances'); + } + } + $this->traces = $traces; + return $this; + } + + public function jsonSerialize() + { + $mapValue = function ($value) { + if ($value instanceof \JsonSerializable) { + return $value->jsonSerialize(); + } + return $value; + }; + return array_map($mapValue, $this->traces); + } +} diff --git a/src/Response.php b/src/Response.php new file mode 100644 index 00000000..83094c9c --- /dev/null +++ b/src/Response.php @@ -0,0 +1,54 @@ +status = $status; + $this->info = $info; + $this->uuid = $uuid; + } + + public function getStatus() + { + return $this->status; + } + + public function getInfo() + { + return $this->info; + } + + public function getUuid() + { + return $this->uuid; + } + + public function wasSuccessful() + { + return $this->status >= 200 && $this->status < 300; + } + + public function getOccurrenceUrl() + { + if (is_null($this->uuid)) { + return null; + } + if (!$this->wasSuccessful()) { + return null; + } + return "https://rollbar.com/occurrence/uuid/?uuid=" . $this->uuid; + } + + public function __toString() + { + $url = $this->getOccurrenceUrl(); + return "Status: $this->status\n" . + "Body: " . json_encode($this->info) . "\n" . + "URL: $url"; + } +} diff --git a/src/ResponseHandlerInterface.php b/src/ResponseHandlerInterface.php new file mode 100644 index 00000000..002bc38c --- /dev/null +++ b/src/ResponseHandlerInterface.php @@ -0,0 +1,6 @@ +configure($config); + } + } + + public static function logger() + { + return self::$logger; + } + + public static function scope($config) + { + if (is_null(self::$logger)) { + return new RollbarLogger($config); + } else { + return self::$logger->scope($config); + } + } + + public static function setupExceptionHandling() + { + set_exception_handler('Rollbar\Rollbar::log'); + } + + public static function log($exc, $extra = null, $level = null) + { + if (is_null(self::$logger)) { + return self::getNotInitializedResponse(); + } + return self::$logger->log($level, $exc, $extra); + } + + public static function setupErrorHandling() + { + set_error_handler('Rollbar\Rollbar::errorHandler'); + } + + public static function errorHandler($errno, $errstr, $errfile, $errline) + { + if (is_null(self::$logger)) { + return; + } + $exception = self::generateErrorWrapper($errno, $errstr, $errfile, $errline); + self::$logger->log(null, $exception); + } + + public static function setupFatalHandling() + { + register_shutdown_function('Rollbar\Rollbar::fatalHandler'); + } + + public static function fatalHandler() + { + $last_error = error_get_last(); + if (!is_null($last_error)) { + $errno = $last_error['type']; + $errstr = $last_error['message']; + $errfile = $last_error['file']; + $errline = $last_error['line']; + $exception = self::generateErrorWrapper($errno, $errstr, $errfile, $errline); + self::$logger->log(null, $exception); + } + } + + private static function generateErrorWrapper($errno, $errstr, $errfile, $errline) + { + // removing this function and the handler function to make sure they're + // not part of the backtrace + $backTrace = array_slice(debug_backtrace(), 2); + return new ErrorWrapper($errno, $errstr, $errfile, $errline, $backTrace); + } + + private static function getNotInitializedResponse() + { + return new Response(0, "Rollbar Not Initialized"); + } +} diff --git a/src/RollbarLogger.php b/src/RollbarLogger.php new file mode 100644 index 00000000..fa6512f1 --- /dev/null +++ b/src/RollbarLogger.php @@ -0,0 +1,70 @@ +config = new Config($config); + } + + public function configure(array $config) + { + $this->config->configure($config); + } + + public function scope(array $config) + { + return new RollbarLogger($this->extend($config)); + } + + public function extend(array $config) + { + return $this->config->extend($config); + } + + public function log($level, $toLog, array $context = array()) + { + $accessToken = $this->getAccessToken(); + $payload = $this->getPayload($accessToken, $level, $toLog, $context); + $response = $this->sendOrIgnore($payload, $accessToken, $toLog); + $this->handleResponse($payload, $response); + return $response; + } + + protected function getPayload($accessToken, $level, $toLog, $context) + { + $data = $this->config->getRollbarData($level, $toLog, $context); + $payload = new Payload($data, $accessToken); + return $this->config->transform($payload, $level, $toLog, $context); + } + + protected function getAccessToken() + { + return $this->config->getAccessToken(); + } + + /** + * @param Payload $payload + * @param string $accessToken + * @param mixed $toLog + * @return Response + */ + protected function sendOrIgnore($payload, $accessToken, $toLog) + { + if ($this->config->checkIgnored($payload, $accessToken, $toLog)) { + return new Response(0, "Ignored"); + } + + return $this->config->send($payload, $accessToken); + } + + protected function handleResponse($payload, $response) + { + $this->config->handleResponse($payload, $response); + } +} diff --git a/src/Senders/AgentSender.php b/src/Senders/AgentSender.php new file mode 100644 index 00000000..eca83fc6 --- /dev/null +++ b/src/Senders/AgentSender.php @@ -0,0 +1,34 @@ +agentLogLocation = $opts['agentLogLocation']; + } + } + + public function send(Payload $payload, $accessToken) + { + if (empty($this->agentLog)) { + $this->loadAgentFile(); + } + fwrite($this->agentLog, json_encode($payload->jsonSerialize()) . "\n"); + } + + private function loadAgentFile() + { + $filename = $this->agentLogLocation . '/rollbar-relay.' . getmypid() . '.' . microtime(true) . '.rollbar'; + $this->agentLog = fopen($filename, 'a'); + } +} diff --git a/src/Senders/CurlSender.php b/src/Senders/CurlSender.php new file mode 100644 index 00000000..4ff2a661 --- /dev/null +++ b/src/Senders/CurlSender.php @@ -0,0 +1,81 @@ +endpoint = $opts['endpoint']; + } + if (array_key_exists('timeout', $opts)) { + Utilities::validateInteger($opts['timeout'], 'opts["timeout"]', 0, null, false); + $this->timeout = $opts['timeout']; + } + if (array_key_exists('proxy', $opts)) { + $this->proxy = $opts['proxy']; + } + + if (array_key_exists('verifyPeer', $opts)) { + Utilities::validateBoolean($opts['verifyPeer'], 'opts["verifyPeer"]', false); + $this->verifyPeer = $opts['verifyPeer']; + } + } + + public function send(Payload $payload, $accessToken) + { + + $ch = curl_init(); + + $this->setCurlOptions($ch, $payload, $accessToken); + $result = curl_exec($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + + curl_close($ch); + + $uuid = $payload->getData()->getUuid(); + return new Response($statusCode, json_decode($result, true), $uuid); + } + + public function setCurlOptions($ch, Payload $payload, $accessToken) + { + curl_setopt($ch, CURLOPT_URL, $this->endpoint); + curl_setopt($ch, CURLOPT_POST, true); + $encoded = json_encode($payload->jsonSerialize()); + curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded); + curl_setopt($ch, CURLOPT_VERBOSE, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, $this->verifyPeer); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-Rollbar-Access-Token: ' . $accessToken)); + curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); + + if ($this->proxy) { + $proxy = is_array($this->proxy) ? $this->proxy : array('address' => $this->proxy); + if (isset($proxy['address'])) { + curl_setopt($ch, CURLOPT_PROXY, $proxy['address']); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + } + if (isset($proxy['username']) && isset($proxy['password'])) { + curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxy['username'] . ':' . $proxy['password']); + } + } + } +} diff --git a/src/Senders/SenderInterface.php b/src/Senders/SenderInterface.php new file mode 100644 index 00000000..36734e9a --- /dev/null +++ b/src/Senders/SenderInterface.php @@ -0,0 +1,8 @@ + $val) { + if ($val) { + return $val; + } + } + return null; + } + + // Modified from: http://stackoverflow.com/a/1176023/456188 + public static function pascalToCamel($input) + { + $s1 = preg_replace('/([^_])([A-Z][a-z]+)/', '$1_$2', $input); + return strtolower(preg_replace('/([a-z0-9])([A-Z])/', '$1_$2', $s1)); + } + + public static function validateString( + $input, + $name = "?", + $len = null, + $allowNull = true + ) { + if (is_null($input)) { + if (!$allowNull) { + throw new \InvalidArgumentException("\$$name must not be null"); + } + return; + } + + if (!is_string($input)) { + throw new \InvalidArgumentException("\$$name must be a string"); + } + if (!is_null($len) && strlen($input) != $len) { + throw new \InvalidArgumentException("\$$name must be $len characters long, was '$input'"); + } + } + + public static function validateBoolean( + $input, + $name = "?", + $allowNull = true + ) { + if (is_null($input)) { + if (!$allowNull) { + throw new \InvalidArgumentException("\$$name must not be null"); + } + return; + } + + if (!is_bool($input)) { + throw new \InvalidArgumentException("\$$name must be a boolean"); + } + } + + public static function validateInteger( + $input, + $name = "?", + $minValue = null, + $maxValue = null, + $allowNull = true + ) { + if (is_null($input)) { + if (!$allowNull) { + throw new \InvalidArgumentException("\$$name must not be null"); + } + return; + } + + if (!is_integer($input)) { + throw new \InvalidArgumentException("\$$name must be an integer"); + } + if (!is_null($minValue) && $input < $minValue) { + throw new \InvalidArgumentException("\$$name must be >= $minValue"); + } + if (!is_null($maxValue) && $input > $maxValue) { + throw new \InvalidArgumentException("\$$name must be <= $maxValue"); + } + } + + public static function serializeForRollbar( + $obj, + array $overrideNames = null, + array $customKeys = null + ) { + $returnVal = array(); + $overrideNames = $overrideNames == null ? array() : $overrideNames; + $customKeys = $customKeys == null ? array() : $customKeys; + + foreach ($obj as $key => $val) { + if ($val instanceof \JsonSerializable) { + $val = $val->jsonSerialize(); + } + $newKey = array_key_exists($key, $overrideNames) + ? $overrideNames[$key] + : Utilities::pascalToCamel($key); + if (in_array($key, $customKeys)) { + $returnVal[$key] = $val; + } elseif (!is_null($val)) { + $returnVal[$newKey] = $val; + } + } + + return $returnVal; + } +} diff --git a/src/rollbar.php b/src/rollbar.php deleted file mode 100644 index d2cfd087..00000000 --- a/src/rollbar.php +++ /dev/null @@ -1,1153 +0,0 @@ -batched) { - register_shutdown_function('Rollbar::flush'); - } - } - - public static function report_exception($exc, $extra_data = null, $payload_data = null) { - if (self::$instance == null) { - return; - } - return self::$instance->report_exception($exc, $extra_data, $payload_data); - } - - public static function report_message($message, $level = Level::ERROR, $extra_data = null, $payload_data = null) { - if (self::$instance == null) { - return; - } - return self::$instance->report_message($message, $level, $extra_data, $payload_data); - } - - public static function report_fatal_error() { - // Catch any fatal errors that are causing the shutdown - $last_error = error_get_last(); - if (!is_null($last_error)) { - switch ($last_error['type']) { - case E_PARSE: - case E_ERROR: - self::$instance->report_php_error($last_error['type'], $last_error['message'], $last_error['file'], $last_error['line']); - break; - } - } - } - - // This function must return false so that the default php error handler runs - public static function report_php_error($errno, $errstr, $errfile, $errline) { - if (self::$instance != null) { - self::$instance->report_php_error($errno, $errstr, $errfile, $errline); - } - return false; - } - - public static function flush() { - if (self::$instance == null) { - return; - } - self::$instance->flush(); - } -} - -class RollbarException { - private $message; - private $exception; - - /** - * RollbarException constructor. - * @param string $message - * @param Exception | Error $exception - */ - public function __construct($message, $exception = null) { - $this->message = $message; - $this->exception = $exception; - } - - public function getMessage() { - return $this->message; - } - - public function getException() { - return $this->exception; - } -} - -// Send errors that have these levels -if (!defined('ROLLBAR_INCLUDED_ERRNO_BITMASK')) { - define('ROLLBAR_INCLUDED_ERRNO_BITMASK', E_ERROR | E_WARNING | E_PARSE | E_CORE_ERROR | E_USER_ERROR | E_RECOVERABLE_ERROR); -} - -class RollbarNotifier { - const VERSION = "0.18.2"; - - // required - public $access_token = ''; - - // optional / defaults - public $base_api_url = 'https://api.rollbar.com/api/1/'; - public $batch_size = 50; - public $batched = true; - public $branch = null; - public $capture_error_backtraces = true; - public $code_version = null; - public $environment = 'production'; - public $error_sample_rates = array(); - // available handlers: blocking, agent - public $handler = 'blocking'; - public $agent_log_location = '/var/tmp'; - public $host = null; - /** @var iRollbarLogger */ - public $logger = null; - public $included_errno = ROLLBAR_INCLUDED_ERRNO_BITMASK; - public $person = null; - public $person_fn = null; - public $root = ''; - public $checkIgnore = null; - public $scrub_fields = array('passwd', 'pass', 'password', 'secret', 'confirm_password', - 'password_confirmation', 'auth_token', 'csrf_token'); - public $shift_function = true; - public $timeout = 3; - public $report_suppressed = false; - public $use_error_reporting = false; - public $proxy = null; - public $include_error_code_context = false; - public $include_exception_code_context = false; - public $enable_utf8_sanitization = true; - - private $config_keys = array('access_token', 'base_api_url', 'batch_size', 'batched', 'branch', - 'capture_error_backtraces', 'code_version', 'environment', 'error_sample_rates', 'handler', - 'agent_log_location', 'host', 'logger', 'included_errno', 'person', 'person_fn', 'root', 'checkIgnore', - 'scrub_fields', 'shift_function', 'timeout', 'report_suppressed', 'use_error_reporting', 'proxy', - 'include_error_code_context', 'include_exception_code_context', 'enable_utf8_sanitization'); - - // cached values for request/server/person data - private $_php_context = null; - private $_request_data = null; - private $_server_data = null; - private $_person_data = null; - - // payload queue, used when $batched is true - private $_queue = array(); - - // file handle for agent log - private $_agent_log = null; - - private $_iconv_available = null; - - private $_mt_randmax; - - private $_curl_ipresolve_supported; - - /** @var iSourceFileReader $_source_file_reader */ - private $_source_file_reader; - - public function __construct($config) { - foreach ($this->config_keys as $key) { - if (isset($config[$key])) { - $this->$key = $config[$key]; - } - } - $this->_source_file_reader = new SourceFileReader(); - - if (!$this->access_token && $this->handler != 'agent') { - $this->log_error('Missing access token'); - } - - // fill in missing values in error_sample_rates - $levels = array(E_WARNING, E_NOTICE, E_USER_ERROR, E_USER_WARNING, - E_USER_NOTICE, E_STRICT, E_RECOVERABLE_ERROR); - - // PHP 5.3.0 - if (defined('E_DEPRECATED')) { - $levels = array_merge($levels, array(E_DEPRECATED, E_USER_DEPRECATED)); - } - - // PHP 5.3.0 - $this->_curl_ipresolve_supported = defined('CURLOPT_IPRESOLVE'); - - $curr = 1; - for ($i = 0, $num = count($levels); $i < $num; $i++) { - $level = $levels[$i]; - if (isset($this->error_sample_rates[$level])) { - $curr = $this->error_sample_rates[$level]; - } else { - $this->error_sample_rates[$level] = $curr; - } - } - - // cache this value - $this->_mt_randmax = mt_getrandmax(); - } - - public function report_exception($exc, $extra_data = null, $payload_data = null) { - try { - if ( !is_a( $exc, BASE_EXCEPTION ) ) { - throw new Exception(sprintf('Report exception requires an instance of %s.', BASE_EXCEPTION )); - } - - return $this->_report_exception($exc, $extra_data, $payload_data); - } catch (Exception $e) { - try { - $this->log_error("Exception while reporting exception"); - } catch (Exception $e) { - // swallow - } - } - } - - public function report_message($message, $level = Level::ERROR, $extra_data = null, $payload_data = null) { - try { - return $this->_report_message($message, $level, $extra_data, $payload_data); - } catch (Exception $e) { - try { - $this->log_error("Exception while reporting message"); - } catch (Exception $e) { - // swallow - } - } - } - - public function report_php_error($errno, $errstr, $errfile, $errline) { - try { - return $this->_report_php_error($errno, $errstr, $errfile, $errline); - } catch (Exception $e) { - try { - $this->log_error("Exception while reporting php error"); - } catch (Exception $e) { - // swallow - } - } - } - - /** - * Flushes the queue. - * Called internally when the queue exceeds $batch_size, and by Rollbar::flush - * on shutdown. - */ - public function flush() { - $queue_size = $this->queueSize(); - if ($queue_size > 0) { - $this->log_info('Flushing queue of size ' . $queue_size); - $this->send_batch($this->_queue); - $this->_queue = array(); - } - } - - /** - * Returns the current queue size. - */ - public function queueSize() { - return count($this->_queue); - } - - /** - * Run the checkIgnore function and determine whether to send the Exception to the API or not. - * - * @param bool $isUncaught - * @param RollbarException $exception - * @param array $payload Data being sent to the API - * @return bool - */ - protected function _shouldIgnore($isUncaught, RollbarException $exception, array $payload) - { - try { - if (is_callable($this->checkIgnore) - && call_user_func_array($this->checkIgnore, array($isUncaught,$exception,$payload)) - ) { - $this->log_info('This item was not sent to Rollbar because it was ignored. ' - . 'This can happen if a custom checkIgnore() function was used.'); - - return true; - } - } catch (Exception $e) { - // Disable the custom checkIgnore and report errors in the checkIgnore function - $this->checkIgnore = null; - $this->log_error("Removing custom checkIgnore(). Error while calling custom checkIgnore function:\n" - . $e->getMessage()); - } - - return false; - } - - /** - * @param \Throwable|\Exception $exc - * @param mixed $extra_data - * @param mixed$payload_data - * @return string the uuid of the occurrence - */ - protected function _report_exception( $exc, $extra_data = null, $payload_data = null) { - if (!$this->check_config()) { - return; - } - - if (error_reporting() === 0 && !$this->report_suppressed) { - // ignore - return; - } - - $data = $this->build_base_data(); - - $trace_chain = $this->build_exception_trace_chain($exc, $extra_data); - - if (count($trace_chain) > 1) { - $data['body']['trace_chain'] = $trace_chain; - } else { - $data['body']['trace'] = $trace_chain[0]; - } - - // request, server, person data - if ('http' === $this->_php_context) { - $data['request'] = $this->build_request_data(); - } - $data['server'] = $this->build_server_data(); - $data['person'] = $this->build_person_data(); - - // merge $payload_data into $data - // (overriding anything already present) - if ($payload_data !== null && is_array($payload_data)) { - foreach ($payload_data as $key => $val) { - $data[$key] = $val; - } - } - - $data = $this->_sanitize_keys($data); - array_walk_recursive($data, array($this, '_sanitize_utf8')); - - $payload = $this->build_payload($data); - - // Determine whether to send the request to the API. - if ($this->_shouldIgnore(true, new RollbarException($exc->getMessage(), $exc), $payload)) { - return; - } - - $this->send_payload($payload); - - return $data['uuid']; - } - - protected function _sanitize_utf8(&$value) { - if (!$this->enable_utf8_sanitization) - return; - - if (!isset($this->_iconv_available)) { - $this->_iconv_available = function_exists('iconv'); - } - if (is_string($value) && $this->_iconv_available) { - $value = @iconv('UTF-8', 'UTF-8//IGNORE', $value); - } - } - - protected function _sanitize_keys(array $data) { - $response = array(); - foreach ($data as $key => $value) { - $this->_sanitize_utf8($key); - if (is_array($value)) { - $response[$key] = $this->_sanitize_keys($value); - } else { - $response[$key] = $value; - } - } - - return $response; - } - - protected function _report_php_error($errno, $errstr, $errfile, $errline) { - if (!$this->check_config()) { - return; - } - - if (error_reporting() === 0 && !$this->report_suppressed) { - // ignore - return; - } - - if ($this->use_error_reporting && (error_reporting() & $errno) === 0) { - // ignore - return; - } - - if ($this->included_errno != -1 && ($errno & $this->included_errno) != $errno) { - // ignore - return; - } - - if (isset($this->error_sample_rates[$errno])) { - // get a float in the range [0, 1) - // mt_rand() is inclusive, so add 1 to mt_randmax - $float_rand = mt_rand() / ($this->_mt_randmax + 1); - if ($float_rand > $this->error_sample_rates[$errno]) { - // skip - return; - } - } - - $data = $this->build_base_data(); - - // set error level and error constant name - $level = Level::INFO; - $constant = '#' . $errno; - switch ($errno) { - case 1: - $level = Level::ERROR; - $constant = 'E_ERROR'; - break; - case 2: - $level = Level::WARNING; - $constant = 'E_WARNING'; - break; - case 4: - $level = Level::CRITICAL; - $constant = 'E_PARSE'; - break; - case 8: - $level = Level::INFO; - $constant = 'E_NOTICE'; - break; - case 256: - $level = Level::ERROR; - $constant = 'E_USER_ERROR'; - break; - case 512: - $level = Level::WARNING; - $constant = 'E_USER_WARNING'; - break; - case 1024: - $level = Level::INFO; - $constant = 'E_USER_NOTICE'; - break; - case 2048: - $level = Level::INFO; - $constant = 'E_STRICT'; - break; - case 4096: - $level = Level::ERROR; - $constant = 'E_RECOVERABLE_ERROR'; - break; - case 8192: - $level = Level::INFO; - $constant = 'E_DEPRECATED'; - break; - case 16384: - $level = Level::INFO; - $constant = 'E_USER_DEPRECATED'; - break; - } - $data['level'] = $level; - - // use the whole $errstr. may want to split this by colon for better de-duping. - $error_class = $constant . ': ' . $errstr; - - // build something that looks like an exception - $data['body'] = array( - 'trace' => array( - 'frames' => $this->build_error_frames($errfile, $errline), - 'exception' => array( - 'class' => $error_class - ) - ) - ); - - // request, server, person data - $data['request'] = $this->build_request_data(); - $data['server'] = $this->build_server_data(); - $data['person'] = $this->build_person_data(); - - array_walk_recursive($data, array($this, '_sanitize_utf8')); - - $payload = $this->build_payload($data); - - // Determine whether to send the request to the API. - $exception = new ErrorException($error_class, 0, $errno, $errfile, $errline); - if ($this->_shouldIgnore(true, new RollbarException($exception->getMessage(), $exception), $payload)) { - return; - } - - $this->send_payload($payload); - - return $data['uuid']; - } - - protected function _report_message($message, $level, $extra_data, $payload_data) { - if (!$this->check_config()) { - return; - } - - $data = $this->build_base_data(); - $data['level'] = strtolower($level); - - $message_obj = array('body' => $message); - if ($extra_data !== null && is_array($extra_data)) { - // merge keys from $extra_data to $message_obj - foreach ($extra_data as $key => $val) { - if ($key == 'body') { - // rename to 'body_' to avoid clobbering - $key = 'body_'; - } - $message_obj[$key] = $val; - } - } - $data['body']['message'] = $message_obj; - - $data['request'] = $this->build_request_data(); - $data['server'] = $this->build_server_data(); - $data['person'] = $this->build_person_data(); - - // merge $payload_data into $data - // (overriding anything already present) - if ($payload_data !== null && is_array($payload_data)) { - foreach ($payload_data as $key => $val) { - $data[$key] = $val; - } - } - - array_walk_recursive($data, array($this, '_sanitize_utf8')); - - $payload = $this->build_payload($data); - - // Determine whether to send the request to the API. - if ($this->_shouldIgnore(true, new RollbarException($message), $payload)) { - return; - } - - $this->send_payload($payload); - - return $data['uuid']; - } - - protected function check_config() { - return $this->handler == 'agent' || ($this->access_token && strlen($this->access_token) == 32); - } - - protected function build_request_data() { - if ($this->_request_data === null) { - $request = array( - 'url' => $this->scrub_url($this->current_url()), - 'user_ip' => $this->user_ip(), - 'headers' => $this->headers(), - 'method' => isset($_SERVER['REQUEST_METHOD']) ? $_SERVER['REQUEST_METHOD'] : null, - ); - - if ($_GET) { - $request['GET'] = $this->scrub_request_params($_GET); - } - if ($_POST) { - $request['POST'] = $this->scrub_request_params($_POST); - } - if (isset($_SESSION) && $_SESSION) { - $request['session'] = $this->scrub_request_params($_SESSION); - } - $this->_request_data = $request; - } - - return $this->_request_data; - } - - protected function scrub_url($url) { - $url_query = parse_url($url, PHP_URL_QUERY); - if (!$url_query) return $url; - parse_str($url_query, $parsed_output); - // using x since * requires URL-encoding - $scrubbed_params = $this->scrub_request_params($parsed_output, 'x'); - $scrubbed_url = str_replace($url_query, http_build_query($scrubbed_params), $url); - return $scrubbed_url; - } - - protected function scrub_request_params($params, $replacement = '*') { - $scrubbed = array(); - $potential_regex_filters = array_filter($this->scrub_fields, function($field) { - return strpos($field, '/') === 0; - }); - foreach ($params as $k => $v) { - if ($this->_key_should_be_scrubbed($k, $potential_regex_filters)) { - $scrubbed[$k] = $this->_scrub($v, $replacement); - } elseif (is_array($v)) { - // recursively handle array params - $scrubbed[$k] = $this->scrub_request_params($v, $replacement); - } else { - $scrubbed[$k] = $v; - } - } - - return $scrubbed; - } - - protected function _key_should_be_scrubbed($key, $potential_regex_filters) { - if (in_array(strtolower($key), $this->scrub_fields, true)) return true; - foreach ($potential_regex_filters as $potential_regex) { - if (@preg_match($potential_regex, $key)) return true; - } - return false; - } - - protected function _scrub($value, $replacement = '*') { - $count = is_array($value) ? count($value) : strlen($value); - return str_repeat($replacement, $count); - } - - protected function headers() { - $headers = array(); - foreach ($this->scrub_request_params($_SERVER) as $key => $val) { - if (substr($key, 0, 5) == 'HTTP_') { - // convert HTTP_CONTENT_TYPE to Content-Type, HTTP_HOST to Host, etc. - $name = strtolower(substr($key, 5)); - if (strpos($name, '_') != -1) { - $name = preg_replace('/ /', '-', ucwords(preg_replace('/_/', ' ', $name))); - } else { - $name = ucfirst($name); - } - $headers[$name] = $val; - } - } - - if (count($headers) > 0) { - return $headers; - } else { - // serializes to emtpy json object - return new stdClass; - } - } - - protected function current_url() { - if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])) { - $proto = strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']); - } else if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') { - $proto = 'https'; - } else { - $proto = 'http'; - } - - if (!empty($_SERVER['HTTP_X_FORWARDED_HOST'])) { - $host = $_SERVER['HTTP_X_FORWARDED_HOST']; - } else if (!empty($_SERVER['HTTP_HOST'])) { - $parts = explode(':', $_SERVER['HTTP_HOST']); - $host = $parts[0]; - } else if (!empty($_SERVER['SERVER_NAME'])) { - $host = $_SERVER['SERVER_NAME']; - } else { - $host = 'unknown'; - } - - if (!empty($_SERVER['HTTP_X_FORWARDED_PORT'])) { - $port = $_SERVER['HTTP_X_FORWARDED_PORT']; - } else if (!empty($_SERVER['SERVER_PORT'])) { - $port = $_SERVER['SERVER_PORT']; - } else if ($proto === 'https') { - $port = 443; - } else { - $port = 80; - } - - $path = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '/'; - - $url = $proto . '://' . $host; - - if (($proto == 'https' && $port != 443) || ($proto == 'http' && $port != 80)) { - $url .= ':' . $port; - } - - $url .= $path; - - return $url; - } - - protected function user_ip() { - $forwardfor = isset($_SERVER['HTTP_X_FORWARDED_FOR']) ? $_SERVER['HTTP_X_FORWARDED_FOR'] : null; - if ($forwardfor) { - // return everything until the first comma - $parts = explode(',', $forwardfor); - return $parts[0]; - } - $realip = isset($_SERVER['HTTP_X_REAL_IP']) ? $_SERVER['HTTP_X_REAL_IP'] : null; - if ($realip) { - return $realip; - } - return isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : null; - } - - /** - * @param \Throwable|\Exception $exc - * @param mixed $extra_data - * @return array - */ - protected function build_exception_trace($exc, $extra_data = null) - { - $message = $exc->getMessage(); - - $trace = array( - 'frames' => $this->build_exception_frames($exc), - 'exception' => array( - 'class' => get_class($exc), - 'message' => !empty($message) ? $message : 'unknown', - ), - ); - - if ($extra_data !== null) { - $trace['extra'] = $extra_data; - } - - return $trace; - } - - /** - * @param \Throwable|\Exception $exc - * @param array $extra_data - * @return array - */ - protected function build_exception_trace_chain( $exc, $extra_data = null) - { - $chain = array(); - $chain[] = $this->build_exception_trace($exc, $extra_data); - - $previous = $exc->getPrevious(); - - while ( is_a( $previous, BASE_EXCEPTION ) ) { - $chain[] = $this->build_exception_trace($previous); - $previous = $previous->getPrevious(); - } - - return $chain; - } - - /** - * @param \Throwable|\Exception $exc - * @return array - */ - protected function build_exception_frames($exc) { - $frames = array(); - - foreach ($exc->getTrace() as $frame) { - $framedata = array( - 'filename' => isset($frame['file']) ? $frame['file'] : '', - 'lineno' => isset($frame['line']) ? $frame['line'] : 0, - 'method' => $frame['function'] - // TODO include args? need to sanitize first. - ); - if($this->include_exception_code_context && isset($frame['file']) && isset($frame['line'])) { - $this->add_frame_code_context($frame['file'], $frame['line'], $framedata); - } - $frames[] = $framedata; - } - - // rollbar expects most recent call to be last, not first - $frames = array_reverse($frames); - - // add top-level file and line to end of the reversed array - $file = $exc->getFile(); - $line = $exc->getLine(); - $framedata = array( - 'filename' => $file, - 'lineno' => $line - ); - if($this->include_exception_code_context) { - $this->add_frame_code_context($file, $line, $framedata); - } - $frames[] = $framedata; - - $this->shift_method($frames); - - return $frames; - } - - protected function shift_method(&$frames) { - if ($this->shift_function) { - // shift 'method' values down one frame, so they reflect where the call - // occurs (like Rollbar expects), instead of what is being called. - for ($i = count($frames) - 1; $i > 0; $i--) { - $frames[$i]['method'] = $frames[$i - 1]['method']; - } - $frames[0]['method'] = '
'; - } - } - - protected function build_error_frames($errfile, $errline) { - if ($this->capture_error_backtraces) { - $frames = array(); - $backtrace = debug_backtrace(); - foreach ($backtrace as $frame) { - // skip frames in this file - if (isset($frame['file']) && $frame['file'] == __FILE__) { - continue; - } - // skip the confusing set_error_handler frame - if ($frame['function'] == 'report_php_error' && count($frames) == 0) { - continue; - } - - $framedata = array( - // Sometimes, file and line are not set. See: - // http://stackoverflow.com/questions/4581969/why-is-debug-backtrace-not-including-line-number-sometimes - 'filename' => isset($frame['file']) ? $frame['file'] : "", - 'lineno' => isset($frame['line']) ? $frame['line'] : 0, - 'method' => $frame['function'] - ); - if($this->include_error_code_context && isset($frame['file']) && isset($frame['line'])) { - $this->add_frame_code_context($frame['file'], $frame['line'], $framedata); - } - $frames[] = $framedata; - } - - // rollbar expects most recent call last, not first - $frames = array_reverse($frames); - - // add top-level file and line to end of the reversed array - $framedata = array( - 'filename' => $errfile, - 'lineno' => $errline - ); - if($this->include_error_code_context) { - $this->add_frame_code_context($errfile, $errline, $framedata); - } - $frames[] = $framedata; - - $this->shift_method($frames); - - return $frames; - } else { - return array( - array( - 'filename' => $errfile, - 'lineno' => $errline - ) - ); - } - } - - protected function build_server_data() { - if ($this->_server_data === null) { - $server_data = array(); - - if ($this->host === null) { - // PHP 5.3.0 - if (function_exists('gethostname')) { - $this->host = gethostname(); - } else { - $this->host = php_uname('n'); - } - } - $server_data['host'] = $this->host; - $server_data['argv'] = isset($_SERVER['argv']) ? $_SERVER['argv'] : null; - - if ($this->branch) { - $server_data['branch'] = $this->branch; - } - if ($this->root) { - $server_data['root'] = $this->root; - } - $this->_server_data = $server_data; - } - return $this->_server_data; - } - - protected function build_person_data() { - // return cached value if non-null - // it *is* possible for it to really be null (i.e. user is not logged in) - // but we'll keep trying anyway until we get a logged-in user value. - if ($this->_person_data == null) { - // first priority: try to use $this->person - if ($this->person && is_array($this->person)) { - if (isset($this->person['id'])) { - $this->_person_data = $this->person; - return $this->_person_data; - } - } - - // second priority: try to use $this->person_fn - if ($this->person_fn && is_callable($this->person_fn)) { - $data = @call_user_func($this->person_fn); - if (isset($data['id'])) { - $this->_person_data = $data; - return $this->_person_data; - } - } - } else { - return $this->_person_data; - } - - return null; - } - - protected function build_base_data($level = Level::ERROR) { - if (null === $this->_php_context) { - $this->_php_context = $this->get_php_context(); - } - - $data = array( - 'timestamp' => time(), - 'environment' => $this->environment, - 'level' => $level, - 'language' => 'php', - 'framework' => 'php', - 'php_context' => $this->_php_context, - 'notifier' => array( - 'name' => 'rollbar-php', - 'version' => self::VERSION - ), - 'uuid' => $this->uuid4() - ); - - if ($this->code_version) { - $data['code_version'] = $this->code_version; - } - - return $data; - } - - protected function build_payload($data) { - $payload = array( - 'data' => $data - ); - - if ($this->access_token) { - $payload['access_token'] = $this->access_token; - } - - return $payload; - } - - protected function send_payload($payload) { - if ($this->batched) { - if ($this->queueSize() >= $this->batch_size) { - // flush queue before adding payload to queue - $this->flush(); - } - $this->_queue[] = $payload; - } else { - $this->_send_payload($payload); - } - } - - /** - * Sends a single payload to the /item endpoint. - * $payload - php array - */ - protected function _send_payload($payload) { - if ($this->handler == 'agent') { - $this->_send_payload_agent($payload); - } else { - $this->_send_payload_blocking($payload); - } - } - - protected function _send_payload_blocking($payload) { - $this->log_info("Sending payload"); - $access_token = $payload['access_token']; - $post_data = json_encode($payload); - $this->make_api_call('item', $access_token, $post_data); - } - - protected function _send_payload_agent($payload) { - // Only open this the first time - if (empty($this->_agent_log)) { - $this->load_agent_file(); - } - $this->log_info("Writing payload to file"); - fwrite($this->_agent_log, json_encode($payload) . "\n"); - } - - /** - * Sends a batch of payloads to the /batch endpoint. - * A batch is just an array of standalone payloads. - * $batch - php array of payloads - */ - protected function send_batch($batch) { - if ($this->handler == 'agent') { - $this->send_batch_agent($batch); - } else { - $this->send_batch_blocking($batch); - } - } - - protected function send_batch_agent($batch) { - $this->log_info("Writing batch to file"); - - // Only open this the first time - if (empty($this->_agent_log)) { - $this->load_agent_file(); - } - - foreach ($batch as $item) { - fwrite($this->_agent_log, json_encode($item) . "\n"); - } - } - - protected function send_batch_blocking($batch) { - $this->log_info("Sending batch"); - $access_token = $batch[0]['access_token']; - $post_data = json_encode($batch); - $this->make_api_call('item_batch', $access_token, $post_data); - } - - protected function get_php_context() { - return php_sapi_name() === 'cli' || defined('STDIN') ? 'cli' : 'http'; - } - - protected function make_api_call($action, $access_token, $post_data) { - $url = $this->base_api_url . $action . '/'; - - $ch = curl_init(); - curl_setopt($ch, CURLOPT_URL, $url); - curl_setopt($ch, CURLOPT_POST, true); - curl_setopt($ch, CURLOPT_POSTFIELDS, $post_data); - curl_setopt($ch, CURLOPT_VERBOSE, false); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_TIMEOUT, $this->timeout); - curl_setopt($ch, CURLOPT_HTTPHEADER, array('X-Rollbar-Access-Token: ' . $access_token)); - - if ($this->proxy) { - $proxy = is_array($this->proxy) ? $this->proxy : array('address' => $this->proxy); - - if (isset($proxy['address'])) { - curl_setopt($ch, CURLOPT_PROXY, $proxy['address']); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - } - - if (isset($proxy['username']) && isset($proxy['password'])) { - curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxy['username'] . ':' . $proxy['password']); - } - } - - if ($this->_curl_ipresolve_supported) { - curl_setopt($ch, CURLOPT_IPRESOLVE, CURL_IPRESOLVE_V4); - } - - $result = curl_exec($ch); - $status_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); - curl_close($ch); - - if ($status_code != 200) { - $this->log_warning('Got unexpected status code from Rollbar API ' . $action . - ': ' .$status_code); - $this->log_warning('Output: ' .$result); - } else { - $this->log_info('Success'); - } - } - - /* Logging */ - - protected function log_info($msg) { - $this->log_message("INFO", $msg); - } - - protected function log_warning($msg) { - $this->log_message("WARNING", $msg); - } - - protected function log_error($msg) { - $this->log_message("ERROR", $msg); - } - - protected function log_message($level, $msg) { - if ($this->logger !== null) { - $this->logger->log($level, $msg); - } - } - - // from http://www.php.net/manual/en/function.uniqid.php#94959 - protected function uuid4() { - mt_srand(); - return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x', - // 32 bits for "time_low" - mt_rand(0, 0xffff), mt_rand(0, 0xffff), - - // 16 bits for "time_mid" - mt_rand(0, 0xffff), - - // 16 bits for "time_hi_and_version", - // four most significant bits holds version number 4 - mt_rand(0, 0x0fff) | 0x4000, - - // 16 bits, 8 bits for "clk_seq_hi_res", - // 8 bits for "clk_seq_low", - // two most significant bits holds zero and one for variant DCE1.1 - mt_rand(0, 0x3fff) | 0x8000, - - // 48 bits for "node" - mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff) - ); - } - - protected function load_agent_file() { - $this->_agent_log = fopen($this->agent_log_location . '/rollbar-relay.' . getmypid() . '.' . microtime(true) . '.rollbar', 'a'); - } - - protected function add_frame_code_context($file, $line, array &$framedata) { - $source = $this->get_source_file_reader()->read_as_array($file); - if (is_array($source)) { - $source = str_replace(array("\n", "\t", "\r"), '', $source); - $total = count($source); - $line = $line - 1; - $framedata['code'] = $source[$line]; - $offset = 6; - $min = max($line - $offset, 0); - if ($min !== $line) { - $framedata['context']['pre'] = array_slice($source, $min, $line - $min); - } - $max = min($line + $offset, $total); - if ($max !== $line) { - $framedata['context']['post'] = array_slice($source, $line + 1, $max - $line); - } - } - } - - protected function get_source_file_reader() { return $this->_source_file_reader; } -} - -interface iRollbarLogger { - public function log($level, $msg); -} - -class Ratchetio extends Rollbar {} - -interface iSourceFileReader { - - /** - * @param string $file_path - * @return string[] - */ - public function read_as_array($file_path); -} - -class SourceFileReader implements iSourceFileReader { - - public function read_as_array($file_path) { return file($file_path); } -} diff --git a/tests/AgentTest.php b/tests/AgentTest.php new file mode 100644 index 00000000..4d3a153b --- /dev/null +++ b/tests/AgentTest.php @@ -0,0 +1,62 @@ +path)) { + mkdir($this->path); + } + } + + public function testAgent() + { + Rollbar\Rollbar::init(array( + 'access_token' => 'ad865e76e7fb496fab096ac07b1dbabb', + 'environment' => 'testing', + 'agent_log_location' => $this->path, + 'handler' => 'agent' + ), false, false, false); + $logger = Rollbar\Rollbar::logger(); + $logger->info("this is a test"); + $file = fopen($this->path . '/rollbar-relay.' . getmypid() . '.' . microtime(true) . '.rollbar', 'r'); + $line = fgets($file); + $this->assertContains('this is a test', $line); + } + + protected function tearDown() + { + $this->rrmdir($this->path); + } + + private function rrmdir($dir) + { + if (!is_dir($dir)) { + return; + } + + $objects = scandir($dir); + foreach ($objects as $object) { + if ($object != "." && $object != "..") { + if (filetype($dir . "/" . $object) == "dir") { + $this->rrmdir($dir . "/" . $object); + } else { + unlink($dir . "/" . $object); + } + } + } + reset($objects); + rmdir($dir); + } +} diff --git a/tests/BackwardsCompatibilityConfigTest.php b/tests/BackwardsCompatibilityConfigTest.php new file mode 100644 index 00000000..8d4f9735 --- /dev/null +++ b/tests/BackwardsCompatibilityConfigTest.php @@ -0,0 +1,64 @@ + 'ad865e76e7fb496fab096ac07b1dbabb', + 'agent_log_location' => '/var/log/rollbar-php', + 'base_api_url' => 'http://dev:8090/api/1/', + 'batch_size' => 50, + 'batched' => true, + 'branch' => 'other', + 'capture_error_stacktraces' => true, + 'checkIgnore' => function ($isUncaught, $exception, $payload) { + $check = isset($_SERVER['HTTP_USER_AGENT']) && + strpos($_SERVER['HTTP_USER_AGENT'], 'Baiduspider') !== false; + if ($check) { + // ignore baidu spider + return true; + } + + // no other ignores + return false; + }, + 'code_version' => '1.2.3', + 'environment' => 'production', + 'error_sample_rates' => array( + E_WARNING => 0.5, + E_ERROR => 1 + ), + 'handler' => 'blocking', + 'host' => 'my_host', + 'include_error_code_context' => true, + 'included_errno' => E_ERROR | E_WARNING, + 'logger' => new FakeLog(), + 'person' => array( + 'id' => 1, + 'username' => 'test-user', + 'email' => 'test@rollbar.com' + ), + 'person_fn' => function () { + return array( + 'id' => 1, + 'username' => 'test-user', + 'email' => 'test@rollbar.com' + ); + }, + 'root' => '/Users/brian/www/app', + 'scrub_fields' => array('test'), + 'shift_function' => false, + 'timeout' => 10, + 'report_suppressed' => true, + 'use_error_reporting' => true, + 'proxy' => array( + 'address' => '127.0.0.1:8080', + 'username' => 'my_user', + 'password' => 'my_password' + ) + )); + } +} diff --git a/tests/BodyTest.php b/tests/BodyTest.php new file mode 100644 index 00000000..d30686c1 --- /dev/null +++ b/tests/BodyTest.php @@ -0,0 +1,30 @@ +assertEquals($value, $body->getValue()); + + $mock2 = m::mock("Rollbar\Payload\ContentInterface"); + $this->assertEquals($mock2, $body->setValue($mock2)->getValue()); + } + + public function testEncode() + { + $value = m::mock("Rollbar\Payload\ContentInterface") + ->shouldReceive("jsonSerialize") + ->andReturn("{CONTENT}") + ->shouldReceive("getKey") + ->andReturn("content_interface") + ->mock(); + $body = new Body($value); + $encoded = json_encode($body->jsonSerialize()); + $this->assertEquals("{\"content_interface\":\"{CONTENT}\"}", $encoded); + } +} diff --git a/tests/ConfigTest.php b/tests/ConfigTest.php new file mode 100644 index 00000000..8200b8ea --- /dev/null +++ b/tests/ConfigTest.php @@ -0,0 +1,226 @@ +error = new ErrorWrapper(E_ERROR, "test", null, null, null); + } + + public function tearDown() + { + m::close(); + } + + private $token = "abcd1234efef5678abcd1234567890be"; + private $env = "rollbar-php-testing"; + + public function testAccessToken() + { + $config = new Config(array( + 'access_token' => $this->token, + 'environment' => $this->env + )); + $this->assertEquals($this->token, $config->getAccessToken()); + } + + public function testAccessTokenFromEnvironment() + { + $_ENV['ROLLBAR_ACCESS_TOKEN'] = $this->token; + $config = new Config(array( + 'environment' => 'testing' + )); + $this->assertEquals($this->token, $config->getAccessToken()); + } + + public function testDataBuilder() + { + $arr = array( + "access_token" => $this->token, + "environment" => $this->env, + "dataBuilder" => "Rollbar\FakeDataBuilder", + "dataBuilderOptions" => array("options") + ); + $config = new Config($arr); + $this->assertEquals($arr, array_pop(FakeDataBuilder::$args)); + } + + public function testExtend() + { + $arr = array( + "access_token" => $this->token, + "environment" => $this->env + ); + $config = new Config($arr); + $extended = $config->extend(array("one" => 1, "arr" => array())); + $expected = array( + "access_token" => $this->token, + "environment" => $this->env, + "one" => 1, + "arr" => array() + ); + $this->assertEquals($expected, $extended); + } + + public function testConfigure() + { + $arr = array( + "access_token" => $this->token, + "environment" => $this->env + ); + $config = new Config($arr); + $config->configure(array("one" => 1, "arr" => array())); + $expected = array( + "access_token" => $this->token, + "environment" => $this->env, + "one" => 1, + "arr" => array() + ); + $this->assertEquals($expected, $config->getConfigArray()); + } + + public function testExplicitDataBuilder() + { + $fdb = new FakeDataBuilder(array()); + $arr = array( + "access_token" => $this->token, + "environment" => $this->env, + "dataBuilder" => $fdb + ); + $config = new Config($arr); + $expected = array(LogLevel::EMERGENCY, "oops", array()); + $config->getRollbarData($expected[0], $expected[1], $expected[2]); + $this->assertEquals($expected, array_pop(FakeDataBuilder::$logged)); + } + + public function testTransformer() + { + $p = m::mock("Rollbar\Payload\Payload"); + $pPrime = m::mock("Rollbar\Payload\Payload"); + $transformer = m::mock("Rollbar\TransformerInterface"); + $transformer->shouldReceive('transform') + ->once() + ->with($p, "error", "message", "extra_data") + ->andReturn($pPrime) + ->mock(); + $config = new Config(array( + "access_token" => $this->token, + "environment" => $this->env, + "transformer" => $transformer + )); + $this->assertEquals($pPrime, $config->transform($p, "error", "message", "extra_data")); + } + + public function testMinimumLevel() + { + $c = new Config(array( + "access_token" => $this->token, + "environment" => $this->env, + "minimumLevel" => "warning" + )); + $this->runConfigTest($c); + + $c->configure(array("minimumLevel" => Level::WARNING())); + $this->runConfigTest($c); + + $c->configure(array("minimumLevel" => Level::WARNING()->toInt())); + $this->runConfigTest($c); + } + + private function runConfigTest($config) + { + $debugData = m::mock("Rollbar\Payload\Data") + ->shouldReceive('getLevel') + ->andReturn(Level::DEBUG()) + ->mock(); + $debug = new Payload($debugData, $this->token); + $this->assertTrue($config->checkIgnored($debug, null, $this->error)); + + $criticalData = m::mock("Rollbar\Payload\Data") + ->shouldReceive('getLevel') + ->andReturn(Level::CRITICAL()) + ->mock(); + $critical = new Payload($criticalData, $this->token); + $this->assertFalse($config->checkIgnored($critical, null, $this->error)); + + $warningData = m::mock("Rollbar\Payload\Data") + ->shouldReceive('getLevel') + ->andReturn(Level::warning()) + ->mock(); + $warning = new Payload($warningData, $this->token); + $this->assertFalse($config->checkIgnored($warning, null, $this->error)); + } + + public function testReportSuppressed() + { + $this->assertTrue(true, "Don't know how to unit test this. PRs welcome"); + } + + public function testFilter() + { + $d = m::mock("Rollbar\Payload\Data") + ->shouldReceive("getLevel") + ->andReturn(Level::CRITICAL()) + ->mock(); + $p = m::mock("Rollbar\Payload\Payload") + ->shouldReceive("getData") + ->andReturn($d) + ->mock(); + $filter = m::mock("Rollbar\FilterInterface") + ->shouldReceive("shouldSend") + ->twice() + ->andReturn(true, false) + ->mock(); + $c = new Config(array( + "access_token" => $this->token, + "environment" => $this->env, + "filter" => $filter + )); + $this->assertTrue($c->checkIgnored($p, "fake_access_token", $this->error)); + $this->assertFalse($c->checkIgnored($p, "fake_access_token", $this->error)); + } + + public function testSender() + { + $p = m::mock("Rollbar\Payload\Payload"); + $sender = m::mock("Rollbar\Senders\SenderInterface") + ->shouldReceive("send") + ->with($p, $this->token) + ->once() + ->mock(); + $c = new Config(array( + "access_token" => $this->token, + "environment" => $this->env, + "sender" => $sender + )); + $c->send($p, $this->token); + } + + public function testCheckIgnore() + { + $called = false; + $c = new Config(array( + "access_token" => $this->token, + "environment" => $this->env, + "checkIgnore" => function () use (&$called) { + $called = true; + } + )); + $data = new Data($this->env, new Body(new Message("test"))); + $data->setLevel(Level::fromName('error')); + $c->checkIgnored(new Payload($data, $this->token), $this->token, $this->error); + + $this->assertTrue($called); + } +} diff --git a/tests/ContextTest.php b/tests/ContextTest.php new file mode 100644 index 00000000..51e2dce4 --- /dev/null +++ b/tests/ContextTest.php @@ -0,0 +1,38 @@ +assertEquals($pre, $context->getPre()); + + $pre2 = array("lineone", "linetwo"); + $this->assertEquals($pre2, $context->setPre($pre2)->getPre()); + } + + public function testContextPost() + { + $post = array("four", "five"); + $context = new Context(array(), $post); + $this->assertEquals($post, $context->getPost()); + + $post2 = array("six", "seven", "eight"); + $this->assertEquals($post2, $context->setPost($post2)->getPost()); + } + + public function testEncode() + { + $context = new Context(array(), array()); + $encoded = json_encode($context->jsonSerialize()); + $this->assertEquals('{"pre":[],"post":[]}', $encoded); + + $context = new Context(array("one"), array("three")); + $encoded = json_encode($context->jsonSerialize()); + $this->assertEquals('{"pre":["one"],"post":["three"]}', $encoded); + } +} diff --git a/tests/DataBuilderTest.php b/tests/DataBuilderTest.php new file mode 100644 index 00000000..aed98af3 --- /dev/null +++ b/tests/DataBuilderTest.php @@ -0,0 +1,164 @@ +dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests' + )); + } + + public function testMakeData() + { + $output = $this->dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $this->assertEquals('tests', $output->getEnvironment()); + } + + public function testBranchKey() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'branch' => 'test-branch' + )); + + $output = $dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $this->assertEquals('test-branch', $output->getServer()->getBranch()); + } + + public function testCodeVersion() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'code_version' => '3.4.1' + )); + $output = $dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $this->assertEquals('3.4.1', $output->getCodeVersion()); + } + + public function testHost() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'host' => 'my host' + )); + $output = $dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $this->assertEquals('my host', $output->getServer()->getHost()); + } + + public function testFramesWithoutContext() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'include_error_code_context' => false + )); + $output = $dataBuilder->makeFrames(new \Exception()); + $this->assertNull($output[0]->getContext()); + } + + public function testFramesWithContext() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'include_error_code_context' => true + )); + $backTrace = array( + array( + 'file' => __DIR__ . '/DataBuilderTest.php', + 'line' => 68, + 'function' => 'testFramesWithoutContext' + ), + array( + 'file' => __DIR__ . '/DataBuilderTest.php', + 'line' => 79, + 'function' => 'testFramesWithContext' + ), + ); + $output = $dataBuilder->makeFrames(new ErrorWrapper(null, null, null, null, $backTrace)); + $pre = $output[0]->getContext()->getPre(); + $expected = array( + ' \'host\' => \'my host\'', + ' ));', + ' $output = $dataBuilder->makeData(Level::fromName(\'error\'), "testing", array());', + ' $this->assertEquals(\'my host\', $output->getServer()->getHost());', + ' }', + '' + ); + $this->assertEquals($expected, $pre); + } + + public function testPerson() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'person' => array( + 'id' => '123', + 'email' => 'test@test.com' + ) + )); + $output = $dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $this->assertEquals('test@test.com', $output->getPerson()->getEmail()); + } + + public function testPersonFunc() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'person_fn' => function () { + return array( + 'id' => '123', + 'email' => 'test@test.com' + ); + } + )); + $output = $dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $this->assertEquals('test@test.com', $output->getPerson()->getEmail()); + } + + public function testRoot() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'root' => '/var/www/app' + )); + $output = $dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $this->assertEquals('/var/www/app', $output->getServer()->getRoot()); + } + + public function testScrubFields() + { + $dataBuilder = new DataBuilder(array( + 'accessToken' => 'abcd1234efef5678abcd1234567890be', + 'environment' => 'tests', + 'scrub_fields' => array('test') + )); + $_POST['test'] = 'blah'; + $output = $dataBuilder->makeData(Level::fromName('error'), "testing", array()); + $post = $output->getRequest()->getPost(); + $this->assertEquals('********', $post['test']); + } +} diff --git a/tests/DataTest.php b/tests/DataTest.php new file mode 100644 index 00000000..78ef8d7d --- /dev/null +++ b/tests/DataTest.php @@ -0,0 +1,202 @@ +body = m::mock("Rollbar\Payload\Body"); + $this->data = new Data("test", $this->body); + } + + public function testEnvironmentMustBeString() + { + try { + $data = new Data(1, $this->body); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertContains("must be a string", $e->getMessage()); + } + + try { + $data = new Data(null, $this->body); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertContains("must not be null", $e->getMessage()); + } + + $data = new Data("env", $this->body); + $this->assertEquals("env", $data->getEnvironment()); + + $this->assertEquals("test", $data->setEnvironment("test")->getEnvironment()); + } + + public function testBody() + { + $data = new Data("env", $this->body); + $this->assertEquals($this->body, $data->getBody()); + + $body2 = m::mock("Rollbar\Payload\Body"); + $this->assertEquals($body2, $data->setBody($body2)->getBody()); + } + + public function testLevel() + { + $level = m::mock("Rollbar\Payload\Level"); + $this->assertEquals($level, $this->data->setLevel($level)->getLevel()); + } + + public function testTimestamp() + { + $timestamp = time(); + $this->assertEquals($timestamp, $this->data->setTimestamp($timestamp)->getTimestamp()); + } + + public function testCodeVersion() + { + $codeVersion = "v0.18.1"; + $this->assertEquals($codeVersion, $this->data->setCodeVersion($codeVersion)->getCodeVersion()); + } + + public function testPlatform() + { + $platform = "Linux"; + $this->assertEquals($platform, $this->data->setPlatform($platform)->getPlatform()); + } + + public function testLanguage() + { + $language = "PHP"; + $this->assertEquals($language, $this->data->setLanguage($language)->getLanguage()); + } + + public function testFramework() + { + $framework = "Laravel"; + $this->assertEquals($framework, $this->data->setFramework($framework)->getFramework()); + } + + public function testContext() + { + $context = "SuperController->getResource()"; + $this->assertEquals($context, $this->data->setContext($context)->getContext()); + } + + public function testRequest() + { + $request = m::mock("Rollbar\Payload\Request"); + $this->assertEquals($request, $this->data->setRequest($request)->getRequest()); + } + + public function testPerson() + { + $person = m::mock("Rollbar\Payload\Person"); + ; + $this->assertEquals($person, $this->data->setPerson($person)->getPerson()); + } + + public function testServer() + { + $server = m::mock("Rollbar\Payload\Server"); + $this->assertEquals($server, $this->data->setServer($server)->getServer()); + } + + public function testCustom() + { + $custom = array( + "x" => 1, + "y" => 2, + "z" => 3, + ); + $this->assertEquals($custom, $this->data->setCustom($custom)->getCustom()); + } + + public function testFingerprint() + { + $fingerprint = "bad-error-with-database"; + $this->assertEquals($fingerprint, $this->data->setFingerprint($fingerprint)->getFingerprint()); + } + + public function testTitle() + { + $title = "End of the World as we know it"; + $this->assertEquals($title, $this->data->setTitle($title)->getTitle()); + } + + public function testUuid() + { + $uuid = "21EC2020-3AEA-4069-A2DD-08002B30309D"; + $this->assertEquals($uuid, $this->data->setUuid($uuid)->getUuid()); + } + + public function testNotifier() + { + $notifier = m::mock("Rollbar\Payload\Notifier"); + $this->assertEquals($notifier, $this->data->setNotifier($notifier)->getNotifier()); + } + + public function testEncode() + { + $time = time(); + $level = $this->mockSerialize("Rollbar\Payload\Level", "{LEVEL}"); + $body = $this->mockSerialize($this->body, "{BODY}"); + $request = $this->mockSerialize("Rollbar\Payload\Request", "{REQUEST}"); + $person = $this->mockSerialize("Rollbar\Payload\Person", "{PERSON}"); + $server = $this->mockSerialize("Rollbar\Payload\Server", "{SERVER}"); + $notifier = $this->mockSerialize("Rollbar\Payload\Notifier", "{NOTIFIER}"); + + $data = $this->data + ->setEnvironment("testing") + ->setBody($body) + ->setLevel($level) + ->setTimestamp($time) + ->setCodeVersion("v0.17.3") + ->setPlatform("LAMP") + ->setLanguage("PHP 5.4") + ->setFramework("CakePHP") + ->setContext("AppController->updatePerson") + ->setRequest($request) + ->setPerson($person) + ->setServer($server) + ->setCustom(array("x" => "hello", "extra" => new \ArrayObject())) + ->setFingerprint("big-fingerprint") + ->setTitle("The Title") + ->setUuid("123e4567-e89b-12d3-a456-426655440000") + ->setNotifier($notifier); + + $encoded = json_encode($data->jsonSerialize()); + + $this->assertContains("\"environment\":\"testing\"", $encoded); + $this->assertContains("\"body\":\"{BODY}\"", $encoded); + $this->assertContains("\"level\":\"{LEVEL}\"", $encoded); + $this->assertContains("\"timestamp\":$time", $encoded); + $this->assertContains("\"code_version\":\"v0.17.3\"", $encoded); + $this->assertContains("\"platform\":\"LAMP\"", $encoded); + $this->assertContains("\"language\":\"PHP 5.4\"", $encoded); + $this->assertContains("\"framework\":\"CakePHP\"", $encoded); + $this->assertContains("\"context\":\"AppController->updatePerson\"", $encoded); + $this->assertContains("\"request\":\"{REQUEST}\"", $encoded); + $this->assertContains("\"person\":\"{PERSON}\"", $encoded); + $this->assertContains("\"server\":\"{SERVER}\"", $encoded); + $this->assertContains("\"custom\":{\"x\":\"hello\",\"extra\":{}}", $encoded); + $this->assertContains("\"fingerprint\":\"big-fingerprint\"", $encoded); + $this->assertContains("\"title\":\"The Title\"", $encoded); + $this->assertContains("\"uuid\":\"123e4567-e89b-12d3-a456-426655440000\"", $encoded); + $this->assertContains("\"notifier\":\"{NOTIFIER}\"", $encoded); + } + + private function mockSerialize($mock, $returnVal) + { + if (is_string($mock)) { + $mock = m::mock("$mock, \JsonSerializable"); + } + return $mock->shouldReceive("jsonSerialize") + ->andReturn($returnVal) + ->mock(); + } +} diff --git a/tests/DefaultsTest.php b/tests/DefaultsTest.php new file mode 100644 index 00000000..71c2258b --- /dev/null +++ b/tests/DefaultsTest.php @@ -0,0 +1,137 @@ +d = new Defaults(); + } + + public function testGet() + { + $d = Defaults::get(); + $this->assertInstanceOf("Rollbar\Defaults", $d); + } + + public function testMessageLevel() + { + $this->assertEquals("warning", $this->d->messageLevel()); + $this->assertEquals("error", $this->d->messageLevel(Level::ERROR())); + } + + public function testExceptionLevel() + { + $this->assertEquals("error", $this->d->exceptionLevel()); + $this->assertEquals("warning", $this->d->exceptionLevel(Level::Warning())); + } + + public function testErrorLevels() + { + $expected = array( + E_ERROR => "error", + E_WARNING => "warning", + E_PARSE => "critical", + E_NOTICE => "debug", + E_CORE_ERROR => "critical", + E_CORE_WARNING => "warning", + E_COMPILE_ERROR => "critical", + E_COMPILE_WARNING => "warning", + E_USER_ERROR => "error", + E_USER_WARNING => "warning", + E_USER_NOTICE => "debug", + E_STRICT => "info", + E_RECOVERABLE_ERROR => "error", + E_DEPRECATED => "info", + E_USER_DEPRECATED => "info" + ); + $this->assertEquals($expected, $this->d->errorLevels()); + } + + public function testPsrLevels() + { + $expected = $this->defaultPsrLevels = array( + LogLevel::EMERGENCY => "critical", + "emergency" => "critical", + LogLevel::ALERT => "critical", + "alert" => "critical", + LogLevel::CRITICAL => "critical", + "critical" => "critical", + LogLevel::ERROR => "error", + "error" => "error", + LogLevel::WARNING => "warning", + "warning" => "warning", + LogLevel::NOTICE => "info", + "notice" => "info", + LogLevel::INFO => "info", + "info" => "info", + LogLevel::DEBUG => "debug", + "debug" => "debug" + ); + $this->assertEquals($expected, $this->d->psrLevels()); + } + + public function testGitHash() + { + $val = exec('git rev-parse --verify HEAD'); + $this->assertEquals($val, $this->d->gitHash()); + } + + public function testGitBranch() + { + $val = exec('git rev-parse --abbrev-ref HEAD'); + $this->assertEquals($val, $this->d->gitBranch()); + } + + public function testServerRoot() + { + $_ENV["HEROKU_APP_DIR"] = "abc123"; + $d = new Defaults(); + $this->assertEquals("abc123", $d->serverRoot()); + } + + public function testPlatform() + { + $this->assertEquals(php_uname('a'), $this->d->platform()); + } + + public function testNotifier() + { + $this->assertEquals(Notifier::defaultNotifier(), $this->d->notifier()); + } + + public function testBaseException() + { + if (version_compare(phpversion(), '7.0', '<')) { + $expected = "\Exception"; + } else { + $expected = "\Throwable"; + } + $base = $this->d->baseException(); + $this->assertEquals($expected, $base); + } + + public function testScrubFields() + { + $expected = array( + 'passwd', + 'password', + 'secret', + 'confirm_password', + 'password_confirmation', + 'auth_token', + 'csrf_token', + 'access_token' + ); + $this->assertEquals($expected, $this->d->scrubFields()); + } +} diff --git a/tests/ErrorWrapperTest.php b/tests/ErrorWrapperTest.php new file mode 100644 index 00000000..27f34cb0 --- /dev/null +++ b/tests/ErrorWrapperTest.php @@ -0,0 +1,21 @@ +assertEquals("FAKE BACKTRACE", $errWrapper->getBacktrace()); + } + + public function testGetClassName() + { + $errWrapper = new ErrorWrapper(E_ERROR, "Message Content", null, null, null); + $this->assertEquals("E_ERROR: Message Content", $errWrapper->getClassName()); + + $errWrapper = new ErrorWrapper(3, "Fake Error Number", null, null, null); + $this->assertEquals("#3: Fake Error Number", $errWrapper->getClassName()); + } +} diff --git a/tests/ExceptionInfoTest.php b/tests/ExceptionInfoTest.php new file mode 100644 index 00000000..061438f4 --- /dev/null +++ b/tests/ExceptionInfoTest.php @@ -0,0 +1,32 @@ +assertEquals($class, $exc->getClass()); + + $this->assertEquals("TestClass", $exc->setClass("TestClass")->getClass()); + } + + public function testMessage() + { + $message = "A message"; + $exc = new ExceptionInfo("C", $message); + $this->assertEquals($message, $exc->getMessage()); + + $this->assertEquals("Another", $exc->setMessage("Another")->getMessage()); + } + + public function testDescription() + { + $description = "long form"; + $exc = new ExceptionInfo("C", "s", $description); + $this->assertEquals($description, $exc->getDescription()); + + $this->assertEquals("longer form", $exc->setDescription("longer form")->getDescription()); + $this->assertNull($exc->setDescription(null)->getDescription()); + } +} diff --git a/tests/FakeDataBuilder.php b/tests/FakeDataBuilder.php new file mode 100644 index 00000000..7b893474 --- /dev/null +++ b/tests/FakeDataBuilder.php @@ -0,0 +1,19 @@ +exception = m::mock("Rollbar\Payload\ExceptionInfo"); + $this->frame = new Frame("tests/FrameTest.php", $this->exception); + } + + public function testFilename() + { + $frame = new Frame("filename.php"); + $this->assertEquals("filename.php", $frame->getFilename()); + $frame->setFilename("other.php"); + $this->assertEquals("other.php", $frame->getFilename()); + } + + public function testLineno() + { + $this->frame->setLineno(5); + $this->assertEquals(5, $this->frame->getLineno()); + } + + public function testColno() + { + $this->frame->setColno(5); + $this->assertEquals(5, $this->frame->getColno()); + } + + public function testMethod() + { + $this->frame->setMethod("method"); + $this->assertEquals("method", $this->frame->getMethod()); + } + + public function testCode() + { + $this->frame->setCode("code->whatever()"); + $this->assertEquals("code->whatever()", $this->frame->getCode()); + } + + public function testContext() + { + $context = m::mock("Rollbar\Payload\Context"); + $this->frame->setContext($context); + $this->assertEquals($context, $this->frame->getContext()); + } + + public function testArgs() + { + $this->frame->setArgs(array()); + $this->assertEquals(array(), $this->frame->getArgs()); + + $this->frame->setArgs(array(1, "hi")); + $this->assertEquals(array(1, "hi"), $this->frame->getArgs()); + } + + public function testKwargs() + { + $this->frame->setKwargs(array("hi" => "bye")); + $this->assertEquals(array("hi" => "bye"), $this->frame->getKwargs()); + } + + public function testEncode() + { + $context = m::mock("Rollbar\Payload\Context, \JsonSerializable") + ->shouldReceive("jsonSerialize") + ->andReturn("{CONTEXT}") + ->mock(); + $this->exception + ->shouldReceive("jsonSerialize") + ->andReturn("{EXC}") + ->mock(); + $this->frame->setFilename("rollbar.php") + ->setLineno(1024) + ->setColno(42) + ->setMethod("testEncode()") + ->setCode('$frame->setFilename("rollbar.php")') + ->setContext($context) + ->setArgs(array("hello", "world")) + ->setKwargs(array("whatever" => "Faked")); + + $actual = json_encode($this->frame->jsonSerialize()); + $expected = '{' . + '"filename":"rollbar.php",' . + '"lineno":1024,"colno":42,' . + '"method":"testEncode()",' . + '"code":"$frame->setFilename(\"rollbar.php\")",' . + '"context":"{CONTEXT}",' . + '"args":["hello","world"],' . + '"kwargs":{"whatever":"Faked"}' . + '}'; + + $this->assertEquals($expected, $actual); + } +} diff --git a/tests/LevelTest.php b/tests/LevelTest.php new file mode 100644 index 00000000..ddb7cec9 --- /dev/null +++ b/tests/LevelTest.php @@ -0,0 +1,23 @@ +assertNull($l); + + $l = Level::CRITICAL(); + $this->assertNotNull($l); + $this->assertSame(Level::CRITICAL(), $l); + $this->assertSame(Level::critical(), $l); + + $l = Level::Info(); + $this->assertNotNull($l); + $this->assertSame(Level::INFO(), $l); + $this->assertSame('"info"', json_encode($l->jsonSerialize())); + } +} diff --git a/tests/MessageTest.php b/tests/MessageTest.php new file mode 100644 index 00000000..6d0fc71a --- /dev/null +++ b/tests/MessageTest.php @@ -0,0 +1,43 @@ +assertEquals("Test", $msg->getBody()); + + $this->assertEquals("Test2", $msg->setBody("Test2")->getBody()); + } + + public function testExtra() + { + $msg = new Message("M", array( + "hello" => "world" + )); + $this->assertEquals("world", $msg->hello); + $msg->hello = "Świat"; // Polish for "World" + $this->assertEquals("Świat", $msg->hello); + // Unicode Ś == u015a + $this->assertEquals('{"body":"M","hello":"\u015awiat"}', json_encode($msg->jsonSerialize())); + } + + public function testMessageCustom() + { + $msg = new Message("Test"); + $msg->CustomData = "custom data"; + $msg->whatever = 15; + + $this->assertEquals("custom data", $msg->CustomData); + $this->assertEquals(15, $msg->whatever); + + $expected = '{"body":"Test","CustomData":"custom data","whatever":15}'; + $this->assertEquals($expected, json_encode($msg->jsonSerialize())); + } + + public function testMessageKey() + { + $msg = new Message("Test"); + $this->assertEquals("message", $msg->getKey()); + } +} diff --git a/tests/NotifierTest.php b/tests/NotifierTest.php new file mode 100644 index 00000000..779712d1 --- /dev/null +++ b/tests/NotifierTest.php @@ -0,0 +1,34 @@ +assertEquals($name, $notifier->getName()); + + $name2 = "RollbarPHP"; + $this->assertEquals($name2, $notifier->setName($name2)->getName()); + } + + public function testVersion() + { + $version = Notifier::VERSION; + $notifier = new Notifier("PHP-Rollbar", $version); + $this->assertEquals($version, $notifier->getVersion()); + + $version2 = "0.9"; + $this->assertEquals($version2, $notifier->setVersion($version2)->getVersion()); + } + + public function testEncode() + { + $notifier = Notifier::defaultNotifier(); + $encoded = json_encode($notifier->jsonSerialize()); + $this->assertEquals('{"name":"rollbar-php","version":"1.0.0-beta"}', $encoded); + } +} diff --git a/tests/PayloadTest.php b/tests/PayloadTest.php new file mode 100644 index 00000000..56429e42 --- /dev/null +++ b/tests/PayloadTest.php @@ -0,0 +1,62 @@ +assertEquals($data, $payload->getData()); + + $data2 = m::mock("Rollbar\Payload\Data"); + $this->assertEquals($data2, $payload->setData($data2)->getData()); + } + + public function testPayloadAccessToken() + { + $data = m::mock("Rollbar\Payload\Data"); + $accessToken = "012345678901234567890123456789ab"; + + $payload = new Payload($data, $accessToken); + $this->assertEquals($accessToken, $payload->getAccessToken()); + + $accessToken = "too_short"; + try { + new Payload($data, $accessToken); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertContains("32", $e->getMessage()); + } + + $accessToken = "too_longtoo_longtoo_longtoo_longtoo_longtoo_long"; + try { + new Payload($data, $accessToken); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertContains("32", $e->getMessage()); + } + + $accessToken = "012345678901234567890123456789ab"; + $payload = new Payload($data, $accessToken); + $this->assertEquals($accessToken, $payload->getAccessToken()); + + $at2 = "ab012345678901234567890123456789"; + $this->assertEquals($at2, $payload->setAccessToken($at2)->getAccessToken()); + } + + public function testEncode() + { + $data = m::mock('Rollbar\Payload\Data, \JsonSerializable') + ->shouldReceive('jsonSerialize') + ->andReturn(new \ArrayObject()) + ->mock(); + $payload = new Payload($data, "012345678901234567890123456789ab"); + $encoded = json_encode($payload->jsonSerialize()); + $json = '{"data":{},"access_token":"012345678901234567890123456789ab"}'; + $this->assertEquals($json, $encoded); + } +} diff --git a/tests/PersonTest.php b/tests/PersonTest.php new file mode 100644 index 00000000..2843d754 --- /dev/null +++ b/tests/PersonTest.php @@ -0,0 +1,57 @@ +assertEquals($id, $person->getId()); + + $id2 = "RollbarPHP"; + $this->assertEquals($id2, $person->setId($id2)->getId()); + } + + public function testUsername() + { + $username = "user@rollbar.com"; + $person = new Person("15", $username); + $this->assertEquals($username, $person->getUsername()); + + $username2 = "user-492"; + $this->assertEquals($username2, $person->setUsername($username2)->getUsername()); + } + + public function testEmail() + { + $email = "1.0.0"; + $person = new Person("Rollbar_Master", null, $email); + $this->assertEquals($email, $person->getEmail()); + + $email2 = "1.0.1"; + $this->assertEquals($email2, $person->setEmail($email2)->getEmail()); + } + + public function testExtra() + { + $person = new Person("42"); + $person->test = "testing"; + $this->assertEquals("testing", $person->test); + } + + public function testEncode() + { + $person = new Person("1024"); + $person->setUsername("username") + ->setEmail("user@gmail.com"); + $person->Settings = array( + "send_email" => true + ); + $encoded = json_encode($person->jsonSerialize()); + $expected ='{"id":"1024","username":"username","email":"user@gmail.com","Settings":{"send_email":true}}'; + $this->assertEquals($expected, $encoded); + } +} diff --git a/tests/RequestTest.php b/tests/RequestTest.php new file mode 100644 index 00000000..7caed71a --- /dev/null +++ b/tests/RequestTest.php @@ -0,0 +1,166 @@ +setUrl($url); + $this->assertEquals($url, $request->getUrl()); + + $url2 = "www.google.com"; + $this->assertEquals($url2, $request->setUrl($url2)->getUrl()); + } + + public function testMethod() + { + $method = "POST"; + $request = new Request(); + $request->setMethod($method); + $this->assertEquals($method, $request->getMethod()); + + $method2 = "GET"; + $this->assertEquals($method2, $request->setMethod($method2)->getMethod()); + } + + public function testHeaders() + { + $headers = array("Auth-X" => "abc352", "Hello" => "World"); + $request = new Request(); + $request->setHeaders($headers); + $this->assertEquals($headers, $request->getHeaders()); + + $headers2 = array("Goodbye" => "And thanks for all the fish"); + $this->assertEquals($headers2, $request->setHeaders($headers2)->getHeaders()); + } + + public function testParams() + { + $params = array( + "controller" => "project", + "action" => "index" + ); + $request = new Request(); + $request->setParams($params); + $this->assertEquals($params, $request->getParams()); + + $params2 = array("War" => "and Peace"); + $this->assertEquals($params2, $request->setParams($params2)->getParams()); + } + + public function testGet() + { + $get = array("query" => "where's waldo?", "page" => 15); + $request = new Request(); + $request->setGet($get); + $this->assertEquals($get, $request->getGet()); + + $get2 = array("skip" => "4", "bucket_size" => "25"); + $this->assertEquals($get2, $request->setGet($get2)->getGet()); + } + + public function testQueryString() + { + $queryString = "?slug=Rollbar&update=true"; + $request = new Request(); + $request->setQueryString($queryString); + $this->assertEquals($queryString, $request->getQueryString()); + + $queryString2 = "?search=Hello%2a"; + $actual = $request->setQueryString($queryString2)->getQueryString(); + $this->assertEquals($queryString2, $actual); + } + + public function testPost() + { + $post = array("Big" => "Data"); + $request = new Request(); + $request->setPost($post); + $this->assertEquals($post, $request->getPost()); + + $post2 = array( + "data" => array( + "Data" => "Parameters" + ), + "access_token" => "123abc" + ); + $this->assertEquals($post2, $request->setPost($post2)->getPost()); + } + + public function testBody() + { + $body = "a long string\nwith new lines and stuff"; + $request = new Request(); + $request->setBody($body); + $this->assertEquals($body, $request->getBody()); + + $body2 = "In the city of York there existed a society of magicians..."; + $this->assertEquals($body2, $request->setBody($body2)->getBody()); + } + + public function testUserIp() + { + $userIp = "192.0.1.12"; + $request = new Request(); + $request->setUserIp($userIp); + $this->assertEquals($userIp, $request->getUserIp()); + + $userIp2 = "172.68.205.3"; + $this->assertEquals($userIp2, $request->setUserIp($userIp2)->getUserIp()); + } + + public function testExtra() + { + $request = new Request(); + $request->test = "testing"; + $this->assertEquals("testing", $request->test); + } + + public function testEncode() + { + $request = new Request(); + $request->setUrl("www.rollbar.com/account/project") + ->setMethod("GET") + ->setHeaders(array( + "CSRF-TOKEN" => "42", + "X-SPEED" => "THEFLASH" + )) + ->setParams(array( + "controller" => "ProjectController", + "method" => "index" + )) + ->setGet(array( + "fetch_account" => "true", + "error_level" => "11" + )) + ->setQueryString("?fetch_account=true&error_level=11") + ->setUserIp("170.16.58.0"); + + $request->test = "testing"; + + $expected = '{' . + '"url":"www.rollbar.com\\/account\\/project",' . + '"method":"GET",' . + '"headers":{' . + '"CSRF-TOKEN":"42","X-SPEED":"THEFLASH"' . + '},' . + '"params":{' . + '"controller":"ProjectController",' . + '"method":"index"' . + '},' . + '"GET":{' . + '"fetch_account":"true",' . + '"error_level":"11"' . + '},' . + '"query_string":"?fetch_account=true&error_level=11",' . + '"user_ip":"170.16.58.0",' . + '"test":"testing"' . + '}'; + + $this->assertEquals($expected, json_encode($request->jsonSerialize())); + } +} diff --git a/tests/ResponseTest.php b/tests/ResponseTest.php new file mode 100644 index 00000000..68870a56 --- /dev/null +++ b/tests/ResponseTest.php @@ -0,0 +1,41 @@ +5)); + $this->assertEquals(200, $r->getStatus()); + } + + public function testInfo() + { + $r = new Response(200, "FAKE INFO"); + $this->assertEquals("FAKE INFO", $r->getInfo()); + } + + public function testUuid() + { + $r = new Response(200, "FAKE INFO", "abc123"); + $this->assertEquals("abc123", $r->getUuid()); + } + + public function testWasSuccessful() + { + $response = new Response(200, null); + $this->assertTrue($response->wasSuccessful()); + $response = new Response(199, null); + $this->assertFalse($response->wasSuccessful()); + $response = new Response(300, null); + $this->assertFalse($response->wasSuccessful()); + } + + public function testUrl() + { + $expected = "https://rollbar.com/occurrence/uuid/?uuid=abc123"; + $response = new Response(200, "fake", "abc123"); + $this->assertEquals($expected, $response->getOccurrenceUrl()); + } +} diff --git a/tests/RollbarLoggerTest.php b/tests/RollbarLoggerTest.php new file mode 100644 index 00000000..067562d2 --- /dev/null +++ b/tests/RollbarLoggerTest.php @@ -0,0 +1,57 @@ + "accessaccesstokentokentokentoken", + "environment" => "testing-php" + )); + $l->configure(array("extraData" => 15)); + $extended = $l->scope(array())->extend(array()); + $this->assertEquals(15, $extended['extraData']); + } + + public function testLog() + { + $l = new RollbarLogger(array( + "access_token" => "ad865e76e7fb496fab096ac07b1dbabb", + "environment" => "testing-php" + )); + $response = $l->log(LogLevel::WARNING, "Testing PHP Notifier", array()); + $this->assertEquals(200, $response->getStatus()); + } + + public function testErrorSampleRates() + { + $l = new RollbarLogger(array( + "access_token" => "ad865e76e7fb496fab096ac07b1dbabb", + "environment" => "testing-php", + "error_sample_rates" => array( + E_ERROR => 0 + ) + )); + $response = $l->log(null, new ErrorWrapper(E_ERROR, '', null, null, array()), array()); + $this->assertEquals(0, $response->getStatus()); + } + + public function testIncludedErrNo() + { + $l = new RollbarLogger(array( + "access_token" => "ad865e76e7fb496fab096ac07b1dbabb", + "environment" => "testing-php", + "included_errno" => E_ERROR | E_WARNING + )); + $response = $l->log(null, new ErrorWrapper(E_USER_ERROR, '', null, null, array()), array()); + $this->assertEquals(0, $response->getStatus()); + } +} diff --git a/tests/RollbarNotifierTest.php b/tests/RollbarNotifierTest.php deleted file mode 100644 index 49517c1b..00000000 --- a/tests/RollbarNotifierTest.php +++ /dev/null @@ -1,699 +0,0 @@ - ROLLBAR_TEST_TOKEN, - 'environment' => 'test', - 'root' => '/path/to/code/root', - 'code_version' => RollbarNotifier::VERSION, - 'batched' => false - ); - - private static $mockErrorFileSource = array( - "_server = $_SERVER; - } - - public function tearDown() { - m::close(); - $_SERVER = $this->_server; - } - - public function testConstruct() { - $notifier = new RollbarNotifier(self::$simpleConfig); - - $this->assertEquals('ad865e76e7fb496fab096ac07b1dbabb', $notifier->access_token); - $this->assertEquals('test', $notifier->environment); - } - - public function testSimpleMessage() { - $notifier = m::mock('RollbarNotifier[_send_payload_blocking]', array(self::$simpleConfig)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('_send_payload_blocking')->once(); - - $uuid = $notifier->report_message("Hello world"); - $this->assertValidUUID($uuid); - } - - public function testSimpleError() { - $notifier = new RollbarNotifier(self::$simpleConfig); - - $uuid = $notifier->report_php_error(E_ERROR, "Runtime error", "the_file.php", 1); - $this->assertValidUUID($uuid); - } - - public function testSimpleException() { - $notifier = new RollbarNotifier(self::$simpleConfig); - - $uuid = null; - try { - throw new Exception("test exception"); - } catch (Exception $e) { - $uuid = $notifier->report_exception($e); - } - - $this->assertValidUUID($uuid); - } - - public function testCheckIgnore() { - $config = self::$simpleConfig; - $config['checkIgnore'] = function ($isUncaught, $caller_args, $payload) { - if (isset($_SERVER['HTTP_USER_AGENT']) && strpos($_SERVER['HTTP_USER_AGENT'], 'Baiduspider') !== false) { - // ignore baidu spider - return true; - } - - // no other ignores - return false; - }; - - $notifier = new RollbarNotifier($config); - - // Should ignore this exception. - $_SERVER = array('HTTP_USER_AGENT' => 'Baiduspider'); - $this->assertNull($notifier->report_exception(new Exception("test exception"))); - - // Shouldn't ignore this exception. - $_SERVER = array(); - $this->assertValidUUID($notifier->report_exception(new Exception("test exception"))); - } - - public function testFlush() { - $config = self::$simpleConfig; - $config['batched'] = true; - $notifier = new RollbarNotifier($config); - $this->assertEquals(0, $notifier->queueSize()); - - $notifier->report_message("Hello world"); - $this->assertEquals(1, $notifier->queueSize()); - - $notifier->flush(); - $this->assertEquals(0, $notifier->queueSize()); - } - - public function testMessageWithStaticPerson() { - $config = self::$simpleConfig; - $config['person'] = array('id' => '123', 'username' => 'example', 'email' => 'example@example.com'); - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello world'); - - $this->assertEquals('Hello world', $payload['data']['body']['message']['body']); - $this->assertEquals('123', $payload['data']['person']['id']); - $this->assertEquals('example', $payload['data']['person']['username']); - $this->assertEquals('example@example.com', $payload['data']['person']['email']); - - $this->assertValidUUID($uuid); - } - - public function testMessageWithDynamicPerson() { - $config = self::$simpleConfig; - $config['person_fn'] = 'dummy_rollbar_person_fn'; - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello world'); - - $this->assertEquals('Hello world', $payload['data']['body']['message']['body']); - $this->assertEquals('456', $payload['data']['person']['id']); - $this->assertEquals('dynamic', $payload['data']['person']['username']); - $this->assertEquals('dynamic@example.com', $payload['data']['person']['email']); - - $this->assertValidUUID($uuid); - } - - public function testExceptionWithInvalidConfig() { - $config = self::$simpleConfig; - $config['access_token'] = 'hello'; - $notifier = new RollbarNotifier($config); - - $result = 'dummy'; - try { - throw new Exception("test exception"); - } catch (Exception $e) { - $result = $notifier->report_exception($e); - } - - $this->assertNull($result); - } - - public function testErrorWithoutCaptureBacktrace() { - $config = self::$simpleConfig; - $config['capture_error_backtraces'] = false; - $notifier = new RollbarNotifier($config); - - $uuid = $notifier->report_php_error(E_WARNING, 'Some warning', 'the_file.php', 2); - $this->assertValidUUID($uuid); - } - - public function testAgentBatched() { - $config = self::$simpleConfig; - $config['handler'] = 'agent'; - $config['batched'] = true; - $notifier = new RollbarNotifier($config); - - $uuid = $notifier->report_message('Hello world'); - $this->assertValidUUID($uuid); - - $notifier->flush(); - - $this->assertEquals(0, $notifier->queueSize()); - } - - public function testAgentNonbatched() { - $config = self::$simpleConfig; - $config['handler'] = 'agent'; - $config['batched'] = false; - $notifier = new RollbarNotifier($config); - - $uuid = $notifier->report_message('Hello world'); - $this->assertValidUUID($uuid); - - $this->assertEquals(0, $notifier->queueSize()); - } - - public function testBlockingBatched() { - $config = self::$simpleConfig; - $config['batched'] = true; - $notifier = new RollbarNotifier($config); - - $uuid = $notifier->report_message('Hello world'); - $this->assertValidUUID($uuid); - - $this->assertEquals(1, $notifier->queueSize()); - - $notifier->flush(); - - $this->assertEquals(0, $notifier->queueSize()); - } - - public function testBlockingNonbatched() { - $config = self::$simpleConfig; - $config['batched'] = false; - $notifier = new RollbarNotifier($config); - - $uuid = $notifier->report_message('Hello world'); - $this->assertValidUUID($uuid); - - $this->assertEquals(0, $notifier->queueSize()); - } - - public function testFlushAtBatchSize() { - $config = self::$simpleConfig; - $config['batched'] = true; - $config['batch_size'] = 2; - - $notifier = new RollbarNotifier($config); - - $notifier->report_message("one"); - $this->assertEquals(1, $notifier->queueSize()); - - $notifier->report_message("two"); - $this->assertEquals(2, $notifier->queueSize()); - - $notifier->report_message("three"); - $this->assertEquals(1, $notifier->queueSize()); - } - - public function testExceptionWithExtraAndPayloadData() { - $notifier = m::mock('RollbarNotifier[send_payload]', array(self::$simpleConfig)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = null; - try { - throw new Exception("test"); - } catch (Exception $e) { - $uuid = $notifier->report_exception($e, array('this_is' => 'extra'), array('title' => 'custom title')); - } - - $this->assertEquals('extra', $payload['data']['body']['trace']['extra']['this_is']); - $this->assertEquals('custom title', $payload['data']['title']); - - $this->assertValidUUID($uuid); - } - - public function testExceptionWithPreviousExceptions() - { - $first = new Exception('First exception'); - $second = new Exception('Second exception', null, $first); - $third = new Exception('Third exception', null, $second); - - $notifier = m::mock('RollbarNotifier[send_payload]', array(self::$simpleConfig)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload') - ->once() - ->passthru() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_exception($third, array('this_is' => 'extra')); - $chain = isset($payload['data']['body']['trace_chain']) ? $payload['data']['body']['trace_chain'] : null; - - $this->assertValidUUID($uuid); - $this->assertInternalType('array', $chain); - $this->assertEquals(3, count($chain)); - $this->assertEquals($third->getMessage(), $chain[0]['exception']['message']); - $this->assertEquals($second->getMessage(), $chain[1]['exception']['message']); - $this->assertEquals($first->getMessage(), $chain[2]['exception']['message']); - $this->assertEquals('extra', $chain[0]['extra']['this_is']); - } - - public function testMessageWithExtraAndPayloadData() { - $notifier = m::mock('RollbarNotifier[send_payload]', array(self::$simpleConfig)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello', 'info', array('extra_key' => 'extra_val'), - array('title' => 'custom title', 'level' => 'warning')); - - $this->assertEquals('Hello', $payload['data']['body']['message']['body']); - $this->assertEquals('warning', $payload['data']['level']); // payload data takes precedence - $this->assertEquals('extra_val', $payload['data']['body']['message']['extra_key']); - $this->assertEquals('custom title', $payload['data']['title']); - - $this->assertValidUUID($uuid); - } - - public function testMessageWithRequestData() { - $_GET = array('get_key' => 'get_value'); - $_POST = array( - 'post_key' => 'post_value', - 'password' => 'hunter2', - 'something_special' => 'excalibur' - ); - $_SESSION = array('session_key' => 'session_value'); - $_SERVER = array( - 'HTTP_HOST' => 'example.com', - 'REQUEST_URI' => '/example.php', - 'REQUEST_METHOD' => 'POST', - 'REMOTE_ADDR' => '127.0.0.1' - ); - - $config = self::$simpleConfig; - $config['scrub_fields'] = array('password', 'something_special'); - - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello'); - - $this->assertEquals('get_value', $payload['data']['request']['GET']['get_key']); - $this->assertEquals('post_value', $payload['data']['request']['POST']['post_key']); - $this->assertEquals('*******', $payload['data']['request']['POST']['password']); - $this->assertEquals('*********', $payload['data']['request']['POST']['something_special']); - $this->assertEquals('session_value', $payload['data']['request']['session']['session_key']); - $this->assertEquals('http://example.com/example.php', $payload['data']['request']['url']); - $this->assertEquals('POST', $payload['data']['request']['method']); - $this->assertEquals('127.0.0.1', $payload['data']['request']['user_ip']); - $this->assertEquals('example.com', $payload['data']['request']['headers']['Host']); - } - - public function testMessageWithCliData() { - $_SERVER = array( - 'REMOTE_ADDR' => '127.0.0.1', - 'argv' => array( - 'somescript', - '-vvvvv', - 'test:command', - '--arg=value', - ) - ); - $config = self::$simpleConfig; - - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $notifier->report_message('Hello'); - - $this->assertEquals('127.0.0.1', $payload['data']['request']['user_ip']); - $this->assertEquals($_SERVER['argv'], $payload['data']['server']['argv']); - $this->assertEquals(php_sapi_name(), $payload['data']['php_context']); - } - - public function testParamScrubbing() { - $_GET = array( - 'get_key' => 'get_value', - 'auth_token' => '12345', - 'client_password' => 'hunter2', - 'Something_Special_CaSeS' => 'number-six' - ); - $_POST = array( - 'post_key' => 'post_value', - 'PASSWORD' => 'hunter2', - 'something_special' => 'excalibur', - 'Something_Special_CaSeS' => 'number-six', - 'array_token' => array( - 'secret_key' => 'secret_value' - ), - 'array_key' => array( - 'subarray_key' => 'subarray_value', - 'subarray_password' => 'hunter2', - 'something_special' => 'excalibur', - 'Something_Special_CaSeS' => 'number-six' - ) - ); - $_SESSION = array( - 'session_key' => 'session_value', - 'SeSsIoN_pAssWoRd' => 'hunter2', - 'Something_Special_CaSeS' => '**********' - ); - $_SERVER = array( - 'HTTP_HOST' => 'example.com', - 'REQUEST_URI' => '/example.php?access_token=12345&harry=potter', - 'REQUEST_METHOD' => 'POST', - 'HTTP_PASSWORD' => 'hunter2', - 'HTTP_AUTH_TOKEN' => '12345', - 'REMOTE_ADDR' => '127.0.0.1' - ); - - $config = self::$simpleConfig; - $config['scrub_fields'] = array('something_special', 'something_special_cases', '/token|password/i'); - - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello'); - - $this->assertSame(array( - 'get_key' => 'get_value', - 'auth_token' => '*****', - 'client_password' => '*******', - 'Something_Special_CaSeS' => '**********', - ), $payload['data']['request']['GET']); - - $this->assertSame(array( - 'post_key' => 'post_value', - 'PASSWORD' => '*******', - 'something_special' => '*********', - 'Something_Special_CaSeS' => '**********', - 'array_token' => '*', - 'array_key' => array( - 'subarray_key' => 'subarray_value', - 'subarray_password' => '*******', - 'something_special' => '*********', - 'Something_Special_CaSeS' => '**********', - ) - ), $payload['data']['request']['POST']); - - $this->assertSame(array( - 'session_key' => 'session_value', - 'SeSsIoN_pAssWoRd' => '*******', - 'Something_Special_CaSeS' => '**********', - ), $payload['data']['request']['session']); - - $this->assertSame(array( - 'Host' => 'example.com', - 'Password' => '*******', - 'Auth-Token' => '*****', - ), $payload['data']['request']['headers']); - - $this->assertSame("http://example.com/example.php?access_token=xxxxx&harry=potter", $payload['data']['request']['url']); - } - - public function urlScrubbingEdgeCasesDataProvider() { - return array( - array('/example.php', array('blah'), 'http://example.com/example.php'), - array('/example.php?blah=hello', array('blah'), 'http://example.com/example.php?blah=xxxxx'), - array('/example.php?nested%5Bblah%5D=hello', array('blah'), 'http://example.com/example.php?nested%5Bblah%5D=xxxxx'), - array('/nonsense39423t#$Y*%@(Y', array('blah'), 'http://example.com/nonsense39423t#$Y*%@(Y'), - array('/nonsense_params?39423t=#$Y*%@(Y', array('blah'), 'http://example.com/nonsense_params?39423t=#$Y*%@(Y'), - array('/nonsense_with_spaces?39423t=#$Y *%@(Y', array('blah'), 'http://example.com/nonsense_with_spaces?39423t=#$Y *%@(Y'), - array('', array('blah'), 'http://example.com'), - ); - } - - /** @dataProvider urlScrubbingEdgeCasesDataProvider */ - public function testUrlScrubbingEdgeCases($uri, $scrub_fields, $expected_scrubbed_url) { - $_SERVER = array( - 'HTTP_HOST' => "example.com", - 'REQUEST_URI' => $uri, - 'REMOTE_ADDR' => '127.0.0.1' - ); - $config = self::$simpleConfig; - $config['scrub_fields'] = $scrub_fields; - - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello'); - - $this->assertSame($expected_scrubbed_url, $payload['data']['request']['url']); - } - - public function testServerBranchDefaultsEmpty() { - $config = self::$simpleConfig; - - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello'); - - $this->assertFalse(isset($payload['data']['server']['branch'])); - } - - public function testServerBranchConfig() { - $config = self::$simpleConfig; - $config['branch'] = 'my-branch'; - - $notifier = m::mock('RollbarNotifier[send_payload]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - - $uuid = $notifier->report_message('Hello'); - - $this->assertEquals($payload['data']['server']['branch'], 'my-branch'); - } - - public function testErrorPrePostCodeContextPayloadData() { - - // arrange - $mock_error_file_path = '/foo/bar/baz.php'; - $mock_error_file_source = self::$mockErrorFileSource; - $payload = null; - $config = self::$simpleConfig; - $config['include_error_code_context'] = true; - $notifier = m::mock('RollbarNotifier[send_payload,get_source_file_reader]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - $reader = m::mock('SourceFileReader'); - $reader->shouldReceive('read_as_array') - ->atLeast() - ->once() - ->andReturnUsing(function($file) use ($mock_error_file_path, $mock_error_file_source) { - if ($file === $mock_error_file_path) { - return $mock_error_file_source; - } - return file($file); - }); - $notifier->shouldReceive('get_source_file_reader') - ->andReturn($reader); - - // act - $notifier->report_php_error(1, 'foo', $mock_error_file_path, 5); - - // assert - $mock_error_file_frame = null; - foreach($payload['data']['body']['trace']['frames'] as $frame) { - if($frame['filename'] === $mock_error_file_path) { - $mock_error_file_frame = $frame; - break; - } - } - $this->assertNotNull($mock_error_file_frame); - $this->assertEquals('public function getBaz($qux) { return $qux; }', $mock_error_file_frame['code']); - $this->assertEquals(array( - 'assertEquals(array( - '', - 'private function getFred() { return 123; }', - '}' - ), $mock_error_file_frame['context']['post']); - } - - public function testExceptionPrePostCodeContextPayloadData() { - - // arrange - $mock_error_file_path = '/foo/bar/baz.php'; - $mock_error_file_source = self::$mockErrorFileSource; - $payload = null; - $config = self::$simpleConfig; - $config['include_exception_code_context'] = true; - $notifier = m::mock('RollbarNotifier[send_payload,get_source_file_reader]', array($config)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('send_payload')->once() - ->with(m::on(function($input) use (&$payload) { - $payload = $input; - return true; - })); - $reader = m::mock('SourceFileReader'); - $reader->shouldReceive('read_as_array') - ->atLeast() - ->once() - ->andReturnUsing(function($file) use ($mock_error_file_path, $mock_error_file_source) { - if ($file === $mock_error_file_path) { - return $mock_error_file_source; - } - return file($file); - }); - $notifier->shouldReceive('get_source_file_reader') - ->andReturn($reader); - $Exception = new ErrorException('foo', 1, 1, $mock_error_file_path, 5); - - // act - $notifier->report_exception($Exception); - - // assert - $mock_error_file_frame = null; - foreach($payload['data']['body']['trace']['frames'] as $frame) { - if($frame['filename'] === $mock_error_file_path) { - $mock_error_file_frame = $frame; - break; - } - } - $this->assertNotNull($mock_error_file_frame); - $this->assertEquals('public function getBaz($qux) { return $qux; }', $mock_error_file_frame['code']); - $this->assertEquals(array( - 'assertEquals(array( - '', - 'private function getFred() { return 123; }', - '}' - ), $mock_error_file_frame['context']['post']); - } - - /* --- Internal exceptions --- */ - - public function testInternalExceptionInReportException() { - $notifier = m::mock('RollbarNotifier[_report_exception,log_error]', array(self::$simpleConfig)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('_report_exception')->once()->andThrow(new Exception("internal error")); - $notifier->shouldReceive('log_error')->once(); - - $uuid = 'dummy'; - try { - throw new Exception("test"); - } catch (Exception $e) { - $uuid = $notifier->report_exception($e); - } - - $this->assertNull($uuid); - } - - public function testInternalExceptionInReportMessage() { - $notifier = m::mock('RollbarNotifier[_report_message,log_error]', array(self::$simpleConfig)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('_report_message')->once()->andThrow(new Exception("internal error")); - $notifier->shouldReceive('log_error')->once(); - - $uuid = $notifier->report_message("hello"); - $this->assertNull($uuid); - } - - public function testInternalExceptionInReportPhpError() { - $notifier = m::mock('RollbarNotifier[_report_php_error,log_error]', array(self::$simpleConfig)) - ->shouldAllowMockingProtectedMethods(); - $notifier->shouldReceive('_report_php_error')->once()->andThrow(new Exception("internal error")); - $notifier->shouldReceive('log_error')->once(); - - $uuid = $notifier->report_php_error(E_NOTICE, "Some notice", "the_file.php", 123); - $this->assertNull($uuid); - } - - - /* --- Helper methods --- */ - - private function assertValidUUID($uuid) { - $this->assertStringMatchesFormat('%x-%x-%x-%x-%x', $uuid); - } - - - -} - -function dummy_rollbar_person_fn() { - return array('id' => 456, 'username' => 'dynamic', 'email' => 'dynamic@example.com'); -} - -?> diff --git a/tests/RollbarTest.php b/tests/RollbarTest.php deleted file mode 100644 index c43396b4..00000000 --- a/tests/RollbarTest.php +++ /dev/null @@ -1,135 +0,0 @@ - ROLLBAR_TEST_TOKEN, - 'environment' => 'test' - ); - - protected function setUp() { - Rollbar::$instance = null; - } - - public function testInit() { - Rollbar::init(self::$simpleConfig); - - $this->assertEquals(ROLLBAR_TEST_TOKEN, Rollbar::$instance->access_token); - $this->assertEquals('test', Rollbar::$instance->environment); - } - - public function testSimpleMessage() { - Rollbar::init(self::$simpleConfig); - - $uuid = Rollbar::report_message("Hello world"); - $this->assertStringMatchesFormat('%x-%x-%x-%x-%x', $uuid); - } - - public function testMessageBeforeInit() { - $uuid = Rollbar::report_message("Hello world"); - $this->assertNull($uuid); - } - - public function testSimpleError() { - Rollbar::init(self::$simpleConfig); - - $result = Rollbar::report_php_error(E_ERROR, "Runtime error", "the_file.php", 1); - // always returns false. - $this->assertFalse($result); - } - - public function testErrorBeforeInit() { - $uuid = Rollbar::report_php_error(E_ERROR, "Runtime error", "the_file.php", 1); - $this->assertFalse($uuid); - } - - public function testSimpleException() { - Rollbar::init(self::$simpleConfig); - - $uuid = null; - try { - throw new Exception("test exception"); - } catch (Exception $e) { - $uuid = Rollbar::report_exception($e); - } - - $this->assertStringMatchesFormat('%x-%x-%x-%x-%x', $uuid); - } - - public function testExceptionBeforeInit() { - $uuid = null; - try { - throw new Exception("test exception"); - } catch (Exception $e) { - $uuid = Rollbar::report_exception($e); - } - $this->assertNull($uuid); - } - - public function testFlush() { - Rollbar::init(self::$simpleConfig); - $this->assertEquals(0, Rollbar::$instance->queueSize()); - - Rollbar::report_message("Hello world"); - $this->assertEquals(1, Rollbar::$instance->queueSize()); - - Rollbar::flush(); - $this->assertEquals(0, Rollbar::$instance->queueSize()); - } - - public function testScrub() { - Rollbar::init(self::$simpleConfig); - - $method = new ReflectionMethod(get_class(Rollbar::$instance), 'scrub_request_params'); - $method->setAccessible(true); - - Rollbar::$instance->scrub_fields = array('secret', 'scrubme'); - - $this->assertEquals( - $method->invoke( - Rollbar::$instance, - array( - 'some_item', - 'apples' => array( - 'green', - 'red' - ), - 'bananas' => array( - 'yellow' - ), - 'secret' => 'shh', - 'a' => array( - 'b' => array( - 'secret' => 'deep', - 'scrubme' => 'secrets' - ) - ) - ) - ), - array( - 'some_item', - 'apples' => array( - 'green', - 'red' - ), - 'bananas' => array( - 'yellow' - ), - 'secret' => '***', - 'a' => array( - 'b' => array( - 'secret' => '****', - 'scrubme' => '*******' - ) - ) - ) - ); - } - -} - -?> diff --git a/tests/ServerTest.php b/tests/ServerTest.php new file mode 100644 index 00000000..ccc97ebb --- /dev/null +++ b/tests/ServerTest.php @@ -0,0 +1,81 @@ +setHost($val); + $this->assertEquals($val, $server->getHost()); + + $val2 = "TEST2"; + $this->assertEquals($val2, $server->setHost($val2)->getHost()); + } + + public function testRoot() + { + $val = "TEST"; + $server = new Server(); + $server->setRoot($val); + $this->assertEquals($val, $server->getRoot()); + + $val2 = "TEST2"; + $this->assertEquals($val2, $server->setRoot($val2)->getRoot()); + } + + public function testBranch() + { + $val = "TEST"; + $server = new Server(); + $server->setBranch($val); + $this->assertEquals($val, $server->getBranch()); + + $val2 = "TEST2"; + $this->assertEquals($val2, $server->setBranch($val2)->getBranch()); + } + + public function testCodeVersion() + { + $val = "TEST"; + $server = new Server(); + $server->setCodeVersion($val); + $this->assertEquals($val, $server->getCodeVersion()); + + $val2 = "TEST2"; + $this->assertEquals($val2, $server->setCodeVersion($val2)->getCodeVersion()); + } + + public function testExtra() + { + $server = new Server(); + $server->test = "testing"; + $this->assertEquals("testing", $server->test); + } + + public function testEncode() + { + $server = new Server(); + $server->setHost("server2-ec-us") + ->setRoot("/home/app/testingRollbar") + ->setBranch("master") + ->setCodeVersion("#dca015"); + $server->test = array(1, 2, "3", array()); + $expected = '{' . + '"host":"server2-ec-us",' . + '"root":"\\/home\\/app\\/testingRollbar",' . + '"branch":"master",' . + '"code_version":"#dca015",' . + '"test":' . + '[' . + '1,' . + '2,' . + '"3",' . + '[]' . + ']' . + '}'; + $this->assertEquals($expected, json_encode($server->jsonSerialize())); + } +} diff --git a/tests/TraceChainTest.php b/tests/TraceChainTest.php new file mode 100644 index 00000000..56704fb2 --- /dev/null +++ b/tests/TraceChainTest.php @@ -0,0 +1,48 @@ +trace1 = m::mock("Rollbar\Payload\Trace"); + $this->trace2 = m::mock("Rollbar\Payload\Trace"); + } + + public function testTraces() + { + $chain = array($this->trace1); + $traceChain = new TraceChain($chain); + $this->assertEquals($chain, $traceChain->getTraces()); + + $traceChain = new TraceChain($chain); + $chain = array($this->trace1, $this->trace2); + $traceChain->setTraces($chain); + $this->assertEquals($chain, $traceChain->getTraces()); + } + + public function testKey() + { + $chain = new TraceChain(array($this->trace1)); + $this->assertEquals("trace_chain", $chain->getKey()); + } + + public function testEncode() + { + $trace1 = m::mock("Rollbar\Payload\Trace") + ->shouldReceive("jsonSerialize") + ->andReturn("TRACE1") + ->mock(); + $trace2 = m::mock("Rollbar\Payload\Trace") + ->shouldReceive("jsonSerialize") + ->andReturn("TRACE2") + ->mock(); + $chain = new TraceChain(array($trace1, $trace2)); + $this->assertEquals('["TRACE1","TRACE2"]', json_encode($chain->jsonSerialize())); + } +} diff --git a/tests/TraceTest.php b/tests/TraceTest.php new file mode 100644 index 00000000..cf261218 --- /dev/null +++ b/tests/TraceTest.php @@ -0,0 +1,68 @@ +assertEquals(array(), $trace->getFrames()); + $this->assertEquals($exc, $trace->getException()); + + $trace = new Trace($frames, $exc); + $this->assertEquals($frames, $trace->getFrames()); + $this->assertEquals($exc, $trace->getException()); + + try { + $trace = new Trace($badFrames, $exc); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertEquals("\$frames must all be Rollbar\Payload\Frames", $e->getMessage()); + } + } + + public function testFrames() + { + $frames = array( + new Frame("one.php"), + new Frame("two.php") + ); + $exc = m::mock("Rollbar\Payload\ExceptionInfo"); + $trace = new Trace(array(), $exc); + $this->assertEquals($frames, $trace->setFrames($frames)->getFrames()); + } + + public function testException() + { + $exc = m::mock("Rollbar\Payload\ExceptionInfo"); + $trace = new Trace(array(), $exc); + $this->assertEquals($exc, $trace->getException()); + + $exc2 = m::mock("Rollbar\Payload\ExceptionInfo"); + $this->assertEquals($exc2, $trace->setException($exc2)->getException()); + } + + public function testEncode() + { + $value = m::mock("Rollbar\Payload\ExceptionInfo, \JsonSerializable") + ->shouldReceive("jsonSerialize") + ->andReturn("{EXCEPTION}") + ->mock(); + $trace = new Trace(array(), $value); + $encoded = json_encode($trace->jsonSerialize()); + $this->assertEquals("{\"frames\":[],\"exception\":\"{EXCEPTION}\"}", $encoded); + } + + public function testTraceKey() + { + $trace = new Trace(array(), m::mock("Rollbar\Payload\ExceptionInfo")); + $this->assertEquals("trace", $trace->getKey()); + } +} diff --git a/tests/UtilitiesTest.php b/tests/UtilitiesTest.php new file mode 100644 index 00000000..07d1a040 --- /dev/null +++ b/tests/UtilitiesTest.php @@ -0,0 +1,127 @@ +assertTrue(Utilities::coalesce(false, false, true)); + $this->assertNull(Utilities::coalesce(false, false)); + $this->assertEquals(5, Utilities::coalesce(false, false, 5)); + } + + public function testPascaleToCamel() + { + $toTest = array( + array("TestMe", "test_me"), + array("USA", "usa"), + array("PHPUnit_Framework_TestCase", "php_unit_framework_test_case"), + ); + foreach ($toTest as $vals) { + $this->assertEquals($vals[1], Utilities::pascalToCamel($vals[0])); + } + } + + public function testValidateString() + { + Utilities::validateString(""); + Utilities::validateString("true"); + Utilities::validateString("four", "local", 4); + Utilities::validateString(null); + + try { + Utilities::validateString(null, "null", null, false); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertEquals($e->getMessage(), "\$null must not be null"); + } + + try { + Utilities::validateString(1, "number"); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertEquals($e->getMessage(), "\$number must be a string"); + } + + try { + Utilities::validateString("1", "str", 2); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertEquals($e->getMessage(), "\$str must be 2 characters long, was '1'"); + } + } + + public function testValidateInteger() + { + Utilities::validateInteger(null); + Utilities::validateInteger(0); + Utilities::validateInteger(1, "one", 0, 2); + + try { + Utilities::validateInteger(null, "null", null, null, false); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertEquals($e->getMessage(), "\$null must not be null"); + } + + try { + Utilities::validateInteger(0, "zero", 1); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertEquals($e->getMessage(), "\$zero must be >= 1"); + } + + try { + Utilities::validateInteger(0, "zero", null, -1); + $this->fail("Above should throw"); + } catch (\InvalidArgumentException $e) { + $this->assertEquals($e->getMessage(), "\$zero must be <= -1"); + } + } + + public function testValidateBooleanThrowsException() + { + $this->setExpectedException(get_class(new \InvalidArgumentException())); + Utilities::validateBoolean(null, "foo", false); + } + + public function testValidateBooleanWithInvalidBoolean() + { + $this->setExpectedException(get_class(new \InvalidArgumentException())); + Utilities::validateBoolean("not a boolean"); + } + + public function testValidateBoolean() + { + Utilities::validateBoolean(true, "foo", false); + Utilities::validateBoolean(true); + Utilities::validateBoolean(null); + } + + public function testSerializeForRollbar() + { + $obj = array( + "OneTwo" => array(1, 2), + "klass" => "Numbers", + "PHPUnitTest" => "testSerializeForRollbar", + "myCustomKey" => null, + "myNullValue" => null, + ); + $result = Utilities::serializeForRollbar($obj, array("klass" => "class"), array("myCustomKey")); + + $this->assertArrayNotHasKey("OneTwo", $result); + $this->assertArrayHasKey("one_two", $result); + + $this->assertArrayNotHasKey("klass", $result); + $this->assertArrayHasKey("class", $result); + + $this->assertArrayNotHasKey("PHPUnitTest", $result); + $this->assertArrayHasKey("php_unit_test", $result); + + $this->assertArrayNotHasKey("my_custom_key", $result); + $this->assertArrayHasKey("myCustomKey", $result); + $this->assertNull($result["myCustomKey"]); + + $this->assertArrayNotHasKey("myNullValue", $result); + $this->assertArrayNotHasKey("my_null_value", $result); + } +}