diff --git a/Classes/Command/SentryCommandController.php b/Classes/Command/SentryCommandController.php
index 043f3ee..05cd3fc 100644
--- a/Classes/Command/SentryCommandController.php
+++ b/Classes/Command/SentryCommandController.php
@@ -23,6 +23,10 @@
final class SentryCommandController extends CommandController
{
+ const TEST_MODE_MESSAGE = 'message';
+ const TEST_MODE_THROW = 'throw';
+ const TEST_MODE_ERROR = 'error';
+
/**
* @Flow\Inject
* @var SentryClient
@@ -39,13 +43,12 @@ final class SentryCommandController extends CommandController
*
* @throws SentryClientTestException
*/
- public function testCommand(): void
+ public function testCommand(string $mode = self::TEST_MODE_THROW): void
{
$this->output->outputLine('Testing Sentry setup …');
$this->output->outputLine('Using the following configuration:');
$options = $this->sentryClient->getOptions();
-
$this->output->outputTable([
['DSN', $options->getDsn()],
['Environment', $options->getEnvironment()],
@@ -57,7 +60,24 @@ public function testCommand(): void
'Value'
]);
- $eventId = $this->sentryClient->captureMessage(
+ switch ($mode) {
+ case self::TEST_MODE_MESSAGE:
+ $this->captureMessage();
+ break;
+ case self::TEST_MODE_THROW:
+ $this->throwException();
+ break;
+ case self::TEST_MODE_ERROR:
+ $this->triggerError();
+ break;
+ default:
+ $this->output->outputLine('Unknown mode given');
+ }
+ }
+
+ private function captureMessage(): void
+ {
+ $captureResult = $this->sentryClient->captureMessage(
'Flownative Sentry Plugin Test',
Severity::debug(),
[
@@ -65,11 +85,34 @@ public function testCommand(): void
]
);
- $this->outputLine('An informational message was sent to Sentry Event ID: #%s', [$eventId]);
$this->outputLine();
- $this->outputLine('This command will now throw an exception for testing purposes.');
+ if ($captureResult->suceess) {
+ $this->outputLine('An informational message was sent to Sentry Event ID: #%s', [$captureResult->eventId]);
+ } else {
+ $this->outputLine('Sending an informational message to Sentry failed: %s', [$captureResult->message]);
+ }
$this->outputLine();
+ }
+ private function throwException(): void
+ {
+ $this->outputLine();
+ $this->outputLine('This command will now throw an exception for testing purposes.');
+ $this->outputLine();
(new ThrowingClass())->throwException(new StringableTestArgument((string)M_PI));
}
+
+ private function triggerError(): void
+ {
+ $this->outputLine();
+ $this->outputLine('This command will now cause a return type error for testing purposes.');
+ $this->outputLine();
+
+ $function = static function (): int {
+ /** @noinspection PhpStrictTypeCheckingInspection */
+ return 'wrong type';
+ };
+ /** @noinspection PhpExpressionResultUnusedInspection */
+ $function();
+ }
}
diff --git a/Classes/Log/SentryFileBackend.php b/Classes/Log/SentryFileBackend.php
index 5a02e68..b6b09ef 100644
--- a/Classes/Log/SentryFileBackend.php
+++ b/Classes/Log/SentryFileBackend.php
@@ -15,14 +15,13 @@
use Flownative\Sentry\SentryClientTrait;
use Neos\Flow\Log\Backend\FileBackend;
-use Sentry\Severity;
+use Sentry\Breadcrumb;
+use Sentry\SentrySdk;
class SentryFileBackend extends FileBackend
{
use SentryClientTrait;
- private bool $capturingMessage = false;
-
/**
* Appends the given message along with the additional information into the log.
*
@@ -36,29 +35,50 @@ class SentryFileBackend extends FileBackend
*/
public function append(string $message, int $severity = LOG_INFO, $additionalData = null, ?string $packageKey = null, ?string $className = null, ?string $methodName = null): void
{
- if ($this->capturingMessage) {
- return;
+ try {
+ SentrySdk::getCurrentHub()->addBreadcrumb(
+ new Breadcrumb(
+ $this->getBreadcrumbLevel($severity),
+ $this->getBreadcrumbType($severity),
+ basename($this->logFileUrl),
+ $message,
+ ($additionalData ?? []) + array_filter([
+ 'packageKey' => $packageKey, 'className' => $className, 'methodName' => $methodName
+ ]),
+ time()
+ )
+ );
+ } catch (\Throwable $throwable) {
+ parent::append(
+ sprintf('%s (%s)', $throwable->getMessage(), $throwable->getCode()),
+ LOG_WARNING,
+ null,
+ 'Flownative.Sentry',
+ __CLASS__,
+ __METHOD__
+ );
}
- try {
- $this->capturingMessage = true;
+ parent::append($message, $severity, $additionalData, $packageKey, $className, $methodName);
+ }
- $sentryClient = self::getSentryClient();
- if ($severity <= LOG_NOTICE && $sentryClient) {
- $sentrySeverity = match ($severity) {
- LOG_WARNING => Severity::warning(),
- LOG_ERR => Severity::error(),
- LOG_CRIT, LOG_ALERT, LOG_EMERG => Severity::fatal(),
- default => Severity::info(),
- };
+ private function getBreadcrumbLevel(int $severity): string
+ {
+ return match ($severity) {
+ LOG_EMERG, LOG_ALERT, LOG_CRIT => Breadcrumb::LEVEL_FATAL,
+ LOG_ERR => Breadcrumb::LEVEL_ERROR,
+ LOG_WARNING => Breadcrumb::LEVEL_WARNING,
+ LOG_NOTICE, LOG_INFO => Breadcrumb::LEVEL_INFO,
+ default => Breadcrumb::LEVEL_DEBUG,
+ };
+ }
- $sentryClient->captureMessage($message, $sentrySeverity, ['Additional Data' => $additionalData]);
- }
- parent::append($message, $severity, $additionalData, $packageKey, $className, $methodName);
- } catch (\Throwable $throwable) {
- echo sprintf('SentryFileBackend: %s (%s)', $throwable->getMessage(), $throwable->getCode());
- } finally {
- $this->capturingMessage = false;
+ private function getBreadcrumbType(int $severity): string
+ {
+ if ($severity >= LOG_ERR) {
+ return Breadcrumb::TYPE_ERROR;
}
+
+ return Breadcrumb::TYPE_DEFAULT;
}
}
diff --git a/Classes/Package.php b/Classes/Package.php
new file mode 100644
index 0000000..ab84ffd
--- /dev/null
+++ b/Classes/Package.php
@@ -0,0 +1,24 @@
+getSignalSlotDispatcher();
+
+ $dispatcher->connect(Sequence::class, 'afterInvokeStep', function ($step) {
+ if ($step->getIdentifier() === 'neos.flow:objectmanagement:runtime') {
+ // instantiate client to set up Sentry and register error handler early
+ /** @noinspection PhpExpressionResultUnusedInspection */
+ new SentryClient();
+ }
+ });
+ }
+}
diff --git a/Classes/SentryClient.php b/Classes/SentryClient.php
index 135a54b..55eea0a 100644
--- a/Classes/SentryClient.php
+++ b/Classes/SentryClient.php
@@ -17,8 +17,6 @@
use Flownative\Sentry\Context\UserContextServiceInterface;
use Flownative\Sentry\Context\WithExtraDataInterface;
use Flownative\Sentry\Log\CaptureResult;
-use GuzzleHttp\Psr7\ServerRequest;
-use Jenssegers\Agent\Agent;
use Neos\Flow\Annotations as Flow;
use Neos\Flow\Core\Bootstrap;
use Neos\Flow\Error\WithReferenceCodeInterface;
@@ -31,7 +29,6 @@
use Neos\Flow\Utility\Environment;
use Neos\Utility\Arrays;
use Psr\Log\LoggerInterface;
-use Psr\Log\LogLevel;
use Sentry\Event;
use Sentry\EventHint;
use Sentry\EventId;
@@ -93,6 +90,10 @@ public function injectSettings(array $settings): void
public function initializeObject(): void
{
+ if (empty($this->dsn)) {
+ return;
+ }
+
$representationSerializer = new RepresentationSerializer(
new Options([])
);
@@ -102,10 +103,6 @@ public function initializeObject(): void
$representationSerializer
);
- if (empty($this->dsn)) {
- return;
- }
-
\Sentry\init([
'dsn' => $this->dsn,
'environment' => $this->environment,
@@ -116,10 +113,9 @@ public function initializeObject(): void
FLOW_PATH_ROOT . '/Packages/Framework/Neos.Flow/Classes/Aop/',
FLOW_PATH_ROOT . '/Packages/Framework/Neos.Flow/Classes/Error/',
FLOW_PATH_ROOT . '/Packages/Framework/Neos.Flow/Classes/Log/',
- FLOW_PATH_ROOT . '/Packages/Libraries/neos/flow-log/'
+ FLOW_PATH_ROOT . '/Packages/Libraries/neos/flow-log/',
],
- 'default_integrations' => false,
- 'attach_stacktrace' => true
+ 'attach_stacktrace' => true,
]);
$client = SentrySdk::getCurrentHub()->getClient();
@@ -136,38 +132,18 @@ private function setTags(): void
try {
$flowPackage = $this->packageManager->getPackage('Neos.Flow');
$flowVersion = $flowPackage->getInstalledVersion();
- } catch (UnknownPackageException $e) {
+ } catch (UnknownPackageException) {
}
}
if (empty($flowVersion)) {
$flowVersion = FLOW_VERSION_BRANCH;
}
- $currentSession = null;
- if ($this->sessionManager) {
- $currentSession = $this->sessionManager->getCurrentSession();
- }
+ $currentSession = $this->sessionManager?->getCurrentSession();
SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope) use ($flowVersion, $currentSession): void {
$scope->setTag('flow_version', $flowVersion);
$scope->setTag('flow_context', (string)Bootstrap::$staticObjectManager->get(Environment::class)->getContext());
- $scope->setTag('php_version', PHP_VERSION);
-
- if (PHP_SAPI !== 'cli') {
- $scope->setTag('uri',
- (string)ServerRequest::fromGlobals()->getUri());
-
- $agent = new Agent();
- $scope->setContext('client_os', [
- 'name' => $agent->platform(),
- 'version' => $agent->version($agent->platform())
- ]);
-
- $scope->setContext('client_browser', [
- 'name' => $agent->browser(),
- 'version' => $agent->version($agent->browser())
- ]);
- }
if ($currentSession instanceof Session && $currentSession->isStarted()) {
$scope->setTag('flow_session_sha1', sha1($currentSession->getId()));
@@ -188,13 +164,18 @@ public function captureThrowable(Throwable $throwable, array $extraData = [], ar
if (empty($this->dsn)) {
return new CaptureResult(
false,
- 'Failed capturing message, because no Sentry DSN was set. Please check your settings.',
+ 'Failed capturing throwable, because no Sentry DSN was set. Please check your settings.',
''
);
}
- $message = '';
- $sentryEventId = '';
+ if ($this->excludeException($throwable)) {
+ return new CaptureResult(
+ true,
+ 'Skipped capturing throwable, it is in excludeExceptionTypes',
+ ''
+ );
+ }
if ($throwable instanceof WithReferenceCodeInterface) {
$extraData['Reference Code'] = $throwable->getReferenceCode();
@@ -211,58 +192,47 @@ public function captureThrowable(Throwable $throwable, array $extraData = [], ar
$tags['exception_code'] = (string)$throwable->getCode();
- $captureException = (!in_array(get_class($throwable), $this->excludeExceptionTypes, true));
- if ($captureException) {
- $this->configureScope($extraData, $tags);
- if ($throwable instanceof Exception && $throwable->getStatusCode() === 404) {
- SentrySdk::getCurrentHub()->configureScope(static function (Scope $scope): void {
- $scope->setLevel(Severity::warning());
- });
- }
- $event = Event::createEvent();
- $this->addThrowableToEvent($throwable, $event);
- $sentryEventId = SentrySdk::getCurrentHub()->captureEvent($event);
- } else {
- $message = 'ignored';
- }
+ $this->setTags();
+ $this->configureScope($extraData, $tags);
+ $event = Event::createEvent();
+ $this->addThrowableToEvent($throwable, $event);
+ $sentryEventId = SentrySdk::getCurrentHub()->captureEvent($event);
+
return new CaptureResult(
- true,
- $message,
+ true,
+ '',
(string)$sentryEventId
);
}
- public function captureMessage(string $message, Severity $severity, array $extraData = [], array $tags = []): ?EventId
+ public function captureMessage(string $message, Severity $severity, array $extraData = [], array $tags = []): CaptureResult
{
if (empty($this->dsn)) {
- if ($this->logger) {
- $this->logger->warning('Sentry: Failed capturing message, because no Sentry DSN was set. Please check your settings.');
- }
- return null;
- }
-
- if (preg_match('/Sentry: [0-9a-f]{32}/', $message) === 1) {
- return null;
+ return new CaptureResult(
+ false,
+ 'Failed capturing message, because no Sentry DSN was set. Please check your settings.',
+ ''
+ );
}
+ $this->setTags();
$this->configureScope($extraData, $tags);
$eventHint = EventHint::fromArray([
- 'stacktrace' => $this->prepareStacktrace()
+ 'stacktrace' => $this->prepareStacktrace(),
]);
- $sentryEventId = \Sentry\captureMessage($message, $severity, $eventHint);
-
- if ($this->logger) {
- $this->logger->log(
- (string)$severity,
- sprintf(
- '%s (Sentry: %s)',
- $message,
- $sentryEventId
- )
- );
- }
+ $sentryEventId = SentrySdk::getCurrentHub()->captureMessage($message, $severity, $eventHint);
+
+ return new CaptureResult(
+ true,
+ $message,
+ (string)$sentryEventId
+ );
+ }
- return $sentryEventId;
+ private function excludeException(\Throwable $throwable): bool
+ {
+ $excludedExceptions = array_keys(array_filter($this->excludeExceptionTypes));
+ return in_array(get_class($throwable), $excludedExceptions, true);
}
private function configureScope(array $extraData, array $tags): void
@@ -282,7 +252,6 @@ private function configureScope(array $extraData, array $tags): void
$scope->setTag($tagKey, $tagValue);
}
$scope->setUser($userContext->toArray());
- $scope->setLevel(null);
});
}
@@ -326,7 +295,7 @@ private function prepareStacktrace(\Throwable $throwable = null): Stacktrace
$frame->getRawFunctionName(),
$frame->getAbsoluteFilePath(),
$frame->getVars(),
- strpos($classPathAndFilename, 'Packages/Framework/') === false
+ !str_contains($classPathAndFilename, 'Packages/Framework/')
);
}
return new Stacktrace($frames);
diff --git a/Classes/Test/JsonSerializableTestArgument.php b/Classes/Test/JsonSerializableTestArgument.php
index 72cdcec..ad26c11 100644
--- a/Classes/Test/JsonSerializableTestArgument.php
+++ b/Classes/Test/JsonSerializableTestArgument.php
@@ -22,7 +22,7 @@ public function __construct(int $value)
$this->value = $value;
}
- public function jsonSerialize()
+ public function jsonSerialize(): int
{
return $this->value;
}
diff --git a/Configuration/Settings.yaml b/Configuration/Settings.yaml
index 24b4687..f9b7559 100644
--- a/Configuration/Settings.yaml
+++ b/Configuration/Settings.yaml
@@ -5,13 +5,21 @@ Flownative:
release: "%env:SENTRY_RELEASE%"
sampleRate: 1.0
capture:
- excludeExceptionTypes: []
+ excludeExceptionTypes:
+ 'Neos\Flow\Mvc\Controller\Exception\InvalidControllerException': true
+ 'Neos\Flow\Mvc\Exception\NoMatchingRouteException': true
+ 'Neos\Flow\Mvc\Exception\NoSuchActionException': true
+ 'Neos\Flow\Mvc\Exception\NoSuchControllerException': true
+ 'Neos\Flow\Property\Exception\TargetNotFoundException': true
Neos:
Flow:
log:
- systemLogger:
- backend: Flownative\Sentry\Log\SentryFileBackend
+ psr3:
+ Neos\Flow\Log\PsrLoggerFactory:
+ systemLogger:
+ default:
+ class: 'Flownative\Sentry\Log\SentryFileBackend'
throwables:
storageClass: 'Flownative\Sentry\Log\SentryStorage'
optionsByImplementation:
diff --git a/README.md b/README.md
index f8b0402..02ceb0b 100644
--- a/README.md
+++ b/README.md
@@ -9,10 +9,13 @@ reporting of errors to [Sentry](https://www.sentry.io)
## Key Features
-This package makes sure that exceptions and errors logged by the Flow
-framework also end up in Sentry. This client takes some extra care to
-clean up paths and filenames of stacktraces so you get good overview
-while looking at an issue in the Sentry UI.
+This package makes sure that throwables and exceptions logged in a Flow
+application also end up in Sentry. This is done by implementing Flow's
+`ThrowableStorageInterface` and configuring the default implementation.
+
+This packages takes some extra care to clean up paths and filenames of
+stacktraces so you get a good overview while looking at an issue in the
+Sentry UI.
## Installation
@@ -29,7 +32,13 @@ $ composer require flownative/sentry
You need to at least specify a DSN to be used as a logging target. Apart
from that, you can configure the Sentry environment and release. These
options can either be set in the Flow settings or, more conveniently, by
-setting the respective environment variables.
+setting the respective environment variables:
+
+- `SENTRY_DSN`
+- `SENTRY_ENVIRONMENT`
+- `SENTRY_RELEASE`
+
+The package uses these environment variables by default in the settings:
```yaml
Flownative:
@@ -51,34 +60,60 @@ The default is 1 – 100% percent of all errors are sampled.
Throwables (that includes exceptions and runtime errors) are logged as
Sentry events. You may specify a list of exceptions which should not be
-recorded. If such an exception is thrown, it will only be logged as a
-"notice".
+recorded.
```yaml
Flownative:
Sentry:
capture:
excludeExceptionTypes:
- - 'Neos\Flow\Mvc\Controller\Exception\InvalidControllerException'
+ 'Neos\Flow\Mvc\Controller\Exception\InvalidControllerException': true
```
-If an ignored exception is handled by this Sentry client, it is logged
-similar to the following message:
+By default all Flow exceptions with a status code of 404 are ignored. in case
+you want to see those in Sentry, you can include them case-by-case like so:
-```
-… NOTICE Exception 12345: The exception message (Ref: 202004161706040c28ae | Sentry: ignored)
+```yaml
+Flownative:
+ Sentry:
+ capture:
+ excludeExceptionTypes:
+ 'Neos\Flow\Mvc\Controller\Exception\InvalidControllerException': false
+ 'Neos\Flow\Mvc\Exception\NoSuchControllerException': false
```
## Additional Data
-Exceptions declared in an application can optionally implement
-`WithAdditionalDataInterface` provided by this package. If they do, the
-array returned by `getAdditionalData()` will be visible in the "additional
+Exceptions declared in an application can optionally implement
+`WithAdditionalDataInterface` provided by this package. If they do, the
+array returned by `getAdditionalData()` will be visible in the "additional
data" section in Sentry.
-Note that the array must only contain values of simple types, such as
+Note that the array must only contain values of simple types, such as
strings, booleans or integers.
+## Logging integration
+
+### Breadcrumb handler
+
+This package configures a logging backend to add messages as breadcrumbs to
+be sent to Sentry when an exception happens. This provides more information
+on what happened before an exception.
+
+For more information on breadcrumbs see the Sentry documentation at
+https://docs.sentry.io/platforms/php/enriching-events/breadcrumbs/
+
+### Monolog
+
+In case you want to store all log messages in Sentry, one way is to configure
+Flow to use monolog for logging and then add the `Sentry\Monolog\Handler` to
+the setup.
+
+Keep in mind that the breadcrumb handler provided by this package might be
+disabled when doing this, depending on your configuration. Sentry provides
+a monolog integration for that purpose, see `Sentry\Monolog\BreadcrumbHandler`
+and https://docs.sentry.io/platforms/php/integrations/monolog/.
+
## Testing the Client
This package provides a command controller which allows you to log a
@@ -90,35 +125,39 @@ Run the following command in your terminal to test your configuration:
./flow sentry:test
Testing Sentry setup …
Using the following configuration:
-+-------------+------------------------------------------------------------+
-| Option | Value |
-+-------------+------------------------------------------------------------+
++-------------+----------------------------------------------------------+
+| Option | Value |
++-------------+----------------------------------------------------------+
| DSN | https://abc123456789abcdef1234567890ab@sentry.io/1234567 |
-| Environment | development |
-| Release | dev |
-| Server Name | test_container |
-| Sample Rate | 1 |
-+-------------+------------------------------------------------------------+
-An informational message was sent to Sentry Event ID: #587abc123457abcd8f873b4212345678
+| Environment | development |
+| Release | dev |
+| Server Name | test_container |
+| Sample Rate | 1 |
++-------------+----------------------------------------------------------+
This command will now throw an exception for testing purposes.
-Test exception in SentryCommandController
+Test exception in ThrowingClass
- Type: Flownative\Sentry\Exception\SentryClientTestException
- Code: 1614759519
+ Type: Flownative\Sentry\Test\SentryClientTestException
+ Code: 1662712736
File: Data/Temporary/Development/SubContextBeach/SubContextInstance/Cache/Code/Fl
- ow_Object_Classes/Flownative_Sentry_Command_SentryCommandController.php
- Line: 79
+ ow_Object_Classes/Flownative_Sentry_Test_ThrowingClass.php
+ Line: 41
Nested exception:
-Test "previous" exception thrown by the SentryCommandController
+Test "previous" exception in ThrowingClass
Type: RuntimeException
- Code: 1614759554
+ Code: 1662712735
File: Data/Temporary/Development/SubContextBeach/SubContextInstance/Cache/Code/Fl
- ow_Object_Classes/Flownative_Sentry_Command_SentryCommandController.php
- Line: 78
+ ow_Object_Classes/Flownative_Sentry_Test_ThrowingClass.php
+ Line: 40
-Open Data/Logs/Exceptions/2021030308325919ecbf.txt for a full stack trace.
+Open Data/Logs/Exceptions/202411181211403b652e.txt for a full stack trace.
```
+
+There are two more test modes for message capturing and error handling:
+
+- `./flow sentry:test --mode message`
+- `./flow sentry:test --mode error`
diff --git a/composer.json b/composer.json
index 2527c06..62efd54 100644
--- a/composer.json
+++ b/composer.json
@@ -17,8 +17,7 @@
"ext-json": "*",
"php": "^8.1",
"neos/flow": "^8.0 || ^9.0 || @dev",
- "sentry/sentry": "^4.0",
- "jenssegers/agent": "^2.6"
+ "sentry/sentry": "^4.0"
},
"autoload": {
"psr-4": {