Skip to content
29 changes: 29 additions & 0 deletions src/Integration/FatalErrorListenerIntegration.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

namespace Sentry\Integration;

use Sentry\Breadcrumb;
use Sentry\ErrorHandler;
use Sentry\Exception\FatalErrorException;
use Sentry\SentrySdk;
use Sentry\State\Scope;

/**
* This integration hooks into the error handler and captures fatal errors.
Expand All @@ -15,6 +17,12 @@
*/
final class FatalErrorListenerIntegration extends AbstractErrorListenerIntegration
{
/**
* Intentionally looser than ErrorHandler::OOM_MESSAGE_MATCHER — matches the
* prefixed FatalErrorException message and needs no capture groups.
*/
private const OOM_MESSAGE_MATCHER = '/Allowed memory size of \d+ bytes exhausted/';

/**
* {@inheritdoc}
*/
Expand All @@ -36,6 +44,27 @@ public function setupOnce(): void
return;
}

if (preg_match(self::OOM_MESSAGE_MATCHER, $exception->getMessage()) === 1) {
$currentHub->configureScope(static function (Scope $scope): void {
$strippedBreadcrumbs = array_map(static function (Breadcrumb $breadcrumb): Breadcrumb {
return new Breadcrumb(
$breadcrumb->getLevel(),
$breadcrumb->getType(),
$breadcrumb->getCategory(),
$breadcrumb->getMessage(),
[],
$breadcrumb->getTimestamp()
);
}, $scope->getBreadcrumbs());

$scope->clearBreadcrumbs();

foreach ($strippedBreadcrumbs as $breadcrumb) {
$scope->addBreadcrumb($breadcrumb, \count($strippedBreadcrumbs));
}
});
}

$integration->captureException($currentHub, $exception);
});
}
Expand Down
10 changes: 10 additions & 0 deletions src/State/Scope.php
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,16 @@ public function addBreadcrumb(Breadcrumb $breadcrumb, int $maxBreadcrumbs = 100)
return $this;
}

/**
* Gets the breadcrumbs.
*
* @return Breadcrumb[]
*/
public function getBreadcrumbs(): array
{
return $this->breadcrumbs;
}

/**
* Clears all the breadcrumbs.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
Test that when handling a out of memory error the memory limit is increased with 5 MiB and the event is serialized and ready to be sent
--SKIPIF--
<?php
if (PHP_VERSION_ID >= 80400) {
if (PHP_VERSION_ID >= 80500) {
die('skip - only works for PHP 8.4 and below');
}
--INI--
Expand All @@ -24,6 +24,8 @@ use Sentry\Transport\Result;
use Sentry\Transport\ResultStatus;
use Sentry\Transport\TransportInterface;

error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);

$vendor = __DIR__;

while (!file_exists($vendor . '/vendor')) {
Expand All @@ -32,8 +34,6 @@ while (!file_exists($vendor . '/vendor')) {

require $vendor . '/vendor/autoload.php';

error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);

$options = new Options([
'dsn' => 'http://public@example.com/sentry/1',
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
--TEST--
Test that when handling an OOM error with large breadcrumbs, breadcrumb metadata is stripped to prevent secondary OOM during serialization
--SKIPIF--
<?php
if (PHP_VERSION_ID >= 80500) {
die('skip - only works for PHP 8.4 and below');
}
Comment thread
spawnia marked this conversation as resolved.
--INI--
memory_limit=67108864
--FILE--
<?php

declare(strict_types=1);

namespace Sentry\Tests;

use Sentry\Breadcrumb;
use Sentry\ClientBuilder;
use Sentry\Event;
use Sentry\Options;
use Sentry\SentrySdk;
use Sentry\Serializer\PayloadSerializer;
use Sentry\Serializer\PayloadSerializerInterface;
use Sentry\Transport\Result;
use Sentry\Transport\ResultStatus;
use Sentry\Transport\TransportInterface;

error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);

$vendor = __DIR__;

while (!file_exists($vendor . '/vendor')) {
$vendor = \dirname($vendor);
}

require $vendor . '/vendor/autoload.php';

$options = new Options([
'dsn' => 'http://public@example.com/sentry/1',
]);

$transport = new class(new PayloadSerializer($options)) implements TransportInterface {
private $payloadSerializer;

public function __construct(PayloadSerializerInterface $payloadSerializer)
{
$this->payloadSerializer = $payloadSerializer;
}

public function send(Event $event): Result
{
$breadcrumbs = $event->getBreadcrumbs();
echo 'Breadcrumb count: ' . \count($breadcrumbs) . \PHP_EOL;

if (\count($breadcrumbs) > 0) {
$firstBreadcrumb = $breadcrumbs[0];
echo 'First breadcrumb category: ' . $firstBreadcrumb->getCategory() . \PHP_EOL;
echo 'First breadcrumb has metadata: ' . (empty($firstBreadcrumb->getMetadata()) ? 'no' : 'yes') . \PHP_EOL;
}

$this->payloadSerializer->serialize($event);

echo 'Transport called' . \PHP_EOL;

return new Result(ResultStatus::success());
}

public function close(?int $timeout = null): Result
{
return new Result(ResultStatus::success());
}
};

$options->setTransport($transport);

$client = (new ClientBuilder($options))->getClient();

SentrySdk::init()->bindClient($client);

// Add 100 breadcrumbs with ~100KB metadata each to simulate the real-world scenario
$hub = SentrySdk::getCurrentHub();
$hub->configureScope(function (\Sentry\State\Scope $scope): void {
for ($i = 0; $i < 100; ++$i) {
$scope->addBreadcrumb(new Breadcrumb(
Breadcrumb::LEVEL_INFO,
Breadcrumb::TYPE_DEFAULT,
'db.query',
'SELECT * FROM large_table WHERE id = ?',
['bindings' => str_repeat('x', 100 * 1024)]
));
}
});

// Trigger OOM - the remaining memory after breadcrumbs is limited
$array = [];
for ($i = 0; $i < 100000000; ++$i) {
$array[] = 'sentry';
}
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line %d
Breadcrumb count: 100
First breadcrumb category: db.query
First breadcrumb has metadata: no
Transport called
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
--TEST--
Test that when handling an OOM error with large breadcrumbs, breadcrumb metadata is stripped to prevent secondary OOM during serialization
--SKIPIF--
<?php
if (PHP_VERSION_ID < 80500) {
die('skip - only works for PHP 8.5 and above');
}
--INI--
memory_limit=67108864
--FILE--
<?php

declare(strict_types=1);

namespace Sentry\Tests;

use Sentry\Breadcrumb;
use Sentry\ClientBuilder;
use Sentry\Event;
use Sentry\Options;
use Sentry\SentrySdk;
use Sentry\Serializer\PayloadSerializer;
use Sentry\Serializer\PayloadSerializerInterface;
use Sentry\Transport\Result;
use Sentry\Transport\ResultStatus;
use Sentry\Transport\TransportInterface;

error_reporting(E_ALL & ~E_DEPRECATED & ~E_USER_DEPRECATED);

$vendor = __DIR__;

while (!file_exists($vendor . '/vendor')) {
$vendor = \dirname($vendor);
}

require $vendor . '/vendor/autoload.php';

$options = new Options([
'dsn' => 'http://public@example.com/sentry/1',
]);

$transport = new class(new PayloadSerializer($options)) implements TransportInterface {
private $payloadSerializer;

public function __construct(PayloadSerializerInterface $payloadSerializer)
{
$this->payloadSerializer = $payloadSerializer;
}

public function send(Event $event): Result
{
$breadcrumbs = $event->getBreadcrumbs();
echo 'Breadcrumb count: ' . \count($breadcrumbs) . \PHP_EOL;

if (\count($breadcrumbs) > 0) {
$firstBreadcrumb = $breadcrumbs[0];
echo 'First breadcrumb category: ' . $firstBreadcrumb->getCategory() . \PHP_EOL;
echo 'First breadcrumb has metadata: ' . (empty($firstBreadcrumb->getMetadata()) ? 'no' : 'yes') . \PHP_EOL;
}

$this->payloadSerializer->serialize($event);

echo 'Transport called' . \PHP_EOL;

return new Result(ResultStatus::success());
}

public function close(?int $timeout = null): Result
{
return new Result(ResultStatus::success());
}
};

$options->setTransport($transport);

$client = (new ClientBuilder($options))->getClient();

SentrySdk::init()->bindClient($client);

// Add 100 breadcrumbs with ~100KB metadata each to simulate the real-world scenario
$hub = SentrySdk::getCurrentHub();
$hub->configureScope(function (\Sentry\State\Scope $scope): void {
for ($i = 0; $i < 100; ++$i) {
$scope->addBreadcrumb(new Breadcrumb(
Breadcrumb::LEVEL_INFO,
Breadcrumb::TYPE_DEFAULT,
'db.query',
'SELECT * FROM large_table WHERE id = ?',
['bindings' => str_repeat('x', 100 * 1024)]
));
}
});

// Trigger OOM - the remaining memory after breadcrumbs is limited
$array = [];
for ($i = 0; $i < 100000000; ++$i) {
$array[] = 'sentry';
}
--EXPECTF--
Fatal error: Allowed memory size of %d bytes exhausted (tried to allocate %d bytes) in %s on line %d
Stack trace:
%A
Breadcrumb count: 100
First breadcrumb category: db.query
First breadcrumb has metadata: no
Transport called