From e5eefa685fe2c6d6c624718b42f451efdab97f9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Doursenaud?= Date: Thu, 6 Aug 2015 18:25:51 +0200 Subject: [PATCH 1/6] Add Raven client for Sentry --- composer.json | 3 +- composer.lock | 56 +- .../includes/composer/autoload_namespaces.php | 1 + htdocs/includes/composer/installed.json | 56 + htdocs/includes/raven/raven/.gitignore | 5 + htdocs/includes/raven/raven/.php_cs | 12 + htdocs/includes/raven/raven/.travis.yml | 30 + htdocs/includes/raven/raven/AUTHORS | 4 + htdocs/includes/raven/raven/CHANGES | 39 + htdocs/includes/raven/raven/LICENSE | 12 + htdocs/includes/raven/raven/Makefile | 17 + htdocs/includes/raven/raven/README.rst | 273 + htdocs/includes/raven/raven/bin/raven | 88 + htdocs/includes/raven/raven/composer.json | 35 + .../raven/raven/lib/Raven/Autoloader.php | 43 + .../includes/raven/raven/lib/Raven/Client.php | 909 +++ .../includes/raven/raven/lib/Raven/Compat.php | 136 + .../raven/raven/lib/Raven/Context.php | 23 + .../raven/raven/lib/Raven/CurlHandler.php | 117 + .../raven/raven/lib/Raven/ErrorHandler.php | 175 + .../raven/raven/lib/Raven/Processor.php | 20 + .../raven/lib/Raven/SanitizeDataProcessor.php | 102 + .../raven/raven/lib/Raven/Serializer.php | 77 + .../raven/raven/lib/Raven/Stacktrace.php | 252 + .../includes/raven/raven/lib/Raven/Util.php | 33 + .../raven/raven/lib/Raven/data/cacert.pem | 5134 +++++++++++++++++ htdocs/includes/raven/raven/phpunit.xml | 25 + .../raven/test/Raven/Tests/ClientTest.php | 624 ++ .../raven/test/Raven/Tests/CompatTest.php | 46 + .../test/Raven/Tests/ErrorHandlerTest.php | 87 + .../Raven/Tests/SanitizeDataProcessorTest.php | 180 + .../raven/test/Raven/Tests/SerializerTest.php | 48 + .../raven/test/Raven/Tests/StacktraceTest.php | 223 + .../raven/raven/test/Raven/Tests/UtilTest.php | 32 + .../raven/test/Raven/Tests/resources/a.php | 11 + .../raven/test/Raven/Tests/resources/b.php | 3 + .../includes/raven/raven/test/bootstrap.php | 17 + 37 files changed, 8946 insertions(+), 2 deletions(-) create mode 100644 htdocs/includes/raven/raven/.gitignore create mode 100644 htdocs/includes/raven/raven/.php_cs create mode 100644 htdocs/includes/raven/raven/.travis.yml create mode 100644 htdocs/includes/raven/raven/AUTHORS create mode 100644 htdocs/includes/raven/raven/CHANGES create mode 100644 htdocs/includes/raven/raven/LICENSE create mode 100644 htdocs/includes/raven/raven/Makefile create mode 100644 htdocs/includes/raven/raven/README.rst create mode 100755 htdocs/includes/raven/raven/bin/raven create mode 100644 htdocs/includes/raven/raven/composer.json create mode 100644 htdocs/includes/raven/raven/lib/Raven/Autoloader.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/Client.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/Compat.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/Context.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/CurlHandler.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/ErrorHandler.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/Processor.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/SanitizeDataProcessor.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/Serializer.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/Stacktrace.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/Util.php create mode 100644 htdocs/includes/raven/raven/lib/Raven/data/cacert.pem create mode 100644 htdocs/includes/raven/raven/phpunit.xml create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/CompatTest.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/ErrorHandlerTest.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/SanitizeDataProcessorTest.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/SerializerTest.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/StacktraceTest.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/UtilTest.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/resources/a.php create mode 100644 htdocs/includes/raven/raven/test/Raven/Tests/resources/b.php create mode 100644 htdocs/includes/raven/raven/test/bootstrap.php diff --git a/composer.json b/composer.json index 7588f7da49c..d5e2d7f411e 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,8 @@ "mobiledetect/mobiledetectlib": "2.8.3", "phpoffice/phpexcel": "1.8.0", "restler/framework": "^3.0", - "tecnick.com/tcpdf": "6.2.6" + "tecnick.com/tcpdf": "6.2.6", + "raven/raven": "^0.12.0" }, "suggest": { "ext-mysqlnd": "To use with MySQL or MariaDB", diff --git a/composer.lock b/composer.lock index de85792664e..9e662436d4d 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "ca5a74259e0d1773089838908d58cb31", + "hash": "32e1fa78cc95c32b154a38e07706874c", "packages": [ { "name": "ccampbell/chromephp", @@ -199,6 +199,60 @@ ], "time": "2014-03-02 15:22:49" }, + { + "name": "raven/raven", + "version": "0.12.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/raven-php.git", + "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/raven-php/zipball/bd247ca2a8fd9ccfb99b60285c9b31286384a92b", + "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.2.4" + }, + "require-dev": { + "fabpot/php-cs-fixer": "^1.8.0", + "phpunit/phpunit": "^4.6.6" + }, + "bin": [ + "bin/raven" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12.x-dev" + } + }, + "autoload": { + "psr-0": { + "Raven_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "authors": [ + { + "name": "David Cramer", + "email": "dcramer@gmail.com" + } + ], + "description": "A PHP client for Sentry (http://getsentry.com)", + "homepage": "http://getsentry.com", + "keywords": [ + "log", + "logging" + ], + "time": "2015-05-19 20:20:08" + }, { "name": "restler/framework", "version": "3.0.0", diff --git a/htdocs/includes/composer/autoload_namespaces.php b/htdocs/includes/composer/autoload_namespaces.php index 5ff83246d01..27e1e79f483 100644 --- a/htdocs/includes/composer/autoload_namespaces.php +++ b/htdocs/includes/composer/autoload_namespaces.php @@ -6,6 +6,7 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname(dirname($vendorDir)); return array( + 'Raven_' => array($vendorDir . '/raven/raven/lib'), 'PHPExcel' => array($vendorDir . '/phpoffice/phpexcel/Classes'), 'Luracast\\Restler' => array($vendorDir . '/restler/framework'), 'Detection' => array($vendorDir . '/mobiledetect/mobiledetectlib/namespaced'), diff --git a/htdocs/includes/composer/installed.json b/htdocs/includes/composer/installed.json index 5b2a50000f6..f2b965e2b6e 100644 --- a/htdocs/includes/composer/installed.json +++ b/htdocs/includes/composer/installed.json @@ -340,5 +340,61 @@ "pdf417", "qrcode" ] + }, + { + "name": "raven/raven", + "version": "0.12.0", + "version_normalized": "0.12.0.0", + "source": { + "type": "git", + "url": "https://github.com/getsentry/raven-php.git", + "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/getsentry/raven-php/zipball/bd247ca2a8fd9ccfb99b60285c9b31286384a92b", + "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.2.4" + }, + "require-dev": { + "fabpot/php-cs-fixer": "^1.8.0", + "phpunit/phpunit": "^4.6.6" + }, + "time": "2015-05-19 20:20:08", + "bin": [ + "bin/raven" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.12.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-0": { + "Raven_": "lib/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "authors": [ + { + "name": "David Cramer", + "email": "dcramer@gmail.com" + } + ], + "description": "A PHP client for Sentry (http://getsentry.com)", + "homepage": "http://getsentry.com", + "keywords": [ + "log", + "logging" + ] } ] diff --git a/htdocs/includes/raven/raven/.gitignore b/htdocs/includes/raven/raven/.gitignore new file mode 100644 index 00000000000..9cfdae25943 --- /dev/null +++ b/htdocs/includes/raven/raven/.gitignore @@ -0,0 +1,5 @@ +*.lock +package.xml +/vendor +.idea +.php_cs.cache diff --git a/htdocs/includes/raven/raven/.php_cs b/htdocs/includes/raven/raven/.php_cs new file mode 100644 index 00000000000..790c396bf0d --- /dev/null +++ b/htdocs/includes/raven/raven/.php_cs @@ -0,0 +1,12 @@ +in(__DIR__) +; + +return Symfony\CS\Config\Config::create() + ->setUsingCache(true) + ->setUsingLinter(true) + ->level(Symfony\CS\FixerInterface::PSR2_LEVEL) + ->finder($finder) +; diff --git a/htdocs/includes/raven/raven/.travis.yml b/htdocs/includes/raven/raven/.travis.yml new file mode 100644 index 00000000000..8182db27174 --- /dev/null +++ b/htdocs/includes/raven/raven/.travis.yml @@ -0,0 +1,30 @@ +language: php + +php: + - 5.3 + - 5.4 + - 5.5 + - 5.6 + - 7.0 + - hhvm + +matrix: + allow_failures: + - php: hhvm + - php: 7.0 + fast_finish: true + +sudo: false + +cache: + directories: + - $HOME/.composer/cache + +before_install: + - composer self-update + +install: travis_retry composer install --no-interaction --prefer-source + +script: + - vendor/bin/php-cs-fixer fix --config-file=.php_cs --verbose --diff --dry-run + - vendor/bin/phpunit diff --git a/htdocs/includes/raven/raven/AUTHORS b/htdocs/includes/raven/raven/AUTHORS new file mode 100644 index 00000000000..b9769880bbd --- /dev/null +++ b/htdocs/includes/raven/raven/AUTHORS @@ -0,0 +1,4 @@ +The Raven PHP client was originally written by Michael van Tellingen +and is maintained by the Sentry Team. + +http://github.com/getsentry/raven-php/contributors \ No newline at end of file diff --git a/htdocs/includes/raven/raven/CHANGES b/htdocs/includes/raven/raven/CHANGES new file mode 100644 index 00000000000..6d52a4bfd49 --- /dev/null +++ b/htdocs/includes/raven/raven/CHANGES @@ -0,0 +1,39 @@ +0.12.0 +------ + +- Bumped protocol version to 6. +- Fixed an issue with the async curl handler (GH-216). +- Removed UDP transport. + +0.11.0 +------ + +- New configuration parameter: 'release' +- New configuration parameter: 'message_limit' +- New configuration parameter: 'curl_ssl_version' +- New configuration parameter: 'curl_ipv4' +- New configuration parameter: 'verify_ssl' +- Updated remote endpoint to use modern project-based path. +- Expanded default sanitizer support to include 'auth_pw' attribute. + +0.10.0 +------ + +- Added a default certificate bundle which includes common root CA's + as well as getsentry.com's CA. + +0.9.1 +----- + +- Change default curl connection to 'sync' +- Improve CLI reporting + + +0.9.0 +----- + +- Protocol version 5 +- Default to asynchronous HTTP handler using curl_multi. + + +(For previous versions see the commit history) diff --git a/htdocs/includes/raven/raven/LICENSE b/htdocs/includes/raven/raven/LICENSE new file mode 100644 index 00000000000..c53c66caf22 --- /dev/null +++ b/htdocs/includes/raven/raven/LICENSE @@ -0,0 +1,12 @@ +Copyright (c) 2012 Sentry Team and individual contributors. +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + + 3. Neither the name of the Raven nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/htdocs/includes/raven/raven/Makefile b/htdocs/includes/raven/raven/Makefile new file mode 100644 index 00000000000..5e574ece113 --- /dev/null +++ b/htdocs/includes/raven/raven/Makefile @@ -0,0 +1,17 @@ +.PHONY: test + +develop: + composer install --dev + make setup-git + +cs: + vendor/bin/php-cs-fixer fix --config-file=.php_cs --verbose --diff + +cs-dry-run: + vendor/bin/php-cs-fixer fix --config-file=.php_cs --verbose --diff --dry-run + +test: + vendor/bin/phpunit + +setup-git: + git config branch.autosetuprebase always diff --git a/htdocs/includes/raven/raven/README.rst b/htdocs/includes/raven/raven/README.rst new file mode 100644 index 00000000000..dec8319799f --- /dev/null +++ b/htdocs/includes/raven/raven/README.rst @@ -0,0 +1,273 @@ +raven-php +========= + +.. image:: https://secure.travis-ci.org/getsentry/raven-php.png?branch=master + :target: http://travis-ci.org/getsentry/raven-php + + +raven-php is a PHP client for `Sentry `_. + +.. code-block:: php + + // Instantiate a new client with a compatible DSN + $client = new Raven_Client('http://public:secret@example.com/1'); + + // Capture a message + $event_id = $client->getIdent($client->captureMessage('my log message')); + if ($client->getLastError() !== null) { + printf('There was an error sending the event to Sentry: %s', $client->getLastError()); + } + + // Capture an exception + $event_id = $client->getIdent($client->captureException($ex)); + + // Provide some additional data with an exception + $event_id = $client->getIdent($client->captureException($ex, array( + 'extra' => array( + 'php_version' => phpversion() + ), + ))); + + // Give the user feedback + echo "Sorry, there was an error!"; + echo "Your reference ID is " . $event_id; + + // Install error handlers and shutdown function to catch fatal errors + $error_handler = new Raven_ErrorHandler($client); + $error_handler->registerExceptionHandler(); + $error_handler->registerErrorHandler(); + $error_handler->registerShutdownFunction(); + +Installation +------------ + +Install with Composer +~~~~~~~~~~~~~~~~~~~~~ + +If you're using `Composer `_ to manage +dependencies, you can add Raven with it. + +:: + + $ composer require raven/raven:$VERSION + +(replace ``$VERSION`` with one of the available versions on `Packagist `_) +or to get the latest version off the master branch: + +:: + + $ composer require raven/raven:dev-master + +Note that using unstable versions is not recommended and should be avoided. Also +you should define a maximum version, e.g. by doing ``>=0.6,<1.0`` or ``~0.6``. + +Alternatively, use the ``^`` operator for specifying a version, e.g., + +:: + + $ composer require raven/raven:^0.11.0 + +Composer will take care of the autoloading for you, so if you require the +``vendor/autoload.php``, you're good to go. + + +Install source from GitHub +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To install the source code: + +:: + + $ git clone git://github.com/getsentry/raven-php.git + +And including it using the autoloader: + +.. code-block:: php + + require_once '/path/to/Raven/library/Raven/Autoloader.php'; + Raven_Autoloader::register(); + +Testing Your Connection +----------------------- + +The PHP client includes a simple helper script to test your connection and credentials with +the Sentry master server: + +.. code-block:: bash + + $ bin/raven test https://public:secret@app.getsentry.com/1 + Client configuration: + -> servers: [https://sentry.example.com/api/store/] + -> project: 1 + -> public_key: public + -> secret_key: secret + + Sending a test event: + -> event ID: f1765c9aed4f4ceebe5a93df9eb2d34f + + Done! + +.. note:: The CLI enforces the synchronous option on HTTP requests whereas the default configuration is asyncrhonous. + +Configuration +------------- + +Several options exist that allow you to configure the behavior of the ``Raven_Client``. These are passed as the +second parameter of the constructor, and is expected to be an array of key value pairs: + +.. code-block:: php + + $client = new Raven_Client($dsn, array( + 'option_name' => 'value', + )); + +``name`` +~~~~~~~~ + +A string to override the default value for the server's hostname. + +Defaults to ``Raven_Compat::gethostname()``. + +``tags`` +~~~~~~~~ + +An array of tags to apply to events in this context. + +.. code-block:: php + + 'tags' => array( + 'php_version' => phpversion(), + ) + + +``curl_method`` +~~~~~~~~~~~~~~~ + +Defaults to 'sync'. + +Available methods: + +- sync (default): send requests immediately when they're made +- async: uses a curl_multi handler for best-effort asynchronous submissions +- exec: asynchronously send events by forking a curl process for each item + +``curl_path`` +~~~~~~~~~~~~~ + +Defaults to 'curl'. + +Specify the path to the curl binary to be used with the 'exec' curl method. + + +``trace`` +~~~~~~~~~ + +Set this to ``false`` to disable reflection tracing (function calling arguments) in stacktraces. + + +``logger`` +~~~~~~~~~~ + +Adjust the default logger name for messages. + +Defaults to ``php``. + +``ca_cert`` +~~~~~~~~~~~ + +The path to the CA certificate bundle. + +Defaults to the common bundle which includes getsentry.com: ./data/cacert.pem + +Caveats: + +- The CA bundle is ignored unless curl throws an error suggesting it needs a cert. +- The option is only currently used within the synchronous curl transport. + +``curl_ssl_version`` +~~~~~~~~~~~~~~~~~~~~ + +The SSL version (2 or 3) to use. +By default PHP will try to determine this itself, although in some cases this must be set manually. + +``message_limit`` +~~~~~~~~~~~~~~~~~ + +Defaults to 1024 characters. + +This value is used to truncate message and frame variables. However it is not guarantee that length of whole message will be restricted by this value. + +``processors`` +~~~~~~~~~~~~~~~~~ + +An array of classes to use to process data before it is sent to Sentry. By default, Raven_SanitizeDataProcessor is used + +``processorOptions`` +~~~~~~~~~~~~~~~~~ +Options that will be passed on to a setProcessorOptions() function in a Raven_Processor sub-class before that Processor is added to the list of processors used by Raven_Client + +An example of overriding the regular expressions in Raven_SanitizeDataProcessor is below: + +.. code-block:: php + + 'processorOptions' => array( + 'Raven_SanitizeDataProcessor' => array( + 'fields_re' => '/(user_password|user_token|user_secret)/i', + 'values_re' => '/^(?:\d[ -]*?){15,16}$/' + ) + ) + +Providing Request Context +------------------------- + +Most of the time you're not actually calling out to Raven directly, but you still want to provide some additional context. This lifecycle generally constists of something like the following: + +- Set some context via a middleware (e.g. the logged in user) +- Send all given context with any events during the request lifecycle +- Cleanup context + +There are three primary methods for providing request context: + +.. code-block:: php + + // bind the logged in user + $client->user_context(array('email' => 'foo@example.com')); + + // tag the request with something interesting + $client->tags_context(array('interesting' => 'yes')); + + // provide a bit of additional context + $client->extra_context(array('happiness' => 'very')); + + +If you're performing additional requests during the lifecycle, you'll also need to ensure you cleanup the context (to reset its state): + +.. code-block:: php + + $client->context->clear(); + + +Contributing +------------ + +First, make sure you can run the test suite. Install development dependencies : + +:: + + $ composer install + +You may now use phpunit : + +:: + + $ vendor/bin/phpunit + + + +Resources +--------- + +* `Bug Tracker `_ +* `Code `_ +* `Mailing List `_ +* `IRC `_ (irc.freenode.net, #sentry) diff --git a/htdocs/includes/raven/raven/bin/raven b/htdocs/includes/raven/raven/bin/raven new file mode 100755 index 00000000000..4de251918db --- /dev/null +++ b/htdocs/includes/raven/raven/bin/raven @@ -0,0 +1,88 @@ +#!/usr/bin/php +getMessage()); + } + + $client = new Raven_Client($dsn, array( + 'trace' => true, + 'curl_method' => 'sync', + )); + + $config = get_object_vars($client); + $required_keys = array('servers', 'project', 'public_key', 'secret_key'); + + echo "Client configuration:\n"; + foreach ($required_keys as $key) { + if (empty($config[$key])) { + exit("ERROR: Missing configuration for $key"); + } + if (is_array($config[$key])) { + echo "-> $key: [".implode(", ", $config[$key])."]\n"; + } else { + echo "-> $key: $config[$key]\n"; + } + + } + echo "\n"; + + echo "Sending a test event:\n"; + + $ex = raven_cli_test("command name", array("foo" => "bar")); + $event_id = $client->captureException($ex); + + echo "-> event ID: $event_id\n"; + + $last_error = $client->getLastError(); + if (!empty($last_error)) { + exit("ERROR: There was an error sending the test event:\n " . $last_error); + } + + echo "\n"; + echo "Done!"; +} + + +function main() { + global $argv; + + $cmd = $argv[1]; + + switch ($cmd) { + case 'test': + cmd_test(@$argv[2]); + break; + default: + exit('Usage: raven test '); + } +} + +main(); diff --git a/htdocs/includes/raven/raven/composer.json b/htdocs/includes/raven/raven/composer.json new file mode 100644 index 00000000000..b8799a6c6df --- /dev/null +++ b/htdocs/includes/raven/raven/composer.json @@ -0,0 +1,35 @@ +{ + "name": "raven/raven", + "type": "library", + "description": "A PHP client for Sentry (http://getsentry.com)", + "keywords": ["log", "logging"], + "homepage": "http://getsentry.com", + "license": "BSD", + "authors": [ + { + "name": "David Cramer", + "email": "dcramer@gmail.com" + } + ], + "require-dev": { + "fabpot/php-cs-fixer": "^1.8.0", + "phpunit/phpunit": "^4.6.6" + }, + "require": { + "php": ">=5.2.4", + "ext-curl": "*" + }, + "bin": [ + "bin/raven" + ], + "autoload": { + "psr-0" : { + "Raven_" : "lib/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "0.12.x-dev" + } + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/Autoloader.php b/htdocs/includes/raven/raven/lib/Raven/Autoloader.php new file mode 100644 index 00000000000..d5adf1a58c4 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/Autoloader.php @@ -0,0 +1,43 @@ +logger = Raven_Util::get($options, 'logger', 'php'); + $this->servers = Raven_Util::get($options, 'servers'); + $this->secret_key = Raven_Util::get($options, 'secret_key'); + $this->public_key = Raven_Util::get($options, 'public_key'); + $this->project = Raven_Util::get($options, 'project', 1); + $this->auto_log_stacks = (bool) Raven_Util::get($options, 'auto_log_stacks', false); + $this->name = Raven_Util::get($options, 'name', Raven_Compat::gethostname()); + $this->site = Raven_Util::get($options, 'site', $this->_server_variable('SERVER_NAME')); + $this->tags = Raven_Util::get($options, 'tags', array()); + $this->release = Raven_util::get($options, 'release', null); + $this->trace = (bool) Raven_Util::get($options, 'trace', true); + $this->timeout = Raven_Util::get($options, 'timeout', 2); + $this->message_limit = Raven_Util::get($options, 'message_limit', self::MESSAGE_LIMIT); + $this->exclude = Raven_Util::get($options, 'exclude', array()); + $this->severity_map = null; + $this->shift_vars = (bool) Raven_Util::get($options, 'shift_vars', true); + $this->http_proxy = Raven_Util::get($options, 'http_proxy'); + $this->extra_data = Raven_Util::get($options, 'extra', array()); + $this->send_callback = Raven_Util::get($options, 'send_callback', null); + $this->curl_method = Raven_Util::get($options, 'curl_method', 'sync'); + $this->curl_path = Raven_Util::get($options, 'curl_path', 'curl'); + $this->curl_ipv4 = Raven_util::get($options, 'curl_ipv4', true); + $this->ca_cert = Raven_util::get($options, 'ca_cert', $this->get_default_ca_cert()); + $this->verify_ssl = Raven_util::get($options, 'verify_ssl', true); + $this->curl_ssl_version = Raven_Util::get($options, 'curl_ssl_version'); + + $this->processors = $this->setProcessorsFromOptions($options); + + $this->_lasterror = null; + $this->_user = null; + $this->context = new Raven_Context(); + + if ($this->curl_method == 'async') { + $this->_curl_handler = new Raven_CurlHandler($this->get_curl_options()); + } + } + + public static function getDefaultProcessors() + { + return array( + 'Raven_SanitizeDataProcessor', + ); + } + + /** + * Sets the Raven_Processor sub-classes to be used when data is processed before being + * sent to Sentry. + * + * @param $options + * @return array + */ + public function setProcessorsFromOptions($options) + { + $processors = array(); + foreach (Raven_util::get($options, 'processors', self::getDefaultProcessors()) as $processor) { + $new_processor = new $processor($this); + + if (isset($options['processorOptions']) && is_array($options['processorOptions'])) { + if (isset($options['processorOptions'][$processor]) && method_exists($processor, 'setProcessorOptions')) { + $new_processor->setProcessorOptions($options['processorOptions'][$processor]); + } + } + $processors[] = $new_processor; + } + return $processors; + } + + /** + * Parses a Raven-compatible DSN and returns an array of its values. + * + * @param string $dsn Raven compatible DSN: http://raven.readthedocs.org/en/latest/config/#the-sentry-dsn + * @return array parsed DSN + */ + public static function parseDSN($dsn) + { + $url = parse_url($dsn); + $scheme = (isset($url['scheme']) ? $url['scheme'] : ''); + if (!in_array($scheme, array('http', 'https'))) { + throw new InvalidArgumentException('Unsupported Sentry DSN scheme: ' . (!empty($scheme) ? $scheme : '')); + } + $netloc = (isset($url['host']) ? $url['host'] : null); + $netloc.= (isset($url['port']) ? ':'.$url['port'] : null); + $rawpath = (isset($url['path']) ? $url['path'] : null); + if ($rawpath) { + $pos = strrpos($rawpath, '/', 1); + if ($pos !== false) { + $path = substr($rawpath, 0, $pos); + $project = substr($rawpath, $pos + 1); + } else { + $path = ''; + $project = substr($rawpath, 1); + } + } else { + $project = null; + $path = ''; + } + $username = (isset($url['user']) ? $url['user'] : null); + $password = (isset($url['pass']) ? $url['pass'] : null); + if (empty($netloc) || empty($project) || empty($username) || empty($password)) { + throw new InvalidArgumentException('Invalid Sentry DSN: ' . $dsn); + } + + return array( + 'servers' => array(sprintf('%s://%s%s/api/%s/store/', $scheme, $netloc, $path, $project)), + 'project' => $project, + 'public_key' => $username, + 'secret_key' => $password, + ); + } + + public function getLastError() + { + return $this->_lasterror; + } + + /** + * Given an identifier, returns a Sentry searchable string. + */ + public function getIdent($ident) + { + // XXX: We don't calculate checksums yet, so we only have the ident. + return $ident; + } + + /** + * Deprecated + */ + public function message($message, $params=array(), $level=self::INFO, + $stack=false, $vars = null) + { + return $this->captureMessage($message, $params, $level, $stack, $vars); + } + + /** + * Deprecated + */ + public function exception($exception) + { + return $this->captureException($exception); + } + + /** + * Log a message to sentry + */ + public function captureMessage($message, $params=array(), $level_or_options=array(), + $stack=false, $vars = null) + { + // Gracefully handle messages which contain formatting characters, but were not + // intended to be used with formatting. + if (!empty($params)) { + $formatted_message = vsprintf($message, $params); + } else { + $formatted_message = $message; + } + + if ($level_or_options === null) { + $data = array(); + } elseif (!is_array($level_or_options)) { + $data = array( + 'level' => $level_or_options, + ); + } else { + $data = $level_or_options; + } + + $data['message'] = $formatted_message; + $data['sentry.interfaces.Message'] = array( + 'message' => $message, + 'params' => $params, + ); + + return $this->capture($data, $stack, $vars); + } + + /** + * Log an exception to sentry + */ + public function captureException($exception, $culprit_or_options=null, $logger=null, $vars=null) + { + $has_chained_exceptions = version_compare(PHP_VERSION, '5.3.0', '>='); + + if (in_array(get_class($exception), $this->exclude)) { + return null; + } + + if (!is_array($culprit_or_options)) { + $data = array(); + if ($culprit_or_options !== null) { + $data['culprit'] = $culprit_or_options; + } + } else { + $data = $culprit_or_options; + } + + // TODO(dcramer): DRY this up + $message = $exception->getMessage(); + if (empty($message)) { + $message = get_class($exception); + } + + $exc = $exception; + do { + $exc_data = array( + 'value' => $exc->getMessage(), + 'type' => get_class($exc), + 'module' => $exc->getFile() .':'. $exc->getLine(), + ); + + /**'sentry.interfaces.Exception' + * Exception::getTrace doesn't store the point at where the exception + * was thrown, so we have to stuff it in ourselves. Ugh. + */ + $trace = $exc->getTrace(); + $frame_where_exception_thrown = array( + 'file' => $exc->getFile(), + 'line' => $exc->getLine(), + ); + + array_unshift($trace, $frame_where_exception_thrown); + + // manually trigger autoloading, as it's not done in some edge cases due to PHP bugs (see #60149) + if (!class_exists('Raven_Stacktrace')) { + spl_autoload_call('Raven_Stacktrace'); + } + + $exc_data['stacktrace'] = array( + 'frames' => Raven_Stacktrace::get_stack_info( + $trace, $this->trace, $this->shift_vars, $vars, $this->message_limit + ), + ); + + $exceptions[] = $exc_data; + } while ($has_chained_exceptions && $exc = $exc->getPrevious()); + + $data['message'] = $message; + $data['sentry.interfaces.Exception'] = array( + 'values' => array_reverse($exceptions), + ); + if ($logger !== null) { + $data['logger'] = $logger; + } + + if (empty($data['level'])) { + if (method_exists($exception, 'getSeverity')) { + $data['level'] = $this->translateSeverity($exception->getSeverity()); + } else { + $data['level'] = self::ERROR; + } + } + + return $this->capture($data, $trace, $vars); + } + + /** + * Log an query to sentry + */ + public function captureQuery($query, $level=self::INFO, $engine = '') + { + $data = array( + 'message' => $query, + 'level' => $level, + 'sentry.interfaces.Query' => array( + 'query' => $query + ) + ); + + if ($engine !== '') { + $data['sentry.interfaces.Query']['engine'] = $engine; + } + return $this->capture($data, false); + } + + protected function is_http_request() + { + return isset($_SERVER['REQUEST_METHOD']) && PHP_SAPI !== 'cli'; + } + + protected function get_http_data() + { + $env = $headers = array(); + + foreach ($_SERVER as $key => $value) { + if (0 === strpos($key, 'HTTP_')) { + if (in_array($key, array('HTTP_CONTENT_TYPE', 'HTTP_CONTENT_LENGTH'))) { + continue; + } + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', substr($key, 5)))))] = $value; + } elseif (in_array($key, array('CONTENT_TYPE', 'CONTENT_LENGTH'))) { + $headers[str_replace(' ', '-', ucwords(strtolower(str_replace('_', ' ', $key))))] = $value; + } else { + $env[$key] = $value; + } + } + + $result = array( + 'method' => $this->_server_variable('REQUEST_METHOD'), + 'url' => $this->get_current_url(), + 'query_string' => $this->_server_variable('QUERY_STRING'), + ); + + // dont set this as an empty array as PHP will treat it as a numeric array + // instead of a mapping which goes against the defined Sentry spec + if (!empty($_POST)) { + $result['data'] = $_POST; + } + if (!empty($_COOKIE)) { + $result['cookies'] = $_COOKIE; + } + if (!empty($headers)) { + $result['headers'] = $headers; + } + if (!empty($env)) { + $result['env'] = $env; + } + + return array( + 'sentry.interfaces.Http' => $result, + ); + } + + protected function get_user_data() + { + $user = $this->context->user; + if ($user === null) { + if (!session_id()) { + return array(); + } + $user = array( + 'id' => session_id(), + ); + if (!empty($_SESSION)) { + $user['data'] = $_SESSION; + } + } + return array( + 'sentry.interfaces.User' => $user, + ); + } + + protected function get_extra_data() + { + return $this->extra_data; + } + + public function get_default_data() + { + return array( + 'server_name' => $this->name, + 'project' => $this->project, + 'site' => $this->site, + 'logger' => $this->logger, + 'tags' => $this->tags, + 'platform' => 'php', + ); + } + + public function capture($data, $stack, $vars = null) + { + if (!isset($data['timestamp'])) { + $data['timestamp'] = gmdate('Y-m-d\TH:i:s\Z'); + } + if (!isset($data['level'])) { + $data['level'] = self::ERROR; + } + if (!isset($data['tags'])) { + $data['tags'] = array(); + } + if (!isset($data['extra'])) { + $data['extra'] = array(); + } + if (!isset($data['event_id'])) { + $data['event_id'] = $this->uuid4(); + } + + if (isset($data['message'])) { + $data['message'] = substr($data['message'], 0, $this->message_limit); + } + + $data = array_merge($this->get_default_data(), $data); + + if ($this->is_http_request()) { + $data = array_merge($this->get_http_data(), $data); + } + + $data = array_merge($this->get_user_data(), $data); + + if ($this->release) { + $data['release'] = $this->release; + } + + $data['tags'] = array_merge( + $this->tags, + $this->context->tags, + $data['tags']); + + $data['extra'] = array_merge( + $this->get_extra_data(), + $this->context->extra, + $data['extra']); + + if ((!$stack && $this->auto_log_stacks) || $stack === true) { + $stack = debug_backtrace(); + + // Drop last stack + array_shift($stack); + } + + if (!empty($stack)) { + // manually trigger autoloading, as it's not done in some edge cases due to PHP bugs (see #60149) + if (!class_exists('Raven_Stacktrace')) { + spl_autoload_call('Raven_Stacktrace'); + } + + if (!isset($data['sentry.interfaces.Stacktrace'])) { + $data['sentry.interfaces.Stacktrace'] = array( + 'frames' => Raven_Stacktrace::get_stack_info( + $stack, $this->trace, $this->shift_vars, $vars, $this->message_limit + ), + ); + } + } + + $this->sanitize($data); + $this->process($data); + + if (!$this->store_errors_for_bulk_send) { + $this->send($data); + } else { + if (empty($this->error_data)) { + $this->error_data = array(); + } + $this->error_data[] = $data; + } + + return $data['event_id']; + } + + public function sanitize(&$data) + { + // manually trigger autoloading, as it's not done in some edge cases due to PHP bugs (see #60149) + if (!class_exists('Raven_Serializer')) { + spl_autoload_call('Raven_Serializer'); + } + + $data = Raven_Serializer::serialize($data); + } + + /** + * Process data through all defined Raven_Processor sub-classes + * + * @param array $data Associative array of data to log + */ + public function process(&$data) + { + foreach ($this->processors as $processor) { + $processor->process($data); + } + } + + public function sendUnsentErrors() + { + if (!empty($this->error_data)) { + foreach ($this->error_data as $data) { + $this->send($data); + } + unset($this->error_data); + } + if ($this->store_errors_for_bulk_send) { + //in case an error occurs after this is called, on shutdown, send any new errors. + $this->store_errors_for_bulk_send = !defined('RAVEN_CLIENT_END_REACHED'); + } + } + + /** + * Wrapper to handle encoding and sending data to all defined Sentry servers + * + * @param array $data Associative array of data to log + */ + public function send($data) + { + if (is_callable($this->send_callback) && !call_user_func($this->send_callback, $data)) { + // if send_callback returns falsely, end native send + return; + } + + if (!$this->servers) { + return; + } + + $message = Raven_Compat::json_encode($data); + + if (function_exists("gzcompress")) { + $message = gzcompress($message); + } + $message = base64_encode($message); // PHP's builtin curl_* function are happy without this, but the exec method requires it + + foreach ($this->servers as $url) { + $client_string = 'raven-php/' . self::VERSION; + $timestamp = microtime(true); + $headers = array( + 'User-Agent' => $client_string, + 'X-Sentry-Auth' => $this->get_auth_header( + $timestamp, $client_string, $this->public_key, + $this->secret_key), + 'Content-Type' => 'application/octet-stream' + ); + + $this->send_remote($url, $message, $headers); + } + } + + /** + * Send data to Sentry + * + * @param string $url Full URL to Sentry + * @param array $data Associative array of data to log + * @param array $headers Associative array of headers + */ + private function send_remote($url, $data, $headers=array()) + { + $parts = parse_url($url); + $parts['netloc'] = $parts['host'].(isset($parts['port']) ? ':'.$parts['port'] : null); + $this->send_http($url, $data, $headers); + } + + protected function get_default_ca_cert() + { + return dirname(__FILE__) . DIRECTORY_SEPARATOR . 'data' . DIRECTORY_SEPARATOR . 'cacert.pem'; + } + + protected function get_curl_options() + { + $options = array( + CURLOPT_VERBOSE => false, + CURLOPT_SSL_VERIFYHOST => 2, + CURLOPT_SSL_VERIFYPEER => $this->verify_ssl, + CURLOPT_CAINFO => $this->ca_cert, + CURLOPT_USERAGENT => 'raven-php/' . self::VERSION, + ); + if ($this->http_proxy) { + $options[CURLOPT_PROXY] = $this->http_proxy; + } + if ($this->curl_ssl_version) { + $options[CURLOPT_SSLVERSION] = $this->curl_ssl_version; + } + if ($this->curl_ipv4) { + $options[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4; + } + if (defined('CURLOPT_TIMEOUT_MS')) { + // MS is available in curl >= 7.16.2 + $timeout = max(1, ceil(1000 * $this->timeout)); + + // some versions of PHP 5.3 don't have this defined correctly + if (!defined('CURLOPT_CONNECTTIMEOUT_MS')) { + //see http://stackoverflow.com/questions/9062798/php-curl-timeout-is-not-working/9063006#9063006 + define('CURLOPT_CONNECTTIMEOUT_MS', 156); + } + + $options[CURLOPT_CONNECTTIMEOUT_MS] = $timeout; + $options[CURLOPT_TIMEOUT_MS] = $timeout; + } else { + // fall back to the lower-precision timeout. + $timeout = max(1, ceil($this->timeout)); + $options[CURLOPT_CONNECTTIMEOUT] = $timeout; + $options[CURLOPT_TIMEOUT] = $timeout; + } + return $options; + } + + /** + * Send the message over http to the sentry url given + * + * @param string $url URL of the Sentry instance to log to + * @param array $data Associative array of data to log + * @param array $headers Associative array of headers + */ + private function send_http($url, $data, $headers=array()) + { + if ($this->curl_method == 'async') { + $this->_curl_handler->enqueue($url, $data, $headers); + } elseif ($this->curl_method == 'exec') { + $this->send_http_asynchronous_curl_exec($url, $data, $headers); + } else { + $this->send_http_synchronous($url, $data, $headers); + } + } + + /** + * Send the cURL to Sentry asynchronously. No errors will be returned from cURL + * + * @param string $url URL of the Sentry instance to log to + * @param array $data Associative array of data to log + * @param array $headers Associative array of headers + * @return bool + */ + private function send_http_asynchronous_curl_exec($url, $data, $headers) + { + // TODO(dcramer): support ca_cert + $cmd = $this->curl_path.' -X POST '; + foreach ($headers as $key => $value) { + $cmd .= '-H \''. $key. ': '. $value. '\' '; + } + $cmd .= '-d \''. $data .'\' '; + $cmd .= '\''. $url .'\' '; + $cmd .= '-m 5 '; // 5 second timeout for the whole process (connect + send) + $cmd .= '> /dev/null 2>&1 &'; // ensure exec returns immediately while curl runs in the background + + exec($cmd); + + return true; // The exec method is just fire and forget, so just assume it always works + } + + /** + * Send a blocking cURL to Sentry and check for errors from cURL + * + * @param string $url URL of the Sentry instance to log to + * @param array $data Associative array of data to log + * @param array $headers Associative array of headers + * @return bool + */ + private function send_http_synchronous($url, $data, $headers) + { + $new_headers = array(); + foreach ($headers as $key => $value) { + array_push($new_headers, $key .': '. $value); + } + // XXX(dcramer): Prevent 100-continue response form server (Fixes GH-216) + $new_headers[] = 'Expect:'; + + $curl = curl_init($url); + curl_setopt($curl, CURLOPT_POST, 1); + curl_setopt($curl, CURLOPT_HTTPHEADER, $new_headers); + curl_setopt($curl, CURLOPT_POSTFIELDS, $data); + curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); + + $options = $this->get_curl_options(); + $ca_cert = $options[CURLOPT_CAINFO]; + unset($options[CURLOPT_CAINFO]); + curl_setopt_array($curl, $options); + + curl_exec($curl); + + $errno = curl_errno($curl); + // CURLE_SSL_CACERT || CURLE_SSL_CACERT_BADFILE + if ($errno == 60 || $errno == 77) { + curl_setopt($curl, CURLOPT_CAINFO, $ca_cert); + curl_exec($curl); + } + + $code = curl_getinfo($curl, CURLINFO_HTTP_CODE); + $success = ($code == 200); + if (!$success) { + // It'd be nice just to raise an exception here, but it's not very PHP-like + $this->_lasterror = curl_error($curl); + } else { + $this->_lasterror = null; + } + curl_close($curl); + + return $success; + } + + /** + * Generate a Sentry authorization header string + * + * @param string $timestamp Timestamp when the event occurred + * @param string $client HTTP client name (not Raven_Client object) + * @param string $api_key Sentry API key + * @param string $secret_key Sentry API key + * @return string + */ + protected function get_auth_header($timestamp, $client, $api_key, $secret_key) + { + $header = array( + sprintf('sentry_timestamp=%F', $timestamp), + "sentry_client={$client}", + sprintf('sentry_version=%s', self::PROTOCOL), + ); + + if ($api_key) { + $header[] = "sentry_key={$api_key}"; + } + + if ($secret_key) { + $header[] = "sentry_secret={$secret_key}"; + } + + + return sprintf('Sentry %s', implode(', ', $header)); + } + + /** + * Generate an uuid4 value + * + * @return string + */ + private function uuid4() + { + $uuid = 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) + ); + + return str_replace('-', '', $uuid); + } + + /** + * Return the URL for the current request + * + * @return string|null + */ + private function get_current_url() + { + // When running from commandline the REQUEST_URI is missing. + if (!isset($_SERVER['REQUEST_URI'])) { + return null; + } + + $schema = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off' + || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://"; + + // HTTP_HOST is a client-supplied header that is optional in HTTP 1.0 + $host = (!empty($_SERVER['HTTP_HOST']) ? $_SERVER['HTTP_HOST'] + : (!empty($_SERVER['LOCAL_ADDR']) ? $_SERVER['LOCAL_ADDR'] + : (!empty($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : ''))); + + return $schema . $host . $_SERVER['REQUEST_URI']; + } + + /** + * Get the value of a key from $_SERVER + * + * @param string $key Key whose value you wish to obtain + * @return string Key's value + */ + private function _server_variable($key) + { + if (isset($_SERVER[$key])) { + return $_SERVER[$key]; + } + + return ''; + } + + /** + * Translate a PHP Error constant into a Sentry log level group + * + * @param string $severity PHP E_$x error constant + * @return string Sentry log level group + */ + public function translateSeverity($severity) + { + if (is_array($this->severity_map) && isset($this->severity_map[$severity])) { + return $this->severity_map[$severity]; + } + switch ($severity) { + case E_ERROR: return Raven_Client::ERROR; + case E_WARNING: return Raven_Client::WARN; + case E_PARSE: return Raven_Client::ERROR; + case E_NOTICE: return Raven_Client::INFO; + case E_CORE_ERROR: return Raven_Client::ERROR; + case E_CORE_WARNING: return Raven_Client::WARN; + case E_COMPILE_ERROR: return Raven_Client::ERROR; + case E_COMPILE_WARNING: return Raven_Client::WARN; + case E_USER_ERROR: return Raven_Client::ERROR; + case E_USER_WARNING: return Raven_Client::WARN; + case E_USER_NOTICE: return Raven_Client::INFO; + case E_STRICT: return Raven_Client::INFO; + case E_RECOVERABLE_ERROR: return Raven_Client::ERROR; + } + if (version_compare(PHP_VERSION, '5.3.0', '>=')) { + switch ($severity) { + case E_DEPRECATED: return Raven_Client::WARN; + case E_USER_DEPRECATED: return Raven_Client::WARN; + } + } + return Raven_Client::ERROR; + } + + /** + * Provide a map of PHP Error constants to Sentry logging groups to use instead + * of the defaults in translateSeverity() + * + * @param array $map + */ + public function registerSeverityMap($map) + { + $this->severity_map = $map; + } + + /** + * Convenience function for setting a user's ID and Email + * + * @param string $id User's ID + * @param string|null $email User's email + * @param array $data Additional user data + */ + public function set_user_data($id, $email=null, $data=array()) + { + $this->user_context(array_merge(array( + 'id' => $id, + 'email' => $email, + ), $data)); + } + + /** + * Sets user context. + * + * @param array $data Associative array of user data + */ + public function user_context($data) + { + $this->context->user = $data; + } + + /** + * Appends tags context. + * + * @param array $data Associative array of tags + */ + public function tags_context($data) + { + $this->context->tags = array_merge($this->context->tags, $data); + } + + /** + * Appends additional context. + * + * @param array $data Associative array of extra data + */ + public function extra_context($data) + { + $this->context->extra = array_merge($this->context->extra, $data); + } + + /** + * @param array $processors + */ + public function setProcessors(array $processors) + { + $this->processors = $processors; + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/Compat.php b/htdocs/includes/raven/raven/lib/Raven/Compat.php new file mode 100644 index 00000000000..08c85a99740 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/Compat.php @@ -0,0 +1,136 @@ + $size) { + $key = str_pad(pack($pack, $algo($key)), $size, chr(0x00)); + } else { + $key = str_pad($key, $size, chr(0x00)); + } + + $keyLastPos = strlen($key) - 1; + for ($i = 0; $i < $keyLastPos; $i++) { + $opad[$i] = $opad[$i] ^ $key[$i]; + $ipad[$i] = $ipad[$i] ^ $key[$i]; + } + + $output = $algo($opad.pack($pack, $algo($ipad.$data))); + + return ($raw_output) ? pack($pack, $output) : $output; + } + + /** + * Note that we discard the options given to be compatible + * with PHP < 5.3 + */ + public static function json_encode($value, $options=0) + { + if (function_exists('json_encode')) { + return json_encode($value); + } + + return self::_json_encode($value); + } + + /** + * Implementation taken from + * http://www.mike-griffiths.co.uk/php-json_encode-alternative/ + */ + public static function _json_encode($value) + { + static $jsonReplaces = array( + array('\\', '/', "\n", "\t", "\r", "\b", "\f", '"'), + array('\\\\', '\\/', '\\n', '\\t', '\\r', '\\b', '\\f', '\"')); + + if (is_null($value)) { + return 'null'; + } + if ($value === false) { + return 'false'; + } + if ($value === true) { + return 'true'; + } + + if (is_scalar($value)) { + + // Always use '.' for floats. + if (is_float($value)) { + return floatval(str_replace(',', '.', strval($value))); + } + if (is_string($value)) { + return sprintf('"%s"', + str_replace($jsonReplaces[0], $jsonReplaces[1], $value)); + } else { + return $value; + } + } + + $isList = true; + for ($i = 0, reset($value); $i $v) { + $result[] = self::_json_encode($k) . ':' . self::_json_encode($v); + } + + return '{' . join(',', $result) . '}'; + } + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/Context.php b/htdocs/includes/raven/raven/lib/Raven/Context.php new file mode 100644 index 00000000000..ef56adb4ab5 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/Context.php @@ -0,0 +1,23 @@ +clear(); + } + + /** + * Clean up existing context. + */ + public function clear() + { + $this->tags = array(); + $this->extra = array(); + $this->user = null; + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/CurlHandler.php b/htdocs/includes/raven/raven/lib/Raven/CurlHandler.php new file mode 100644 index 00000000000..cce4f8a9851 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/CurlHandler.php @@ -0,0 +1,117 @@ +options = $options; + $this->multi_handle = curl_multi_init(); + $this->requests = array(); + $this->join_timeout = 5; + + register_shutdown_function(array($this, 'join')); + } + + public function __destruct() + { + $this->join(); + } + + public function enqueue($url, $data=null, $headers=array()) + { + $ch = curl_init(); + + $new_headers = array(); + foreach ($headers as $key => $value) { + array_push($new_headers, $key .': '. $value); + } + // XXX(dcramer): Prevent 100-continue response form server (Fixes GH-216) + $new_headers[] = 'Expect:'; + + curl_setopt($ch, CURLOPT_HTTPHEADER, $new_headers); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_URL, $url); + + curl_setopt_array($ch, $this->options); + + if (isset($data)) { + curl_setopt($ch, CURLOPT_POST, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $data); + } + + curl_multi_add_handle($this->multi_handle, $ch); + + $fd = (int)$ch; + $this->requests[$fd] = 1; + + $this->select(); + + return $fd; + } + + public function join($timeout=null) + { + if (!isset($timeout)) { + $timeout = $this->join_timeout; + } + $start = time(); + do { + $this->select(); + if (count($this->requests) === 0) { + break; + } + usleep(10000); + } while ($timeout !== 0 && time() - $start > $timeout); + } + + // http://se2.php.net/manual/en/function.curl-multi-exec.php + private function select() + { + do { + $mrc = curl_multi_exec($this->multi_handle, $active); + } while ($mrc == CURLM_CALL_MULTI_PERFORM); + + while ($active && $mrc == CURLM_OK) { + if (curl_multi_select($this->multi_handle) !== -1) { + do { + $mrc = curl_multi_exec($this->multi_handle, $active); + } while ($mrc == CURLM_CALL_MULTI_PERFORM); + } else { + return; + } + } + + while ($info = curl_multi_info_read($this->multi_handle)) { + $ch = $info['handle']; + $fd = (int)$ch; + + curl_multi_remove_handle($this->multi_handle, $ch); + + if (!isset($this->requests[$fd])) { + return; + } + + unset($this->requests[$fd]); + } + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/ErrorHandler.php b/htdocs/includes/raven/raven/lib/Raven/ErrorHandler.php new file mode 100644 index 00000000000..3632e62e269 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/ErrorHandler.php @@ -0,0 +1,175 @@ +registerExceptionHandler(); + * $error_handler->registerErrorHandler(); + * $error_handler->registerShutdownFunction(); + * + * @package raven + */ + +class Raven_ErrorHandler +{ + private $old_exception_handler; + private $call_existing_exception_handler = false; + private $old_error_handler; + private $call_existing_error_handler = false; + private $reservedMemory; + private $send_errors_last = false; + private $error_types = -1; + + /** + * @var array + * Error types that can be processed by the handler + */ + private $validErrorTypes = array( + E_ERROR, + E_WARNING, + E_PARSE, + E_NOTICE, + E_CORE_ERROR, + E_CORE_WARNING, + E_COMPILE_ERROR, + E_COMPILE_WARNING, + E_USER_ERROR, + E_USER_WARNING, + E_USER_NOTICE, + E_STRICT, + E_RECOVERABLE_ERROR, + E_DEPRECATED, + E_USER_DEPRECATED, + ); + + /** + * @var array + * Error types that are always processed by the handler + */ + private $defaultErrorTypes = array( + E_ERROR, + E_PARSE, + E_CORE_ERROR, + E_CORE_WARNING, + E_COMPILE_ERROR, + E_COMPILE_WARNING, + E_STRICT, + ); + + public function __construct($client, $send_errors_last = false) + { + $this->client = $client; + register_shutdown_function(array($this, 'detectShutdown')); + if ($send_errors_last) { + $this->send_errors_last = true; + $this->client->store_errors_for_bulk_send = true; + register_shutdown_function(array($this->client, 'sendUnsentErrors')); + } + } + + public function handleException($e, $isError = false, $vars = null) + { + $e->event_id = $this->client->getIdent($this->client->captureException($e, null, null, $vars)); + + if (!$isError && $this->call_existing_exception_handler && $this->old_exception_handler) { + call_user_func($this->old_exception_handler, $e); + } + } + + public function handleError($code, $message, $file = '', $line = 0, $context=array()) + { + if ($this->error_types & $code & error_reporting()) { + $e = new ErrorException($message, 0, $code, $file, $line); + $this->handleException($e, true, $context); + } + + if ($this->call_existing_error_handler) { + if ($this->old_error_handler) { + return call_user_func($this->old_error_handler, $code, $message, $file, $line, $context); + } else { + return false; + } + } + } + + /** + * Nothing by default, use it in child classes for catching other types of errors + * Only constants from $this->validErrorTypes can be used + * + * @return array + */ + protected function getAdditionalErrorTypesToProcess() + { + return array(); + } + + /** + * @return array + */ + private function getErrorTypesToProcess() + { + $additionalErrorTypes = array_intersect($this->getAdditionalErrorTypesToProcess(), $this->validErrorTypes); + // array_unique so bitwise "or" operation wouldn't fail if some error type gets repeated + return array_unique($this->defaultErrorTypes + $additionalErrorTypes); + } + + public function handleFatalError() + { + if (null === $lastError = error_get_last()) { + return; + } + + unset($this->reservedMemory); + + $errors = 0; + foreach ($this->getErrorTypesToProcess() as $errorType) { + $errors |= $errorType; + } + + if ($lastError['type'] & $errors) { + $e = new ErrorException( + @$lastError['message'], @$lastError['type'], @$lastError['type'], + @$lastError['file'], @$lastError['line'] + ); + $this->handleException($e, true); + } + } + + public function registerExceptionHandler($call_existing_exception_handler = true) + { + $this->old_exception_handler = set_exception_handler(array($this, 'handleException')); + $this->call_existing_exception_handler = $call_existing_exception_handler; + } + + public function registerErrorHandler($call_existing_error_handler = true, $error_types = -1) + { + $this->error_types = $error_types; + $this->old_error_handler = set_error_handler(array($this, 'handleError'), error_reporting()); + $this->call_existing_error_handler = $call_existing_error_handler; + } + + public function registerShutdownFunction($reservedMemorySize = 10) + { + register_shutdown_function(array($this, 'handleFatalError')); + + $this->reservedMemory = str_repeat('x', 1024 * $reservedMemorySize); + } + + public function detectShutdown() + { + if (!defined('RAVEN_CLIENT_END_REACHED')) { + define('RAVEN_CLIENT_END_REACHED', true); + } + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/Processor.php b/htdocs/includes/raven/raven/lib/Raven/Processor.php new file mode 100644 index 00000000000..7e37400d14e --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/Processor.php @@ -0,0 +1,20 @@ +client = $client; + } + + /** + * Process and sanitize data, modifying the existing value if necessary. + * + * @param array $data Array of log data + */ + abstract public function process(&$data); +} diff --git a/htdocs/includes/raven/raven/lib/Raven/SanitizeDataProcessor.php b/htdocs/includes/raven/raven/lib/Raven/SanitizeDataProcessor.php new file mode 100644 index 00000000000..dbf1c78bb29 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/SanitizeDataProcessor.php @@ -0,0 +1,102 @@ +client = $client; + $this->fields_re = self::FIELDS_RE; + $this->values_re = self::VALUES_RE; + } + + /** + * Override the default processor options + * + * @param array $options Associative array of processor options + */ + public function setProcessorOptions(array $options) + { + if (isset($options['fields_re'])) { + $this->fields_re = $options['fields_re']; + } + + if (isset($options['values_re'])) { + $this->values_re = $options['values_re']; + } + } + + /** + * Replace any array values with our mask if the field name or the value matches a respective regex + * + * @param mixed $item Associative array value + * @param string $key Associative array key + */ + public function sanitize(&$item, $key) + { + if (empty($item)) { + return; + } + + if (preg_match($this->values_re, $item)) { + $item = self::MASK; + } + + if (empty($key)) { + return; + } + + if (preg_match($this->fields_re, $key)) { + $item = self::MASK; + } + } + + public function process(&$data) + { + array_walk_recursive($data, array($this, 'sanitize')); + } + + /** + * @return string + */ + public function getFieldsRe() + { + return $this->fields_re; + } + + /** + * @param string $fields_re + */ + public function setFieldsRe($fields_re) + { + $this->fields_re = $fields_re; + } + + /** + * @return string + */ + public function getValuesRe() + { + return $this->values_re; + } + + /** + * @param string $values_re + */ + public function setValuesRe($values_re) + { + $this->values_re = $values_re; + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/Serializer.php b/htdocs/includes/raven/raven/lib/Raven/Serializer.php new file mode 100644 index 00000000000..3423dbf0fe9 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/Serializer.php @@ -0,0 +1,77 @@ + $v) { + $new[self::serializeValue($k)] = self::serialize($v, $max_depth, $_depth + 1); + } + + return $new; + } else { + return self::serializeValue($value); + } + } + + public static function serializeValue($value) + { + if ($value === null) { + return 'null'; + } elseif ($value === false) { + return 'false'; + } elseif ($value === true) { + return 'true'; + } elseif (is_float($value) && (int) $value == $value) { + return $value.'.0'; + } elseif (is_object($value) || gettype($value) == 'object') { + return 'Object '.get_class($value); + } elseif (is_resource($value)) { + return 'Resource '.get_resource_type($value); + } elseif (is_array($value)) { + return 'Array of length ' . count($value); + } elseif (is_integer($value)) { + return (integer) $value; + } else { + $value = (string) $value; + + if (function_exists('mb_convert_encoding')) { + $value = mb_convert_encoding($value, 'UTF-8', 'UTF-8'); + } + + return $value; + } + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/Stacktrace.php b/htdocs/includes/raven/raven/lib/Raven/Stacktrace.php new file mode 100644 index 00000000000..bc5d4f8195b --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/Stacktrace.php @@ -0,0 +1,252 @@ + $abs_path, + 'filename' => $context['filename'], + 'lineno' => (int) $context['lineno'], + 'module' => $module, + 'function' => $nextframe['function'], + 'pre_context' => $context['prefix'], + 'context_line' => $context['line'], + 'post_context' => $context['suffix'], + ); + // dont set this as an empty array as PHP will treat it as a numeric array + // instead of a mapping which goes against the defined Sentry spec + if (!empty($vars)) { + foreach ($vars as $key => $value) { + if (is_string($value) || is_numeric($value)) { + $vars[$key] = substr($value, 0, $frame_var_limit); + } + } + $frame['vars'] = $vars; + } + + $result[] = $frame; + } + + return array_reverse($result); + } + + public static function get_caller_frame_context($frame) + { + if (!isset($frame['args'])) { + return array(); + } + + $i = 1; + $args = array(); + foreach ($frame['args'] as $arg) { + $args['param'.$i] = $arg; + $i++; + } + return $args; + } + + public static function get_frame_context($frame, $frame_arg_limit = Raven_Client::MESSAGE_LIMIT) + { + // The reflection API seems more appropriate if we associate it with the frame + // where the function is actually called (since we're treating them as function context) + if (!isset($frame['function'])) { + return array(); + } + + if (!isset($frame['args'])) { + return array(); + } + + if (strpos($frame['function'], '__lambda_func') !== false) { + return array(); + } + if (isset($frame['class']) && $frame['class'] == 'Closure') { + return array(); + } + if (strpos($frame['function'], '{closure}') !== false) { + return array(); + } + if (in_array($frame['function'], self::$statements)) { + if (empty($frame['args'])) { + // No arguments + return array(); + } else { + // Sanitize the file path + return array($frame['args'][0]); + } + } + try { + if (isset($frame['class'])) { + if (method_exists($frame['class'], $frame['function'])) { + $reflection = new ReflectionMethod($frame['class'], $frame['function']); + } elseif ($frame['type'] === '::') { + $reflection = new ReflectionMethod($frame['class'], '__callStatic'); + } else { + $reflection = new ReflectionMethod($frame['class'], '__call'); + } + } else { + $reflection = new ReflectionFunction($frame['function']); + } + } catch (ReflectionException $e) { + return array(); + } + + $params = $reflection->getParameters(); + + $args = array(); + foreach ($frame['args'] as $i => $arg) { + if (isset($params[$i])) { + // Assign the argument by the parameter name + if (is_array($arg)) { + foreach ($arg as $key => $value) { + if (is_string($value) || is_numeric($value)) { + $arg[$key] = substr($value, 0, $frame_arg_limit); + } + } + } + $args[$params[$i]->name] = $arg; + } else { + // TODO: Sentry thinks of these as context locals, so they must be named + // Assign the argument by number + // $args[$i] = $arg; + } + } + + return $args; + } + + private static function read_source_file($filename, $lineno, $context_lines = 5) + { + $frame = array( + 'prefix' => array(), + 'line' => '', + 'suffix' => array(), + 'filename' => $filename, + 'lineno' => $lineno, + ); + + if ($filename === null || $lineno === null) { + return $frame; + } + + // Code which is eval'ed have a modified filename.. Extract the + // correct filename + linenumber from the string. + $matches = array(); + $matched = preg_match("/^(.*?)\((\d+)\) : eval\(\)'d code$/", + $filename, $matches); + if ($matched) { + $frame['filename'] = $filename = $matches[1]; + $frame['lineno'] = $lineno = $matches[2]; + } + + // In the case of an anonymous function, the filename is sent as: + // "() : runtime-created function" + // Extract the correct filename + linenumber from the string. + $matches = array(); + $matched = preg_match("/^(.*?)\((\d+)\) : runtime-created function$/", + $filename, $matches); + if ($matched) { + $frame['filename'] = $filename = $matches[1]; + $frame['lineno'] = $lineno = $matches[2]; + } + + try { + $file = new SplFileObject($filename); + $target = max(0, ($lineno - ($context_lines + 1))); + $file->seek($target); + $cur_lineno = $target+1; + while (!$file->eof()) { + $line = rtrim($file->current(), "\r\n"); + if ($cur_lineno == $lineno) { + $frame['line'] = $line; + } elseif ($cur_lineno < $lineno) { + $frame['prefix'][] = $line; + } elseif ($cur_lineno > $lineno) { + $frame['suffix'][] = $line; + } + $cur_lineno++; + if ($cur_lineno > $lineno + $context_lines) { + break; + } + $file->next(); + } + } catch (RuntimeException $exc) { + return $frame; + } + + return $frame; + } +} diff --git a/htdocs/includes/raven/raven/lib/Raven/Util.php b/htdocs/includes/raven/raven/lib/Raven/Util.php new file mode 100644 index 00000000000..dbd2d11d0a5 --- /dev/null +++ b/htdocs/includes/raven/raven/lib/Raven/Util.php @@ -0,0 +1,33 @@ + + + + + + ./test/Raven/ + + + + + + ./lib/Raven/ + + + diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php new file mode 100644 index 00000000000..cdec2fd4756 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php @@ -0,0 +1,624 @@ +__sent_events; + } + public function send($data) + { + if (is_callable($this->send_callback) && !call_user_func($this->send_callback, $data)) { + // if send_callback returns falsely, end native send + return; + } + $this->__sent_events[] = $data; + } + public function is_http_request() + { + return true; + } + public function get_auth_header($timestamp, $client, $api_key, $secret_key) + { + return parent::get_auth_header($timestamp, $client, $api_key, $secret_key); + } + public function get_http_data() + { + return parent::get_http_data(); + } + public function get_user_data() + { + return parent::get_user_data(); + } +} + +class Raven_Tests_ClientTest extends PHPUnit_Framework_TestCase +{ + private function create_exception() + { + try { + throw new Exception('Foo bar'); + } catch (Exception $ex) { + return $ex; + } + } + + private function create_chained_exception() + { + try { + throw new Exception('Foo bar'); + } catch (Exception $ex) { + try { + throw new Exception('Child exc', 0, $ex); + } catch (Exception $ex2) { + return $ex2; + } + } + } + + public function testParseDsnHttp() + { + $result = Raven_Client::parseDsn('http://public:secret@example.com/1'); + + $this->assertEquals($result['project'], 1); + $this->assertEquals($result['servers'], array('http://example.com/api/1/store/')); + $this->assertEquals($result['public_key'], 'public'); + $this->assertEquals($result['secret_key'], 'secret'); + } + + public function testParseDsnHttps() + { + $result = Raven_Client::parseDsn('https://public:secret@example.com/1'); + + $this->assertEquals($result['project'], 1); + $this->assertEquals($result['servers'], array('https://example.com/api/1/store/')); + $this->assertEquals($result['public_key'], 'public'); + $this->assertEquals($result['secret_key'], 'secret'); + } + + public function testParseDsnPath() + { + $result = Raven_Client::parseDsn('http://public:secret@example.com/app/1'); + + $this->assertEquals($result['project'], 1); + $this->assertEquals($result['servers'], array('http://example.com/app/api/1/store/')); + $this->assertEquals($result['public_key'], 'public'); + $this->assertEquals($result['secret_key'], 'secret'); + } + + public function testParseDsnPort() + { + $result = Raven_Client::parseDsn('http://public:secret@example.com:9000/app/1'); + + $this->assertEquals($result['project'], 1); + $this->assertEquals($result['servers'], array('http://example.com:9000/app/api/1/store/')); + $this->assertEquals($result['public_key'], 'public'); + $this->assertEquals($result['secret_key'], 'secret'); + } + + public function testParseDsnInvalidScheme() + { + try { + Raven_Client::parseDsn('gopher://public:secret@/1'); + $this->fail(); + } catch (Exception $e) { + return; + } + } + + public function testParseDsnMissingNetloc() + { + try { + Raven_Client::parseDsn('http://public:secret@/1'); + $this->fail(); + } catch (Exception $e) { + return; + } + } + + public function testParseDsnMissingProject() + { + try { + Raven_Client::parseDsn('http://public:secret@example.com'); + $this->fail(); + } catch (Exception $e) { + return; + } + } + + /** + * @expectedException InvalidArgumentException + */ + public function testParseDsnMissingPublicKey() + { + Raven_Client::parseDsn('http://:secret@example.com/1'); + } + /** + * @expectedException InvalidArgumentException + */ + public function testParseDsnMissingSecretKey() + { + Raven_Client::parseDsn('http://public@example.com/1'); + } + + public function testDsnFirstArgument() + { + $client = new Raven_Client('http://public:secret@example.com/1'); + + $this->assertEquals($client->project, 1); + $this->assertEquals($client->servers, array('http://example.com/api/1/store/')); + $this->assertEquals($client->public_key, 'public'); + $this->assertEquals($client->secret_key, 'secret'); + } + + public function testDsnFirstArgumentWithOptions() + { + $client = new Raven_Client('http://public:secret@example.com/1', array( + 'site' => 'foo', + )); + + $this->assertEquals($client->project, 1); + $this->assertEquals($client->servers, array('http://example.com/api/1/store/')); + $this->assertEquals($client->public_key, 'public'); + $this->assertEquals($client->secret_key, 'secret'); + $this->assertEquals($client->site, 'foo'); + } + + public function testOptionsFirstArgument() + { + $client = new Raven_Client(array( + 'servers' => array('http://example.com/api/1/store/'), + 'project' => 1, + )); + + $this->assertEquals($client->servers, array('http://example.com/api/1/store/')); + } + + public function testOptionsFirstArgumentWithOptions() + { + $client = new Raven_Client(array( + 'servers' => array('http://example.com/api/1/store/'), + 'project' => 1, + ), array( + 'site' => 'foo', + )); + + $this->assertEquals($client->servers, array('http://example.com/api/1/store/')); + $this->assertEquals($client->site, 'foo'); + } + + public function testOptionsExtraData() + { + $client = new Dummy_Raven_Client(array('extra' => array('foo' => 'bar'))); + + $client->captureMessage('Test Message %s', array('foo')); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['extra']['foo'], 'bar'); + } + + public function testCaptureMessageDoesHandleUninterpolatedMessage() + { + $client = new Dummy_Raven_Client(); + + $client->captureMessage('Test Message %s'); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['message'], 'Test Message %s'); + } + + public function testCaptureMessageDoesHandleInterpolatedMessage() + { + $client = new Dummy_Raven_Client(); + + $client->captureMessage('Test Message %s', array('foo')); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['message'], 'Test Message foo'); + } + + public function testCaptureMessageSetsInterface() + { + $client = new Dummy_Raven_Client(); + + $client->captureMessage('Test Message %s', array('foo')); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['sentry.interfaces.Message'], array( + 'message' => 'Test Message %s', + 'params' => array('foo'), + )); + } + + public function testCaptureMessageHandlesOptionsAsThirdArg() + { + $client = new Dummy_Raven_Client(); + + $client->captureMessage('Test Message %s', array('foo'), array( + 'level' => Dummy_Raven_Client::WARNING, + 'extra' => array('foo' => 'bar') + )); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['level'], Dummy_Raven_Client::WARNING); + $this->assertEquals($event['extra']['foo'], 'bar'); + } + + public function testCaptureMessageHandlesLevelAsThirdArg() + { + $client = new Dummy_Raven_Client(); + + $client->captureMessage('Test Message %s', array('foo'), Dummy_Raven_Client::WARNING); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['level'], Dummy_Raven_Client::WARNING); + } + + public function testCaptureExceptionSetsInterfaces() + { + # TODO: it'd be nice if we could mock the stacktrace extraction function here + $client = new Dummy_Raven_Client(); + $ex = $this->create_exception(); + $client->captureException($ex); + + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + + $exc = $event['sentry.interfaces.Exception']; + $this->assertEquals(count($exc['values']), 1); + $this->assertEquals($exc['values'][0]['value'], 'Foo bar'); + $this->assertEquals($exc['values'][0]['type'], 'Exception'); + $this->assertFalse(empty($exc['values'][0]['module'])); + + $this->assertFalse(empty($exc['values'][0]['stacktrace']['frames'])); + $frames = $exc['values'][0]['stacktrace']['frames']; + $frame = $frames[count($frames) - 1]; + $this->assertTrue($frame['lineno'] > 0); + $this->assertEquals($frame['module'], 'ClientTest.php:Raven_Tests_ClientTest'); + $this->assertEquals($frame['function'], 'create_exception'); + $this->assertFalse(isset($frame['vars'])); + $this->assertEquals($frame['context_line'], ' throw new Exception(\'Foo bar\');'); + $this->assertFalse(empty($frame['pre_context'])); + $this->assertFalse(empty($frame['post_context'])); + } + + public function testCaptureExceptionChainedException() + { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + $this->markTestSkipped('PHP 5.3 required for chained exceptions.'); + } + + # TODO: it'd be nice if we could mock the stacktrace extraction function here + $client = new Dummy_Raven_Client(); + $ex = $this->create_chained_exception(); + $client->captureException($ex); + + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + + $exc = $event['sentry.interfaces.Exception']; + $this->assertEquals(count($exc['values']), 2); + $this->assertEquals($exc['values'][0]['value'], 'Foo bar'); + $this->assertEquals($exc['values'][1]['value'], 'Child exc'); + } + + public function testCaptureExceptionDifferentLevelsInChainedExceptionsBug() + { + if (version_compare(PHP_VERSION, '5.3.0', '<')) { + $this->markTestSkipped('PHP 5.3 required for chained exceptions.'); + } + + $client = new Dummy_Raven_Client(); + $e1 = new ErrorException('First', 0, E_DEPRECATED); + $e2 = new ErrorException('Second', 0, E_NOTICE, __FILE__, __LINE__, $e1); + $e3 = new ErrorException('Third', 0, E_ERROR, __FILE__, __LINE__, $e2); + + $client->captureException($e1); + $client->captureException($e2); + $client->captureException($e3); + $events = $client->getSentEvents(); + + $event = array_pop($events); + $this->assertEquals($event['level'], Dummy_Raven_Client::ERROR); + + $event = array_pop($events); + $this->assertEquals($event['level'], Dummy_Raven_Client::INFO); + + $event = array_pop($events); + $this->assertEquals($event['level'], Dummy_Raven_Client::WARNING); + } + + public function testCaptureExceptionHandlesOptionsAsSecondArg() + { + $client = new Dummy_Raven_Client(); + $ex = $this->create_exception(); + $client->captureException($ex, array('culprit' => 'test')); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['culprit'], 'test'); + } + + public function testCaptureExceptionHandlesCulpritAsSecondArg() + { + $client = new Dummy_Raven_Client(); + $ex = $this->create_exception(); + $client->captureException($ex, 'test'); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals($event['culprit'], 'test'); + } + + public function testCaptureExceptionHandlesExcludeOption() + { + $client = new Dummy_Raven_Client(array( + 'exclude' => array('Exception'), + )); + $ex = $this->create_exception(); + $client->captureException($ex, 'test'); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 0); + } + + public function testDoesRegisterProcessors() + { + $client = new Dummy_Raven_Client(array( + 'processors' => array('Raven_SanitizeDataProcessor'), + )); + $this->assertEquals(count($client->processors), 1); + $this->assertTrue($client->processors[0] instanceof Raven_SanitizeDataProcessor); + } + + public function testProcessDoesCallProcessors() + { + $data = array("key"=>"value"); + + $processor = $this->getMock('Processor', array('process')); + $processor->expects($this->once()) + ->method('process') + ->with($data); + + $client = new Dummy_Raven_Client(); + $client->processors[] = $processor; + $client->process($data); + } + + public function testDefaultProcessorsAreUsed() + { + $client = new Dummy_Raven_Client(); + $defaults = Dummy_Raven_Client::getDefaultProcessors(); + + $this->assertEquals(count($client->processors), count($defaults)); + } + + public function testDefaultProcessorsContainSanitizeDataProcessor() + { + $defaults = Dummy_Raven_Client::getDefaultProcessors(); + + $this->assertTrue(in_array('Raven_SanitizeDataProcessor', $defaults)); + } + + public function testGetDefaultData() + { + $client = new Dummy_Raven_Client(); + $expected = array( + 'platform' => 'php', + 'project' => $client->project, + 'server_name' => $client->name, + 'site' => $client->site, + 'logger' => $client->logger, + 'tags' => $client->tags, + ); + $this->assertEquals($expected, $client->get_default_data()); + } + + /** + * @backupGlobals + */ + public function testGetHttpData() + { + $_SERVER = array( + 'REDIRECT_STATUS' => '200', + 'CONTENT_TYPE' => 'text/xml', + 'CONTENT_LENGTH' => '99', + 'HTTP_HOST' => 'getsentry.com', + 'HTTP_ACCEPT' => 'text/html', + 'HTTP_ACCEPT_CHARSET' => 'utf-8', + 'HTTP_COOKIE' => 'cupcake: strawberry', + 'HTTP_CONTENT_TYPE' => 'text/html', + 'HTTP_CONTENT_LENGTH' => '1000', + 'SERVER_PORT' => '443', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_METHOD' => 'PATCH', + 'QUERY_STRING' => 'q=bitch&l=en', + 'REQUEST_URI' => '/welcome/', + 'SCRIPT_NAME' => '/index.php', + ); + $_POST = array( + 'stamp' => '1c', + ); + $_COOKIE = array( + 'donut' => 'chocolat', + ); + + $expected = array( + 'sentry.interfaces.Http' => array( + 'method' => 'PATCH', + 'url' => 'https://getsentry.com/welcome/', + 'query_string' => 'q=bitch&l=en', + 'data' => array( + 'stamp' => '1c', + ), + 'cookies' => array( + 'donut' => 'chocolat', + ), + 'headers' => array( + 'Host' => 'getsentry.com', + 'Accept' => 'text/html', + 'Accept-Charset' => 'utf-8', + 'Cookie' => 'cupcake: strawberry', + 'Content-Type' => 'text/xml', + 'Content-Length' => '99', + ), + 'env' => array( + 'REDIRECT_STATUS' => '200', + 'SERVER_PORT' => '443', + 'SERVER_PROTOCOL' => 'HTTP/1.1', + 'REQUEST_METHOD' => 'PATCH', + 'QUERY_STRING' => 'q=bitch&l=en', + 'REQUEST_URI' => '/welcome/', + 'SCRIPT_NAME' => '/index.php', + ), + ) + ); + + $client = new Dummy_Raven_Client(); + $this->assertEquals($expected, $client->get_http_data()); + } + + public function testGetUserDataWithSetUser() + { + $client = new Dummy_Raven_Client(); + + $id = 'unique_id'; + $email = 'foo@example.com'; + + $user = array( + 'username' => 'my_user', + ); + + $client->set_user_data($id, $email, $user); + + $expected = array( + 'sentry.interfaces.User' => array( + 'id' => 'unique_id', + 'username' => 'my_user', + 'email' => 'foo@example.com', + ) + ); + + $this->assertEquals($expected, $client->get_user_data()); + } + + public function testGetUserDataWithNoUser() + { + $client = new Dummy_Raven_Client(); + + $expected = array( + 'sentry.interfaces.User' => array( + 'id' => session_id(), + ) + ); + $this->assertEquals($expected, $client->get_user_data()); + } + + public function testGetAuthHeader() + { + $client = new Dummy_Raven_Client(); + + $clientstring = 'raven-php/test'; + $timestamp = '1234341324.340000'; + + $expected = "Sentry sentry_timestamp={$timestamp}, sentry_client={$clientstring}, " . + "sentry_version=" . Dummy_Raven_Client::PROTOCOL . ", " . + "sentry_key=publickey, sentry_secret=secretkey"; + + $this->assertEquals($expected, $client->get_auth_header($timestamp, 'raven-php/test', 'publickey', 'secretkey')); + } + + public function testCaptureMessageWithUserContext() + { + $client = new Dummy_Raven_Client(); + + $client->user_context(array('email' => 'foo@example.com')); + + $client->captureMessage('test'); + $events = $client->getSentEvents(); + $this->assertEquals(1, count($events)); + $event = array_pop($events); + $this->assertEquals(array( + 'email' => 'foo@example.com', + ), $event['sentry.interfaces.User']); + } + + public function testCaptureMessageWithTagsContext() + { + $client = new Dummy_Raven_Client(); + + $client->tags_context(array('foo' => 'bar')); + $client->tags_context(array('biz' => 'boz')); + $client->tags_context(array('biz' => 'baz')); + + $client->captureMessage('test'); + $events = $client->getSentEvents(); + $this->assertEquals(1, count($events)); + $event = array_pop($events); + $this->assertEquals(array( + 'foo' => 'bar', + 'biz' => 'baz', + ), $event['tags']); + } + + public function testCaptureMessageWithExtraContext() + { + $client = new Dummy_Raven_Client(); + + $client->extra_context(array('foo' => 'bar')); + $client->extra_context(array('biz' => 'boz')); + $client->extra_context(array('biz' => 'baz')); + + $client->captureMessage('test'); + $events = $client->getSentEvents(); + $this->assertEquals(1, count($events)); + $event = array_pop($events); + $this->assertEquals(array( + 'foo' => 'bar', + 'biz' => 'baz', + ), $event['extra']); + } + + public function cb1($data) + { + $this->assertEquals('test', $data['message']); + return false; + } + + public function cb2($data) + { + $this->assertEquals('test', $data['message']); + return true; + } + + public function testSendCallback() + { + $client = new Dummy_Raven_Client(array('send_callback' => array($this, 'cb1'))); + $client->captureMessage('test'); + $events = $client->getSentEvents(); + $this->assertEquals(0, count($events)); + + $client = new Dummy_Raven_Client(array('send_callback' => array($this, 'cb2'))); + $client->captureMessage('test'); + $events = $client->getSentEvents(); + $this->assertEquals(1, count($events)); + } +} diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/CompatTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/CompatTest.php new file mode 100644 index 00000000000..93f7c411c09 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/CompatTest.php @@ -0,0 +1,46 @@ +assertEquals(Raven_Compat::gethostname(), Raven_Compat::_gethostname()); + $this->assertTrue(strlen(Raven_Compat::_gethostname()) > 0); + } + + public function test_hash_hmac() + { + $result = Raven_Compat::hash_hmac('sha1', 'foo', 'bar'); + $this->assertEquals('85d155c55ed286a300bd1cf124de08d87e914f3a', $result); + + $result = Raven_Compat::_hash_hmac('sha1', 'foo', 'bar'); + $this->assertEquals('85d155c55ed286a300bd1cf124de08d87e914f3a', $result); + } + + public function test_json_encode() + { + $result = Raven_Compat::json_encode(array('foo' => array('bar' => 1))); + $this->assertEquals('{"foo":{"bar":1}}', $result); + + $result = Raven_Compat::_json_encode(array('foo' => array('bar' => 1))); + $this->assertEquals('{"foo":{"bar":1}}', $result); + + $result = Raven_Compat::_json_encode(array(1, 2, 3, 4, 'foo', 'bar')); + $this->assertEquals('[1,2,3,4,"foo","bar"]', $result); + + $result = Raven_Compat::_json_encode(array(1, 'foo', 'foobar' => 'bar')); + $this->assertEquals('{0:1,1:"foo","foobar":"bar"}', $result); + + $result = Raven_Compat::_json_encode(array(array())); + $this->assertEquals('[[]]', $result); + } +} diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/ErrorHandlerTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/ErrorHandlerTest.php new file mode 100644 index 00000000000..656b7593550 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/ErrorHandlerTest.php @@ -0,0 +1,87 @@ +errorLevel = error_reporting(); + } + + public function tearDown() + { + error_reporting($this->errorLevel); + } + + public function testErrorsAreLoggedAsExceptions() + { + $client = $this->getMock('Client', array('captureException', 'getIdent')); + $client->expects($this->once()) + ->method('captureException') + ->with($this->isInstanceOf('ErrorException')); + + $handler = new Raven_ErrorHandler($client); + $handler->handleError(E_WARNING, 'message'); + } + + public function testExceptionsAreLogged() + { + $client = $this->getMock('Client', array('captureException', 'getIdent')); + $client->expects($this->once()) + ->method('captureException') + ->with($this->isInstanceOf('ErrorException')); + + $e = new ErrorException('message', 0, E_WARNING, '', 0); + + $handler = new Raven_ErrorHandler($client); + $handler->handleException($e); + } + + public function testErrorHandlerCheckSilentReporting() + { + $client = $this->getMock('Client', array('captureException', 'getIdent')); + $client->expects($this->never()) + ->method('captureException'); + + $handler = new Raven_ErrorHandler($client); + $handler->registerErrorHandler(false); + + @trigger_error('Silent', E_USER_WARNING); + } + + public function testErrorHandlerBlockErrorReporting() + { + $client = $this->getMock('Client', array('captureException', 'getIdent')); + $client->expects($this->never()) + ->method('captureException'); + + $handler = new Raven_ErrorHandler($client); + $handler->registerErrorHandler(false); + + error_reporting(E_USER_ERROR); + trigger_error('Warning', E_USER_WARNING); + } + + public function testErrorHandlerPassErrorReportingPass() + { + $client = $this->getMock('Client', array('captureException', 'getIdent')); + $client->expects($this->once()) + ->method('captureException'); + + $handler = new Raven_ErrorHandler($client); + $handler->registerErrorHandler(false); + + error_reporting(E_USER_WARNING); + trigger_error('Warning', E_USER_WARNING); + } +} diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/SanitizeDataProcessorTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/SanitizeDataProcessorTest.php new file mode 100644 index 00000000000..1b8be37ae78 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/SanitizeDataProcessorTest.php @@ -0,0 +1,180 @@ + array( + 'data' => array( + 'foo' => 'bar', + 'password' => 'hello', + 'the_secret' => 'hello', + 'a_password_here' => 'hello', + 'mypasswd' => 'hello', + 'authorization' => 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=', + 'card_number' => array( + '1111', + '2222', + '3333', + '4444' + ) + ), + ) + ); + + $client = new Raven_Client(); + $processor = new Raven_SanitizeDataProcessor($client); + $processor->process($data); + + $vars = $data['sentry.interfaces.Http']['data']; + $this->assertEquals($vars['foo'], 'bar'); + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['password']); + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['the_secret']); + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['a_password_here']); + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['mypasswd']); + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['authorization']); + + $this->markTestIncomplete('Array scrubbing has not been implemented yet.'); + + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['card_number']['0']); + } + + public function testDoesFilterCreditCard() + { + $data = array( + 'ccnumba' => '4242424242424242' + ); + + $client = new Raven_Client(); + $processor = new Raven_SanitizeDataProcessor($client); + $processor->process($data); + + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $data['ccnumba']); + } + + /** + * @covers setProcessorOptions + * + */ + public function testSettingProcessorOptions() + { + $client = new Raven_Client(); + $processor = new Raven_SanitizeDataProcessor($client); + + $this->assertEquals($processor->getFieldsRe(), '/(authorization|password|passwd|secret|password_confirmation|card_number|auth_pw)/i', 'got default fields'); + $this->assertEquals($processor->getValuesRe(), '/^(?:\d[ -]*?){13,16}$/', 'got default values'); + + $options = array( + 'fields_re' => '/(api_token)/i', + 'values_re' => '/^(?:\d[ -]*?){15,16}$/' + ); + + $processor->setProcessorOptions($options); + + $this->assertEquals($processor->getFieldsRe(), '/(api_token)/i', 'overwrote fields'); + $this->assertEquals($processor->getValuesRe(), '/^(?:\d[ -]*?){15,16}$/', 'overwrote values'); + } + + /** + * @dataProvider overrideDataProvider + * + * @param $processorOptions + * @param $client_options + * @param $dsn + */ + public function testOverrideOptions($processorOptions, $client_options, $dsn) + { + $client = new Raven_Client($dsn, $client_options); + $processor = $client->processors[0]; + + $this->assertInstanceOf('Raven_SanitizeDataProcessor', $processor); + $this->assertEquals($processor->getFieldsRe(), $processorOptions['Raven_SanitizeDataProcessor']['fields_re'], 'overwrote fields'); + $this->assertEquals($processor->getValuesRe(), $processorOptions['Raven_SanitizeDataProcessor']['values_re'], 'overwrote values'); + } + + /** + * @depends testOverrideOptions + * @dataProvider overrideDataProvider + * + * @param $processorOptions + * @param $client_options + * @param $dsn + */ + public function testOverridenSanitize($processorOptions, $client_options, $dsn) + { + $data = array( + 'sentry.interfaces.Http' => array( + 'data' => array( + 'foo' => 'bar', + 'password' => 'hello', + 'the_secret' => 'hello', + 'a_password_here' => 'hello', + 'mypasswd' => 'hello', + 'api_token' => 'nioenio3nrio3jfny89nby9bhr#RML#R', + 'authorization' => 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=', + 'card_number' => array( + '1111111111111111', + '2222', + ) + ), + ) + ); + + $client = new Raven_Client($dsn, $client_options); + $processor = $client->processors[0]; + + $this->assertInstanceOf('Raven_SanitizeDataProcessor', $processor); + $this->assertEquals($processor->getFieldsRe(), $processorOptions['Raven_SanitizeDataProcessor']['fields_re'], 'overwrote fields'); + $this->assertEquals($processor->getValuesRe(), $processorOptions['Raven_SanitizeDataProcessor']['values_re'], 'overwrote values'); + + $processor->process($data); + + $vars = $data['sentry.interfaces.Http']['data']; + $this->assertEquals($vars['foo'], 'bar', 'did not alter foo'); + $this->assertEquals($vars['password'], 'hello', 'did not alter password'); + $this->assertEquals($vars['the_secret'], 'hello', 'did not alter the_secret'); + $this->assertEquals($vars['a_password_here'], 'hello', 'did not alter a_password_here'); + $this->assertEquals($vars['mypasswd'], 'hello', 'did not alter mypasswd'); + $this->assertEquals($vars['authorization'], 'Basic dXNlcm5hbWU6cGFzc3dvcmQ=', 'did not alter authorization'); + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['api_token'], 'masked api_token'); + + $this->assertEquals(Raven_SanitizeDataProcessor::MASK, $vars['card_number']['0'], 'masked card_number[0]'); + $this->assertEquals($vars['card_number']['1'], $vars['card_number']['1'], 'did not alter card_number[1]'); + } + + /** + * Provides data for testing overriding the processor options + * + * @return array + */ + public static function overrideDataProvider() + { + $processorOptions = array( + 'Raven_SanitizeDataProcessor' => array( + 'fields_re' => '/(api_token)/i', + 'values_re' => '/^(?:\d[ -]*?){15,16}$/' + ) + ); + + $client_options = array( + 'processors' => array('Raven_SanitizeDataProcessor'), + 'processorOptions' => $processorOptions + ); + + $dsn = 'http://9aaa31f9a05b4e72aaa06aa8157a827a:9aa7aa82a9694a08a1a7589a2a035a9a@sentry.domain.tld/1'; + + return array( + array($processorOptions, $client_options, $dsn) + ); + } +} diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/SerializerTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/SerializerTest.php new file mode 100644 index 00000000000..af157130b97 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/SerializerTest.php @@ -0,0 +1,48 @@ +assertEquals(array('1', '2', '3'), $result); + } + + public function testObjectsAreStrings() + { + $input = new Raven_StacktraceTestObject(); + $result = Raven_Serializer::serialize($input); + $this->assertEquals('Object Raven_StacktraceTestObject', $result); + } + + public function testIntsAreInts() + { + $input = 1; + $result = Raven_Serializer::serialize($input); + $this->assertTrue(is_integer($result)); + $this->assertEquals(1, $result); + } + + public function testRecursionMaxDepth() + { + $input = array(); + $input[] = &$input; + $result = Raven_Serializer::serialize($input, 3); + $this->assertEquals(array(array(array('Array of length 1'))), $result); + } +} diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/StacktraceTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/StacktraceTest.php new file mode 100644 index 00000000000..60f97de9585 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/StacktraceTest.php @@ -0,0 +1,223 @@ + 0) { + return call_user_func('raven_test_recurse', $times, $callback); + } + + return call_user_func($callback); +} + +function raven_test_create_stacktrace($args=null, $times=3) +{ + return raven_test_recurse($times, 'debug_backtrace'); +} + +class Raven_Tests_StacktraceTest extends PHPUnit_Framework_TestCase +{ + public function testCanTraceParamContext() + { + $stack = raven_test_create_stacktrace(array('biz', 'baz'), 0); + + $frame = $stack[2]; + $params = Raven_Stacktrace::get_frame_context($frame); + $this->assertEquals($params['args'], array('biz', 'baz')); + $this->assertEquals($params['times'], 0); + } + + public function testSimpleTrace() + { + $stack = array( + array( + "file" => dirname(__FILE__) . "/resources/a.php", + "line" => 11, + "function" => "a_test", + "args"=> array( + "friend", + ), + ), + array( + "file" => dirname(__FILE__) . "/resources/b.php", + "line" => 3, + "args"=> array( + "/tmp/a.php", + ), + "function" => "include_once", + ), + ); + + $frames = Raven_Stacktrace::get_stack_info($stack, true); + + $frame = $frames[0]; + $this->assertEquals('b.php', $frame["module"]); + $this->assertEquals(3, $frame["lineno"]); + $this->assertNull($frame["function"]); + $this->assertEquals("include_once '/tmp/a.php';", $frame["context_line"]); + $frame = $frames[1]; + $this->assertEquals('a.php', $frame["module"]); + $this->assertEquals(11, $frame["lineno"]); + $this->assertEquals('include_once', $frame["function"]); + $this->assertEquals('a_test($foo);', $frame["context_line"]); + } + + public function testSimpleUnshiftedTrace() + { + $stack = array( + array( + "file" => dirname(__FILE__) . "/resources/a.php", + "line" => 11, + "function" => "a_test", + "args"=> array( + "friend", + ), + ), + array( + "file" => dirname(__FILE__) . "/resources/b.php", + "line" => 3, + "args"=> array( + "/tmp/a.php", + ), + "function" => "include_once", + ), + ); + + $frames = Raven_Stacktrace::get_stack_info($stack, true, false); + + $frame = $frames[0]; + $this->assertEquals('b.php', $frame["module"]); + $this->assertEquals(3, $frame["lineno"]); + $this->assertNull($frame["function"]); + $this->assertEquals('/tmp/a.php', $frame['vars']['param1']); + $this->assertEquals("include_once '/tmp/a.php';", $frame["context_line"]); + $frame = $frames[1]; + $this->assertEquals('a.php', $frame["module"]); + $this->assertEquals(11, $frame["lineno"]); + $this->assertEquals('include_once', $frame["function"]); + $this->assertEquals('friend', $frame['vars']['param1']); + $this->assertEquals('a_test($foo);', $frame["context_line"]); + } + + public function testShiftedCaptureVars() + { + $stack = array( + array( + "file" => dirname(__FILE__) . "/resources/a.php", + "line" => 11, + "function" => "a_test", + "args"=> array( + "friend", + ), + ), + array( + "file" => dirname(__FILE__) . "/resources/b.php", + "line" => 3, + "args"=> array( + "/tmp/a.php", + ), + "function" => "include_once", + ), + ); + + $vars = array( + "foo" => "bar", + "baz" => "zoom" + ); + + $frames = Raven_Stacktrace::get_stack_info($stack, true, true, $vars); + + $frame = $frames[0]; + $this->assertEquals('b.php', $frame["module"]); + $this->assertEquals(3, $frame["lineno"]); + $this->assertNull($frame["function"]); + $this->assertEquals("include_once '/tmp/a.php';", $frame["context_line"]); + $this->assertFalse(isset($frame['vars'])); + $frame = $frames[1]; + $this->assertEquals('a.php', $frame["module"]); + $this->assertEquals(11, $frame["lineno"]); + $this->assertEquals('include_once', $frame["function"]); + $this->assertEquals('a_test($foo);', $frame["context_line"]); + $this->assertEquals($vars, $frame['vars']); + } + + public function testUnshiftedCaptureVars() + { + $stack = array( + array( + "file" => dirname(__FILE__) . "/resources/a.php", + "line" => 11, + "function" => "a_test", + "args"=> array( + "friend", + ), + ), + array( + "file" => dirname(__FILE__) . "/resources/b.php", + "line" => 3, + "args"=> array( + "/tmp/a.php", + ), + "function" => "include_once", + ), + ); + + $vars = array( + "foo" => "bar", + "baz" => "zoom" + ); + + $frames = Raven_Stacktrace::get_stack_info($stack, true, false, $vars); + + $frame = $frames[0]; + $this->assertEquals('b.php', $frame["module"]); + $this->assertEquals(3, $frame["lineno"]); + $this->assertNull($frame["function"]); + $this->assertEquals(array('param1' => '/tmp/a.php'), $frame['vars']); + $this->assertEquals("include_once '/tmp/a.php';", $frame["context_line"]); + $frame = $frames[1]; + $this->assertEquals('a.php', $frame["module"]); + $this->assertEquals(11, $frame["lineno"]); + $this->assertEquals('include_once', $frame["function"]); + $this->assertEquals($vars, $frame['vars']); + $this->assertEquals('a_test($foo);', $frame["context_line"]); + } + + public function testDoesFixFrameInfo() + { + /** + * PHP's way of storing backstacks seems bass-ackwards to me + * 'function' is not the function you're in; it's any function being + * called, so we have to shift 'function' down by 1. Ugh. + */ + $stack = raven_test_create_stacktrace(); + + $frames = Raven_Stacktrace::get_stack_info($stack, true); + // just grab the last few frames + $frames = array_slice($frames, -5); + $frame = $frames[0]; + $this->assertEquals('StacktraceTest.php:Raven_Tests_StacktraceTest', $frame['module']); + $this->assertEquals('testDoesFixFrameInfo', $frame['function']); + $frame = $frames[1]; + $this->assertEquals('StacktraceTest.php', $frame['module']); + $this->assertEquals('raven_test_create_stacktrace', $frame['function']); + $frame = $frames[2]; + $this->assertEquals('StacktraceTest.php', $frame['module']); + $this->assertEquals('raven_test_recurse', $frame['function']); + $frame = $frames[3]; + $this->assertEquals('StacktraceTest.php', $frame['module']); + $this->assertEquals('raven_test_recurse', $frame['function']); + $frame = $frames[4]; + $this->assertEquals('StacktraceTest.php', $frame['module']); + $this->assertEquals('raven_test_recurse', $frame['function']); + } +} diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/UtilTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/UtilTest.php new file mode 100644 index 00000000000..856b7709755 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/UtilTest.php @@ -0,0 +1,32 @@ + 'bar'); + $result = Raven_Util::get($input, 'baz', 'foo'); + $this->assertEquals('foo', $result); + } + + public function testGetReturnsPresentValuesEvenWhenEmpty() + { + $input = array('foo' => ''); + $result = Raven_Util::get($input, 'foo', 'bar'); + $this->assertEquals('', $result); + } +} diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/resources/a.php b/htdocs/includes/raven/raven/test/Raven/Tests/resources/a.php new file mode 100644 index 00000000000..78ae29c80e3 --- /dev/null +++ b/htdocs/includes/raven/raven/test/Raven/Tests/resources/a.php @@ -0,0 +1,11 @@ + Date: Thu, 6 Aug 2015 19:19:06 +0200 Subject: [PATCH 2/6] NEW Syslog displays configuration errors --- htdocs/admin/syslog.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/htdocs/admin/syslog.php b/htdocs/admin/syslog.php index 3eebfebb37b..7d713760382 100644 --- a/htdocs/admin/syslog.php +++ b/htdocs/admin/syslog.php @@ -106,8 +106,17 @@ if ($action == 'set') $activeModules = $newActiveModules; dolibarr_set_const($db, 'SYSLOG_HANDLERS', json_encode($activeModules), 'chaine',0,'',0); + // Check configuration + foreach ($activeModules as $modulename) { + /** + * @var LogHandler + */ + $module = new $modulename; + $error = $module->checkConfiguration(); + } - if (! $error) + + if (! $error) { $db->commit(); setEventMessage($langs->trans("SetupSaved")); @@ -115,7 +124,8 @@ if ($action == 'set') else { $db->rollback(); - setEventMessage($langs->trans("Error"),'errors'); + setEventMessage($error, 'errors'); + } } From 7bb917e6371033be02c0f268d58cb716db46c1d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Doursenaud?= Date: Thu, 6 Aug 2015 21:55:29 +0200 Subject: [PATCH 3/6] NEW Support logging to a Sentry server Sentry is an Open Source error logging and aggregation platform. It can be used on-premise or hosted at https://getsentry.com --- .../core/modules/syslog/mod_syslog_sentry.php | 187 ++++++++++++++++++ htdocs/langs/en_US/admin.lang | 2 + 2 files changed, 189 insertions(+) create mode 100644 htdocs/core/modules/syslog/mod_syslog_sentry.php diff --git a/htdocs/core/modules/syslog/mod_syslog_sentry.php b/htdocs/core/modules/syslog/mod_syslog_sentry.php new file mode 100644 index 00000000000..3625405aeaa --- /dev/null +++ b/htdocs/core/modules/syslog/mod_syslog_sentry.php @@ -0,0 +1,187 @@ + + * Copyright (C) 2004-2009 Laurent Destailleur + * Copyright (C) 2015 Raphaël Doursenaud + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +require_once DOL_DOCUMENT_ROOT . '/includes/autoload.php'; +require_once DOL_DOCUMENT_ROOT . '/core/modules/syslog/logHandler.php'; + +/** + * Class to manage logging to Sentry + * + * @see https://docs.getsentry.com/on-premise/clients/php/ + */ +class mod_syslog_sentry extends LogHandler implements LogHandlerInterface +{ + /** + * @var string Log handler code + */ + public $code = 'sentry'; + + /** + * Return name of logger + * + * @return string Name of logger + */ + public function getName() + { + return 'Sentry'; + } + + /** + * Version of the module ('x.y.z' or 'dolibarr' or 'experimental' or 'development') + * + * @return string + */ + public function getVersion() + { + return 'dolibarr'; + } + + /** + * Content of the info tooltip. + * + * @return false|string + */ + public function getInfo() + { + global $langs; + return $langs->trans('SyslogSentryFromProject'); + } + + /** + * Is the module active ? + * + * @return int + */ + public function isActive() + { + return 1; + } + + /** + * Return array of configuration data + * + * @return array Return array of configuration data + */ + public function configure() + { + global $langs; + return array( + array( + 'constant' => 'SYSLOG_SENTRY_DSN', + 'name' => $langs->trans('SyslogSentryDSN'), + 'default' => '', + 'attr' => 'size="100" placeholder="https://:@app.getsentry.com/"' + ) + ); + } + + /** + * Return if configuration is valid + * + * @return array Array of errors. Empty array if ok. + */ + public function checkConfiguration() + { + global $conf; + + $errors = array(); + + $dsn = $conf->global->SYSLOG_SENTRY_DSN; + + try { + $client = new Raven_Client( + $dsn, + array('curl_method' => 'sync') + ); + } catch (InvalidArgumentException $ex) { + $errors[] = "ERROR: There was an error parsing your DSN:\n " . $ex->getMessage(); + } + + if (!$errors) { + // Send test event and check for errors + $client->captureMessage('TEST: Sentry syslog configuration check', null, Raven_Client::DEBUG); + $last_error = $client->getLastError(); + if ($last_error) { + $errors[] = $last_error; + } + } + + if (!$errors) { + // Install handlers + $error_handler = new Raven_ErrorHandler($client); + $error_handler->registerExceptionHandler(); + $error_handler->registerErrorHandler(); + $error_handler->registerShutdownFunction(); + } + + return $errors; + } + + /** + * Export the message + * + * @param array $content Array containing the info about the message + * @return void + */ + public function export($content) + { + global $conf; + $dsn = $conf->global->SYSLOG_SENTRY_DSN; + $client = new Raven_Client( + $dsn, + array('curl_method' => 'exec') + ); + + $client->user_context(array( + 'username' => ($content['user'] ? $content['user'] : ''), + 'ip_address' => $content['ip'] + )); + + $client->tags_context(array( + 'version' => DOL_VERSION + )); + + $client->registerSeverityMap(array( + LOG_EMERG => Raven_Client::FATAL, + LOG_ALERT => Raven_Client::FATAL, + LOG_CRIT => Raven_Client::ERROR, + LOG_ERR => Raven_Client::ERROR, + LOG_WARNING => Raven_Client::WARNING, + LOG_NOTICE => Raven_Client::WARNING, + LOG_INFO => Raven_Client::INFO, + LOG_DEBUG => Raven_Client::DEBUG, + )); + + if (substr($content['message'], 0, 3) === 'sql') { + global $db; + $query = substr($content['message'], 4, strlen($content['message'])); + $client->captureQuery( + $query, + $client->translateSeverity($content['level']), + $db->type + ); + } else { + $client->captureMessage( + $content['message'], + null, + $client->translateSeverity($content['level']) + ); + } + } +} diff --git a/htdocs/langs/en_US/admin.lang b/htdocs/langs/en_US/admin.lang index 298c580c170..a52e9974564 100755 --- a/htdocs/langs/en_US/admin.lang +++ b/htdocs/langs/en_US/admin.lang @@ -1411,6 +1411,8 @@ SyslogFilename=File name and path YouCanUseDOL_DATA_ROOT=You can use DOL_DATA_ROOT/dolibarr.log for a log file in Dolibarr "documents" directory. You can set a different path to store this file. ErrorUnknownSyslogConstant=Constant %s is not a known Syslog constant OnlyWindowsLOG_USER=Windows only supports LOG_USER +SyslogSentryDSN=Sentry DSN +SyslogSentryFromProject=DSN from your Sentry project ##### Donations ##### DonationsSetup=Donation module setup DonationsReceiptModel=Template of donation receipt From 7ef5cbfb96a27caa56015f05befe4ea109fb78bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Doursenaud?= Date: Sat, 29 Aug 2015 17:29:00 +0200 Subject: [PATCH 4/6] Updated raven-php to v0.12.1 --- composer.lock | 10 ++++----- htdocs/includes/bin/raven | 1 + htdocs/includes/composer/LICENSE | 21 +++++++++++++++++++ htdocs/includes/composer/installed.json | 12 +++++------ htdocs/includes/raven/raven/CHANGES | 6 ++++++ .../includes/raven/raven/lib/Raven/Client.php | 8 ++++++- .../raven/test/Raven/Tests/ClientTest.php | 11 ++++++++++ 7 files changed, 57 insertions(+), 12 deletions(-) create mode 120000 htdocs/includes/bin/raven create mode 100644 htdocs/includes/composer/LICENSE diff --git a/composer.lock b/composer.lock index 9e662436d4d..2856ef0b749 100644 --- a/composer.lock +++ b/composer.lock @@ -201,16 +201,16 @@ }, { "name": "raven/raven", - "version": "0.12.0", + "version": "0.12.1", "source": { "type": "git", "url": "https://github.com/getsentry/raven-php.git", - "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b" + "reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/raven-php/zipball/bd247ca2a8fd9ccfb99b60285c9b31286384a92b", - "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b", + "url": "https://api.github.com/repos/getsentry/raven-php/zipball/b325984c792ff89f985b73da9a3ad8ed8b520bca", + "reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca", "shasum": "" }, "require": { @@ -251,7 +251,7 @@ "log", "logging" ], - "time": "2015-05-19 20:20:08" + "time": "2015-08-25 22:38:46" }, { "name": "restler/framework", diff --git a/htdocs/includes/bin/raven b/htdocs/includes/bin/raven new file mode 120000 index 00000000000..9080498f871 --- /dev/null +++ b/htdocs/includes/bin/raven @@ -0,0 +1 @@ +../raven/raven/bin/raven \ No newline at end of file diff --git a/htdocs/includes/composer/LICENSE b/htdocs/includes/composer/LICENSE new file mode 100644 index 00000000000..c8d57af8b27 --- /dev/null +++ b/htdocs/includes/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) 2015 Nils Adermann, Jordi Boggiano + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + diff --git a/htdocs/includes/composer/installed.json b/htdocs/includes/composer/installed.json index f2b965e2b6e..88bf8b37f43 100644 --- a/htdocs/includes/composer/installed.json +++ b/htdocs/includes/composer/installed.json @@ -343,17 +343,17 @@ }, { "name": "raven/raven", - "version": "0.12.0", - "version_normalized": "0.12.0.0", + "version": "0.12.1", + "version_normalized": "0.12.1.0", "source": { "type": "git", "url": "https://github.com/getsentry/raven-php.git", - "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b" + "reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/getsentry/raven-php/zipball/bd247ca2a8fd9ccfb99b60285c9b31286384a92b", - "reference": "bd247ca2a8fd9ccfb99b60285c9b31286384a92b", + "url": "https://api.github.com/repos/getsentry/raven-php/zipball/b325984c792ff89f985b73da9a3ad8ed8b520bca", + "reference": "b325984c792ff89f985b73da9a3ad8ed8b520bca", "shasum": "" }, "require": { @@ -364,7 +364,7 @@ "fabpot/php-cs-fixer": "^1.8.0", "phpunit/phpunit": "^4.6.6" }, - "time": "2015-05-19 20:20:08", + "time": "2015-08-25 22:38:46", "bin": [ "bin/raven" ], diff --git a/htdocs/includes/raven/raven/CHANGES b/htdocs/includes/raven/raven/CHANGES index 6d52a4bfd49..9996ac8e196 100644 --- a/htdocs/includes/raven/raven/CHANGES +++ b/htdocs/includes/raven/raven/CHANGES @@ -1,3 +1,9 @@ +0.12.1 +------ + +- Dont send empty values for various context. + + 0.12.0 ------ diff --git a/htdocs/includes/raven/raven/lib/Raven/Client.php b/htdocs/includes/raven/raven/lib/Raven/Client.php index 94668883824..64e990eac8c 100644 --- a/htdocs/includes/raven/raven/lib/Raven/Client.php +++ b/htdocs/includes/raven/raven/lib/Raven/Client.php @@ -16,7 +16,7 @@ class Raven_Client { - const VERSION = '0.12.0'; + const VERSION = '0.12.1'; const PROTOCOL = '6'; const DEBUG = 'debug'; @@ -449,6 +449,12 @@ class Raven_Client $this->context->extra, $data['extra']); + // avoid empty arrays (which dont convert to dicts) + if (empty($data['extra'])) + unset($data['extra']); + if (empty($data['tags'])) + unset($data['tags']); + if ((!$stack && $this->auto_log_stacks) || $stack === true) { $stack = debug_backtrace(); diff --git a/htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php b/htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php index cdec2fd4756..702ec07f34e 100644 --- a/htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php +++ b/htdocs/includes/raven/raven/test/Raven/Tests/ClientTest.php @@ -209,6 +209,17 @@ class Raven_Tests_ClientTest extends PHPUnit_Framework_TestCase $this->assertEquals($event['extra']['foo'], 'bar'); } + public function testEmptyExtraData() + { + $client = new Dummy_Raven_Client(array('extra' => array())); + + $client->captureMessage('Test Message %s', array('foo')); + $events = $client->getSentEvents(); + $this->assertEquals(count($events), 1); + $event = array_pop($events); + $this->assertEquals(array_key_exists('extra', $event), false); + } + public function testCaptureMessageDoesHandleUninterpolatedMessage() { $client = new Dummy_Raven_Client(); From a6ef289470219db2954bc14bc567aaca9fcfc514 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Doursenaud?= Date: Mon, 31 Aug 2015 17:21:03 +0200 Subject: [PATCH 5/6] New Added client-side Sentry logging Using Raven.js allows logging client-side javascript errors to the configured Sentry server. --- htdocs/includes/raven-js/.bower.json | 18 + htdocs/includes/raven-js/.gitignore | 22 + htdocs/includes/raven-js/.jshintrc | 9 + htdocs/includes/raven-js/.travis.yml | 8 + htdocs/includes/raven-js/AUTHORS | 1 + htdocs/includes/raven-js/Gruntfile.js | 254 +++ htdocs/includes/raven-js/LICENSE | 9 + htdocs/includes/raven-js/Makefile | 20 + htdocs/includes/raven-js/README.md | 13 + htdocs/includes/raven-js/bower.json | 7 + htdocs/includes/raven-js/dist/raven.js | 1909 +++++++++++++++++ htdocs/includes/raven-js/dist/raven.min.js | 3 + htdocs/includes/raven-js/dist/raven.min.map | 1 + htdocs/includes/raven-js/docs/Makefile | 153 ++ .../raven-js/docs/changelog/index.rst | 138 ++ htdocs/includes/raven-js/docs/conf.py | 244 +++ .../includes/raven-js/docs/config/index.rst | 220 ++ .../raven-js/docs/contributing/index.rst | 99 + htdocs/includes/raven-js/docs/index.rst | 42 + .../includes/raven-js/docs/install/index.rst | 61 + htdocs/includes/raven-js/docs/make.bat | 190 ++ .../includes/raven-js/docs/plugins/index.rst | 20 + htdocs/includes/raven-js/docs/tips/index.rst | 79 + htdocs/includes/raven-js/docs/usage/index.rst | 156 ++ htdocs/includes/raven-js/example/Makefile | 3 + htdocs/includes/raven-js/example/file.min.js | 2 + .../raven-js/example/file.sourcemap.js | 1 + htdocs/includes/raven-js/example/file1.js | 4 + htdocs/includes/raven-js/example/file2.js | 12 + htdocs/includes/raven-js/example/index.html | 41 + htdocs/includes/raven-js/example/scratch.js | 42 + htdocs/includes/raven-js/package.json | 32 + htdocs/includes/raven-js/plugins/angular.js | 36 + htdocs/includes/raven-js/plugins/backbone.js | 55 + htdocs/includes/raven-js/plugins/console.js | 43 + htdocs/includes/raven-js/plugins/ember.js | 29 + htdocs/includes/raven-js/plugins/jquery.js | 75 + htdocs/includes/raven-js/plugins/native.js | 33 + htdocs/includes/raven-js/plugins/require.js | 14 + htdocs/includes/raven-js/src/raven.js | 830 +++++++ .../includes/raven-js/template/_copyright.js | 11 + htdocs/includes/raven-js/template/_footer.js | 19 + htdocs/includes/raven-js/template/_header.js | 2 + htdocs/includes/raven-js/test/index.html | 62 + htdocs/includes/raven-js/test/raven.test.js | 1773 +++++++++++++++ .../raven-js/vendor/TraceKit/tracekit.js | 1044 +++++++++ htdocs/main.inc.php | 42 +- 47 files changed, 7872 insertions(+), 9 deletions(-) create mode 100644 htdocs/includes/raven-js/.bower.json create mode 100644 htdocs/includes/raven-js/.gitignore create mode 100644 htdocs/includes/raven-js/.jshintrc create mode 100644 htdocs/includes/raven-js/.travis.yml create mode 100644 htdocs/includes/raven-js/AUTHORS create mode 100644 htdocs/includes/raven-js/Gruntfile.js create mode 100644 htdocs/includes/raven-js/LICENSE create mode 100644 htdocs/includes/raven-js/Makefile create mode 100644 htdocs/includes/raven-js/README.md create mode 100644 htdocs/includes/raven-js/bower.json create mode 100644 htdocs/includes/raven-js/dist/raven.js create mode 100644 htdocs/includes/raven-js/dist/raven.min.js create mode 100644 htdocs/includes/raven-js/dist/raven.min.map create mode 100644 htdocs/includes/raven-js/docs/Makefile create mode 100644 htdocs/includes/raven-js/docs/changelog/index.rst create mode 100644 htdocs/includes/raven-js/docs/conf.py create mode 100644 htdocs/includes/raven-js/docs/config/index.rst create mode 100644 htdocs/includes/raven-js/docs/contributing/index.rst create mode 100644 htdocs/includes/raven-js/docs/index.rst create mode 100644 htdocs/includes/raven-js/docs/install/index.rst create mode 100644 htdocs/includes/raven-js/docs/make.bat create mode 100644 htdocs/includes/raven-js/docs/plugins/index.rst create mode 100644 htdocs/includes/raven-js/docs/tips/index.rst create mode 100644 htdocs/includes/raven-js/docs/usage/index.rst create mode 100644 htdocs/includes/raven-js/example/Makefile create mode 100644 htdocs/includes/raven-js/example/file.min.js create mode 100644 htdocs/includes/raven-js/example/file.sourcemap.js create mode 100644 htdocs/includes/raven-js/example/file1.js create mode 100644 htdocs/includes/raven-js/example/file2.js create mode 100644 htdocs/includes/raven-js/example/index.html create mode 100644 htdocs/includes/raven-js/example/scratch.js create mode 100644 htdocs/includes/raven-js/package.json create mode 100644 htdocs/includes/raven-js/plugins/angular.js create mode 100644 htdocs/includes/raven-js/plugins/backbone.js create mode 100644 htdocs/includes/raven-js/plugins/console.js create mode 100644 htdocs/includes/raven-js/plugins/ember.js create mode 100644 htdocs/includes/raven-js/plugins/jquery.js create mode 100644 htdocs/includes/raven-js/plugins/native.js create mode 100644 htdocs/includes/raven-js/plugins/require.js create mode 100644 htdocs/includes/raven-js/src/raven.js create mode 100644 htdocs/includes/raven-js/template/_copyright.js create mode 100644 htdocs/includes/raven-js/template/_footer.js create mode 100644 htdocs/includes/raven-js/template/_header.js create mode 100644 htdocs/includes/raven-js/test/index.html create mode 100644 htdocs/includes/raven-js/test/raven.test.js create mode 100644 htdocs/includes/raven-js/vendor/TraceKit/tracekit.js diff --git a/htdocs/includes/raven-js/.bower.json b/htdocs/includes/raven-js/.bower.json new file mode 100644 index 00000000000..f42bb67b949 --- /dev/null +++ b/htdocs/includes/raven-js/.bower.json @@ -0,0 +1,18 @@ +{ + "name": "raven-js", + "version": "1.1.19", + "dependencies": {}, + "main": "dist/raven.js", + "ignore": {}, + "homepage": "https://github.com/getsentry/raven-js", + "_release": "1.1.19", + "_resolution": { + "type": "version", + "tag": "1.1.19", + "commit": "82b9c07b7545c6c10e297709a741eaa9b75f64e8" + }, + "_source": "git://github.com/getsentry/raven-js.git", + "_target": "~1.1.19", + "_originalSource": "raven-js", + "_direct": true +} \ No newline at end of file diff --git a/htdocs/includes/raven-js/.gitignore b/htdocs/includes/raven-js/.gitignore new file mode 100644 index 00000000000..02111f91fe4 --- /dev/null +++ b/htdocs/includes/raven-js/.gitignore @@ -0,0 +1,22 @@ +.DS_Store + +# Thumbnails +._* + +# Files that might appear on external disk +.Spotlight-V100 +.Trashes + +docs/html +docs/doctrees + +build +node_modules +npm-debug.log + +scratch/ + +*.pyc + +.idea +aws.json diff --git a/htdocs/includes/raven-js/.jshintrc b/htdocs/includes/raven-js/.jshintrc new file mode 100644 index 00000000000..6e4ec735331 --- /dev/null +++ b/htdocs/includes/raven-js/.jshintrc @@ -0,0 +1,9 @@ +{ + "es3": true, + "globalstrict": true, + "browser": true, + "predef": [ + "TraceKit", + "console" + ] +} diff --git a/htdocs/includes/raven-js/.travis.yml b/htdocs/includes/raven-js/.travis.yml new file mode 100644 index 00000000000..3b4f88c1e93 --- /dev/null +++ b/htdocs/includes/raven-js/.travis.yml @@ -0,0 +1,8 @@ +# language doesn't matter, we're only using phantom.js +language: node_js +node_js: + - "0.10" +script: + - ./node_modules/.bin/grunt test build +notifications: + irc: "irc.freenode.org#sentry" diff --git a/htdocs/includes/raven-js/AUTHORS b/htdocs/includes/raven-js/AUTHORS new file mode 100644 index 00000000000..7f1ae423217 --- /dev/null +++ b/htdocs/includes/raven-js/AUTHORS @@ -0,0 +1 @@ +https://github.com/getsentry/raven-js/graphs/contributors diff --git a/htdocs/includes/raven-js/Gruntfile.js b/htdocs/includes/raven-js/Gruntfile.js new file mode 100644 index 00000000000..fce8eb17272 --- /dev/null +++ b/htdocs/includes/raven-js/Gruntfile.js @@ -0,0 +1,254 @@ +module.exports = function(grunt) { + "use strict"; + + var _ = require('lodash'); + var path = require('path'); + + var coreFiles = [ + 'template/_header.js', + 'vendor/**/*.js', + 'src/**/*.js', + 'template/_footer.js' + ]; + + var plugins = grunt.option('plugins'); + // Create plugin paths and verify hey exist + plugins = _.map(plugins ? plugins.split(',') : [], function (plugin) { + var path = 'plugins/' + plugin + '.js'; + + if(!grunt.file.exists(path)) + throw new Error("Plugin '" + plugin + "' not found in plugins directory."); + + return path; + }); + + // Taken from http://dzone.com/snippets/calculate-all-combinations + var combine = function (a) { + var fn = function (n, src, got, all) { + if (n === 0) { + all.push(got); + return; + } + + for (var j = 0; j < src.length; j++) { + fn(n - 1, src.slice(j + 1), got.concat([src[j]]), all); + } + }; + + var all = [a]; + + for (var i = 0; i < a.length; i++) { + fn(i, a, [], all); + } + + return all; + }; + + var pluginCombinations = combine(grunt.file.expand('plugins/*.js')); + var pluginConcatFiles = _.reduce(pluginCombinations, function (dict, comb) { + var key = _.map(comb, function (plugin) { + return path.basename(plugin, '.js'); + }); + key.sort(); + + var dest = path.join('build/', key.join(','), '/raven.js'); + dict[dest] = coreFiles.concat(comb); + + return dict; + }, {}); + + var gruntConfig = { + pkg: grunt.file.readJSON('package.json'), + aws: grunt.file.exists('aws.json') ? grunt.file.readJSON('aws.json'): {}, + + clean: ['build'], + concat: { + options: { + separator: '\n', + banner: grunt.file.read('template/_copyright.js'), + process: true + }, + core: { + src: coreFiles.concat(plugins), + dest: 'build/raven.js' + }, + all: { + files: pluginConcatFiles + } + }, + + uglify: { + options: { + sourceMap: function (dest) { + return path.join(path.dirname(dest), + path.basename(dest, '.js')) + + '.map'; + }, + sourceMappingURL: function (dest) { + return path.basename(dest, '.js') + '.map'; + }, + preserveComments: 'some' + }, + dist: { + src: ['build/**/*.js'], + ext: '.min.js', + expand: true + } + }, + + fixSourceMaps: { + all: ['build/**/*.map'] + }, + + jshint: { + options: { + jshintrc: '.jshintrc' + }, + all: ['Gruntfile.js', 'src/**/*.js', 'plugins/**/*.js'] + }, + + mocha: { + all: { + options: { + mocha: { + ignoreLeaks: true, + grep: grunt.option('grep') + }, + log: true, + reporter: 'Dot', + run: true + }, + src: ['test/index.html'], + nonull: true + } + }, + + release: { + options: { + npm: false, + commitMessage: 'Release <%= version %>' + } + }, + + s3: { + options: { + key: '<%= aws.key %>', + secret: '<%= aws.secret %>', + bucket: '<%= aws.bucket %>', + access: 'public-read', + // Limit concurrency + maxOperations: 20, + headers: { + // Surrogate-Key header for Fastly to purge by release + 'x-amz-meta-surrogate-key': '<%= pkg.release %>' + } + }, + all: { + upload: [{ + src: 'build/**/*', + dest: '<%= pkg.release %>/', + rel: 'build/' + }] + } + }, + + connect: { + test: { + options: { + port: 8000, + debug: true, + keepalive: true + } + }, + + docs: { + options: { + port: 8000, + debug: true, + base: 'docs/html', + keepalive: true + } + } + }, + + copy: { + dist: { + expand: true, + flatten: true, + cwd: 'build/', + src: '**', + dest: 'dist/' + } + } + }; + + grunt.initConfig(gruntConfig); + + // Custom Grunt tasks + grunt.registerTask('version', function() { + var pkg = grunt.config.get('pkg'); + if (grunt.option('dev')) { + pkg.release = 'dev'; + pkg.version = grunt.config.get('gitinfo').local.branch.current.shortSHA; + } else { + pkg.release = pkg.version; + } + grunt.config.set('pkg', pkg); + }); + + grunt.registerMultiTask('fixSourceMaps', function () { + this.files.forEach(function (f) { + var result; + var sources = f.src.filter(function (filepath) { + if (!grunt.file.exists(filepath)) { + grunt.log.warn('Source file "' + filepath + '" not found.'); + return false; + } else { + return true; + } + }).forEach(function (filepath) { + var base = path.dirname(filepath); + var sMap = grunt.file.readJSON(filepath); + sMap.file = path.relative(base, sMap.file); + sMap.sources = _.map(sMap.sources, path.relative.bind(path, base)); + + grunt.file.write(filepath, JSON.stringify(sMap)); + // Print a success message. + grunt.log.writeln('File "' + filepath + '" fixed.'); + }); + }); + }); + + // Grunt contrib tasks + grunt.loadNpmTasks('grunt-contrib-uglify'); + grunt.loadNpmTasks('grunt-contrib-concat'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-connect'); + grunt.loadNpmTasks('grunt-contrib-copy'); + + // 3rd party Grunt tasks + grunt.loadNpmTasks('grunt-mocha'); + grunt.loadNpmTasks('grunt-release'); + grunt.loadNpmTasks('grunt-s3'); + grunt.loadNpmTasks('grunt-gitinfo'); + + // Build tasks + grunt.registerTask('_prep', ['clean', 'gitinfo', 'version']); + grunt.registerTask('concat.core', ['_prep', 'concat:core']); + grunt.registerTask('concat.all', ['_prep', 'concat:all']); + grunt.registerTask('build.core', ['concat.core', 'uglify', 'fixSourceMaps']); + grunt.registerTask('build.all', ['concat.all', 'uglify', 'fixSourceMaps']); + grunt.registerTask('build', ['build.all']); + grunt.registerTask('dist', ['build.core', 'copy:dist']); + + // Test task + grunt.registerTask('test', ['jshint', 'mocha']); + + // Webserver tasks + grunt.registerTask('run:test', ['connect:test']); + grunt.registerTask('run:docs', ['connect:docs']); + + grunt.registerTask('publish', ['test', 'build.all', 's3']); + grunt.registerTask('default', ['test']); +}; diff --git a/htdocs/includes/raven-js/LICENSE b/htdocs/includes/raven-js/LICENSE new file mode 100644 index 00000000000..2752d4c7395 --- /dev/null +++ b/htdocs/includes/raven-js/LICENSE @@ -0,0 +1,9 @@ +Copyright (c) 2014 Matt Robenolt and other contributors +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. +* Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/htdocs/includes/raven-js/Makefile b/htdocs/includes/raven-js/Makefile new file mode 100644 index 00000000000..f378b15f09d --- /dev/null +++ b/htdocs/includes/raven-js/Makefile @@ -0,0 +1,20 @@ +develop: update-submodules + npm install . + +update-submodules: + git submodule init + git submodule update + +docs: + cd docs; $(MAKE) html + +docs-live: + while true; do \ + sleep 2; \ + $(MAKE) docs; \ + done + +clean: + rm -rf docs/html + +.PHONY: develop update-submodules docs docs-live clean diff --git a/htdocs/includes/raven-js/README.md b/htdocs/includes/raven-js/README.md new file mode 100644 index 00000000000..058a44fe41f --- /dev/null +++ b/htdocs/includes/raven-js/README.md @@ -0,0 +1,13 @@ +# Raven.js [![Build Status](https://travis-ci.org/getsentry/raven-js.svg?branch=master)](https://travis-ci.org/getsentry/raven-js) + +Raven.js is a tiny standalone JavaScript client for [Sentry](https://www.getsentry.com/). + +**Raven.js v1.1 requires Sentry v6.0 or later.** + +## Resources + + * [Download](http://ravenjs.com) + * [Documentation](https://raven-js.readthedocs.org) + * [Bug Tracker](https://github.com/getsentry/raven-js/issues) + * [IRC](irc://chat.freenode.net/sentry) (chat.freenode.net, #sentry) + * Follow [@mattrobenolt](https://twitter.com/mattrobenolt) on Twitter for updates diff --git a/htdocs/includes/raven-js/bower.json b/htdocs/includes/raven-js/bower.json new file mode 100644 index 00000000000..ba54c40ac65 --- /dev/null +++ b/htdocs/includes/raven-js/bower.json @@ -0,0 +1,7 @@ +{ + "name": "raven-js", + "version": "1.1.19", + "dependencies": {}, + "main": "dist/raven.js", + "ignore": {} +} diff --git a/htdocs/includes/raven-js/dist/raven.js b/htdocs/includes/raven-js/dist/raven.js new file mode 100644 index 00000000000..195d002ab65 --- /dev/null +++ b/htdocs/includes/raven-js/dist/raven.js @@ -0,0 +1,1909 @@ +/*! Raven.js 1.1.19 (b51bc89) | github.com/getsentry/raven-js */ + +/* + * Includes TraceKit + * https://github.com/getsentry/TraceKit + * + * Copyright 2015 Matt Robenolt and other contributors + * Released under the BSD license + * https://github.com/getsentry/raven-js/blob/master/LICENSE + * + */ +;(function(window, undefined){ +'use strict'; + +/* + TraceKit - Cross brower stack traces - github.com/occ/TraceKit + MIT license +*/ + +var TraceKit = { + remoteFetching: false, + collectWindowErrors: true, + // 3 lines before, the offending line, 3 lines after + linesOfContext: 7 +}; + +// global reference to slice +var _slice = [].slice; +var UNKNOWN_FUNCTION = '?'; + + +/** + * TraceKit.wrap: Wrap any function in a TraceKit reporter + * Example: func = TraceKit.wrap(func); + * + * @param {Function} func Function to be wrapped + * @return {Function} The wrapped func + */ +TraceKit.wrap = function traceKitWrapper(func) { + function wrapped() { + try { + return func.apply(this, arguments); + } catch (e) { + TraceKit.report(e); + throw e; + } + } + return wrapped; +}; + +/** + * TraceKit.report: cross-browser processing of unhandled exceptions + * + * Syntax: + * TraceKit.report.subscribe(function(stackInfo) { ... }) + * TraceKit.report.unsubscribe(function(stackInfo) { ... }) + * TraceKit.report(exception) + * try { ...code... } catch(ex) { TraceKit.report(ex); } + * + * Supports: + * - Firefox: full stack trace with line numbers, plus column number + * on top frame; column number is not guaranteed + * - Opera: full stack trace with line and column numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed + * - IE: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed + * + * In theory, TraceKit should work on all of the following versions: + * - IE5.5+ (only 8.0 tested) + * - Firefox 0.9+ (only 3.5+ tested) + * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require + * Exceptions Have Stacktrace to be enabled in opera:config) + * - Safari 3+ (only 4+ tested) + * - Chrome 1+ (only 5+ tested) + * - Konqueror 3.5+ (untested) + * + * Requires TraceKit.computeStackTrace. + * + * Tries to catch all unhandled exceptions and report them to the + * subscribed handlers. Please note that TraceKit.report will rethrow the + * exception. This is REQUIRED in order to get a useful stack trace in IE. + * If the exception does not reach the top of the browser, you will only + * get a stack trace from the point where TraceKit.report was called. + * + * Handlers receive a stackInfo object as described in the + * TraceKit.computeStackTrace docs. + */ +TraceKit.report = (function reportModuleWrapper() { + var handlers = [], + lastArgs = null, + lastException = null, + lastExceptionStack = null; + + /** + * Add a crash handler. + * @param {Function} handler + */ + function subscribe(handler) { + installGlobalHandler(); + handlers.push(handler); + } + + /** + * Remove a crash handler. + * @param {Function} handler + */ + function unsubscribe(handler) { + for (var i = handlers.length - 1; i >= 0; --i) { + if (handlers[i] === handler) { + handlers.splice(i, 1); + } + } + } + + /** + * Remove all crash handlers. + */ + function unsubscribeAll() { + uninstallGlobalHandler(); + handlers = []; + } + + /** + * Dispatch stack information to all handlers. + * @param {Object.} stack + */ + function notifyHandlers(stack, isWindowError) { + var exception = null; + if (isWindowError && !TraceKit.collectWindowErrors) { + return; + } + for (var i in handlers) { + if (hasKey(handlers, i)) { + try { + handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2))); + } catch (inner) { + exception = inner; + } + } + } + + if (exception) { + throw exception; + } + } + + var _oldOnerrorHandler, _onErrorHandlerInstalled; + + /** + * Ensures all global unhandled exceptions are recorded. + * Supported by Gecko and IE. + * @param {string} message Error message. + * @param {string} url URL of script that generated the exception. + * @param {(number|string)} lineNo The line number at which the error + * occurred. + * @param {?(number|string)} colNo The column number at which the error + * occurred. + * @param {?Error} ex The actual Error object. + */ + function traceKitWindowOnError(message, url, lineNo, colNo, ex) { + var stack = null; + + if (lastExceptionStack) { + TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); + processLastException(); + } else if (ex) { + // New chrome and blink send along a real error object + // Let's just report that like a normal error. + // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror + stack = TraceKit.computeStackTrace(ex); + notifyHandlers(stack, true); + } else { + var location = { + 'url': url, + 'line': lineNo, + 'column': colNo + }; + location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line); + location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line); + stack = { + 'message': message, + 'url': document.location.href, + 'stack': [location] + }; + notifyHandlers(stack, true); + } + + if (_oldOnerrorHandler) { + return _oldOnerrorHandler.apply(this, arguments); + } + + return false; + } + + function installGlobalHandler () + { + if (_onErrorHandlerInstalled) { + return; + } + _oldOnerrorHandler = window.onerror; + window.onerror = traceKitWindowOnError; + _onErrorHandlerInstalled = true; + } + + function uninstallGlobalHandler () + { + if (!_onErrorHandlerInstalled) { + return; + } + window.onerror = _oldOnerrorHandler; + _onErrorHandlerInstalled = false; + _oldOnerrorHandler = undefined; + } + + function processLastException() { + var _lastExceptionStack = lastExceptionStack, + _lastArgs = lastArgs; + lastArgs = null; + lastExceptionStack = null; + lastException = null; + notifyHandlers.apply(null, [_lastExceptionStack, false].concat(_lastArgs)); + } + + /** + * Reports an unhandled Error to TraceKit. + * @param {Error} ex + * @param {?boolean} rethrow If false, do not re-throw the exception. + * Only used for window.onerror to not cause an infinite loop of + * rethrowing. + */ + function report(ex, rethrow) { + var args = _slice.call(arguments, 1); + if (lastExceptionStack) { + if (lastException === ex) { + return; // already caught by an inner catch block, ignore + } else { + processLastException(); + } + } + + var stack = TraceKit.computeStackTrace(ex); + lastExceptionStack = stack; + lastException = ex; + lastArgs = args; + + // If the stack trace is incomplete, wait for 2 seconds for + // slow slow IE to see if onerror occurs or not before reporting + // this exception; otherwise, we will end up with an incomplete + // stack trace + window.setTimeout(function () { + if (lastException === ex) { + processLastException(); + } + }, (stack.incomplete ? 2000 : 0)); + + if (rethrow !== false) { + throw ex; // re-throw to propagate to the top level (and cause window.onerror) + } + } + + report.subscribe = subscribe; + report.unsubscribe = unsubscribe; + report.uninstall = unsubscribeAll; + return report; +}()); + +/** + * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript + * + * Syntax: + * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below) + * Returns: + * s.name - exception name + * s.message - exception message + * s.stack[i].url - JavaScript or HTML file URL + * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work) + * s.stack[i].args - arguments passed to the function, if known + * s.stack[i].line - line number, if known + * s.stack[i].column - column number, if known + * s.stack[i].context - an array of source code lines; the middle element corresponds to the correct line# + * + * Supports: + * - Firefox: full stack trace with line numbers and unreliable column + * number on top frame + * - Opera 10: full stack trace with line and column numbers + * - Opera 9-: full stack trace with line numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the topmost stacktrace element + * only + * - IE: no line numbers whatsoever + * + * Tries to guess names of anonymous functions by looking for assignments + * in the source code. In IE and Safari, we have to guess source file names + * by searching for function bodies inside all page scripts. This will not + * work for scripts that are loaded cross-domain. + * Here be dragons: some function names may be guessed incorrectly, and + * duplicate functions may be mismatched. + * + * TraceKit.computeStackTrace should only be used for tracing purposes. + * Logging of unhandled exceptions should be done with TraceKit.report, + * which builds on top of TraceKit.computeStackTrace and provides better + * IE support by utilizing the window.onerror event to retrieve information + * about the top of the stack. + * + * Note: In IE and Safari, no stack trace is recorded on the Error object, + * so computeStackTrace instead walks its *own* chain of callers. + * This means that: + * * in Safari, some methods may be missing from the stack trace; + * * in IE, the topmost function in the stack trace will always be the + * caller of computeStackTrace. + * + * This is okay for tracing (because you are likely to be calling + * computeStackTrace from the function you want to be the topmost element + * of the stack trace anyway), but not okay for logging unhandled + * exceptions (because your catch block will likely be far away from the + * inner function that actually caused the exception). + * + */ +TraceKit.computeStackTrace = (function computeStackTraceWrapper() { + var debug = false, + sourceCache = {}; + + /** + * Attempts to retrieve source code via XMLHttpRequest, which is used + * to look up anonymous function names. + * @param {string} url URL of source code. + * @return {string} Source contents. + */ + function loadSource(url) { + if (!TraceKit.remoteFetching) { //Only attempt request if remoteFetching is on. + return ''; + } + try { + var getXHR = function() { + try { + return new window.XMLHttpRequest(); + } catch (e) { + // explicitly bubble up the exception if not found + return new window.ActiveXObject('Microsoft.XMLHTTP'); + } + }; + + var request = getXHR(); + request.open('GET', url, false); + request.send(''); + return request.responseText; + } catch (e) { + return ''; + } + } + + /** + * Retrieves source code from the source code cache. + * @param {string} url URL of source code. + * @return {Array.} Source contents. + */ + function getSource(url) { + if (!isString(url)) return []; + if (!hasKey(sourceCache, url)) { + // URL needs to be able to fetched within the acceptable domain. Otherwise, + // cross-domain errors will be triggered. + var source = ''; + if (url.indexOf(document.domain) !== -1) { + source = loadSource(url); + } + sourceCache[url] = source ? source.split('\n') : []; + } + + return sourceCache[url]; + } + + /** + * Tries to use an externally loaded copy of source code to determine + * the name of a function by looking at the name of the variable it was + * assigned to, if any. + * @param {string} url URL of source code. + * @param {(string|number)} lineNo Line number in source code. + * @return {string} The function name, if discoverable. + */ + function guessFunctionName(url, lineNo) { + var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/, + reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/, + line = '', + maxLines = 10, + source = getSource(url), + m; + + if (!source.length) { + return UNKNOWN_FUNCTION; + } + + // Walk backwards from the first line in the function until we find the line which + // matches the pattern above, which is the function definition + for (var i = 0; i < maxLines; ++i) { + line = source[lineNo - i] + line; + + if (!isUndefined(line)) { + if ((m = reGuessFunction.exec(line))) { + return m[1]; + } else if ((m = reFunctionArgNames.exec(line))) { + return m[1]; + } + } + } + + return UNKNOWN_FUNCTION; + } + + /** + * Retrieves the surrounding lines from where an exception occurred. + * @param {string} url URL of source code. + * @param {(string|number)} line Line number in source code to centre + * around for context. + * @return {?Array.} Lines of source code. + */ + function gatherContext(url, line) { + var source = getSource(url); + + if (!source.length) { + return null; + } + + var context = [], + // linesBefore & linesAfter are inclusive with the offending line. + // if linesOfContext is even, there will be one extra line + // *before* the offending line. + linesBefore = Math.floor(TraceKit.linesOfContext / 2), + // Add one extra line if linesOfContext is odd + linesAfter = linesBefore + (TraceKit.linesOfContext % 2), + start = Math.max(0, line - linesBefore - 1), + end = Math.min(source.length, line + linesAfter - 1); + + line -= 1; // convert to 0-based index + + for (var i = start; i < end; ++i) { + if (!isUndefined(source[i])) { + context.push(source[i]); + } + } + + return context.length > 0 ? context : null; + } + + /** + * Escapes special characters, except for whitespace, in a string to be + * used inside a regular expression as a string literal. + * @param {string} text The string. + * @return {string} The escaped string literal. + */ + function escapeRegExp(text) { + return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); + } + + /** + * Escapes special characters in a string to be used inside a regular + * expression as a string literal. Also ensures that HTML entities will + * be matched the same as their literal friends. + * @param {string} body The string. + * @return {string} The escaped string. + */ + function escapeCodeAsRegExpForMatchingInsideHTML(body) { + return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); + } + + /** + * Determines where a code fragment occurs in the source code. + * @param {RegExp} re The function definition. + * @param {Array.} urls A list of URLs to search. + * @return {?Object.} An object containing + * the url, line, and column number of the defined function. + */ + function findSourceInUrls(re, urls) { + var source, m; + for (var i = 0, j = urls.length; i < j; ++i) { + // console.log('searching', urls[i]); + if ((source = getSource(urls[i])).length) { + source = source.join('\n'); + if ((m = re.exec(source))) { + // console.log('Found function in ' + urls[i]); + + return { + 'url': urls[i], + 'line': source.substring(0, m.index).split('\n').length, + 'column': m.index - source.lastIndexOf('\n', m.index) - 1 + }; + } + } + } + + // console.log('no match'); + + return null; + } + + /** + * Determines at which column a code fragment occurs on a line of the + * source code. + * @param {string} fragment The code fragment. + * @param {string} url The URL to search. + * @param {(string|number)} line The line number to examine. + * @return {?number} The column number. + */ + function findSourceInLine(fragment, url, line) { + var source = getSource(url), + re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'), + m; + + line -= 1; + + if (source && source.length > line && (m = re.exec(source[line]))) { + return m.index; + } + + return null; + } + + /** + * Determines where a function was defined within the source code. + * @param {(Function|string)} func A function reference or serialized + * function definition. + * @return {?Object.} An object containing + * the url, line, and column number of the defined function. + */ + function findSourceByFunctionBody(func) { + var urls = [window.location.href], + scripts = document.getElementsByTagName('script'), + body, + code = '' + func, + codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, + eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, + re, + parts, + result; + + for (var i = 0; i < scripts.length; ++i) { + var script = scripts[i]; + if (script.src) { + urls.push(script.src); + } + } + + if (!(parts = codeRE.exec(code))) { + re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+')); + } + + // not sure if this is really necessary, but I don’t have a test + // corpus large enough to confirm that and it was in the original. + else { + var name = parts[1] ? '\\s+' + parts[1] : '', + args = parts[2].split(',').join('\\s*,\\s*'); + + body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+'); + re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}'); + } + + // look for a normal function definition + if ((result = findSourceInUrls(re, urls))) { + return result; + } + + // look for an old-school event handler function + if ((parts = eventRE.exec(code))) { + var event = parts[1]; + body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]); + + // look for a function defined in HTML as an onXXX handler + re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i'); + + if ((result = findSourceInUrls(re, urls[0]))) { + return result; + } + + // look for ??? + re = new RegExp(body); + + if ((result = findSourceInUrls(re, urls))) { + return result; + } + } + + return null; + } + + // Contents of Exception in various browsers. + // + // SAFARI: + // ex.message = Can't find variable: qq + // ex.line = 59 + // ex.sourceId = 580238192 + // ex.sourceURL = http://... + // ex.expressionBeginOffset = 96 + // ex.expressionCaretOffset = 98 + // ex.expressionEndOffset = 98 + // ex.name = ReferenceError + // + // FIREFOX: + // ex.message = qq is not defined + // ex.fileName = http://... + // ex.lineNumber = 59 + // ex.columnNumber = 69 + // ex.stack = ...stack trace... (see the example below) + // ex.name = ReferenceError + // + // CHROME: + // ex.message = qq is not defined + // ex.name = ReferenceError + // ex.type = not_defined + // ex.arguments = ['aa'] + // ex.stack = ...stack trace... + // + // INTERNET EXPLORER: + // ex.message = ... + // ex.name = ReferenceError + // + // OPERA: + // ex.message = ...message... (see the example below) + // ex.name = ReferenceError + // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) + // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' + + /** + * Computes stack trace information from the stack property. + * Chrome and Gecko use this property. + * @param {Error} ex + * @return {?Object.} Stack trace information. + */ + function computeStackTraceFromStackProp(ex) { + if (!ex.stack) { + return null; + } + + var chrome = /^\s*at (.*?) ?\(?((?:file|https?|chrome-extension):.*?):(\d+)(?::(\d+))?\)?\s*$/i, + gecko = /^\s*(.*?)(?:\((.*?)\))?@((?:file|https?|chrome).*?):(\d+)(?::(\d+))?\s*$/i, + lines = ex.stack.split('\n'), + stack = [], + parts, + element, + reference = /^(.*) is undefined$/.exec(ex.message); + + for (var i = 0, j = lines.length; i < j; ++i) { + if ((parts = gecko.exec(lines[i]))) { + element = { + 'url': parts[3], + 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': parts[2] ? parts[2].split(',') : '', + 'line': +parts[4], + 'column': parts[5] ? +parts[5] : null + }; + } else if ((parts = chrome.exec(lines[i]))) { + element = { + 'url': parts[2], + 'func': parts[1] || UNKNOWN_FUNCTION, + 'line': +parts[3], + 'column': parts[4] ? +parts[4] : null + }; + } else { + continue; + } + + if (!element.func && element.line) { + element.func = guessFunctionName(element.url, element.line); + } + + if (element.line) { + element.context = gatherContext(element.url, element.line); + } + + stack.push(element); + } + + if (!stack.length) { + return null; + } + + if (stack[0].line && !stack[0].column && reference) { + stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); + } else if (!stack[0].column && !isUndefined(ex.columnNumber)) { + // FireFox uses this awesome columnNumber property for its top frame + // Also note, Firefox's column number is 0-based and everything else expects 1-based, + // so adding 1 + stack[0].column = ex.columnNumber + 1; + } + + return { + 'name': ex.name, + 'message': ex.message, + 'url': document.location.href, + 'stack': stack + }; + } + + /** + * Computes stack trace information from the stacktrace property. + * Opera 10 uses this property. + * @param {Error} ex + * @return {?Object.} Stack trace information. + */ + function computeStackTraceFromStacktraceProp(ex) { + // Access and store the stacktrace property before doing ANYTHING + // else to it because Opera is not very good at providing it + // reliably in other circumstances. + var stacktrace = ex.stacktrace; + + var testRE = / line (\d+), column (\d+) in (?:]+)>|([^\)]+))\((.*)\) in (.*):\s*$/i, + lines = stacktrace.split('\n'), + stack = [], + parts; + + for (var i = 0, j = lines.length; i < j; i += 2) { + if ((parts = testRE.exec(lines[i]))) { + var element = { + 'line': +parts[1], + 'column': +parts[2], + 'func': parts[3] || parts[4], + 'args': parts[5] ? parts[5].split(',') : [], + 'url': parts[6] + }; + + if (!element.func && element.line) { + element.func = guessFunctionName(element.url, element.line); + } + if (element.line) { + try { + element.context = gatherContext(element.url, element.line); + } catch (exc) {} + } + + if (!element.context) { + element.context = [lines[i + 1]]; + } + + stack.push(element); + } + } + + if (!stack.length) { + return null; + } + + return { + 'name': ex.name, + 'message': ex.message, + 'url': document.location.href, + 'stack': stack + }; + } + + /** + * NOT TESTED. + * Computes stack trace information from an error message that includes + * the stack trace. + * Opera 9 and earlier use this method if the option to show stack + * traces is turned on in opera:config. + * @param {Error} ex + * @return {?Object.} Stack information. + */ + function computeStackTraceFromOperaMultiLineMessage(ex) { + // Opera includes a stack trace into the exception message. An example is: + // + // Statement on line 3: Undefined variable: undefinedFunc + // Backtrace: + // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz + // undefinedFunc(a); + // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy + // zzz(x, y, z); + // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx + // yyy(a, a, a); + // Line 1 of function script + // try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); } + // ... + + var lines = ex.message.split('\n'); + if (lines.length < 4) { + return null; + } + + var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?)\S+)(?:: in function (\S+))?\s*$/i, + lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?)\S+)(?:: in function (\S+))?\s*$/i, + lineRE3 = /^\s*Line (\d+) of function script\s*$/i, + stack = [], + scripts = document.getElementsByTagName('script'), + inlineScriptBlocks = [], + parts, + i, + len, + source; + + for (i in scripts) { + if (hasKey(scripts, i) && !scripts[i].src) { + inlineScriptBlocks.push(scripts[i]); + } + } + + for (i = 2, len = lines.length; i < len; i += 2) { + var item = null; + if ((parts = lineRE1.exec(lines[i]))) { + item = { + 'url': parts[2], + 'func': parts[3], + 'line': +parts[1] + }; + } else if ((parts = lineRE2.exec(lines[i]))) { + item = { + 'url': parts[3], + 'func': parts[4] + }; + var relativeLine = (+parts[1]); // relative to the start of the + + + + + + +TraceKit specific optional settings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Usually there is no need to touch these settings, but they exist in case you need to tweak something. + +fetchContext +------------ + +Enable TraceKit to attempt to fetch source files to look up anonymous function names, this can be useful to enable if you don't get the context for some entries in the stack trace. Default value is ``false``. + +.. code-block:: javascript + + { + fetchContext: true + } + +linesOfContext +-------------- + +The count of lines surrounding the error line that should be used as context in the stack trace, default value is ``11``. Only applicable when ``fetchContext` is enabled. + +.. code-block:: javascript + + { + linesOfContext: 11 + } + +collectWindowErrors +------------------- + +Enable or disable the TraceKit ``window.onerror`` handler, default value is ``true``. + +.. code-block:: javascript + + { + collectWindowErrors: true + } diff --git a/htdocs/includes/raven-js/docs/contributing/index.rst b/htdocs/includes/raven-js/docs/contributing/index.rst new file mode 100644 index 00000000000..2b25abf7f16 --- /dev/null +++ b/htdocs/includes/raven-js/docs/contributing/index.rst @@ -0,0 +1,99 @@ +Contributing +============ + +Setting up an Environment +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To run the test suite and run our code linter, node.js and npm are required. If you don't have node installed, `get it here `_ first. + +Installing all other dependencies is as simple as: + +.. code-block:: sh + + $ npm install + +And if you don't have `Grunt `_ already, feel free to install that globally: + +.. code-block:: sh + + $ npm install -g grunt-cli + +Running the Test Suite +~~~~~~~~~~~~~~~~~~~~~~ + +The test suite is powered by `Mocha `_ and can both run from the command line, or in the browser. + +From the command line: + +.. code-block:: sh + + $ grunt test + +From your browser: + +.. code-block:: sh + + $ grunt run:test + +Then visit: http://localhost:8000/test/ + +Compiling Raven.js +~~~~~~~~~~~~~~~~~~ + +The simplest way to compile your own version of Raven.js is with the supplied grunt command: + +.. code-block:: sh + + $ grunt build + +By default, this will compile raven.js and all of the included plugins. + +If you only want to compile the core raven.js: + +.. code-block:: sh + + $ grunt build.core + +Files are compiled into ``build/``. + +Contributing Back Code +~~~~~~~~~~~~~~~~~~~~~~ + +Please, send over suggestions and bug fixes in the form of pull requests on `GitHub `_. Any nontrivial fixes/features should include tests. +Do not include any changes to the ``dist/`` folder or bump version numbers yourself. + +Documentation +------------- + +The documentation is written using `reStructuredText `_, and compiled using `Sphinx `_. If you don't have Sphinx installed, you can do it using following command (assuming you have Python already installed in your system): + +.. code-block:: sh + + $ pip install sphinx + +Documentation can be then compiled by running: + +.. code-block:: sh + + $ make docs + +Afterwards you can view it in your browser by running following command and than pointing your browser to http://127.0.0.1:8000/: + +.. code-block:: sh + + $ grunt run:docs + + + +Releasing New Version +~~~~~~~~~~~~~~~~~~~~~ + +* Bump version numbers in both ``package.json`` and ``bower.json``. +* ``$ grunt dist`` This will compile a new version and update it in the ``dist/`` folder. +* Confirm that build was fine, etc. +* Commit new version, create a tag. Push to GitHub. +* ``$ grunt publish`` to recompile all plugins and all permutations and upload to S3. +* ``$ npm publish`` to push to npm. +* Confirm that the new version exists behind ``cdn.ravenjs.com`` +* Update version in the ``gh-pages`` branch specifically for http://ravenjs.com/. +* glhf diff --git a/htdocs/includes/raven-js/docs/index.rst b/htdocs/includes/raven-js/docs/index.rst new file mode 100644 index 00000000000..afe07bc9ab9 --- /dev/null +++ b/htdocs/includes/raven-js/docs/index.rst @@ -0,0 +1,42 @@ +Raven.js +======== + +Raven.js is a tiny standalone JavaScript client for `Sentry `_. + +**This version of Raven.js requires Sentry 6.0 or newer.** + + +Getting Started +--------------- + +.. toctree:: + :maxdepth: 2 + + install/index + plugins/index + config/index + usage/index + tips/index + +Developers +---------- +.. toctree:: + :maxdepth: 2 + + contributing/index + +What's New? +----------- +.. toctree:: + :maxdepth: 2 + + changelog/index + +Resources +--------- +* `Download `_ +* `Bug Tracker `_ +* `Code `_ +* `IRC `_ (irc.freenode.net, #sentry) +* :doc:`Changelog ` +* Follow `@mattrobenolt `_ on Twitter for updates! diff --git a/htdocs/includes/raven-js/docs/install/index.rst b/htdocs/includes/raven-js/docs/install/index.rst new file mode 100644 index 00000000000..17feda4e08e --- /dev/null +++ b/htdocs/includes/raven-js/docs/install/index.rst @@ -0,0 +1,61 @@ +Installation +============ + +Raven is distributed in a few different methods, and should get included after any other libraries are included, but before your own scripts. + +So for example: + +.. parsed-literal:: + + + + + + +This allows the ability for Raven's plugins to instrument themselves. If included before something like jQuery, it'd be impossible to use for example, the jquery plugin. + +Using our CDN +~~~~~~~~~~~~~ + +We serve our own builds off of `Fastly `_. They are accessible over both http and https, so we recommend leaving the protocol off. + +Our CDN distributes builds with and without :doc:`plugins `. + +.. parsed-literal:: + + + +**We highly recommend trying out a plugin or two since it'll greatly improve the chances that we can collect good information.** + +This version does not include any plugins. See `ravenjs.com `_ for more information about plugins and getting other builds. + +Bower +~~~~~ + +We also provide a way to deploy Raven via `bower +`_. Useful if you want serve your own scripts instead of depending on our CDN and mantain a ``bower.json`` with a list of dependencies and versions (adding the ``--save`` flag would automatically add it to ``bower.json``). + +.. code-block:: sh + + $ bower install raven-js --save + +.. code-block:: html + + + +Also note that the file is uncompresed but is ready to pass to any decent JavaScript compressor like `uglify `_. + +npm +~~~ + +Raven is published to npm as well. https://www.npmjs.com/package/raven-js + +.. code-block:: sh + + $ npm install raven-js --save + +Requirements +~~~~~~~~~~~~ + +Raven expects the browser to provide `window.JSON` and `window.JSON.stringify`. In Internet Explorer 8+ these are available in `standards mode `_. +You can also use `json2.js `_ to provide the JSON implementation in browsers/modes which doesn't support native JSON diff --git a/htdocs/includes/raven-js/docs/make.bat b/htdocs/includes/raven-js/docs/make.bat new file mode 100644 index 00000000000..13e2848a47d --- /dev/null +++ b/htdocs/includes/raven-js/docs/make.bat @@ -0,0 +1,190 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=_build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . +set I18NSPHINXOPTS=%SPHINXOPTS% . +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% + set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. texinfo to make Texinfo files + echo. gettext to make PO message catalogs + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\raven-js.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\raven-js.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + if errorlevel 1 exit /b 1 + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "texinfo" ( + %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. + goto end +) + +if "%1" == "gettext" ( + %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale + if errorlevel 1 exit /b 1 + echo. + echo.Build finished. The message catalogs are in %BUILDDIR%/locale. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + if errorlevel 1 exit /b 1 + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + if errorlevel 1 exit /b 1 + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + if errorlevel 1 exit /b 1 + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/htdocs/includes/raven-js/docs/plugins/index.rst b/htdocs/includes/raven-js/docs/plugins/index.rst new file mode 100644 index 00000000000..7a9df7b019e --- /dev/null +++ b/htdocs/includes/raven-js/docs/plugins/index.rst @@ -0,0 +1,20 @@ +Plugins +======= + +What are plugins? +~~~~~~~~~~~~~~~~~ + +In Raven.js, plugins are little snippets of code to augment functionality for a specific application/framework. It is highly recommended to checkout the list of plugins and use what apply to your project. + +In order to keep the core small, we have opted to only include the most basic functionality by default, and you can pick and choose which plugins are applicable for you. + +Why are plugins needed at all? +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +JavaScript is pretty restrictive when it comes to exception handling, and there are a lot of things that make it difficult to get relevent information, so it's important that we inject code and wrap things magically so we can extract what we need. See :doc:`/usage/index` for tips regarding that. + + +All Plugins +~~~~~~~~~~~ +* https://github.com/getsentry/raven-js/tree/master/plugins +* `Download `_ diff --git a/htdocs/includes/raven-js/docs/tips/index.rst b/htdocs/includes/raven-js/docs/tips/index.rst new file mode 100644 index 00000000000..8dee09eb7ef --- /dev/null +++ b/htdocs/includes/raven-js/docs/tips/index.rst @@ -0,0 +1,79 @@ +Pro Tips™ +========= + + +Decluttering Sentry +~~~~~~~~~~~~~~~~~~~ + +The first thing to do is to consider constructing a whitelist of domains in which might raise acceptable exceptions. + +If your scripts are loaded from ``cdn.example.com`` and your site is ``example.com`` it'd be reasonable to set ``whitelistUrls`` to: + +.. code-block:: javascript + + whitelistUrls: [ + /https?:\/\/((cdn|www)\.)?example\.com/ + ] + +Since this accepts a regular expression, that would catch anything \*.example.com or example.com exactly. See also: :ref:`Config: whitelistUrls`. + +Next, checkout the list of :doc:`plugins ` we provide and see which are applicable to you. + +The community has compiled a list of common ignore rules for common things, like Facebook, Chrome extensions, etc. So it's recommended to at least check these out and see if they apply to you. `Check out the original gist `_. + +.. code-block:: javascript + + var ravenOptions = { + ignoreErrors: [ + // Random plugins/extensions + 'top.GLOBALS', + // See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html + 'originalCreateNotification', + 'canvas.contentDocument', + 'MyApp_RemoveAllHighlights', + 'http://tt.epicplay.com', + 'Can\'t find variable: ZiteReader', + 'jigsaw is not defined', + 'ComboSearch is not defined', + 'http://loading.retry.widdit.com/', + 'atomicFindClose', + // Facebook borked + 'fb_xd_fragment', + // ISP "optimizing" proxy - `Cache-Control: no-transform` seems to reduce this. (thanks @acdha) + // See http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy + 'bmi_SafeAddOnload', + 'EBCallBackMessageReceived', + // See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx + 'conduitPage' + ], + ignoreUrls: [ + // Facebook flakiness + /graph\.facebook\.com/i, + // Facebook blocked + /connect\.facebook\.net\/en_US\/all\.js/i, + // Woopra flakiness + /eatdifferent\.com\.woopra-ns\.com/i, + /static\.woopra\.com\/js\/woopra\.js/i, + // Chrome extensions + /extensions\//i, + /^chrome:\/\//i, + // Other plugins + /127\.0\.0\.1:4001\/isrunning/i, // Cacaoweb + /webappstoolbarba\.texthelp\.com\//i, + /metrics\.itunes\.apple\.com\.edgesuite\.net\//i + ] + }; + + +Sampling Data +~~~~~~~~~~~~~ + +It happens frequently that errors sent from your frontend can be overwhelming. One solution here is to only send a sample of the events that happen. You can do this via the ``shouldSendCallback`` setting: + +.. code-block:: javascript + + shouldSendCallback: function(data) { + // only send 10% of errors + var sampleRate = 10; + return (Math.random() * 100 <= sampleRate); + } diff --git a/htdocs/includes/raven-js/docs/usage/index.rst b/htdocs/includes/raven-js/docs/usage/index.rst new file mode 100644 index 00000000000..c8e01f9c801 --- /dev/null +++ b/htdocs/includes/raven-js/docs/usage/index.rst @@ -0,0 +1,156 @@ +Usage +===== + +By default, Raven makes a few efforts to try its best to capture meaningful stack traces, but browsers make it pretty difficult. + +The easiest solution is to prevent an error from bubbling all of the way up the stack to ``window``. + +How to actually capture an error correctly +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +try...catch +----------- + +The simplest way, is to try and explicitly capture and report potentially problematic code with a ``try...catch`` block and ``Raven.captureException``. + +.. code-block:: javascript + + try { + doSomething(a[0]) + } catch(e) { + Raven.captureException(e) + } + +**Do not** throw strings! Always throw an actual ``Error`` object. For example: + +.. code-block:: javascript + + throw new Error('broken') // good + throw 'broken' // bad + +It's impossible to retrieve a stack trace from a string. If this happens, Raven transmits the error as a plain message. + +context/wrap +------------ + +``Raven.context`` allows you to wrap any function to be immediately executed. Behind the scenes, Raven is just wrapping your code in a ``try...catch`` block to record the exception before re-throwing it. + +.. code-block:: javascript + + Raven.context(function() { + doSomething(a[0]) + }) + +``Raven.wrap`` wraps a function in a similar way to ``Raven.context``, but instead of executing the function, it returns another function. This is totally awesome for use when passing around a callback. + +.. code-block:: javascript + + var doIt = function() { + // doing cool stuff + } + + setTimeout(Raven.wrap(doIt), 1000) + +Tracking authenticated users +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +While a user is logged in, you can tell Sentry to associate errors with user data. + +.. code-block:: javascript + + Raven.setUserContext({ + email: 'matt@example.com', + id: '123' + }) + +If at any point, the user becomes unauthenticated, you can call ``Raven.setUserContext()`` with no arguments to remove their data. *This would only really be useful in a large web app where the user logs in/out without a page reload.* + +Capturing a specific message +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: javascript + + Raven.captureMessage('Broken!') + +Passing additional data +~~~~~~~~~~~~~~~~~~~~~~~ + +``captureException``, ``context``, ``wrap``, and ``captureMessage`` functions all allow passing additional data to be tagged onto the error, such as ``tags`` or ``extra`` for additional context. + +.. code-block:: javascript + + Raven.captureException(e, {tags: { key: "value" }}) + + Raven.captureMessage('Broken!', {tags: { key: "value" }}) + + Raven.context({tags: { key: "value" }}, function(){ ... }) + + Raven.wrap({logger: "my.module"}, function(){ ... }) + + Raven.captureException(e, {extra: { foo: "bar" }}) + +You can also set context variables globally to be merged in with future exceptions with ``setExtraContext`` and ``setTagsContext``. + +.. code-block:: javascript + + Raven.setExtraContext({ foo: "bar" }) + Raven.setTagsContext({ key: "value" }) + + +Getting back an event id +~~~~~~~~~~~~~~~~~~~~~~~~ + +An event id is a globally unique id for the event that was just sent. This event id can be used to find the exact event from within Sentry. + +This is often used to display for the user and report an error to customer service. + +.. code-block:: javascript + + Raven.lastEventId() + +``Raven.lastEventId()`` will be undefined until an event is sent. After an event is sent, it will contain the string id. + +.. code-block:: javascript + + Raven.captureMessage('Broken!') + alert(Raven.lastEventId()) + + +Check if Raven is setup and ready to go +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. code-block:: javascript + + Raven.isSetup() + + +Dealing with minified source code +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Raven and Sentry now support `Source Maps `_. + +We have provided some instructions to creating Source Maps over at https://www.getsentry.com/docs/sourcemaps/. Also, checkout our `Gruntfile `_ for a good example of what we're doing. + +You can use `Source Map Validator `_ to help verify that things are correct. + +CORS +~~~~ + +If you're hosting your scripts on another domain and things don't get caught by Raven, it's likely that the error will bubble up to ``window.onerror``. If this happens, the error will report some ugly ``Script error`` and Raven will drop it on the floor +since this is a useless error for everybody. + +To help mitigate this, we can tell the browser that these scripts are safe and we're allowing them to expose their errors to us. + +In your `` + +And set an ``Access-Control-Allow-Origin`` HTTP header on that file. + +.. code-block:: console + + Access-Control-Allow-Origin: * + +**Note: both of these steps need to be done or your scripts might not even get executed** diff --git a/htdocs/includes/raven-js/example/Makefile b/htdocs/includes/raven-js/example/Makefile new file mode 100644 index 00000000000..87976776e90 --- /dev/null +++ b/htdocs/includes/raven-js/example/Makefile @@ -0,0 +1,3 @@ + +build: + ../node_modules/.bin/uglifyjs --source-map=file.sourcemap.js -c -o file.min.js file1.js file2.js diff --git a/htdocs/includes/raven-js/example/file.min.js b/htdocs/includes/raven-js/example/file.min.js new file mode 100644 index 00000000000..12b9f811b28 --- /dev/null +++ b/htdocs/includes/raven-js/example/file.min.js @@ -0,0 +1,2 @@ +function add(a,b){"use strict";return a+b}function multiply(a,b){"use strict";return a*b}function divide(a,b){"use strict";try{return multiply(add(a,b),a,b)/c}catch(e){Raven.captureException(e)}} +//@ sourceMappingURL=file.sourcemap.js \ No newline at end of file diff --git a/htdocs/includes/raven-js/example/file.sourcemap.js b/htdocs/includes/raven-js/example/file.sourcemap.js new file mode 100644 index 00000000000..1bd0f6510cf --- /dev/null +++ b/htdocs/includes/raven-js/example/file.sourcemap.js @@ -0,0 +1 @@ +{"version":3,"file":"file.min.js","sources":["file1.js","file2.js"],"names":["add","a","b","multiply","divide","c","e","Raven","captureException"],"mappings":"AAAA,QAASA,KAAIC,EAAGC,GACf,YACA,OAAOD,GAAIC,ECFZ,QAASC,UAASF,EAAGC,GACpB,YACA,OAAOD,GAAIC,EAEZ,QAASE,QAAOH,EAAGC,GAClB,YACA,KACC,MAAOC,UAASH,IAAIC,EAAGC,GAAID,EAAGC,GAAKG,EAClC,MAAOC,GACRC,MAAMC,iBAAiBF"} \ No newline at end of file diff --git a/htdocs/includes/raven-js/example/file1.js b/htdocs/includes/raven-js/example/file1.js new file mode 100644 index 00000000000..eed5827852d --- /dev/null +++ b/htdocs/includes/raven-js/example/file1.js @@ -0,0 +1,4 @@ +function add(a, b) { + "use strict"; + return a + b; +} \ No newline at end of file diff --git a/htdocs/includes/raven-js/example/file2.js b/htdocs/includes/raven-js/example/file2.js new file mode 100644 index 00000000000..8b174356846 --- /dev/null +++ b/htdocs/includes/raven-js/example/file2.js @@ -0,0 +1,12 @@ +function multiply(a, b) { + "use strict"; + return a * b; +} +function divide(a, b) { + "use strict"; + try { + return multiply(add(a, b), a, b) / c; + } catch (e) { + Raven.captureException(e); + } +} diff --git a/htdocs/includes/raven-js/example/index.html b/htdocs/includes/raven-js/example/index.html new file mode 100644 index 00000000000..b7ebbdd018e --- /dev/null +++ b/htdocs/includes/raven-js/example/index.html @@ -0,0 +1,41 @@ + + + + Scratch Disk + + + + + + + + + + + + + + + + + + diff --git a/htdocs/includes/raven-js/example/scratch.js b/htdocs/includes/raven-js/example/scratch.js new file mode 100644 index 00000000000..f1962bbdfc1 --- /dev/null +++ b/htdocs/includes/raven-js/example/scratch.js @@ -0,0 +1,42 @@ +function foo() { + console.log("lol, i don't do anything") +} + +function foo2() { + foo() + console.log('i called foo') +} + +function broken() { + try { + /*fkjdsahfdhskfhdsahfudshafuoidashfudsa*/ fdasfds[0]; // i throw an error h sadhf hadsfdsakf kl;dsjaklf jdklsajfk ljds;klafldsl fkhdas;hf hdsaf hdsalfhjldksahfljkdsahfjkl dhsajkfl hdklsahflkjdsahkfj hdsjakhf dkashfl diusafh kdsjahfkldsahf jkdashfj khdasjkfhdjksahflkjdhsakfhjdksahfjkdhsakf hdajskhf kjdash kjfads fjkadsh jkfdsa jkfdas jkfdjkas hfjkdsajlk fdsajk fjkdsa fjdsa fdkjlsa fjkdaslk hfjlkdsah fhdsahfui + }catch(e) { + Raven.captureException(e); + } +} + +function ready() { + document.getElementById('test').onclick = broken; +} + +function foo3() { + document.getElementById('crap').value = 'barfdasjkfhoadshflkaosfjadiosfhdaskjfasfadsfads'; +} + +function somethingelse() { + document.getElementById('somethingelse').value = 'this is some realy really long message just so our minification is largeeeeeeeeee!'; +} + +function derp() { + fdas[0]; +} + +function testOptions() { + Raven.context({tags: {foo: 'bar'}}, function() { + throw new Error('foo'); + }); +} + +function throwString() { + throw 'oops'; +} diff --git a/htdocs/includes/raven-js/package.json b/htdocs/includes/raven-js/package.json new file mode 100644 index 00000000000..9352edcab38 --- /dev/null +++ b/htdocs/includes/raven-js/package.json @@ -0,0 +1,32 @@ +{ + "name": "raven-js", + "version": "1.1.19", + "license": "BSD-2-Clause", + "homepage": "https://getsentry.com", + "scripts": { + "pretest": "npm install", + "test": "grunt test" + }, + "repository": { + "type": "git", + "url": "git://github.com/getsentry/raven-js.git" + }, + "main": "dist/raven.js", + "devDependencies": { + "chai": "~1.8.1", + "grunt": "~0.4.1", + "grunt-cli": "~0.1.9", + "grunt-contrib-jshint": "~0.6.3", + "grunt-contrib-uglify": "~0.2.2", + "grunt-contrib-concat": "~0.3.0", + "grunt-contrib-clean": "~0.4.0", + "grunt-mocha": "~0.4.1", + "grunt-release": "~0.6.0", + "grunt-s3": "~0.2.0-alpha.3", + "grunt-gitinfo": "~0.1.1", + "grunt-contrib-connect": "~0.5.0", + "grunt-contrib-copy": "~0.4.1", + "sinon": "~1.7.3", + "lodash": "~2.4.0" + } +} diff --git a/htdocs/includes/raven-js/plugins/angular.js b/htdocs/includes/raven-js/plugins/angular.js new file mode 100644 index 00000000000..98bf5892001 --- /dev/null +++ b/htdocs/includes/raven-js/plugins/angular.js @@ -0,0 +1,36 @@ +/** + * Angular.js plugin + * + * Provides an $exceptionHandler for Angular.js + */ +;(function(Raven, angular) { +'use strict'; + +// quit if angular isn't on the page +if (!angular) { + return; +} + +function ngRavenProvider($provide) { + $provide.decorator('$exceptionHandler', [ + 'RavenConfig', '$delegate', + ngRavenExceptionHandler + ]); +} + +function ngRavenExceptionHandler(RavenConfig, $delegate) { + if (!RavenConfig) + throw new Error('RavenConfig must be set before using this'); + + Raven.config(RavenConfig.dsn, RavenConfig.config).install(); + return function angularExceptionHandler(ex, cause) { + $delegate(ex, cause); + Raven.captureException(ex, {extra: {cause: cause}}); + }; +} + +angular.module('ngRaven', []) + .config(['$provide', ngRavenProvider]) + .value('Raven', Raven); + +})(window.Raven, window.angular); diff --git a/htdocs/includes/raven-js/plugins/backbone.js b/htdocs/includes/raven-js/plugins/backbone.js new file mode 100644 index 00000000000..09e0f6b8c1c --- /dev/null +++ b/htdocs/includes/raven-js/plugins/backbone.js @@ -0,0 +1,55 @@ +/** + * Backbone.js plugin + * + * Patches Backbone.Events callbacks. + */ +;(function(window, Raven, Backbone) { +'use strict'; + +// quit if Backbone isn't on the page +if (!Backbone) { + return; +} + +function makeBackboneEventsOn(oldOn) { + return function BackboneEventsOn(name, callback, context) { + var wrapCallback = function (cb) { + if (Object.prototype.toString.call(cb) === '[object Function]') { + var _callback = cb._callback || cb; + cb = Raven.wrap(cb); + cb._callback = _callback; + } + return cb; + }; + if (Object.prototype.toString.call(name) === '[object Object]') { + // Handle event maps. + for (var key in name) { + if (name.hasOwnProperty(key)) { + name[key] = wrapCallback(name[key]); + } + } + } else { + callback = wrapCallback(callback); + } + return oldOn.call(this, name, callback, context); + }; +} + +// We're too late to catch all of these by simply patching Backbone.Events.on +var affectedObjects = [ + Backbone.Events, + Backbone, + Backbone.Model.prototype, + Backbone.Collection.prototype, + Backbone.View.prototype, + Backbone.Router.prototype, + Backbone.History.prototype +], i = 0, l = affectedObjects.length; + +for (; i < l; i++) { + var affected = affectedObjects[i]; + affected.on = makeBackboneEventsOn(affected.on); + affected.bind = affected.on; +} + +}(window, window.Raven, window.Backbone)); diff --git a/htdocs/includes/raven-js/plugins/console.js b/htdocs/includes/raven-js/plugins/console.js new file mode 100644 index 00000000000..4d5ba94090f --- /dev/null +++ b/htdocs/includes/raven-js/plugins/console.js @@ -0,0 +1,43 @@ +/** + * console plugin + * + * Monkey patches console.* calls into Sentry messages with + * their appropriate log levels. (Experimental) + */ +;(function(window, Raven, console) { +'use strict'; + +var originalConsole = console, + logLevels = ['debug', 'info', 'warn', 'error'], + level = logLevels.pop(); + +var logForGivenLevel = function(level) { + var originalConsoleLevel = console[level]; + + // warning level is the only level that doesn't map up + // correctly with what Sentry expects. + if (level === 'warn') level = 'warning'; + return function () { + var args = [].slice.call(arguments); + Raven.captureMessage('' + args, {level: level, logger: 'console'}); + + // this fails for some browsers. :( + if (originalConsoleLevel) { + // IE9 doesn't allow calling apply on console functions directly + // See: https://stackoverflow.com/questions/5472938/does-ie9-support-console-log-and-is-it-a-real-function#answer-5473193 + Function.prototype.bind + .call(originalConsoleLevel, originalConsole) + .apply(originalConsole, args); + } + }; +}; + + +while(level) { + console[level] = logForGivenLevel(level); + level = logLevels.pop(); +} +// export +window.console = console; + +}(window, window.Raven, window.console || {})); diff --git a/htdocs/includes/raven-js/plugins/ember.js b/htdocs/includes/raven-js/plugins/ember.js new file mode 100644 index 00000000000..c6d0551c709 --- /dev/null +++ b/htdocs/includes/raven-js/plugins/ember.js @@ -0,0 +1,29 @@ +/** + * Ember.js plugin + * + * Patches event handler callbacks and ajax callbacks. + */ +;(function(window, Raven, Ember) { +'use strict'; + +// quit if Ember isn't on the page +if (!Ember) { + return; +} + +var _oldOnError = Ember.onerror; +Ember.onerror = function EmberOnError(error) { + Raven.captureException(error); + if (typeof _oldOnError === 'function') { + _oldOnError.call(this, error); + } +}; +Ember.RSVP.on('error', function (reason) { + if (reason instanceof Error) { + Raven.captureException(reason, {extra: {context: 'Unhandled Promise error detected'}}); + } else { + Raven.captureMessage('Unhandled Promise error detected', {extra: {reason: reason}}); + } +}); + +}(window, window.Raven, window.Ember)); diff --git a/htdocs/includes/raven-js/plugins/jquery.js b/htdocs/includes/raven-js/plugins/jquery.js new file mode 100644 index 00000000000..4a5474cdb86 --- /dev/null +++ b/htdocs/includes/raven-js/plugins/jquery.js @@ -0,0 +1,75 @@ +/** + * jQuery plugin + * + * Patches event handler callbacks and ajax callbacks. + */ +;(function(window, Raven, $) { +'use strict'; + +// quit if jQuery isn't on the page +if (!$) { + return; +} + +var _oldEventAdd = $.event.add; +$.event.add = function ravenEventAdd(elem, types, handler, data, selector) { + var _handler; + + if (handler && handler.handler) { + _handler = handler.handler; + handler.handler = Raven.wrap(handler.handler); + } else { + _handler = handler; + handler = Raven.wrap(handler); + } + + // If the handler we are attaching doesn’t have the same guid as + // the original, it will never be removed when someone tries to + // unbind the original function later. Technically as a result of + // this our guids are no longer globally unique, but whatever, that + // never hurt anybody RIGHT?! + if (_handler.guid) { + handler.guid = _handler.guid; + } else { + handler.guid = _handler.guid = $.guid++; + } + + return _oldEventAdd.call(this, elem, types, handler, data, selector); +}; + +var _oldReady = $.fn.ready; +$.fn.ready = function ravenjQueryReadyWrapper(fn) { + return _oldReady.call(this, Raven.wrap(fn)); +}; + +var _oldAjax = $.ajax; +$.ajax = function ravenAjaxWrapper(url, options) { + var keys = ['complete', 'error', 'success'], key; + + // Taken from https://github.com/jquery/jquery/blob/eee2eaf1d7a189d99106423a4206c224ebd5b848/src/ajax.js#L311-L318 + // If url is an object, simulate pre-1.5 signature + if (typeof url === 'object') { + options = url; + url = undefined; + } + + // Force options to be an object + options = options || {}; + + /*jshint -W084*/ + while(key = keys.pop()) { + if ($.isFunction(options[key])) { + options[key] = Raven.wrap(options[key]); + } + } + /*jshint +W084*/ + + try { + return _oldAjax.call(this, url, options); + } catch (e) { + Raven.captureException(e); + throw e; + } +}; + +}(window, window.Raven, window.jQuery)); diff --git a/htdocs/includes/raven-js/plugins/native.js b/htdocs/includes/raven-js/plugins/native.js new file mode 100644 index 00000000000..c641f13a5d1 --- /dev/null +++ b/htdocs/includes/raven-js/plugins/native.js @@ -0,0 +1,33 @@ +/** + * native plugin + * + * Extends support for global error handling for asynchronous browser + * functions. Adopted from Closure Library's errorhandler.js. + */ +;(function extendToAsynchronousCallbacks(window, Raven) { +"use strict"; + +var _helper = function _helper(fnName) { + var originalFn = window[fnName]; + window[fnName] = function ravenAsyncExtension() { + // Make a copy of the arguments + var args = [].slice.call(arguments); + var originalCallback = args[0]; + if (typeof (originalCallback) === 'function') { + args[0] = Raven.wrap(originalCallback); + } + // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it + // also supports only two arguments and doesn't care what this is, so we + // can just call the original function directly. + if (originalFn.apply) { + return originalFn.apply(this, args); + } else { + return originalFn(args[0], args[1]); + } + }; +}; + +_helper('setTimeout'); +_helper('setInterval'); + +}(window, window.Raven)); diff --git a/htdocs/includes/raven-js/plugins/require.js b/htdocs/includes/raven-js/plugins/require.js new file mode 100644 index 00000000000..60378a1b0b9 --- /dev/null +++ b/htdocs/includes/raven-js/plugins/require.js @@ -0,0 +1,14 @@ +/** + * require.js plugin + * + * Automatically wrap define/require callbacks. (Experimental) + */ +;(function(window, Raven) { +'use strict'; + +if (typeof define === 'function' && define.amd) { + window.define = Raven.wrap({deep: false}, define); + window.require = Raven.wrap({deep: false}, require); +} + +}(window, window.Raven)); diff --git a/htdocs/includes/raven-js/src/raven.js b/htdocs/includes/raven-js/src/raven.js new file mode 100644 index 00000000000..a2d2b35604b --- /dev/null +++ b/htdocs/includes/raven-js/src/raven.js @@ -0,0 +1,830 @@ +'use strict'; + +// First, check for JSON support +// If there is no JSON, we no-op the core features of Raven +// since JSON is required to encode the payload +var _Raven = window.Raven, + hasJSON = !!(typeof JSON === 'object' && JSON.stringify), + lastCapturedException, + lastEventId, + globalServer, + globalUser, + globalKey, + globalProject, + globalOptions = { + logger: 'javascript', + ignoreErrors: [], + ignoreUrls: [], + whitelistUrls: [], + includePaths: [], + collectWindowErrors: true, + tags: {}, + maxMessageLength: 100, + extra: {} + }, + authQueryString, + isRavenInstalled = false, + + objectPrototype = Object.prototype, + startTime = now(); + +/* + * The core Raven singleton + * + * @this {Raven} + */ +var Raven = { + VERSION: '<%= pkg.version %>', + + debug: true, + + /* + * Allow multiple versions of Raven to be installed. + * Strip Raven from the global context and returns the instance. + * + * @return {Raven} + */ + noConflict: function() { + window.Raven = _Raven; + return Raven; + }, + + /* + * Configure Raven with a DSN and extra options + * + * @param {string} dsn The public Sentry DSN + * @param {object} options Optional set of of global options [optional] + * @return {Raven} + */ + config: function(dsn, options) { + if (globalServer) { + logDebug('error', 'Error: Raven has already been configured'); + return Raven; + } + if (!dsn) return Raven; + + var uri = parseDSN(dsn), + lastSlash = uri.path.lastIndexOf('/'), + path = uri.path.substr(1, lastSlash); + + // merge in options + if (options) { + each(options, function(key, value){ + globalOptions[key] = value; + }); + } + + // "Script error." is hard coded into browsers for errors that it can't read. + // this is the result of a script being pulled in from an external domain and CORS. + globalOptions.ignoreErrors.push(/^Script error\.?$/); + globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/); + + // join regexp rules into one big rule + globalOptions.ignoreErrors = joinRegExp(globalOptions.ignoreErrors); + globalOptions.ignoreUrls = globalOptions.ignoreUrls.length ? joinRegExp(globalOptions.ignoreUrls) : false; + globalOptions.whitelistUrls = globalOptions.whitelistUrls.length ? joinRegExp(globalOptions.whitelistUrls) : false; + globalOptions.includePaths = joinRegExp(globalOptions.includePaths); + + globalKey = uri.user; + globalProject = uri.path.substr(lastSlash + 1); + + // assemble the endpoint from the uri pieces + globalServer = '//' + uri.host + + (uri.port ? ':' + uri.port : '') + + '/' + path + 'api/' + globalProject + '/store/'; + + if (uri.protocol) { + globalServer = uri.protocol + ':' + globalServer; + } + + if (globalOptions.fetchContext) { + TraceKit.remoteFetching = true; + } + + if (globalOptions.linesOfContext) { + TraceKit.linesOfContext = globalOptions.linesOfContext; + } + + TraceKit.collectWindowErrors = !!globalOptions.collectWindowErrors; + + setAuthQueryString(); + + // return for chaining + return Raven; + }, + + /* + * Installs a global window.onerror error handler + * to capture and report uncaught exceptions. + * At this point, install() is required to be called due + * to the way TraceKit is set up. + * + * @return {Raven} + */ + install: function() { + if (isSetup() && !isRavenInstalled) { + TraceKit.report.subscribe(handleStackInfo); + isRavenInstalled = true; + } + + return Raven; + }, + + /* + * Wrap code within a context so Raven can capture errors + * reliably across domains that is executed immediately. + * + * @param {object} options A specific set of options for this context [optional] + * @param {function} func The callback to be immediately executed within the context + * @param {array} args An array of arguments to be called with the callback [optional] + */ + context: function(options, func, args) { + if (isFunction(options)) { + args = func || []; + func = options; + options = undefined; + } + + return Raven.wrap(options, func).apply(this, args); + }, + + /* + * Wrap code within a context and returns back a new function to be executed + * + * @param {object} options A specific set of options for this context [optional] + * @param {function} func The function to be wrapped in a new context + * @return {function} The newly wrapped functions with a context + */ + wrap: function(options, func) { + // 1 argument has been passed, and it's not a function + // so just return it + if (isUndefined(func) && !isFunction(options)) { + return options; + } + + // options is optional + if (isFunction(options)) { + func = options; + options = undefined; + } + + // At this point, we've passed along 2 arguments, and the second one + // is not a function either, so we'll just return the second argument. + if (!isFunction(func)) { + return func; + } + + // We don't wanna wrap it twice! + if (func.__raven__) { + return func; + } + + function wrapped() { + var args = [], i = arguments.length, + deep = !options || options && options.deep !== false; + // Recursively wrap all of a function's arguments that are + // functions themselves. + + while(i--) args[i] = deep ? Raven.wrap(options, arguments[i]) : arguments[i]; + + try { + /*jshint -W040*/ + return func.apply(this, args); + } catch(e) { + Raven.captureException(e, options); + throw e; + } + } + + // copy over properties of the old function + for (var property in func) { + if (hasKey(func, property)) { + wrapped[property] = func[property]; + } + } + + // Signal that this function has been wrapped already + // for both debugging and to prevent it to being wrapped twice + wrapped.__raven__ = true; + wrapped.__inner__ = func; + + return wrapped; + }, + + /* + * Uninstalls the global error handler. + * + * @return {Raven} + */ + uninstall: function() { + TraceKit.report.uninstall(); + isRavenInstalled = false; + + return Raven; + }, + + /* + * Manually capture an exception and send it over to Sentry + * + * @param {error} ex An exception to be logged + * @param {object} options A specific set of options for this error [optional] + * @return {Raven} + */ + captureException: function(ex, options) { + // If not an Error is passed through, recall as a message instead + if (!isError(ex)) return Raven.captureMessage(ex, options); + + // Store the raw exception object for potential debugging and introspection + lastCapturedException = ex; + + // TraceKit.report will re-raise any exception passed to it, + // which means you have to wrap it in try/catch. Instead, we + // can wrap it here and only re-raise if TraceKit.report + // raises an exception different from the one we asked to + // report on. + try { + TraceKit.report(ex, options); + } catch(ex1) { + if(ex !== ex1) { + throw ex1; + } + } + + return Raven; + }, + + /* + * Manually send a message to Sentry + * + * @param {string} msg A plain message to be captured in Sentry + * @param {object} options A specific set of options for this message [optional] + * @return {Raven} + */ + captureMessage: function(msg, options) { + // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an + // early call; we'll error on the side of logging anything called before configuration since it's + // probably something you should see: + if (!!globalOptions.ignoreErrors.test && globalOptions.ignoreErrors.test(msg)) { + return; + } + + // Fire away! + send( + objectMerge({ + message: msg + '' // Make sure it's actually a string + }, options) + ); + + return Raven; + }, + + /* + * Set/clear a user to be sent along with the payload. + * + * @param {object} user An object representing user data [optional] + * @return {Raven} + */ + setUserContext: function(user) { + globalUser = user; + + return Raven; + }, + + /* + * Set extra attributes to be sent along with the payload. + * + * @param {object} extra An object representing extra data [optional] + * @return {Raven} + */ + setExtraContext: function(extra) { + globalOptions.extra = extra || {}; + + return Raven; + }, + + /* + * Set tags to be sent along with the payload. + * + * @param {object} tags An object representing tags [optional] + * @return {Raven} + */ + setTagsContext: function(tags) { + globalOptions.tags = tags || {}; + + return Raven; + }, + + /* + * Set release version of application + * + * @param {string} release Typically something like a git SHA to identify version + * @return {Raven} + */ + setReleaseContext: function(release) { + globalOptions.release = release; + + return Raven; + }, + + /* + * Set the dataCallback option + * + * @param {function} callback The callback to run which allows the + * data blob to be mutated before sending + * @return {Raven} + */ + setDataCallback: function(callback) { + globalOptions.dataCallback = callback; + + return Raven; + }, + + /* + * Set the shouldSendCallback option + * + * @param {function} callback The callback to run which allows + * introspecting the blob before sending + * @return {Raven} + */ + setShouldSendCallback: function(callback) { + globalOptions.shouldSendCallback = callback; + + return Raven; + }, + + /* + * Get the latest raw exception that was captured by Raven. + * + * @return {error} + */ + lastException: function() { + return lastCapturedException; + }, + + /* + * Get the last event id + * + * @return {string} + */ + lastEventId: function() { + return lastEventId; + }, + + /* + * Determine if Raven is setup and ready to go. + * + * @return {boolean} + */ + isSetup: function() { + return isSetup(); + } +}; + +Raven.setUser = Raven.setUserContext; // To be deprecated + +function triggerEvent(eventType, options) { + var event, key; + + options = options || {}; + + eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1); + + if (document.createEvent) { + event = document.createEvent('HTMLEvents'); + event.initEvent(eventType, true, true); + } else { + event = document.createEventObject(); + event.eventType = eventType; + } + + for (key in options) if (hasKey(options, key)) { + event[key] = options[key]; + } + + if (document.createEvent) { + // IE9 if standards + document.dispatchEvent(event); + } else { + // IE8 regardless of Quirks or Standards + // IE9 if quirks + try { + document.fireEvent('on' + event.eventType.toLowerCase(), event); + } catch(e) {} + } +} + +var dsnKeys = 'source protocol user pass host port path'.split(' '), + dsnPattern = /^(?:(\w+):)?\/\/(\w+)(:\w+)?@([\w\.-]+)(?::(\d+))?(\/.*)/; + +function RavenConfigError(message) { + this.name = 'RavenConfigError'; + this.message = message; +} +RavenConfigError.prototype = new Error(); +RavenConfigError.prototype.constructor = RavenConfigError; + +/**** Private functions ****/ +function parseDSN(str) { + var m = dsnPattern.exec(str), + dsn = {}, + i = 7; + + try { + while (i--) dsn[dsnKeys[i]] = m[i] || ''; + } catch(e) { + throw new RavenConfigError('Invalid DSN: ' + str); + } + + if (dsn.pass) + throw new RavenConfigError('Do not specify your private key in the DSN!'); + + return dsn; +} + +function isUndefined(what) { + return what === void 0; +} + +function isFunction(what) { + return typeof what === 'function'; +} + +function isString(what) { + return objectPrototype.toString.call(what) === '[object String]'; +} + +function isObject(what) { + return typeof what === 'object' && what !== null; +} + +function isEmptyObject(what) { + for (var k in what) return false; + return true; +} + +// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560 +// with some tiny modifications +function isError(what) { + return isObject(what) && + objectPrototype.toString.call(what) === '[object Error]' || + what instanceof Error; +} + +/** + * hasKey, a better form of hasOwnProperty + * Example: hasKey(MainHostObject, property) === true/false + * + * @param {Object} host object to check property + * @param {string} key to check + */ +function hasKey(object, key) { + return objectPrototype.hasOwnProperty.call(object, key); +} + +function each(obj, callback) { + var i, j; + + if (isUndefined(obj.length)) { + for (i in obj) { + if (hasKey(obj, i)) { + callback.call(null, i, obj[i]); + } + } + } else { + j = obj.length; + if (j) { + for (i = 0; i < j; i++) { + callback.call(null, i, obj[i]); + } + } + } +} + + +function setAuthQueryString() { + authQueryString = + '?sentry_version=4' + + '&sentry_client=raven-js/' + Raven.VERSION + + '&sentry_key=' + globalKey; +} + + +function handleStackInfo(stackInfo, options) { + var frames = []; + + if (stackInfo.stack && stackInfo.stack.length) { + each(stackInfo.stack, function(i, stack) { + var frame = normalizeFrame(stack); + if (frame) { + frames.push(frame); + } + }); + } + + triggerEvent('handle', { + stackInfo: stackInfo, + options: options + }); + + processException( + stackInfo.name, + stackInfo.message, + stackInfo.url, + stackInfo.lineno, + frames, + options + ); +} + +function normalizeFrame(frame) { + if (!frame.url) return; + + // normalize the frames data + var normalized = { + filename: frame.url, + lineno: frame.line, + colno: frame.column, + 'function': frame.func || '?' + }, context = extractContextFromFrame(frame), i; + + if (context) { + var keys = ['pre_context', 'context_line', 'post_context']; + i = 3; + while (i--) normalized[keys[i]] = context[i]; + } + + normalized.in_app = !( // determine if an exception came from outside of our app + // first we check the global includePaths list. + !globalOptions.includePaths.test(normalized.filename) || + // Now we check for fun, if the function name is Raven or TraceKit + /(Raven|TraceKit)\./.test(normalized['function']) || + // finally, we do a last ditch effort and check for raven.min.js + /raven\.(min\.)?js$/.test(normalized.filename) + ); + + return normalized; +} + +function extractContextFromFrame(frame) { + // immediately check if we should even attempt to parse a context + if (!frame.context || !globalOptions.fetchContext) return; + + var context = frame.context, + pivot = ~~(context.length / 2), + i = context.length, isMinified = false; + + while (i--) { + // We're making a guess to see if the source is minified or not. + // To do that, we make the assumption if *any* of the lines passed + // in are greater than 300 characters long, we bail. + // Sentry will see that there isn't a context + if (context[i].length > 300) { + isMinified = true; + break; + } + } + + if (isMinified) { + // The source is minified and we don't know which column. Fuck it. + if (isUndefined(frame.column)) return; + + // If the source is minified and has a frame column + // we take a chunk of the offending line to hopefully shed some light + return [ + [], // no pre_context + context[pivot].substr(frame.column, 50), // grab 50 characters, starting at the offending column + [] // no post_context + ]; + } + + return [ + context.slice(0, pivot), // pre_context + context[pivot], // context_line + context.slice(pivot + 1) // post_context + ]; +} + +function processException(type, message, fileurl, lineno, frames, options) { + var stacktrace, label, i; + + // In some instances message is not actually a string, no idea why, + // so we want to always coerce it to one. + message += ''; + + // Sometimes an exception is getting logged in Sentry as + // + // This can only mean that the message was falsey since this value + // is hardcoded into Sentry itself. + // At this point, if the message is falsey, we bail since it's useless + if (type === 'Error' && !message) return; + + if (globalOptions.ignoreErrors.test(message)) return; + + if (frames && frames.length) { + fileurl = frames[0].filename || fileurl; + // Sentry expects frames oldest to newest + // and JS sends them as newest to oldest + frames.reverse(); + stacktrace = {frames: frames}; + } else if (fileurl) { + stacktrace = { + frames: [{ + filename: fileurl, + lineno: lineno, + in_app: true + }] + }; + } + + // Truncate the message to a max of characters + message = truncate(message, globalOptions.maxMessageLength); + + if (globalOptions.ignoreUrls && globalOptions.ignoreUrls.test(fileurl)) return; + if (globalOptions.whitelistUrls && !globalOptions.whitelistUrls.test(fileurl)) return; + + label = lineno ? message + ' at ' + lineno : message; + + // Fire away! + send( + objectMerge({ + // sentry.interfaces.Exception + exception: { + type: type, + value: message + }, + // sentry.interfaces.Stacktrace + stacktrace: stacktrace, + culprit: fileurl, + message: label + }, options) + ); +} + +function objectMerge(obj1, obj2) { + if (!obj2) { + return obj1; + } + each(obj2, function(key, value){ + obj1[key] = value; + }); + return obj1; +} + +function truncate(str, max) { + return str.length <= max ? str : str.substr(0, max) + '\u2026'; +} + +function now() { + return +new Date(); +} + +function getHttpData() { + var http = { + url: document.location.href, + headers: { + 'User-Agent': navigator.userAgent + } + }; + + if (document.referrer) { + http.headers.Referer = document.referrer; + } + + return http; +} + +function send(data) { + if (!isSetup()) return; + + data = objectMerge({ + project: globalProject, + logger: globalOptions.logger, + platform: 'javascript', + // sentry.interfaces.Http + request: getHttpData() + }, data); + + // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge + data.tags = objectMerge(objectMerge({}, globalOptions.tags), data.tags); + data.extra = objectMerge(objectMerge({}, globalOptions.extra), data.extra); + + // Send along our own collected metadata with extra + data.extra = objectMerge({ + 'session:duration': now() - startTime + }, data.extra); + + // If there are no tags/extra, strip the key from the payload alltogther. + if (isEmptyObject(data.tags)) delete data.tags; + + if (globalUser) { + // sentry.interfaces.User + data.user = globalUser; + } + + // Include the release iff it's defined in globalOptions + if (globalOptions.release) data.release = globalOptions.release; + + if (isFunction(globalOptions.dataCallback)) { + data = globalOptions.dataCallback(data) || data; + } + + // Why?????????? + if (!data || isEmptyObject(data)) { + return; + } + + // Check if the request should be filtered or not + if (isFunction(globalOptions.shouldSendCallback) && !globalOptions.shouldSendCallback(data)) { + return; + } + + // Send along an event_id if not explicitly passed. + // This event_id can be used to reference the error within Sentry itself. + // Set lastEventId after we know the error should actually be sent + lastEventId = data.event_id || (data.event_id = uuid4()); + + makeRequest(data); +} + + +function makeRequest(data) { + var img = newImage(), + src = globalServer + authQueryString + '&sentry_data=' + encodeURIComponent(JSON.stringify(data)); + + img.crossOrigin = 'anonymous'; + img.onload = function success() { + triggerEvent('success', { + data: data, + src: src + }); + }; + img.onerror = img.onabort = function failure() { + triggerEvent('failure', { + data: data, + src: src + }); + }; + img.src = src; +} + +// Note: this is shitty, but I can't figure out how to get +// sinon to stub document.createElement without breaking everything +// so this wrapper is just so I can stub it for tests. +function newImage() { + return document.createElement('img'); +} + +function isSetup() { + if (!hasJSON) return false; // needs JSON support + if (!globalServer) { + logDebug('error', 'Error: Raven has not been configured.'); + return false; + } + return true; +} + +function joinRegExp(patterns) { + // Combine an array of regular expressions and strings into one large regexp + // Be mad. + var sources = [], + i = 0, len = patterns.length, + pattern; + + for (; i < len; i++) { + pattern = patterns[i]; + if (isString(pattern)) { + // If it's a string, we need to escape it + // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, "\\$1")); + } else if (pattern && pattern.source) { + // If it's a regexp already, we want to extract the source + sources.push(pattern.source); + } + // Intentionally skip other cases + } + return new RegExp(sources.join('|'), 'i'); +} + +// http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523 +function uuid4() { + return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) { + var r = Math.random()*16|0, + v = c == 'x' ? r : (r&0x3|0x8); + return v.toString(16); + }); +} + +function logDebug(level, message) { + if (window.console && console[level] && Raven.debug) { + console[level](message); + } +} + +function afterLoad() { + // Attempt to initialize Raven on load + var RavenConfig = window.RavenConfig; + if (RavenConfig) { + Raven.config(RavenConfig.dsn, RavenConfig.config).install(); + } +} +afterLoad(); diff --git a/htdocs/includes/raven-js/template/_copyright.js b/htdocs/includes/raven-js/template/_copyright.js new file mode 100644 index 00000000000..f1ee87084f8 --- /dev/null +++ b/htdocs/includes/raven-js/template/_copyright.js @@ -0,0 +1,11 @@ +/*! Raven.js <%= pkg.release %> (<%= gitinfo.local.branch.current.shortSHA %>) | github.com/getsentry/raven-js */ + +/* + * Includes TraceKit + * https://github.com/getsentry/TraceKit + * + * Copyright <%= grunt.template.today('yyyy') %> Matt Robenolt and other contributors + * Released under the BSD license + * https://github.com/getsentry/raven-js/blob/master/LICENSE + * + */ diff --git a/htdocs/includes/raven-js/template/_footer.js b/htdocs/includes/raven-js/template/_footer.js new file mode 100644 index 00000000000..9d102d2458d --- /dev/null +++ b/htdocs/includes/raven-js/template/_footer.js @@ -0,0 +1,19 @@ +// Expose Raven to the world +if (typeof define === 'function' && define.amd) { + // AMD + window.Raven = Raven; + define('raven', [], function() { + return Raven; + }); +} else if (typeof module === 'object') { + // browserify + module.exports = Raven; +} else if (typeof exports === 'object') { + // CommonJS + exports = Raven; +} else { + // Everything else + window.Raven = Raven; +} + +})(typeof window !== 'undefined' ? window : this); diff --git a/htdocs/includes/raven-js/template/_header.js b/htdocs/includes/raven-js/template/_header.js new file mode 100644 index 00000000000..27595a61a2d --- /dev/null +++ b/htdocs/includes/raven-js/template/_header.js @@ -0,0 +1,2 @@ +;(function(window, undefined){ +'use strict'; diff --git a/htdocs/includes/raven-js/test/index.html b/htdocs/includes/raven-js/test/index.html new file mode 100644 index 00000000000..b5188ed0600 --- /dev/null +++ b/htdocs/includes/raven-js/test/index.html @@ -0,0 +1,62 @@ + + + + Raven.js Test Suite + + + + +
+ + + + + + + + + + + + + + + + + + + diff --git a/htdocs/includes/raven-js/test/raven.test.js b/htdocs/includes/raven-js/test/raven.test.js new file mode 100644 index 00000000000..e26a36ef174 --- /dev/null +++ b/htdocs/includes/raven-js/test/raven.test.js @@ -0,0 +1,1773 @@ +function flushRavenState() { + authQueryString = undefined; + hasJSON = !isUndefined(window.JSON); + lastCapturedException = undefined; + lastEventId = undefined; + globalServer = undefined; + globalUser = undefined; + globalProject = undefined; + globalOptions = { + logger: 'javascript', + release: undefined, + ignoreErrors: [], + ignoreUrls: [], + whitelistUrls: [], + includePaths: [], + collectWindowErrors: true, + maxMessageLength: 100, + tags: {}, + extra: {} + }, + startTime = 0 + ; + + Raven.uninstall(); +} + +// window.console must be stubbed in for browsers that don't have it +if (typeof window.console === 'undefined') { + console = {error: function(){}}; +} + +var SENTRY_DSN = 'http://abc@example.com:80/2'; + +function setupRaven() { + Raven.config(SENTRY_DSN); +} + +// patched to return a predictable result +function uuid4() { + return 'abc123'; +} + +// patched to be predictable +function now() { + return 100; +} + +describe('TraceKit', function(){ + describe('stacktrace info', function() { + it('should not remove anonymous functions from the stack', function() { + // mock up an error object with a stack trace that includes both + // named functions and anonymous functions + var stack_str = "" + + " Error: \n" + + " at new (http://example.com/js/test.js:63)\n" + // stack[0] + " at namedFunc0 (http://example.com/js/script.js:10)\n" + // stack[1] + " at http://example.com/js/test.js:65\n" + // stack[2] + " at namedFunc2 (http://example.com/js/script.js:20)\n" + // stack[3] + " at http://example.com/js/test.js:67\n" + // stack[4] + " at namedFunc4 (http://example.com/js/script.js:100001)"; // stack[5] + var mock_err = { stack: stack_str }; + var trace = TraceKit.computeStackTrace.computeStackTraceFromStackProp(mock_err); + + // Make sure TraceKit didn't remove the anonymous functions + // from the stack like it used to :) + assert.equal(trace.stack[0].func, 'new '); + assert.equal(trace.stack[1].func, 'namedFunc0'); + assert.equal(trace.stack[2].func, '?'); + assert.equal(trace.stack[3].func, 'namedFunc2'); + assert.equal(trace.stack[4].func, '?'); + assert.equal(trace.stack[5].func, 'namedFunc4'); + }); + }); + describe('error notifications', function(){ + var testMessage = "__mocha_ignore__"; + var subscriptionHandler; + // TraceKit waits 2000ms for window.onerror to fire, so give the tests + // some extra time. + this.timeout(3000); + + before(function() { + // Prevent the onerror call that's part of our tests from getting to + // mocha's handler, which would treat it as a test failure. + // + // We set this up here and don't ever restore the old handler, because + // we can't do that without clobbering TraceKit's handler, which can only + // be installed once. + var oldOnError = window.onerror; + window.onerror = function(message) { + if (message == testMessage) { + return true; + } + return oldOnError.apply(this, arguments); + }; + }); + + afterEach(function() { + if (subscriptionHandler) { + TraceKit.report.unsubscribe(subscriptionHandler); + subscriptionHandler = null; + } + }); + + function testErrorNotification(collectWindowErrors, callOnError, numReports, done) { + var extraVal = "foo"; + var numDone = 0; + // TraceKit's collectWindowErrors flag shouldn't affect direct calls + // to TraceKit.report, so we parameterize it for the tests. + TraceKit.collectWindowErrors = collectWindowErrors; + + subscriptionHandler = function(stackInfo, extra) { + assert.equal(extra, extraVal); + numDone++; + if (numDone == numReports) { + done(); + } + }; + TraceKit.report.subscribe(subscriptionHandler); + + // TraceKit.report always throws an exception in order to trigger + // window.onerror so it can gather more stack data. Mocha treats + // uncaught exceptions as errors, so we catch it via assert.throws + // here (and manually call window.onerror later if appropriate). + // + // We test multiple reports because TraceKit has special logic for when + // report() is called a second time before either a timeout elapses or + // window.onerror is called (which is why we always call window.onerror + // only once below, after all calls to report()). + for (var i=0; i < numReports; i++) { + var e = new Error('testing'); + assert.throws(function() { + TraceKit.report(e, extraVal); + }, e); + } + // The call to report should work whether or not window.onerror is + // triggered, so we parameterize it for the tests. We only call it + // once, regardless of numReports, because the case we want to test for + // multiple reports is when window.onerror is *not* called between them. + if (callOnError) { + window.onerror(testMessage); + } + } + + Mocha.utils.forEach([false, true], function(collectWindowErrors) { + Mocha.utils.forEach([false, true], function(callOnError) { + Mocha.utils.forEach([1, 2], function(numReports) { + it('it should receive arguments from report() when' + + ' collectWindowErrors is ' + collectWindowErrors + + ' and callOnError is ' + callOnError + + ' and numReports is ' + numReports, function(done) { + testErrorNotification(collectWindowErrors, callOnError, numReports, done); + }); + }); + }); + }); + }); +}); + +describe('globals', function() { + beforeEach(function() { + setupRaven(); + globalOptions.fetchContext = true; + }); + + afterEach(function() { + flushRavenState(); + }); + + describe('getHttpData', function() { + var data = getHttpData(); + + it('should have a url', function() { + assert.equal(data.url, window.location.href); + }); + + it('should have the user-agent header', function() { + assert.equal(data.headers['User-Agent'], navigator.userAgent); + }); + + it('should have referer header when available', function() { + // lol this test is awful + if (window.document.referrer) { + assert.equal(data.headers.Referer, window.document.referrer); + } else { + assert.isUndefined(data.headers.Referer); + } + }); + + }); + + describe('isUndefined', function() { + it('should do as advertised', function() { + assert.isTrue(isUndefined()); + assert.isFalse(isUndefined({})); + assert.isFalse(isUndefined('')); + assert.isTrue(isUndefined(undefined)); + }); + }); + + describe('isFunction', function() { + it('should do as advertised', function() { + assert.isTrue(isFunction(function(){})); + assert.isFalse(isFunction({})); + assert.isFalse(isFunction('')); + assert.isFalse(isFunction(undefined)); + }); + }); + + describe('isString', function() { + it('should do as advertised', function() { + assert.isTrue(isString('')); + assert.isTrue(isString(String(''))); + assert.isTrue(isString(new String(''))); + assert.isFalse(isString({})); + assert.isFalse(isString(undefined)); + assert.isFalse(isString(function(){})); + }); + }); + + describe('isObject', function() { + it('should do as advertised', function() { + assert.isTrue(isObject({})); + assert.isTrue(isObject(new Error())) + assert.isFalse(isObject('')); + }); + }); + + describe('isEmptyObject', function() { + it('should work as advertised', function() { + assert.isTrue(isEmptyObject({})); + assert.isFalse(isEmptyObject({foo: 1})); + }); + }); + + describe('isError', function() { + it('should work as advertised', function() { + assert.isTrue(isError(new Error())); + assert.isTrue(isError(new ReferenceError())); + assert.isTrue(isError(new RavenConfigError())); + assert.isFalse(isError({})); + assert.isFalse(isError('')); + assert.isFalse(isError(true)); + }); + }); + + describe('objectMerge', function() { + it('should work as advertised', function() { + assert.deepEqual(objectMerge({}, {}), {}); + assert.deepEqual(objectMerge({a:1}, {b:2}), {a:1, b:2}); + assert.deepEqual(objectMerge({a:1}), {a:1}); + }); + }); + + describe('truncate', function() { + it('should work as advertised', function() { + assert.equal(truncate('lolol', 3), 'lol\u2026'); + assert.equal(truncate('lolol', 10), 'lolol'); + assert.equal(truncate('lol', 3), 'lol'); + }); + }); + + describe('isSetup', function() { + it('should return false with no JSON support', function() { + globalServer = 'http://localhost/'; + hasJSON = false; + assert.isFalse(isSetup()); + }); + + it('should return false when Raven is not configured', function() { + hasJSON = true; // be explicit + globalServer = undefined; + this.sinon.stub(window, 'logDebug'); + assert.isFalse(isSetup()); + }); + + it('should return true when everything is all gravy', function() { + hasJSON = true; + assert.isTrue(isSetup()); + }); + }); + + describe('logDebug', function() { + var level = 'error', + message = 'foobar'; + + it('should not write to console when Raven.debug is false', function() { + Raven.debug = false; + this.sinon.stub(console, level); + logDebug(level, message); + assert.isFalse(console[level].called); + }); + + it('should write to console when Raven.debug is true', function() { + Raven.debug = true; + this.sinon.stub(console, level); + logDebug(level, message); + assert.isTrue(console[level].calledOnce); + }); + }); + + describe('setAuthQueryString', function() { + it('should return a properly formatted string and cache it', function() { + var expected = '?sentry_version=4&sentry_client=raven-js/<%= pkg.version %>&sentry_key=abc'; + setAuthQueryString(); + assert.strictEqual(authQueryString, expected); + }); + }); + + describe('parseDSN', function() { + it('should do what it advertises', function() { + var pieces = parseDSN('http://abc@example.com:80/2'); + assert.strictEqual(pieces.protocol, 'http'); + assert.strictEqual(pieces.user, 'abc'); + assert.strictEqual(pieces.port, '80'); + assert.strictEqual(pieces.path, '/2'); + assert.strictEqual(pieces.host, 'example.com'); + }); + + it('should parse protocol relative', function() { + var pieces = parseDSN('//user@mattrobenolt.com/'); + assert.strictEqual(pieces.protocol, ''); + assert.strictEqual(pieces.user, 'user'); + assert.strictEqual(pieces.port, ''); + assert.strictEqual(pieces.path, '/'); + assert.strictEqual(pieces.host, 'mattrobenolt.com'); + }); + + it('should parse domain with hyphen', function() { + var pieces = parseDSN('http://user@matt-robenolt.com/1'); + assert.strictEqual(pieces.protocol, 'http'); + assert.strictEqual(pieces.user, 'user'); + assert.strictEqual(pieces.port, ''); + assert.strictEqual(pieces.path, '/1'); + assert.strictEqual(pieces.host, 'matt-robenolt.com'); + }); + + it('should raise a RavenConfigError when setting a password', function() { + try { + parseDSN('http://user:pass@example.com/2'); + } catch(e) { + return assert.equal(e.name, 'RavenConfigError'); + } + // shouldn't hit this + assert.isTrue(false); + }); + + it('should raise a RavenConfigError with an invalid DSN', function() { + try { + parseDSN('lol'); + } catch(e) { + return assert.equal(e.name, 'RavenConfigError'); + } + // shouldn't hit this + assert.isTrue(false); + }); + }); + + describe('normalizeFrame', function() { + it('should handle a normal frame', function() { + var context = [ + ['line1'], // pre + 'line2', // culprit + ['line3'] // post + ]; + this.sinon.stub(window, 'extractContextFromFrame').returns(context); + var frame = { + url: 'http://example.com/path/file.js', + line: 10, + column: 11, + func: 'lol' + // context: [] context is stubbed + }; + + globalOptions.fetchContext = true; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://example.com/path/file.js', + lineno: 10, + colno: 11, + 'function': 'lol', + pre_context: ['line1'], + context_line: 'line2', + post_context: ['line3'], + in_app: true + }); + }); + + it('should handle a frame without context', function() { + this.sinon.stub(window, 'extractContextFromFrame').returns(undefined); + var frame = { + url: 'http://example.com/path/file.js', + line: 10, + column: 11, + func: 'lol' + // context: [] context is stubbed + }; + + globalOptions.fetchContext = true; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://example.com/path/file.js', + lineno: 10, + colno: 11, + 'function': 'lol', + in_app: true + }); + }); + + it('should not mark `in_app` if rules match', function() { + this.sinon.stub(window, 'extractContextFromFrame').returns(undefined); + var frame = { + url: 'http://example.com/path/file.js', + line: 10, + column: 11, + func: 'lol' + // context: [] context is stubbed + }; + + globalOptions.fetchContext = true; + globalOptions.includePaths = /^http:\/\/example\.com/; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://example.com/path/file.js', + lineno: 10, + colno: 11, + 'function': 'lol', + in_app: true + }); + }); + + it('should mark `in_app` if rules do not match', function() { + this.sinon.stub(window, 'extractContextFromFrame').returns(undefined); + var frame = { + url: 'http://lol.com/path/file.js', + line: 10, + column: 11, + func: 'lol' + // context: [] context is stubbed + }; + + globalOptions.fetchContext = true; + globalOptions.includePaths = /^http:\/\/example\.com/; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://lol.com/path/file.js', + lineno: 10, + colno: 11, + 'function': 'lol', + in_app: false + }); + }); + + it('should mark `in_app` for raven.js', function() { + this.sinon.stub(window, 'extractContextFromFrame').returns(undefined); + var frame = { + url: 'http://lol.com/path/raven.js', + line: 10, + column: 11, + func: 'lol' + // context: [] context is stubbed + }; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://lol.com/path/raven.js', + lineno: 10, + colno: 11, + 'function': 'lol', + in_app: false + }); + }); + + it('should mark `in_app` for raven.min.js', function() { + this.sinon.stub(window, 'extractContextFromFrame').returns(undefined); + var frame = { + url: 'http://lol.com/path/raven.min.js', + line: 10, + column: 11, + func: 'lol' + // context: [] context is stubbed + }; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://lol.com/path/raven.min.js', + lineno: 10, + colno: 11, + 'function': 'lol', + in_app: false + }); + }); + + it('should mark `in_app` for Raven', function() { + this.sinon.stub(window, 'extractContextFromFrame').returns(undefined); + var frame = { + url: 'http://lol.com/path/file.js', + line: 10, + column: 11, + func: 'Raven.wrap' + // context: [] context is stubbed + }; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://lol.com/path/file.js', + lineno: 10, + colno: 11, + 'function': 'Raven.wrap', + in_app: false + }); + }); + + it('should mark `in_app` for TraceKit', function() { + this.sinon.stub(window, 'extractContextFromFrame').returns(undefined); + var frame = { + url: 'http://lol.com/path/file.js', + line: 10, + column: 11, + func: 'TraceKit.lol' + // context: [] context is stubbed + }; + + assert.deepEqual(normalizeFrame(frame), { + filename: 'http://lol.com/path/file.js', + lineno: 10, + colno: 11, + 'function': 'TraceKit.lol', + in_app: false + }); + }); + }); + + describe('extractContextFromFrame', function() { + it('should handle a normal frame', function() { + var frame = { + column: 2, + context: [ + 'line1', + 'line2', + 'line3', + 'line4', + 'line5', + 'culprit', + 'line7', + 'line8', + 'line9', + 'line10', + 'line11' + ] + }; + var context = extractContextFromFrame(frame); + assert.deepEqual(context, [ + ['line1', 'line2', 'line3', 'line4', 'line5'], + 'culprit', + ['line7', 'line8', 'line9', 'line10', 'line11'] + ]); + }); + + it('should return nothing if there is no context', function() { + var frame = { + column: 2 + }; + assert.isUndefined(extractContextFromFrame(frame)); + }); + + it('should reject a context if a line is too long without a column', function() { + var frame = { + context: [ + new Array(1000).join('f') // generate a line that is 1000 chars long + ] + }; + assert.isUndefined(extractContextFromFrame(frame)); + }); + + it('should reject a minified context with fetchContext disabled', function() { + var frame = { + column: 2, + context: [ + 'line1', + 'line2', + 'line3', + 'line4', + 'line5', + 'culprit', + 'line7', + 'line8', + 'line9', + 'line10', + 'line11' + ] + }; + globalOptions.fetchContext = false; + assert.isUndefined(extractContextFromFrame(frame)); + }); + + it('should truncate the minified line if there is a column number without sourcemaps enabled', function() { + // Note to future self: + // Array(51).join('f').length === 50 + var frame = { + column: 2, + context: [ + 'aa' + (new Array(51).join('f')) + (new Array(500).join('z')) + ] + }; + assert.deepEqual(extractContextFromFrame(frame), [[], new Array(51).join('f'), []]); + }); + }); + + describe('processException', function() { + it('should respect `ignoreErrors`', function() { + this.sinon.stub(window, 'send'); + + globalOptions.ignoreErrors = joinRegExp(['e1', 'e2']); + processException('Error', 'e1', 'http://example.com', []); + assert.isFalse(window.send.called); + processException('Error', 'e2', 'http://example.com', []); + assert.isFalse(window.send.called); + processException('Error', 'error', 'http://example.com', []); + assert.isTrue(window.send.calledOnce); + }); + + it('should respect `ignoreUrls`', function() { + this.sinon.stub(window, 'send'); + + globalOptions.ignoreUrls = joinRegExp([/.+?host1.+/, /.+?host2.+/]); + processException('Error', 'error', 'http://host1/', []); + assert.isFalse(window.send.called); + processException('Error', 'error', 'http://host2/', []); + assert.isFalse(window.send.called); + processException('Error', 'error', 'http://host3/', []); + assert.isTrue(window.send.calledOnce); + }); + + it('should respect `whitelistUrls`', function() { + this.sinon.stub(window, 'send'); + + globalOptions.whitelistUrls = joinRegExp([/.+?host1.+/, /.+?host2.+/]); + processException('Error', 'error', 'http://host1/', []); + assert.equal(window.send.callCount, 1); + processException('Error', 'error', 'http://host2/', []); + assert.equal(window.send.callCount, 2); + processException('Error', 'error', 'http://host3/', []); + assert.equal(window.send.callCount, 2); + }); + + it('should send a proper payload with frames', function() { + this.sinon.stub(window, 'send'); + + var frames = [ + { + filename: 'http://example.com/file1.js' + }, + { + filename: 'http://example.com/file2.js' + } + ], framesFlipped = frames.slice(0); + + framesFlipped.reverse(); + + processException('Error', 'lol', 'http://example.com/override.js', 10, frames.slice(0), {}); + assert.deepEqual(window.send.lastCall.args, [{ + exception: { + type: 'Error', + value: 'lol' + }, + stacktrace: { + frames: framesFlipped + }, + culprit: 'http://example.com/file1.js', + message: 'lol at 10' + }]); + + processException('Error', 'lol', '', 10, frames.slice(0), {}); + assert.deepEqual(window.send.lastCall.args, [{ + exception: { + type: 'Error', + value: 'lol' + }, + stacktrace: { + frames: framesFlipped + }, + culprit: 'http://example.com/file1.js', + message: 'lol at 10' + }]); + + processException('Error', 'lol', '', 10, frames.slice(0), {extra: 'awesome'}); + assert.deepEqual(window.send.lastCall.args, [{ + exception: { + type: 'Error', + value: 'lol' + }, + stacktrace: { + frames: framesFlipped + }, + culprit: 'http://example.com/file1.js', + message: 'lol at 10', + extra: 'awesome' + }]); + }); + + it('should send a proper payload without frames', function() { + this.sinon.stub(window, 'send'); + + processException('Error', 'lol', 'http://example.com/override.js', 10, [], {}); + assert.deepEqual(window.send.lastCall.args, [{ + exception: { + type: 'Error', + value: 'lol' + }, + stacktrace: { + frames: [{ + filename: 'http://example.com/override.js', + lineno: 10, + in_app: true + }] + }, + culprit: 'http://example.com/override.js', + message: 'lol at 10' + }]); + + processException('Error', 'lol', 'http://example.com/override.js', 10, [], {}); + assert.deepEqual(window.send.lastCall.args, [{ + exception: { + type: 'Error', + value: 'lol' + }, + stacktrace: { + frames: [{ + filename: 'http://example.com/override.js', + lineno: 10, + in_app: true + }] + }, + culprit: 'http://example.com/override.js', + message: 'lol at 10' + }]); + + processException('Error', 'lol', 'http://example.com/override.js', 10, [], {extra: 'awesome'}); + assert.deepEqual(window.send.lastCall.args, [{ + exception: { + type: 'Error', + value: 'lol' + }, + stacktrace: { + frames: [{ + filename: 'http://example.com/override.js', + lineno: 10, + in_app: true + }] + }, + culprit: 'http://example.com/override.js', + message: 'lol at 10', + extra: 'awesome' + }]); + }); + + it('should ignored falsey messages', function() { + this.sinon.stub(window, 'send'); + + processException('Error', '', 'http://example.com', []); + assert.isFalse(window.send.called); + + processException('TypeError', '', 'http://example.com', []); + assert.isTrue(window.send.called); + }); + + it('should not blow up with `undefined` message', function() { + this.sinon.stub(window, 'send'); + + processException('TypeError', undefined, 'http://example.com', []); + assert.isTrue(window.send.called); + }); + + it('should truncate messages to the specified length', function() { + this.sinon.stub(window, 'send'); + + processException('TypeError', new Array(500).join('a'), 'http://example.com', []); + assert.deepEqual(window.send.lastCall.args, [{ + message: new Array(101).join('a')+'\u2026 at ', + exception: { + type: 'TypeError', + value: new Array(101).join('a')+'\u2026' + }, + stacktrace: { + frames: [{ + filename: 'http://example.com', + lineno: [], + in_app: true + }] + }, + culprit: 'http://example.com', + }]); + + globalOptions.maxMessageLength = 150; + + processException('TypeError', new Array(500).join('a'), 'http://example.com', []); + assert.deepEqual(window.send.lastCall.args, [{ + message: new Array(151).join('a')+'\u2026 at ', + exception: { + type: 'TypeError', + value: new Array(151).join('a')+'\u2026' + }, + stacktrace: { + frames: [{ + filename: 'http://example.com', + lineno: [], + in_app: true + }] + }, + culprit: 'http://example.com', + }]); + }); + }); + + describe('send', function() { + it('should check `isSetup`', function() { + this.sinon.stub(window, 'isSetup').returns(false); + this.sinon.stub(window, 'makeRequest'); + + send(); + assert.isTrue(window.isSetup.calledOnce); + assert.isFalse(window.makeRequest.calledOnce); + }); + + it('should build a good data payload', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + this.sinon.stub(window, 'getHttpData').returns({ + url: 'http://localhost/?a=b', + headers: {'User-Agent': 'lolbrowser'} + }); + + globalProject = '2'; + globalOptions = { + logger: 'javascript' + }; + + send({foo: 'bar'}); + assert.deepEqual(window.makeRequest.lastCall.args[0], { + project: '2', + logger: 'javascript', + platform: 'javascript', + request: { + url: 'http://localhost/?a=b', + headers: { + 'User-Agent': 'lolbrowser' + } + }, + event_id: 'abc123', + foo: 'bar', + extra: {'session:duration': 100} + }); + }); + + it('should build a good data payload with a User', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + this.sinon.stub(window, 'getHttpData').returns({ + url: 'http://localhost/?a=b', + headers: {'User-Agent': 'lolbrowser'} + }); + + globalProject = '2'; + globalOptions = { + logger: 'javascript' + }; + + globalUser = {name: 'Matt'}; + + send({foo: 'bar'}); + assert.deepEqual(window.makeRequest.lastCall.args, [{ + project: '2', + logger: 'javascript', + platform: 'javascript', + request: { + url: 'http://localhost/?a=b', + headers: { + 'User-Agent': 'lolbrowser' + } + }, + event_id: 'abc123', + user: { + name: 'Matt' + }, + foo: 'bar', + extra: {'session:duration': 100} + }]); + }); + + it('should merge in global tags', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + this.sinon.stub(window, 'getHttpData').returns({ + url: 'http://localhost/?a=b', + headers: {'User-Agent': 'lolbrowser'} + }); + + globalProject = '2'; + globalOptions = { + logger: 'javascript', + tags: {tag1: 'value1'} + }; + + + send({tags: {tag2: 'value2'}}); + assert.deepEqual(window.makeRequest.lastCall.args, [{ + project: '2', + logger: 'javascript', + platform: 'javascript', + request: { + url: 'http://localhost/?a=b', + headers: { + 'User-Agent': 'lolbrowser' + } + }, + event_id: 'abc123', + tags: {tag1: 'value1', tag2: 'value2'}, + extra: {'session:duration': 100} + }]); + assert.deepEqual(globalOptions, { + logger: 'javascript', + tags: {tag1: 'value1'} + }); + }); + + it('should merge in global extra', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + this.sinon.stub(window, 'getHttpData').returns({ + url: 'http://localhost/?a=b', + headers: {'User-Agent': 'lolbrowser'} + }); + + globalProject = '2'; + globalOptions = { + logger: 'javascript', + extra: {key1: 'value1'} + }; + + + send({extra: {key2: 'value2'}}); + assert.deepEqual(window.makeRequest.lastCall.args, [{ + project: '2', + logger: 'javascript', + platform: 'javascript', + request: { + url: 'http://localhost/?a=b', + headers: { + 'User-Agent': 'lolbrowser' + } + }, + event_id: 'abc123', + extra: {key1: 'value1', key2: 'value2', 'session:duration': 100} + }]); + assert.deepEqual(globalOptions, { + logger: 'javascript', + extra: {key1: 'value1'} + }); + }); + + it('should let dataCallback override everything', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + + globalOptions = { + projectId: 2, + logger: 'javascript', + dataCallback: function() { + return {lol: 'ibrokeit'}; + } + }; + + globalUser = {name: 'Matt'}; + + send({foo: 'bar'}); + assert.deepEqual(window.makeRequest.lastCall.args, [{ + lol: 'ibrokeit', + event_id: 'abc123', + }]); + }); + + it('should ignore dataCallback if it does not return anything', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + this.sinon.stub(window, 'getHttpData').returns({ + url: 'http://localhost/?a=b', + headers: {'User-Agent': 'lolbrowser'} + }); + + globalProject = '2'; + globalOptions = { + logger: 'javascript', + dataCallback: function() { + return; + } + }; + + send({foo: 'bar'}); + assert.deepEqual(window.makeRequest.lastCall.args[0], { + project: '2', + logger: 'javascript', + platform: 'javascript', + request: { + url: 'http://localhost/?a=b', + headers: { + 'User-Agent': 'lolbrowser' + } + }, + event_id: 'abc123', + foo: 'bar', + extra: {'session:duration': 100} + }); + }); + + it('should strip empty tags', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + this.sinon.stub(window, 'getHttpData').returns({ + url: 'http://localhost/?a=b', + headers: {'User-Agent': 'lolbrowser'} + }); + + globalOptions = { + projectId: 2, + logger: 'javascript', + tags: {} + }; + + send({foo: 'bar', tags: {}, extra: {}}); + assert.deepEqual(window.makeRequest.lastCall.args[0], { + project: '2', + logger: 'javascript', + platform: 'javascript', + request: { + url: 'http://localhost/?a=b', + headers: { + 'User-Agent': 'lolbrowser' + } + }, + event_id: 'abc123', + foo: 'bar', + extra: {'session:duration': 100} + }); + }); + + it('should attach release if available', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(window, 'makeRequest'); + this.sinon.stub(window, 'getHttpData').returns({ + url: 'http://localhost/?a=b', + headers: {'User-Agent': 'lolbrowser'} + }); + + globalOptions = { + projectId: 2, + logger: 'javascript', + release: 'abc123', + }; + + send({foo: 'bar'}); + assert.deepEqual(window.makeRequest.lastCall.args[0], { + project: '2', + release: 'abc123', + logger: 'javascript', + platform: 'javascript', + request: { + url: 'http://localhost/?a=b', + headers: { + 'User-Agent': 'lolbrowser' + } + }, + event_id: 'abc123', + foo: 'bar', + extra: {'session:duration': 100} + }); + }); + }); + + describe('makeRequest', function() { + it('should load an Image', function() { + authQueryString = '?lol'; + globalServer = 'http://localhost/'; + var imageCache = []; + this.sinon.stub(window, 'newImage', function(){ var img = {}; imageCache.push(img); return img; }); + + makeRequest({foo: 'bar'}); + assert.equal(imageCache.length, 1); + assert.equal(imageCache[0].src, 'http://localhost/?lol&sentry_data=%7B%22foo%22%3A%22bar%22%7D'); + }); + }); + + describe('handleStackInfo', function() { + it('should work as advertised', function() { + var frame = {url: 'http://example.com'}; + this.sinon.stub(window, 'normalizeFrame').returns(frame); + this.sinon.stub(window, 'processException'); + + var stackInfo = { + name: 'Matt', + message: 'hey', + url: 'http://example.com', + lineno: 10, + stack: [ + frame, frame + ] + }; + + handleStackInfo(stackInfo, {foo: 'bar'}); + assert.deepEqual(window.processException.lastCall.args, [ + 'Matt', 'hey', 'http://example.com', 10, [frame, frame], {foo: 'bar'} + ]); + }); + + it('should work as advertised #integration', function() { + this.sinon.stub(window, 'makeRequest'); + var stackInfo = { + name: 'Error', + message: 'crap', + url: 'http://example.com', + lineno: 10, + stack: [ + { + url: 'http://example.com/file1.js', + line: 10, + column: 11, + func: 'broken', + context: [ + 'line1', + 'line2', + 'line3' + ] + }, + { + url: 'http://example.com/file2.js', + line: 12, + column: 13, + func: 'lol', + context: [ + 'line4', + 'line5', + 'line6' + ] + } + ] + }; + + handleStackInfo(stackInfo, {foo: 'bar'}); + assert.isTrue(window.makeRequest.calledOnce); + /* This is commented out because chai is broken. + + assert.deepEqual(window.makeRequest.lastCall.args, [{ + project: '2', + logger: 'javascript', + platform: 'javascript', + request: { + url: window.location.protocol + '//' + window.location.host + window.location.pathname, + querystring: window.location.search.slice(1) + }, + exception: { + type: 'Error', + value: 'crap' + }, + stacktrace: { + frames: [{ + filename: 'http://example.com/file1.js', + filename: 'file1.js', + lineno: 10, + colno: 11, + 'function': 'broken', + post_context: ['line3'], + context_line: 'line2', + pre_context: ['line1'] + }, { + filename: 'http://example.com/file2.js', + filename: 'file2.js', + lineno: 12, + colno: 13, + 'function': 'lol', + post_context: ['line6'], + context_line: 'line5', + pre_context: ['line4'] + }] + }, + culprit: 'http://example.com', + message: 'crap at 10', + foo: 'bar' + }]); + */ + }); + + it('should ignore frames that dont have a url', function() { + this.sinon.stub(window, 'normalizeFrame').returns(undefined); + this.sinon.stub(window, 'processException'); + + var stackInfo = { + name: 'Matt', + message: 'hey', + url: 'http://example.com', + lineno: 10, + stack: new Array(2) + }; + + handleStackInfo(stackInfo, {foo: 'bar'}); + assert.deepEqual(window.processException.lastCall.args, [ + 'Matt', 'hey', 'http://example.com', 10, [], {foo: 'bar'} + ]); + }); + + it('should not shit when there is no stack object from TK', function() { + this.sinon.stub(window, 'normalizeFrame').returns(undefined); + this.sinon.stub(window, 'processException'); + + var stackInfo = { + name: 'Matt', + message: 'hey', + url: 'http://example.com', + lineno: 10 + // stack: new Array(2) + }; + + handleStackInfo(stackInfo); + assert.isFalse(window.normalizeFrame.called); + assert.deepEqual(window.processException.lastCall.args, [ + 'Matt', 'hey', 'http://example.com', 10, [], undefined + ]); + }); + + it('should detect 2-words patterns (angularjs frequent case)', function() { + this.sinon.stub(window, 'normalizeFrame').returns(undefined); + this.sinon.stub(window, 'processException'); + + var stackInfo = { + name: 'new ', + message: 'hey', + url: 'http://example.com', + lineno: 10 + // stack: new Array(2) + }; + + handleStackInfo(stackInfo); + assert.isFalse(window.normalizeFrame.called); + assert.deepEqual(window.processException.lastCall.args, [ + 'new ', 'hey', 'http://example.com', 10, [], undefined + ]); + }); + }); + + describe('joinRegExp', function() { + it('should work as advertised', function() { + assert.equal(joinRegExp([ + 'a', 'b', 'a.b', /d/, /[0-9]/ + ]).source, 'a|b|a\\.b|d|[0-9]'); + }); + + it('should not process empty or undefined variables', function() { + assert.equal(joinRegExp([ + 'a', 'b', null, undefined + ]).source, 'a|b'); + }); + + it('should skip entries that are not strings or regular expressions in the passed array of patterns', function() { + assert.equal(joinRegExp([ + 'a', 'b', null, 'a.b', undefined, true, /d/, 123, {}, /[0-9]/, [] + ]).source, 'a|b|a\\.b|d|[0-9]'); + }); + }); +}); + +describe('Raven (public API)', function() { + afterEach(function() { + flushRavenState(); + }); + + describe('.VERSION', function() { + it('should have a version', function() { + assert.isString(Raven.VERSION); + }); + }); + + describe('ignore errors', function() { + it('should install default ignore errors', function() { + Raven.config('//abc@example.com/2'); + + assert.isTrue(globalOptions.ignoreErrors.test('Script error'), 'it should install "Script error" by default'); + assert.isTrue(globalOptions.ignoreErrors.test('Script error.'), 'it should install "Script error." by default'); + assert.isTrue(globalOptions.ignoreErrors.test('Javascript error: Script error on line 0'), 'it should install "Javascript error: Script error on line 0" by default'); + assert.isTrue(globalOptions.ignoreErrors.test('Javascript error: Script error. on line 0'), 'it should install "Javascript error: Script error. on line 0" by default'); + }); + }); + + describe('callback function', function() { + it('should callback a function if it is global', function() { + window.RavenConfig = { + dsn: "http://random@some.other.server:80/2", + config: {some: 'config'} + }; + + this.sinon.stub(window, 'isSetup').returns(false); + this.sinon.stub(TraceKit.report, 'subscribe'); + + afterLoad(); + + assert.equal(globalKey, 'random'); + assert.equal(globalServer, 'http://some.other.server:80/api/2/store/'); + + assert.equal(globalOptions.some, 'config'); + assert.equal(globalProject, '2'); + + assert.isTrue(window.isSetup.calledOnce); + assert.isFalse(TraceKit.report.subscribe.calledOnce); + + delete window.RavenConfig; + }); + }); + + describe('.config', function() { + it('should work with a DSN', function() { + assert.equal(Raven, Raven.config(SENTRY_DSN, {foo: 'bar'}), 'it should return Raven'); + assert.equal(globalKey, 'abc'); + assert.equal(globalServer, 'http://example.com:80/api/2/store/'); + assert.equal(globalOptions.foo, 'bar'); + assert.equal(globalProject, '2'); + assert.isTrue(isSetup()); + }); + + it('should work with a protocol relative DSN', function() { + Raven.config('//abc@example.com/2'); + assert.equal(globalKey, 'abc'); + assert.equal(globalServer, '//example.com/api/2/store/'); + assert.equal(globalProject, '2'); + assert.isTrue(isSetup()); + }); + + it('should work should work at a non root path', function() { + Raven.config('//abc@example.com/sentry/2'); + assert.equal(globalKey, 'abc'); + assert.equal(globalServer, '//example.com/sentry/api/2/store/'); + assert.equal(globalProject, '2'); + assert.isTrue(isSetup()); + }); + + it('should noop a falsey dsn', function() { + Raven.config(''); + assert.isFalse(isSetup()); + }); + + it('should return Raven for a falsey dsn', function() { + assert.equal(Raven.config(''), Raven); + }); + + it('should not set global options more than once', function() { + this.sinon.spy(window, 'parseDSN'); + this.sinon.stub(window, 'logDebug'); + setupRaven(); + setupRaven(); + assert.isTrue(parseDSN.calledOnce); + assert.isTrue(logDebug.called); + }); + + describe('whitelistUrls', function() { + it('should be false if none are passed', function() { + Raven.config('//abc@example.com/2'); + assert.equal(globalOptions.whitelistUrls, false); + }); + + it('should join into a single RegExp', function() { + Raven.config('//abc@example.com/2', { + whitelistUrls: [ + /my.app/i, + /other.app/i + ] + }); + + assert.match(globalOptions.whitelistUrls, /my.app|other.app/i); + }); + + it('should handle strings as well', function() { + Raven.config('//abc@example.com/2', { + whitelistUrls: [ + /my.app/i, + "stringy.app" + ] + }); + + assert.match(globalOptions.whitelistUrls, /my.app|stringy.app/i); + }); + }); + + describe('collectWindowErrors', function() { + it('should be true by default', function() { + Raven.config(SENTRY_DSN); + assert.isTrue(TraceKit.collectWindowErrors); + }); + + it('should be true if set to true', function() { + Raven.config(SENTRY_DSN, { + collectWindowErrors: true + }); + + assert.isTrue(TraceKit.collectWindowErrors); + }); + + it('should be false if set to false', function() { + Raven.config(SENTRY_DSN, { + collectWindowErrors: false + }); + + assert.isFalse(TraceKit.collectWindowErrors); + }); + }); + }); + + describe('.install', function() { + it('should check `isSetup`', function() { + this.sinon.stub(window, 'isSetup').returns(false); + this.sinon.stub(TraceKit.report, 'subscribe'); + Raven.install(); + assert.isTrue(window.isSetup.calledOnce); + assert.isFalse(TraceKit.report.subscribe.calledOnce); + }); + + it('should register itself with TraceKit', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(TraceKit.report, 'subscribe'); + assert.equal(Raven, Raven.install()); + assert.isTrue(TraceKit.report.subscribe.calledOnce); + assert.equal(TraceKit.report.subscribe.lastCall.args[0], handleStackInfo); + }); + + it('should not register itself more than once', function() { + this.sinon.stub(window, 'isSetup').returns(true); + this.sinon.stub(TraceKit.report, 'subscribe'); + Raven.install(); + Raven.install(); + assert.isTrue(TraceKit.report.subscribe.calledOnce); + }); + }); + + describe('.wrap', function() { + it('should return a wrapped callback', function() { + var spy = this.sinon.spy(); + var wrapped = Raven.wrap(spy); + assert.isFunction(wrapped); + assert.isTrue(wrapped.__raven__); + wrapped(); + assert.isTrue(spy.calledOnce); + }); + + it('should copy property when wrapping function', function() { + var func = function() {}; + func.test = true; + var wrapped = Raven.wrap(func); + assert.isTrue(wrapped.test); + }); + + it('should not copy prototype property when wrapping function', function() { + var func = function() {}; + func.prototype.test = true; + var wrapped = Raven.wrap(func); + assert.isUndefined(new wrapped().test); + }); + + it('should return the result of a wrapped function', function() { + var func = function() { return 'foo'; }; + var wrapped = Raven.wrap(func); + assert.equal(wrapped(), 'foo'); + }); + + it('should not wrap a non-function', function() { + assert.equal(Raven.wrap('lol'), 'lol'); + assert.equal(Raven.wrap({}, 'lol'), 'lol'); + assert.equal(Raven.wrap(undefined, 'lol'), 'lol'); + var a = [1, 2]; + assert.equal(Raven.wrap(a), a); + }); + + it('should wrap function arguments', function() { + var spy = this.sinon.spy(); + var wrapped = Raven.wrap(function(f) { + assert.isTrue(f.__raven__); + f(); + }); + wrapped(spy); + assert.isTrue(spy.calledOnce); + }); + + it('should not wrap function arguments', function() { + var spy = this.sinon.spy(); + var wrapped = Raven.wrap({ deep: false }, function(f) { + assert.isUndefined(f.__raven__); + f(); + }); + wrapped(spy); + assert.isTrue(spy.calledOnce); + }); + + it('should maintain the correct scope', function() { + var foo = {}; + var bar = function() { + assert.equal(this, foo); + }; + bar.apply(foo, []); + Raven.wrap(bar).apply(foo, []); + }); + + it('should re-raise a thrown exception', function() { + var error = new Error('lol'); + assert.throws(function() { + Raven.wrap(function() { throw error; })(); + }, error); + }); + + }); + + describe('.context', function() { + it('should execute the callback with options', function() { + var spy = this.sinon.spy(); + this.sinon.stub(Raven, 'captureException'); + Raven.context({'foo': 'bar'}, spy); + assert.isTrue(spy.calledOnce); + assert.isFalse(Raven.captureException.called); + }); + + it('should execute the callback with arguments', function() { + var spy = this.sinon.spy(); + var args = [1, 2]; + Raven.context(spy, args); + assert.deepEqual(spy.lastCall.args, args); + }); + + it('should execute the callback without options', function() { + var spy = this.sinon.spy(); + this.sinon.stub(Raven, 'captureException'); + Raven.context(spy); + assert.isTrue(spy.calledOnce); + assert.isFalse(Raven.captureException.called); + }); + + it('should capture the exception with options', function() { + var error = new Error('crap'); + var broken = function() { throw error; }; + this.sinon.stub(Raven, 'captureException'); + assert.throws(function() { + Raven.context({foo: 'bar'}, broken); + }, error); + assert.isTrue(Raven.captureException.called); + assert.deepEqual(Raven.captureException.lastCall.args, [error, {'foo': 'bar'}]); + }); + + it('should capture the exception without options', function() { + var error = new Error('crap'); + var broken = function() { throw error; }; + this.sinon.stub(Raven, 'captureException'); + assert.throws(function() { + Raven.context(broken); + }, error); + assert.isTrue(Raven.captureException.called); + assert.deepEqual(Raven.captureException.lastCall.args, [error, undefined]); + }); + + it('should execute the callback without arguments', function() { + // This is only reproducable in a browser that complains about passing + // undefined to Function.apply + var spy = this.sinon.spy(); + Raven.context(spy); + assert.deepEqual(spy.lastCall.args, []); + }); + + it('should return the result of the wrapped function', function() { + var val = {}; + var func = function() { return val; }; + assert.equal(Raven.context(func), val); + }); + }); + + describe('.uninstall', function() { + it('should uninstall from TraceKit', function() { + this.sinon.stub(TraceKit.report, 'uninstall'); + Raven.uninstall(); + assert.isTrue(TraceKit.report.uninstall.calledOnce); + }); + + it('should set isRavenInstalled flag to false', function() { + isRavenInstalled = true; + this.sinon.stub(TraceKit.report, 'uninstall'); + Raven.uninstall(); + assert.isFalse(isRavenInstalled); + }); + }); + + describe('.setUserContext', function() { + it('should set the globalUser object', function() { + Raven.setUserContext({name: 'Matt'}); + assert.deepEqual(globalUser, {name: 'Matt'}); + }); + + it('should clear the globalUser with no arguments', function() { + globalUser = {name: 'Matt'}; + Raven.setUserContext(); + assert.isUndefined(globalUser); + }); + }); + + describe('.setExtraContext', function() { + it('should set the globalOptions.extra object', function() { + Raven.setExtraContext({name: 'Matt'}); + assert.deepEqual(globalOptions.extra, {name: 'Matt'}); + }); + + it('should clear globalOptions.extra with no arguments', function() { + globalOptions = {name: 'Matt'}; + Raven.setExtraContext(); + assert.deepEqual(globalOptions.extra, {}); + }); + }); + + describe('.setTagsContext', function() { + it('should set the globalOptions.tags object', function() { + Raven.setTagsContext({name: 'Matt'}); + assert.deepEqual(globalOptions.tags, {name: 'Matt'}); + }); + + it('should clear globalOptions.tags with no arguments', function() { + globalOptions = {name: 'Matt'}; + Raven.setTagsContext(); + assert.deepEqual(globalOptions.tags, {}); + }); + }); + + describe('.setReleaseContext', function() { + it('should set the globalOptions.release attribute', function() { + Raven.setReleaseContext('abc123'); + assert.equal(globalOptions.release, 'abc123'); + }); + + it('should clear globalOptions.release with no arguments', function() { + globalOptions.release = 'abc123'; + Raven.setReleaseContext(); + assert.isUndefined(globalOptions.release); + }); + }); + + describe('.setDataCallback', function() { + it('should set the globalOptions.dataCallback attribute', function() { + var foo = function(){}; + Raven.setDataCallback(foo); + assert.equal(globalOptions.dataCallback, foo); + }); + + it('should clear globalOptions.dataCallback with no arguments', function() { + var foo = function(){}; + globalOptions.dataCallback = foo; + Raven.setDataCallback(); + assert.isUndefined(globalOptions.dataCallback); + }); + }); + + describe('.setShouldSendCallback', function() { + it('should set the globalOptions.shouldSendCallback attribute', function() { + var foo = function(){}; + Raven.setShouldSendCallback(foo); + assert.equal(globalOptions.shouldSendCallback, foo); + }); + + it('should clear globalOptions.shouldSendCallback with no arguments', function() { + var foo = function(){}; + globalOptions.shouldSendCallback = foo; + Raven.setShouldSendCallback(); + assert.isUndefined(globalOptions.shouldSendCallback); + }); + }); + + describe('.captureMessage', function() { + it('should work as advertised', function() { + this.sinon.stub(window, 'send'); + Raven.captureMessage('lol', {foo: 'bar'}); + assert.deepEqual(window.send.lastCall.args, [{ + message: 'lol', + foo: 'bar' + }]); + }); + + it('should coerce message to a string', function() { + this.sinon.stub(window, 'send'); + Raven.captureMessage({}); + assert.deepEqual(window.send.lastCall.args, [{ + message: '[object Object]' + }]); + }); + + it('should work as advertised #integration', function() { + var imageCache = []; + this.sinon.stub(window, 'newImage', function(){ var img = {}; imageCache.push(img); return img; }); + + setupRaven(); + Raven.captureMessage('lol', {foo: 'bar'}); + assert.equal(imageCache.length, 1); + // It'd be hard to assert the actual payload being sent + // since it includes the generated url, which is going to + // vary between users running the tests + // Unit tests should cover that the payload was constructed properly + }); + + it('should tag lastEventId #integration', function() { + setupRaven(); + Raven.captureMessage('lol'); + assert.equal(Raven.lastEventId(), 'abc123'); + }); + + it('should respect `ignoreErrors`', function() { + this.sinon.stub(window, 'send'); + + globalOptions.ignoreErrors = joinRegExp(['e1', 'e2']); + Raven.captureMessage('e1'); + assert.isFalse(window.send.called); + Raven.captureMessage('e2'); + assert.isFalse(window.send.called); + Raven.captureMessage('Non-ignored error'); + assert.isTrue(window.send.calledOnce); + }); + }); + + describe('.captureException', function() { + it('should call TraceKit.report', function() { + var error = new Error('crap'); + this.sinon.stub(TraceKit, 'report'); + Raven.captureException(error, {foo: 'bar'}); + assert.isTrue(TraceKit.report.calledOnce); + assert.deepEqual(TraceKit.report.lastCall.args, [error, {foo: 'bar'}]); + }); + + it('should store the last exception', function() { + var error = new Error('crap'); + this.sinon.stub(TraceKit, 'report'); + Raven.captureException(error); + assert.equal(Raven.lastException(), error); + }); + + it('shouldn\'t reraise the if the error is the same error', function() { + var error = new Error('crap'); + this.sinon.stub(TraceKit, 'report').throws(error); + // this would raise if the errors didn't match + Raven.captureException(error, {foo: 'bar'}); + assert.isTrue(TraceKit.report.calledOnce); + }); + + it('should reraise a different error', function() { + var error = new Error('crap1'); + this.sinon.stub(TraceKit, 'report').throws(error); + assert.throws(function() { + Raven.captureException(new Error('crap2')); + }, error); + }); + + it('should capture as a normal message if a non-Error is passed', function() { + this.sinon.stub(Raven, 'captureMessage'); + this.sinon.stub(TraceKit, 'report'); + Raven.captureException('derp'); + assert.equal(Raven.captureMessage.lastCall.args[0], 'derp'); + assert.isFalse(TraceKit.report.called); + Raven.captureException(true); + assert.equal(Raven.captureMessage.lastCall.args[0], true); + assert.isFalse(TraceKit.report.called); + }); + }); + + describe('.isSetup', function() { + it('should work as advertised', function() { + var isSetup = this.sinon.stub(window, 'isSetup'); + isSetup.returns(true); + assert.isTrue(Raven.isSetup()); + isSetup.returns(false); + assert.isFalse(Raven.isSetup()); + }); + }); +}); diff --git a/htdocs/includes/raven-js/vendor/TraceKit/tracekit.js b/htdocs/includes/raven-js/vendor/TraceKit/tracekit.js new file mode 100644 index 00000000000..bccddabedbe --- /dev/null +++ b/htdocs/includes/raven-js/vendor/TraceKit/tracekit.js @@ -0,0 +1,1044 @@ +/* + TraceKit - Cross brower stack traces - github.com/occ/TraceKit + MIT license +*/ + +var TraceKit = { + remoteFetching: false, + collectWindowErrors: true, + // 3 lines before, the offending line, 3 lines after + linesOfContext: 7 +}; + +// global reference to slice +var _slice = [].slice; +var UNKNOWN_FUNCTION = '?'; + + +/** + * TraceKit.wrap: Wrap any function in a TraceKit reporter + * Example: func = TraceKit.wrap(func); + * + * @param {Function} func Function to be wrapped + * @return {Function} The wrapped func + */ +TraceKit.wrap = function traceKitWrapper(func) { + function wrapped() { + try { + return func.apply(this, arguments); + } catch (e) { + TraceKit.report(e); + throw e; + } + } + return wrapped; +}; + +/** + * TraceKit.report: cross-browser processing of unhandled exceptions + * + * Syntax: + * TraceKit.report.subscribe(function(stackInfo) { ... }) + * TraceKit.report.unsubscribe(function(stackInfo) { ... }) + * TraceKit.report(exception) + * try { ...code... } catch(ex) { TraceKit.report(ex); } + * + * Supports: + * - Firefox: full stack trace with line numbers, plus column number + * on top frame; column number is not guaranteed + * - Opera: full stack trace with line and column numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed + * - IE: line and column number for the top frame only; some frames + * may be missing, and column number is not guaranteed + * + * In theory, TraceKit should work on all of the following versions: + * - IE5.5+ (only 8.0 tested) + * - Firefox 0.9+ (only 3.5+ tested) + * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require + * Exceptions Have Stacktrace to be enabled in opera:config) + * - Safari 3+ (only 4+ tested) + * - Chrome 1+ (only 5+ tested) + * - Konqueror 3.5+ (untested) + * + * Requires TraceKit.computeStackTrace. + * + * Tries to catch all unhandled exceptions and report them to the + * subscribed handlers. Please note that TraceKit.report will rethrow the + * exception. This is REQUIRED in order to get a useful stack trace in IE. + * If the exception does not reach the top of the browser, you will only + * get a stack trace from the point where TraceKit.report was called. + * + * Handlers receive a stackInfo object as described in the + * TraceKit.computeStackTrace docs. + */ +TraceKit.report = (function reportModuleWrapper() { + var handlers = [], + lastArgs = null, + lastException = null, + lastExceptionStack = null; + + /** + * Add a crash handler. + * @param {Function} handler + */ + function subscribe(handler) { + installGlobalHandler(); + handlers.push(handler); + } + + /** + * Remove a crash handler. + * @param {Function} handler + */ + function unsubscribe(handler) { + for (var i = handlers.length - 1; i >= 0; --i) { + if (handlers[i] === handler) { + handlers.splice(i, 1); + } + } + } + + /** + * Remove all crash handlers. + */ + function unsubscribeAll() { + uninstallGlobalHandler(); + handlers = []; + } + + /** + * Dispatch stack information to all handlers. + * @param {Object.} stack + */ + function notifyHandlers(stack, isWindowError) { + var exception = null; + if (isWindowError && !TraceKit.collectWindowErrors) { + return; + } + for (var i in handlers) { + if (hasKey(handlers, i)) { + try { + handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2))); + } catch (inner) { + exception = inner; + } + } + } + + if (exception) { + throw exception; + } + } + + var _oldOnerrorHandler, _onErrorHandlerInstalled; + + /** + * Ensures all global unhandled exceptions are recorded. + * Supported by Gecko and IE. + * @param {string} message Error message. + * @param {string} url URL of script that generated the exception. + * @param {(number|string)} lineNo The line number at which the error + * occurred. + * @param {?(number|string)} colNo The column number at which the error + * occurred. + * @param {?Error} ex The actual Error object. + */ + function traceKitWindowOnError(message, url, lineNo, colNo, ex) { + var stack = null; + + if (lastExceptionStack) { + TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message); + processLastException(); + } else if (ex) { + // New chrome and blink send along a real error object + // Let's just report that like a normal error. + // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror + stack = TraceKit.computeStackTrace(ex); + notifyHandlers(stack, true); + } else { + var location = { + 'url': url, + 'line': lineNo, + 'column': colNo + }; + location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line); + location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line); + stack = { + 'message': message, + 'url': document.location.href, + 'stack': [location] + }; + notifyHandlers(stack, true); + } + + if (_oldOnerrorHandler) { + return _oldOnerrorHandler.apply(this, arguments); + } + + return false; + } + + function installGlobalHandler () + { + if (_onErrorHandlerInstalled) { + return; + } + _oldOnerrorHandler = window.onerror; + window.onerror = traceKitWindowOnError; + _onErrorHandlerInstalled = true; + } + + function uninstallGlobalHandler () + { + if (!_onErrorHandlerInstalled) { + return; + } + window.onerror = _oldOnerrorHandler; + _onErrorHandlerInstalled = false; + _oldOnerrorHandler = undefined; + } + + function processLastException() { + var _lastExceptionStack = lastExceptionStack, + _lastArgs = lastArgs; + lastArgs = null; + lastExceptionStack = null; + lastException = null; + notifyHandlers.apply(null, [_lastExceptionStack, false].concat(_lastArgs)); + } + + /** + * Reports an unhandled Error to TraceKit. + * @param {Error} ex + * @param {?boolean} rethrow If false, do not re-throw the exception. + * Only used for window.onerror to not cause an infinite loop of + * rethrowing. + */ + function report(ex, rethrow) { + var args = _slice.call(arguments, 1); + if (lastExceptionStack) { + if (lastException === ex) { + return; // already caught by an inner catch block, ignore + } else { + processLastException(); + } + } + + var stack = TraceKit.computeStackTrace(ex); + lastExceptionStack = stack; + lastException = ex; + lastArgs = args; + + // If the stack trace is incomplete, wait for 2 seconds for + // slow slow IE to see if onerror occurs or not before reporting + // this exception; otherwise, we will end up with an incomplete + // stack trace + window.setTimeout(function () { + if (lastException === ex) { + processLastException(); + } + }, (stack.incomplete ? 2000 : 0)); + + if (rethrow !== false) { + throw ex; // re-throw to propagate to the top level (and cause window.onerror) + } + } + + report.subscribe = subscribe; + report.unsubscribe = unsubscribe; + report.uninstall = unsubscribeAll; + return report; +}()); + +/** + * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript + * + * Syntax: + * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below) + * Returns: + * s.name - exception name + * s.message - exception message + * s.stack[i].url - JavaScript or HTML file URL + * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work) + * s.stack[i].args - arguments passed to the function, if known + * s.stack[i].line - line number, if known + * s.stack[i].column - column number, if known + * s.stack[i].context - an array of source code lines; the middle element corresponds to the correct line# + * + * Supports: + * - Firefox: full stack trace with line numbers and unreliable column + * number on top frame + * - Opera 10: full stack trace with line and column numbers + * - Opera 9-: full stack trace with line numbers + * - Chrome: full stack trace with line and column numbers + * - Safari: line and column number for the topmost stacktrace element + * only + * - IE: no line numbers whatsoever + * + * Tries to guess names of anonymous functions by looking for assignments + * in the source code. In IE and Safari, we have to guess source file names + * by searching for function bodies inside all page scripts. This will not + * work for scripts that are loaded cross-domain. + * Here be dragons: some function names may be guessed incorrectly, and + * duplicate functions may be mismatched. + * + * TraceKit.computeStackTrace should only be used for tracing purposes. + * Logging of unhandled exceptions should be done with TraceKit.report, + * which builds on top of TraceKit.computeStackTrace and provides better + * IE support by utilizing the window.onerror event to retrieve information + * about the top of the stack. + * + * Note: In IE and Safari, no stack trace is recorded on the Error object, + * so computeStackTrace instead walks its *own* chain of callers. + * This means that: + * * in Safari, some methods may be missing from the stack trace; + * * in IE, the topmost function in the stack trace will always be the + * caller of computeStackTrace. + * + * This is okay for tracing (because you are likely to be calling + * computeStackTrace from the function you want to be the topmost element + * of the stack trace anyway), but not okay for logging unhandled + * exceptions (because your catch block will likely be far away from the + * inner function that actually caused the exception). + * + */ +TraceKit.computeStackTrace = (function computeStackTraceWrapper() { + var debug = false, + sourceCache = {}; + + /** + * Attempts to retrieve source code via XMLHttpRequest, which is used + * to look up anonymous function names. + * @param {string} url URL of source code. + * @return {string} Source contents. + */ + function loadSource(url) { + if (!TraceKit.remoteFetching) { //Only attempt request if remoteFetching is on. + return ''; + } + try { + var getXHR = function() { + try { + return new window.XMLHttpRequest(); + } catch (e) { + // explicitly bubble up the exception if not found + return new window.ActiveXObject('Microsoft.XMLHTTP'); + } + }; + + var request = getXHR(); + request.open('GET', url, false); + request.send(''); + return request.responseText; + } catch (e) { + return ''; + } + } + + /** + * Retrieves source code from the source code cache. + * @param {string} url URL of source code. + * @return {Array.} Source contents. + */ + function getSource(url) { + if (!isString(url)) return []; + if (!hasKey(sourceCache, url)) { + // URL needs to be able to fetched within the acceptable domain. Otherwise, + // cross-domain errors will be triggered. + var source = ''; + if (url.indexOf(document.domain) !== -1) { + source = loadSource(url); + } + sourceCache[url] = source ? source.split('\n') : []; + } + + return sourceCache[url]; + } + + /** + * Tries to use an externally loaded copy of source code to determine + * the name of a function by looking at the name of the variable it was + * assigned to, if any. + * @param {string} url URL of source code. + * @param {(string|number)} lineNo Line number in source code. + * @return {string} The function name, if discoverable. + */ + function guessFunctionName(url, lineNo) { + var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/, + reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/, + line = '', + maxLines = 10, + source = getSource(url), + m; + + if (!source.length) { + return UNKNOWN_FUNCTION; + } + + // Walk backwards from the first line in the function until we find the line which + // matches the pattern above, which is the function definition + for (var i = 0; i < maxLines; ++i) { + line = source[lineNo - i] + line; + + if (!isUndefined(line)) { + if ((m = reGuessFunction.exec(line))) { + return m[1]; + } else if ((m = reFunctionArgNames.exec(line))) { + return m[1]; + } + } + } + + return UNKNOWN_FUNCTION; + } + + /** + * Retrieves the surrounding lines from where an exception occurred. + * @param {string} url URL of source code. + * @param {(string|number)} line Line number in source code to centre + * around for context. + * @return {?Array.} Lines of source code. + */ + function gatherContext(url, line) { + var source = getSource(url); + + if (!source.length) { + return null; + } + + var context = [], + // linesBefore & linesAfter are inclusive with the offending line. + // if linesOfContext is even, there will be one extra line + // *before* the offending line. + linesBefore = Math.floor(TraceKit.linesOfContext / 2), + // Add one extra line if linesOfContext is odd + linesAfter = linesBefore + (TraceKit.linesOfContext % 2), + start = Math.max(0, line - linesBefore - 1), + end = Math.min(source.length, line + linesAfter - 1); + + line -= 1; // convert to 0-based index + + for (var i = start; i < end; ++i) { + if (!isUndefined(source[i])) { + context.push(source[i]); + } + } + + return context.length > 0 ? context : null; + } + + /** + * Escapes special characters, except for whitespace, in a string to be + * used inside a regular expression as a string literal. + * @param {string} text The string. + * @return {string} The escaped string literal. + */ + function escapeRegExp(text) { + return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&'); + } + + /** + * Escapes special characters in a string to be used inside a regular + * expression as a string literal. Also ensures that HTML entities will + * be matched the same as their literal friends. + * @param {string} body The string. + * @return {string} The escaped string. + */ + function escapeCodeAsRegExpForMatchingInsideHTML(body) { + return escapeRegExp(body).replace('<', '(?:<|<)').replace('>', '(?:>|>)').replace('&', '(?:&|&)').replace('"', '(?:"|")').replace(/\s+/g, '\\s+'); + } + + /** + * Determines where a code fragment occurs in the source code. + * @param {RegExp} re The function definition. + * @param {Array.} urls A list of URLs to search. + * @return {?Object.} An object containing + * the url, line, and column number of the defined function. + */ + function findSourceInUrls(re, urls) { + var source, m; + for (var i = 0, j = urls.length; i < j; ++i) { + // console.log('searching', urls[i]); + if ((source = getSource(urls[i])).length) { + source = source.join('\n'); + if ((m = re.exec(source))) { + // console.log('Found function in ' + urls[i]); + + return { + 'url': urls[i], + 'line': source.substring(0, m.index).split('\n').length, + 'column': m.index - source.lastIndexOf('\n', m.index) - 1 + }; + } + } + } + + // console.log('no match'); + + return null; + } + + /** + * Determines at which column a code fragment occurs on a line of the + * source code. + * @param {string} fragment The code fragment. + * @param {string} url The URL to search. + * @param {(string|number)} line The line number to examine. + * @return {?number} The column number. + */ + function findSourceInLine(fragment, url, line) { + var source = getSource(url), + re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'), + m; + + line -= 1; + + if (source && source.length > line && (m = re.exec(source[line]))) { + return m.index; + } + + return null; + } + + /** + * Determines where a function was defined within the source code. + * @param {(Function|string)} func A function reference or serialized + * function definition. + * @return {?Object.} An object containing + * the url, line, and column number of the defined function. + */ + function findSourceByFunctionBody(func) { + var urls = [window.location.href], + scripts = document.getElementsByTagName('script'), + body, + code = '' + func, + codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, + eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/, + re, + parts, + result; + + for (var i = 0; i < scripts.length; ++i) { + var script = scripts[i]; + if (script.src) { + urls.push(script.src); + } + } + + if (!(parts = codeRE.exec(code))) { + re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+')); + } + + // not sure if this is really necessary, but I don’t have a test + // corpus large enough to confirm that and it was in the original. + else { + var name = parts[1] ? '\\s+' + parts[1] : '', + args = parts[2].split(',').join('\\s*,\\s*'); + + body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+'); + re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}'); + } + + // look for a normal function definition + if ((result = findSourceInUrls(re, urls))) { + return result; + } + + // look for an old-school event handler function + if ((parts = eventRE.exec(code))) { + var event = parts[1]; + body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]); + + // look for a function defined in HTML as an onXXX handler + re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i'); + + if ((result = findSourceInUrls(re, urls[0]))) { + return result; + } + + // look for ??? + re = new RegExp(body); + + if ((result = findSourceInUrls(re, urls))) { + return result; + } + } + + return null; + } + + // Contents of Exception in various browsers. + // + // SAFARI: + // ex.message = Can't find variable: qq + // ex.line = 59 + // ex.sourceId = 580238192 + // ex.sourceURL = http://... + // ex.expressionBeginOffset = 96 + // ex.expressionCaretOffset = 98 + // ex.expressionEndOffset = 98 + // ex.name = ReferenceError + // + // FIREFOX: + // ex.message = qq is not defined + // ex.fileName = http://... + // ex.lineNumber = 59 + // ex.columnNumber = 69 + // ex.stack = ...stack trace... (see the example below) + // ex.name = ReferenceError + // + // CHROME: + // ex.message = qq is not defined + // ex.name = ReferenceError + // ex.type = not_defined + // ex.arguments = ['aa'] + // ex.stack = ...stack trace... + // + // INTERNET EXPLORER: + // ex.message = ... + // ex.name = ReferenceError + // + // OPERA: + // ex.message = ...message... (see the example below) + // ex.name = ReferenceError + // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message) + // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace' + + /** + * Computes stack trace information from the stack property. + * Chrome and Gecko use this property. + * @param {Error} ex + * @return {?Object.} Stack trace information. + */ + function computeStackTraceFromStackProp(ex) { + if (!ex.stack) { + return null; + } + + var chrome = /^\s*at (.*?) ?\(?((?:file|https?|chrome-extension):.*?):(\d+)(?::(\d+))?\)?\s*$/i, + gecko = /^\s*(.*?)(?:\((.*?)\))?@((?:file|https?|chrome).*?):(\d+)(?::(\d+))?\s*$/i, + lines = ex.stack.split('\n'), + stack = [], + parts, + element, + reference = /^(.*) is undefined$/.exec(ex.message); + + for (var i = 0, j = lines.length; i < j; ++i) { + if ((parts = gecko.exec(lines[i]))) { + element = { + 'url': parts[3], + 'func': parts[1] || UNKNOWN_FUNCTION, + 'args': parts[2] ? parts[2].split(',') : '', + 'line': +parts[4], + 'column': parts[5] ? +parts[5] : null + }; + } else if ((parts = chrome.exec(lines[i]))) { + element = { + 'url': parts[2], + 'func': parts[1] || UNKNOWN_FUNCTION, + 'line': +parts[3], + 'column': parts[4] ? +parts[4] : null + }; + } else { + continue; + } + + if (!element.func && element.line) { + element.func = guessFunctionName(element.url, element.line); + } + + if (element.line) { + element.context = gatherContext(element.url, element.line); + } + + stack.push(element); + } + + if (!stack.length) { + return null; + } + + if (stack[0].line && !stack[0].column && reference) { + stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line); + } else if (!stack[0].column && !isUndefined(ex.columnNumber)) { + // FireFox uses this awesome columnNumber property for its top frame + // Also note, Firefox's column number is 0-based and everything else expects 1-based, + // so adding 1 + stack[0].column = ex.columnNumber + 1; + } + + return { + 'name': ex.name, + 'message': ex.message, + 'url': document.location.href, + 'stack': stack + }; + } + + /** + * Computes stack trace information from the stacktrace property. + * Opera 10 uses this property. + * @param {Error} ex + * @return {?Object.} Stack trace information. + */ + function computeStackTraceFromStacktraceProp(ex) { + // Access and store the stacktrace property before doing ANYTHING + // else to it because Opera is not very good at providing it + // reliably in other circumstances. + var stacktrace = ex.stacktrace; + + var testRE = / line (\d+), column (\d+) in (?:]+)>|([^\)]+))\((.*)\) in (.*):\s*$/i, + lines = stacktrace.split('\n'), + stack = [], + parts; + + for (var i = 0, j = lines.length; i < j; i += 2) { + if ((parts = testRE.exec(lines[i]))) { + var element = { + 'line': +parts[1], + 'column': +parts[2], + 'func': parts[3] || parts[4], + 'args': parts[5] ? parts[5].split(',') : [], + 'url': parts[6] + }; + + if (!element.func && element.line) { + element.func = guessFunctionName(element.url, element.line); + } + if (element.line) { + try { + element.context = gatherContext(element.url, element.line); + } catch (exc) {} + } + + if (!element.context) { + element.context = [lines[i + 1]]; + } + + stack.push(element); + } + } + + if (!stack.length) { + return null; + } + + return { + 'name': ex.name, + 'message': ex.message, + 'url': document.location.href, + 'stack': stack + }; + } + + /** + * NOT TESTED. + * Computes stack trace information from an error message that includes + * the stack trace. + * Opera 9 and earlier use this method if the option to show stack + * traces is turned on in opera:config. + * @param {Error} ex + * @return {?Object.} Stack information. + */ + function computeStackTraceFromOperaMultiLineMessage(ex) { + // Opera includes a stack trace into the exception message. An example is: + // + // Statement on line 3: Undefined variable: undefinedFunc + // Backtrace: + // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz + // undefinedFunc(a); + // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy + // zzz(x, y, z); + // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx + // yyy(a, a, a); + // Line 1 of function script + // try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); } + // ... + + var lines = ex.message.split('\n'); + if (lines.length < 4) { + return null; + } + + var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?)\S+)(?:: in function (\S+))?\s*$/i, + lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?)\S+)(?:: in function (\S+))?\s*$/i, + lineRE3 = /^\s*Line (\d+) of function script\s*$/i, + stack = [], + scripts = document.getElementsByTagName('script'), + inlineScriptBlocks = [], + parts, + i, + len, + source; + + for (i in scripts) { + if (hasKey(scripts, i) && !scripts[i].src) { + inlineScriptBlocks.push(scripts[i]); + } + } + + for (i = 2, len = lines.length; i < len; i += 2) { + var item = null; + if ((parts = lineRE1.exec(lines[i]))) { + item = { + 'url': parts[2], + 'func': parts[3], + 'line': +parts[1] + }; + } else if ((parts = lineRE2.exec(lines[i]))) { + item = { + 'url': parts[3], + 'func': parts[4] + }; + var relativeLine = (+parts[1]); // relative to the start of the '."\n"; } + // Raven.js for client-side Sentry logging support + if (array_key_exists('mod_syslog_sentry', $conf->loghandlers)) { + print '' . "\n"; + print '' . "\n"; + print '' . "\n"; + if (! defined('DISABLE_JQUERY')) { + print '' . "\n"; + } + } + // Global js function print ''."\n"; print ''."\n"; @@ -1321,7 +1331,7 @@ function top_menu($head, $title='', $target='', $disablejs=0, $disablehead=0, $a // For backward compatibility with old modules if (empty($conf->headerdone)) top_htmlhead($head, $title, $disablejs, $disablehead, $arrayofjs, $arrayofcss); - print ''; + print '' . "\n"; if ($conf->use_javascript_ajax) { @@ -1376,15 +1386,29 @@ function top_menu($head, $title='', $target='', $disablejs=0, $disablehead=0, $a paneSelector: "#mainContent" } } - '; - } + ' . "\n"; + } - // Wrapper to show tooltips - print "\n".''; + // Wrapper to show tooltips + print '' . "\n"; + + // Raven.js for client-side Sentry logging support + if (array_key_exists('mod_syslog_sentry', $conf->loghandlers) && ! empty($conf->global->SYSLOG_SENTRY_DSN)) { + + // Filter out secret key + $dsn = parse_url($conf->global->SYSLOG_SENTRY_DSN); + $public_dsn = $dsn['scheme'] . '://' . $dsn['user'] .'@' . $dsn['host'] . $dsn['path']; + + print '\n"; + } } /* From 1618cbc5bca2dc8250a40f39f9eb242cd4624827 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Doursenaud?= Date: Mon, 31 Aug 2015 18:02:52 +0200 Subject: [PATCH 6/6] Added Raven libraries to list --- COPYRIGHT | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/COPYRIGHT b/COPYRIGHT index 35f5f6af2b2..c38873a2f07 100644 --- a/COPYRIGHT +++ b/COPYRIGHT @@ -25,6 +25,7 @@ odtPHP 1.0.1 GPL-2+ b Yes PHPExcel 1.8.0 LGPL-2.1+ Yes Read/Write XLS files, read ODS files php-iban 1.4.7 LGPL-3+ Yes Parse and validate IBAN (and IIBAN) bank account information in PHP PHPPrintIPP 1.3 GPL-2+ Yes Library to send print IPP requests +Raven-php 0.12.1 MIT License Yes Used for server-side error logging with Sentry logger Restler 3.0 LGPL-3+ Yes Library to develop REST Web services TCPDF 6.2.6 LGPL-3+ Yes PDF generation TCPDI 1.0.0 LGPL-3+ / Apache 2.0 Yes FPDI replacement @@ -50,7 +51,8 @@ jQuery TableDnD 0.6 GPL and MIT License Yes jQuery Timepicker 1.1.0 GPL and MIT License Yes JS library Timepicker addon for Datepicker jQuery Tiptip 1.3 GPL and MIT License Yes JS library for tooltips jsGantt 1.2 BSD License Yes JS library (to build Gantt reports) -JsTimezoneDetect 1.0.4 MIT Licence Yes JS library to detect user timezone +JsTimezoneDetect 1.0.4 MIT License Yes JS library to detect user timezone +Raven.js 1.1.19 MIT License Yes Used for client-side error logging with Sentry logger For licenses compatibility informations: http://www.fsf.org/licensing/licenses/index_html