diff --git a/CHANGELOG.md b/CHANGELOG.md index 11c6ca486..30dcb6105 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,26 @@ - ... +## 1.9.0 (2018-05-03) + +- Fixed undefined variable (#588) +- Fix for exceptions throwing exceptions when setting event id (#587) +- Fix monolog handler not accepting Throwable (#586) +- Add `excluded_exceptions` option to exclude exceptions and their extending exceptions (#583) +- Fix `HTTP_X_FORWARDED_PROTO` header detection (#578) +- Fix sending events async in PHP 5 (#576) +- Avoid double reporting due to `ErrorException`s (#574) +- Make it possible to overwrite serializer message limit of 1024 (#559) +- Allow request data to be nested up to 5 levels deep (#554) +- Update serializer to handle UTF-8 characters correctly (#553) + +## 1.8.4 (2018-03-20) + +- Revert ignoring fatal errors on PHP 7+ (#571) +- Add PHP runtime information (#564) +- Cleanup the `site` value if it's empty (#555) +- Add `application/json` input handling (#546) + ## 1.8.3 (2018-02-07) - Serialize breadcrumbs to prevent issues with binary data (#538) diff --git a/README.md b/README.md index cec8d54cd..ebaba4866 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,10 @@ # Sentry for PHP [![Build Status](https://secure.travis-ci.org/getsentry/sentry-php.png?branch=master)](http://travis-ci.org/getsentry/sentry-php) -[![Total Downloads](https://img.shields.io/packagist/dt/sentry/sentry.svg?style=flat-square)](https://packagist.org/packages/sentry/sentry) -[![Downloads per month](https://img.shields.io/packagist/dm/sentry/sentry.svg?style=flat-square)](https://packagist.org/packages/sentry/sentry) -[![Latest stable version](https://img.shields.io/packagist/v/sentry/sentry.svg?style=flat-square)](https://packagist.org/packages/sentry/sentry) -[![License](http://img.shields.io/packagist/l/sentry/sentry.svg?style=flat-square)](https://packagist.org/packages/sentry/sentry) +[![Total Downloads](https://poser.pugx.org/sentry/sentry/downloads)](https://packagist.org/packages/sentry/sentry) +[![Monthly Downloads](https://poser.pugx.org/sentry/sentry/d/monthly)](https://packagist.org/packages/sentry/sentry) +[![Latest Stable Version](https://poser.pugx.org/sentry/sentry/v/stable)](https://packagist.org/packages/sentry/sentry) +[![License](https://poser.pugx.org/sentry/sentry/license)](https://packagist.org/packages/sentry/sentry) [![Scrutinizer Code Quality](https://img.shields.io/scrutinizer/g/getsentry/sentry-php/master.svg)](https://scrutinizer-ci.com/g/getsentry/sentry-php/) [![Code Coverage](https://img.shields.io/scrutinizer/coverage/g/getsentry/sentry-php/master.svg)](https://scrutinizer-ci.com/g/getsentry/sentry-php/) @@ -131,7 +131,7 @@ $ git checkout -b releases/1.9.x 3. Update the hardcoded version tag in ``Client.php``: -``` +```php class Raven_Client { const VERSION = '1.9.0'; @@ -170,7 +170,7 @@ git checkout master 9. Update the version in ``Client.php``: -``` +```php class Raven_Client { const VERSION = '1.10.x-dev'; @@ -179,7 +179,7 @@ class Raven_Client 10. Lastly, update the composer version in ``composer.json``: -``` +```json "extra": { "branch-alias": { "dev-master": "1.10.x-dev" diff --git a/docs/config.rst b/docs/config.rst index b97e8de53..6e193db0d 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -171,7 +171,7 @@ The following settings are available for the client: 'User-Agent' => $client->getUserAgent(), 'X-Sentry-Auth' => $client->getAuthHeader(), ), - 'body' => gzipCompress(jsonEncode($data)), + 'body' => gzcompress(jsonEncode($data)), )) }, @@ -241,6 +241,26 @@ The following settings are available for the client: ) ) +.. describe:: timeout + + The timeout for sending requests to the Sentry server in seconds, default is 2 seconds. + + .. code-block:: php + + 'timeout' => 2, + +.. describe:: excluded_exceptions + + Exception that should not be reported, exceptions extending exceptions in this list will also + be excluded, default is an empty array. + + In the example below, when you exclude ``LogicException`` you will also exclude ``BadFunctionCallException`` + since it extends ``LogicException``. + + .. code-block:: php + + 'excluded_exceptions' => array('LogicException'), + .. _sentry-php-request-context: Providing Request Context diff --git a/docs/integrations/laravel.rst b/docs/integrations/laravel.rst index acd72a337..60b3ba167 100644 --- a/docs/integrations/laravel.rst +++ b/docs/integrations/laravel.rst @@ -64,6 +64,8 @@ step is not required anymore an you can skip ahead to the next one: = 70000 && $record['context']['exception'] instanceof \Throwable) + ) + ) { /** - * @var \Exception + * @var \Exception|\Throwable */ $exc = $record['context']['exception']; diff --git a/lib/Raven/Client.php b/lib/Raven/Client.php index ec45568c8..2f5e89d32 100644 --- a/lib/Raven/Client.php +++ b/lib/Raven/Client.php @@ -33,7 +33,7 @@ /** * Raven PHP Client. * - * @doc https://docs.sentry.io/clients/php/config/ + * @see https://docs.sentry.io/clients/php/config/ */ class Client { @@ -471,7 +471,7 @@ public function sanitize(Event $event) $tagsContext = $event->getTagsContext(); if (!empty($request)) { - $event = $event->withRequest($this->serializer->serialize($request)); + $event = $event->withRequest($this->serializer->serialize($request, 5)); } if (!empty($userContext)) { $event = $event->withUserContext($this->serializer->serialize($userContext, 3)); diff --git a/lib/Raven/Serializer.php b/lib/Raven/Serializer.php index 7d8cdadca..8cc4d443f 100644 --- a/lib/Raven/Serializer.php +++ b/lib/Raven/Serializer.php @@ -50,14 +50,24 @@ class Serializer */ protected $_all_object_serialize = false; + /** + * The default maximum message lengths. Longer strings will be truncated. + * + * @var int + */ + protected $messageLimit; + /** * @param null|string $mb_detect_order + * @param null|int $messageLimit */ - public function __construct($mb_detect_order = null) + public function __construct($mb_detect_order = null, $messageLimit = Client::MESSAGE_LIMIT) { if (null != $mb_detect_order) { $this->mb_detect_order = $mb_detect_order; } + + $this->messageLimit = (int) $messageLimit; } /** @@ -122,19 +132,22 @@ public function serializeObject($object, $max_depth = 3, $_depth = 0, $hashes = protected function serializeString($value) { $value = (string) $value; - if (function_exists('mb_detect_encoding') - && function_exists('mb_convert_encoding') - ) { + + if (extension_loaded('mbstring')) { // we always guarantee this is coerced, even if we can't detect encoding if ($currentEncoding = mb_detect_encoding($value, $this->mb_detect_order)) { $value = mb_convert_encoding($value, 'UTF-8', $currentEncoding); } else { $value = mb_convert_encoding($value, 'UTF-8'); } - } - if (strlen($value) > 1024) { - $value = substr($value, 0, 1014) . ' {clipped}'; + if (mb_strlen($value) > $this->messageLimit) { + $value = mb_substr($value, 0, $this->messageLimit - 10, 'UTF-8') . ' {clipped}'; + } + } else { + if (strlen($value) > $this->messageLimit) { + $value = substr($value, 0, $this->messageLimit - 10) . ' {clipped}'; + } } return $value; @@ -197,4 +210,20 @@ public function getAllObjectSerialize() { return $this->_all_object_serialize; } + + /** + * @return int + */ + public function getMessageLimit() + { + return $this->messageLimit; + } + + /** + * @param int $messageLimit + */ + public function setMessageLimit($messageLimit) + { + $this->messageLimit = $messageLimit; + } } diff --git a/tests/Breadcrumbs/MonologHandlerTest.php b/tests/Breadcrumbs/MonologHandlerTest.php index b2e42c888..129b11496 100644 --- a/tests/Breadcrumbs/MonologHandlerTest.php +++ b/tests/Breadcrumbs/MonologHandlerTest.php @@ -12,6 +12,7 @@ namespace Raven\Tests\Breadcrumbs; use Monolog\Logger; +use ParseError; use PHPUnit\Framework\TestCase; use Raven\Breadcrumbs\Breadcrumb; use Raven\Breadcrumbs\MonologHandler; @@ -44,52 +45,108 @@ protected function getSampleErrorMessage() public function testSimple() { - $client = $client = ClientBuilder::create([ - 'install_default_breadcrumb_handlers' => false, - ])->getClient(); - - $handler = new MonologHandler($client); + $client = $this->createClient(); + $logger = $this->createLoggerWithHandler($client); - $logger = new Logger('sentry'); - $logger->pushHandler($handler); $logger->addWarning('foo'); - $breadcrumbsRecorder = $this->getObjectAttribute($client, 'breadcrumbRecorder'); + $breadcrumbs = $this->getBreadcrumbs($client); + $this->assertCount(1, $breadcrumbs); + $this->assertEquals('foo', $breadcrumbs[0]->getMessage()); + $this->assertEquals(Client::LEVEL_WARNING, $breadcrumbs[0]->getLevel()); + $this->assertEquals('sentry', $breadcrumbs[0]->getCategory()); + } - /** @var \Raven\Breadcrumbs\Breadcrumb[] $breadcrumbs */ - $breadcrumbs = iterator_to_array($breadcrumbsRecorder); + public function testErrorInMessage() + { + $client = $this->createClient(); + $logger = $this->createLoggerWithHandler($client); + + $logger->addError($this->getSampleErrorMessage()); + $breadcrumbs = $this->getBreadcrumbs($client); $this->assertCount(1, $breadcrumbs); + $this->assertEquals(Breadcrumb::TYPE_ERROR, $breadcrumbs[0]->getType()); + $this->assertEquals(Client::LEVEL_ERROR, $breadcrumbs[0]->getLevel()); + $this->assertEquals('sentry', $breadcrumbs[0]->getCategory()); + $this->assertEquals('An unhandled exception', $breadcrumbs[0]->getMetadata()['value']); + } + + public function testExceptionBeingParsed() + { + $client = $this->createClient(); + $logger = $this->createLoggerWithHandler($client); - $this->assertEquals($breadcrumbs[0]->getMessage(), 'foo'); - $this->assertEquals($breadcrumbs[0]->getLevel(), Client::LEVEL_WARNING); - $this->assertEquals($breadcrumbs[0]->getCategory(), 'sentry'); + $logger->addError('A message', ['exception' => new \Exception('Foo bar')]); + + $breadcrumbs = $this->getBreadcrumbs($client); + $this->assertCount(1, $breadcrumbs); + $this->assertEquals(Breadcrumb::TYPE_ERROR, $breadcrumbs[0]->getType()); + $this->assertEquals('Foo bar', $breadcrumbs[0]->getMetadata()['value']); + $this->assertEquals('sentry', $breadcrumbs[0]->getCategory()); + $this->assertEquals(Client::LEVEL_ERROR, $breadcrumbs[0]->getLevel()); + $this->assertNull($breadcrumbs[0]->getMessage()); } - public function testErrorInMessage() + public function testThrowableBeingParsedAsException() + { + if (PHP_VERSION_ID <= 70000) { + $this->markTestSkipped('PHP 7.0 introduced Throwable'); + } + + $client = $this->createClient(); + $logger = $this->createLoggerWithHandler($client); + $throwable = new ParseError('Foo bar'); + + $logger->addError('This is a throwable', ['exception' => $throwable]); + + $breadcrumbs = $this->getBreadcrumbs($client); + $this->assertCount(1, $breadcrumbs); + $this->assertEquals(Breadcrumb::TYPE_ERROR, $breadcrumbs[0]->getType()); + $this->assertEquals('Foo bar', $breadcrumbs[0]->getMetadata()['value']); + $this->assertEquals('sentry', $breadcrumbs[0]->getCategory()); + $this->assertEquals(Client::LEVEL_ERROR, $breadcrumbs[0]->getLevel()); + $this->assertNull($breadcrumbs[0]->getMessage()); + } + + /** + * @return Client + */ + private function createClient() { $client = $client = ClientBuilder::create([ 'install_default_breadcrumb_handlers' => false, ])->getClient(); - $handler = new MonologHandler($client); + return $client; + } + /** + * @param Client $client + * + * @return Logger + */ + private function createLoggerWithHandler(Client $client) + { + $handler = new MonologHandler($client); $logger = new Logger('sentry'); $logger->pushHandler($handler); - $logger->addError($this->getSampleErrorMessage()); + return $logger; + } + + /** + * @param Client $client + * + * @return Breadcrumb[] + */ + private function getBreadcrumbs(Client $client) + { $breadcrumbsRecorder = $this->getObjectAttribute($client, 'breadcrumbRecorder'); - /** @var \Raven\Breadcrumbs\Breadcrumb[] $breadcrumbs */ $breadcrumbs = iterator_to_array($breadcrumbsRecorder); + $this->assertContainsOnlyInstancesOf(Breadcrumb::class, $breadcrumbs); - $this->assertCount(1, $breadcrumbs); - - $metaData = $breadcrumbs[0]->getMetadata(); - - $this->assertEquals($breadcrumbs[0]->getType(), Breadcrumb::TYPE_ERROR); - $this->assertEquals($breadcrumbs[0]->getLevel(), Client::LEVEL_ERROR); - $this->assertEquals($breadcrumbs[0]->getCategory(), 'sentry'); - $this->assertEquals($metaData['value'], 'An unhandled exception'); + return $breadcrumbs; } } diff --git a/tests/ClientTest.php b/tests/ClientTest.php index 72b6e0e05..d13e15e46 100644 --- a/tests/ClientTest.php +++ b/tests/ClientTest.php @@ -27,6 +27,7 @@ use Raven\Processor\ProcessorInterface; use Raven\Processor\ProcessorRegistry; use Raven\Serializer; +use Raven\Tests\Fixtures\classes\CarelessException; use Raven\Transport\TransportInterface; // XXX: Is there a better way to stub the client? @@ -503,32 +504,79 @@ public function testSanitizeUser() $this->assertEquals(['email' => 'foo@example.com'], $event->getUserContext()); } - public function testSanitizeRequest() + /** + * @dataProvider deepRequestProvider + */ + public function testSanitizeRequest(array $postData, array $expectedData) { $client = ClientBuilder::create()->getClient(); $event = new Event($client->getConfig()); $event = $event->withRequest([ - 'context' => [ - 'line' => 1216, - 'stack' => [ - 1, [2], 3, - ], + 'method' => 'POST', + 'url' => 'https://example.com/something', + 'query_string' => '', + 'data' => [ + '_method' => 'POST', + 'data' => $postData, ], ]); $event = $client->sanitize($event); $this->assertArraySubset([ - 'context' => [ - 'line' => 1216, - 'stack' => [ - 1, 'Array of length 1', 3, - ], + 'method' => 'POST', + 'url' => 'https://example.com/something', + 'query_string' => '', + 'data' => [ + '_method' => 'POST', + 'data' => $expectedData, ], ], $event->getRequest()); } + public function deepRequestProvider() + { + return [ + [ + [ + 'MyModel' => [ + 'flatField' => 'my value', + 'nestedField' => [ + 'key' => 'my other value', + ], + ], + ], + [ + 'MyModel' => [ + 'flatField' => 'my value', + 'nestedField' => [ + 'key' => 'my other value', + ], + ], + ], + ], + [ + [ + 'Level 1' => [ + 'Level 2' => [ + 'Level 3' => [ + 'Level 4' => 'something', + ], + ], + ], + ], + [ + 'Level 1' => [ + 'Level 2' => [ + 'Level 3' => 'Array of length 1', + ], + ], + ], + ], + ]; + } + private function assertMixedValueAndArray($expected_value, $actual_value) { if (null === $expected_value) { @@ -795,4 +843,23 @@ public function testSetReprSerializer() $this->assertSame($serializer, $client->getReprSerializer()); } + + public function testHandlingExceptionThrowingAnException() + { + $client = ClientBuilder::create()->getClient(); + $client->captureException($this->createCarelessExceptionWithStacktrace()); + $event = $client->getLastEvent(); + // Make sure the exception is of the careless exception and not the exception thrown inside + // the __set method of that exception caused by setting the event_id on the exception instance + $this->assertSame(CarelessException::class, $event->getException()['values'][0]['type']); + } + + private function createCarelessExceptionWithStacktrace() + { + try { + throw new CarelessException('Foo bar'); + } catch (\Exception $ex) { + return $ex; + } + } } diff --git a/tests/Context/ContextTest.php b/tests/Context/ContextTest.php index 2aeffdbf2..ceba26e67 100644 --- a/tests/Context/ContextTest.php +++ b/tests/Context/ContextTest.php @@ -106,7 +106,7 @@ public function testArrayLikeBehaviour() // Accessing a key that does not exists in the data object should behave // like accessing a non-existent key of an array - $context['foo']; + @$context['foo']; $error = error_get_last(); diff --git a/tests/Fixtures/classes/CarelessException.php b/tests/Fixtures/classes/CarelessException.php new file mode 100644 index 000000000..aa8e94223 --- /dev/null +++ b/tests/Fixtures/classes/CarelessException.php @@ -0,0 +1,13 @@ + 'http://www.example.com:123/foo', + 'method' => 'GET', + 'cookies' => [], + 'headers' => [], + ], + [ + 'url' => 'http://www.example.com:123/foo', + 'method' => 'GET', + 'cookies' => [], + 'headers' => [ + 'Host' => ['www.example.com:123'], + ], + ], + ], [ [ 'uri' => 'http://www.example.com/foo?foo=bar&bar=baz', diff --git a/tests/SerializerAbstractTest.php b/tests/SerializerAbstractTest.php index 4b2d904e5..8d87f81a7 100644 --- a/tests/SerializerAbstractTest.php +++ b/tests/SerializerAbstractTest.php @@ -366,16 +366,27 @@ public function testLongString($serialize_all_objects) if ($serialize_all_objects) { $serializer->setAllObjectSerialize(true); } - for ($i = 0; $i < 100; ++$i) { - foreach ([100, 1000, 1010, 1024, 1050, 1100, 10000] as $length) { - $input = ''; - for ($i = 0; $i < $length; ++$i) { - $input .= chr(mt_rand(0, 255)); - } - $result = $serializer->serialize($input); - $this->assertInternalType('string', $result); - $this->assertLessThanOrEqual(1024, strlen($result)); - } + + foreach ([100, 1000, 1010, 1024, 1050, 1100, 10000] as $length) { + $input = str_repeat('x', $length); + $result = $serializer->serialize($input); + $this->assertInternalType('string', $result); + $this->assertLessThanOrEqual(1024, strlen($result)); + } + } + + public function testLongStringWithOverwrittenMessageLength() + { + $class_name = static::get_test_class(); + /** @var \Raven\Serializer $serializer */ + $serializer = new $class_name(); + $serializer->setMessageLimit(500); + + foreach ([100, 490, 499, 500, 501, 1000, 10000] as $length) { + $input = str_repeat('x', $length); + $result = $serializer->serialize($input); + $this->assertInternalType('string', $result); + $this->assertLessThanOrEqual(500, strlen($result)); } } @@ -409,6 +420,24 @@ public function testSetAllObjectSerialize() $serializer->setAllObjectSerialize(false); $this->assertFalse($serializer->getAllObjectSerialize()); } + + public function testClippingUTF8Characters() + { + if (!extension_loaded('mbstring')) { + $this->markTestSkipped('mbstring extension is not enabled.'); + } + + $testString = 'Прекратите надеяться, что ваши пользователи будут сообщать об ошибках'; + $class_name = static::get_test_class(); + /** @var \Raven\Serializer $serializer */ + $serializer = new $class_name(null, 19); + + $clipped = $serializer->serialize($testString); + + $this->assertEquals('Прекратит {clipped}', $clipped); + $this->assertNotNull(json_encode($clipped)); + $this->assertSame(JSON_ERROR_NONE, json_last_error()); + } } /**