Skip to content

Add support for subscription to certain life cycle events #47

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
May 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 132 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ sentry:
dsn: "https://public:[email protected]/1"
```


## Configuration

The following can be configured via ``app/config/config.yml``:
Expand Down Expand Up @@ -131,6 +130,138 @@ sentry:
error_types: E_ALL & ~E_DEPRECATED & ~E_NOTICE
```


## Customization

It is possible to customize the configuration of the user context, as well
as modify the client immediately before an exception is captured by wiring
up an event subscriber to the events that are emitted by the default
configured `ExceptionListener` (alternatively, you can also just defined
your own custom exception listener).

### Create a Custom ExceptionListener

You can always replace the default `ExceptionListener` with your own custom
listener. To do this, assign a different class to the `exception_listener`
property in your Sentry configuration, e.g.:

```yaml
sentry:
exception_listener: AppBundle\EventListener\MySentryExceptionListener
```

... and then define the custom `ExceptionListener`, e.g.:

```php
// src/AppBundle/EventSubscriber/MySentryEventListener.php
namespace AppBundle\EventSubscriber;

use Sentry\SentryBundle\EventListener\SentryExceptionListenerInterface;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class MySentryExceptionListener implements SentryExceptionListenerInterface
{
// ...

public function __construct(TokenStorageInterface $tokenStorage = null, AuthorizationCheckerInterface $authorizationChecker = null, \Raven_Client $client = null, array $skipCapture, EventDispatcherInterface $dispatcher = null)
{
// ...
}

public function onKernelRequest(GetResponseEvent $event)
{
// ...
}

public function onKernelException(GetResponseForExceptionEvent $event)
{
// ...
}

public function onConsoleException(ConsoleExceptionEvent $event)
{
// ...
}
}
```

As a side note, while the above demonstrates a custom exception listener that
does not extend anything you could choose to extend the default
`ExceptionListener` and only override the functionality that you want to.

### Add an EventSubscriber for Sentry Events

Create a new class, e.g. `MySentryEventSubscriber`:

```php
// src/AppBundle/EventSubscriber/MySentryEventListener.php
namespace AppBundle\EventSubscriber;

use Sentry\SentryBundle\Event\SentryUserContextEvent;
use Sentry\SentryBundle\SentrySymfonyEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class MySentryEventSubscriber implements EventSubscriberInterface
{
/** @var \Raven_Client */
protected $client;

public function __construct(\Raven_Client $client)
{
$this->client = $client;
}

public static function getSubscribedEvents()
{
// return the subscribed events, their methods and priorities
return array(
SentrySymfonyEvents::PRE_CAPTURE => 'onPreCapture',
SentrySymfonyEvents::SET_USER_CONTEXT => 'onSetUserContext'
);
}

public function onSetUserContext(SentryUserContextEvent $event)
{
// ...
}

public function onPreCapture(Event $event)
{
if ($event instanceof GetResponseForExceptionEvent) {
// ...
}
elseif ($event instanceof ConsoleExceptionEvent) {
// ...
}
}
}
```

In the example above, if you subscribe to the `PRE_CAPTURE` event you may
get an event object that caters more toward a response to a web request (e.g.
`GetResponseForExceptionEvent`) or one for actions taken at the command line
(e.g. `ConsoleExceptionEvent`). Depending on what and how the code was
invoked, and whether or not you need to distinguish between these events
during pre-capture, it might be best to test for the type of the event (as is
demonstrated above) before you do any relevant processing of the object.

To configure the above add the following configuration to your services
definitions:

```yaml
app.my_sentry_event_subscriber:
class: AppBundle\EventSubscriber\MySentryEventSubscriber
arguments:
- '@sentry.client'
tags:
- { name: kernel.event_subscriber }
```

[Last stable image]: https://poser.pugx.org/sentry/sentry-symfony/version.svg
[Last unstable image]: https://poser.pugx.org/sentry/sentry-symfony/v/unstable.svg
[Master build image]: https://travis-ci.org/getsentry/sentry-symfony.svg?branch=master
Expand Down
18 changes: 18 additions & 0 deletions src/Sentry/SentryBundle/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ public function getConfigTreeBuilder()
->end()
->scalarNode('exception_listener')
->defaultValue('Sentry\SentryBundle\EventListener\ExceptionListener')
->validate()
->ifTrue($this->getExceptionListenerInvalidationClosure())
->thenInvalid('The "sentry.exception_listener" parameter should be a FQCN of a class implementing the SentryExceptionListenerInterface interface')
->end()
->end()
->arrayNode('skip_capture')
->treatNullLike(array())
Expand Down Expand Up @@ -72,4 +76,18 @@ public function getConfigTreeBuilder()

return $treeBuilder;
}

/**
* @return \Closure
*/
private function getExceptionListenerInvalidationClosure()
{
return function ($value) {
$implements = class_implements($value);
if ($implements === false) {
return true;
}
return !in_array('Sentry\SentryBundle\EventListener\SentryExceptionListenerInterface', $implements, true);
};
}
}
21 changes: 21 additions & 0 deletions src/Sentry/SentryBundle/Event/SentryUserContextEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Sentry\SentryBundle\Event;

use Symfony\Component\EventDispatcher\Event;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;

class SentryUserContextEvent extends Event
{
private $authenticationToken;

public function __construct(TokenInterface $authenticationToken)
{
$this->authenticationToken = $authenticationToken;
}

public function getAuthenticationToken()
{
return $this->authenticationToken;
}
}
26 changes: 20 additions & 6 deletions src/Sentry/SentryBundle/EventListener/ExceptionListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,24 @@
namespace Sentry\SentryBundle\EventListener;

use Sentry\SentryBundle;
use Sentry\SentryBundle\Event\SentryUserContextEvent;
use Sentry\SentryBundle\SentrySymfonyClient;
use Sentry\SentryBundle\SentrySymfonyEvents;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
use Symfony\Component\Security\Core\User\UserInterface;
use Symfony\Component\Console\Event\ConsoleExceptionEvent;

/**
* Class ExceptionListener
* @package Sentry\SentryBundle\EventListener
*/
class ExceptionListener
class ExceptionListener implements SentryExceptionListenerInterface
{
/** @var TokenStorageInterface */
private $tokenStorage;
Expand All @@ -28,6 +31,9 @@ class ExceptionListener
/** @var \Raven_Client */
protected $client;

/** @var EventDispatcherInterface */
protected $eventDispatcher;

/** @var string[] */
protected $skipCapture;

Expand All @@ -37,19 +43,22 @@ class ExceptionListener
* @param AuthorizationCheckerInterface $authorizationChecker
* @param \Raven_Client $client
* @param array $skipCapture
* @param EventDispatcherInterface $dispatcher
*/
public function __construct(
TokenStorageInterface $tokenStorage = null,
AuthorizationCheckerInterface $authorizationChecker = null,
\Raven_Client $client = null,
array $skipCapture
array $skipCapture,
EventDispatcherInterface $dispatcher
) {
if (!$client) {
$client = new SentrySymfonyClient();
}

$this->tokenStorage = $tokenStorage;
$this->authorizationChecker = $authorizationChecker;
$this->dispatcher = $dispatcher;
$this->client = $client;
$this->skipCapture = $skipCapture;
}
Expand Down Expand Up @@ -81,6 +90,9 @@ public function onKernelRequest(GetResponseEvent $event)

if (null !== $token && $this->authorizationChecker->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED)) {
$this->setUserValue($token->getUser());

$contextEvent = new SentryUserContextEvent($token);
$this->dispatcher->dispatch(SentrySymfonyEvents::SET_USER_CONTEXT, $contextEvent);
}
}

Expand All @@ -90,11 +102,12 @@ public function onKernelRequest(GetResponseEvent $event)
public function onKernelException(GetResponseForExceptionEvent $event)
{
$exception = $event->getException();

if ($this->shouldExceptionCaptureBeSkipped($exception)) {
return;
}

$this->dispatcher->dispatch(SentrySymfonyEvents::PRE_CAPTURE, $event);
$this->client->captureException($exception);
}

Expand All @@ -105,7 +118,7 @@ public function onConsoleException(ConsoleExceptionEvent $event)
{
$command = $event->getCommand();
$exception = $event->getException();

if ($this->shouldExceptionCaptureBeSkipped($exception)) {
return;
}
Expand All @@ -117,9 +130,10 @@ public function onConsoleException(ConsoleExceptionEvent $event)
),
);

$this->dispatcher->dispatch(SentrySymfonyEvents::PRE_CAPTURE, $event);
$this->client->captureException($exception, $data);
}

private function shouldExceptionCaptureBeSkipped(\Exception $exception)
{
foreach ($this->skipCapture as $className) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

namespace Sentry\SentryBundle\EventListener;

use Symfony\Component\Console\Event\ConsoleExceptionEvent;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;

interface SentryExceptionListenerInterface
{

/**
* Used to capture information from the request before any possible error
* event is encountered by listening on core.request.
*
* Most commonly used for assigning the username to the security context
* used by Sentry for each request.
*
* @param GetResponseEvent $event
*/
public function onKernelRequest(GetResponseEvent $event);

/**
* When an exception occurs as part of a web request, this method will be
* triggered for capturing the error.
*
* @param GetResponseForExceptionEvent $event
*/
public function onKernelException(GetResponseForExceptionEvent $event);

/**
* When an exception occurs on the command line, this method will be
* triggered for capturing the error.
*
* @param ConsoleExceptionEvent $event
*/
public function onConsoleException(ConsoleExceptionEvent $event);
}
1 change: 1 addition & 0 deletions src/Sentry/SentryBundle/Resources/config/services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ services:
- '@?security.authorization_checker'
- '@sentry.client'
- '%sentry.skip_capture%'
- '@event_dispatcher'
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are adding a mandatory reference to service but permit service to be null in class itself. This is contradictory because Symfony will error out if event_dispatcher is not present in this case. Either make this service optional or disallow null in the constructor.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I already commented on that up above. I agree, but the service should be taken for granted since we require symfony/symfony, so I would prefer no nullable in the class.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry missed it. Yes, I reckon non-nullable parameter in the constructor would be the correct way to go then.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I made the recommended change to the code (removing the null checks since we're requiring the event dispatcher). I guess I wasn't really thinking, and I also didn't notice the @? prefix for services. I don't know if I've ever used that before, but I do most of my service definitions in XML in my own projects, so it is possible I just never knew about that with the YAML. I'm assuming that the @? allows for a non-mandatory reference and null values to be passed into the function/constructor/whatever?

Any way, the changes can be found in this commit: d921cc5

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, the @? is used to silence failures for missing references and passing a null instead.

tags:
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
Expand Down
31 changes: 31 additions & 0 deletions src/Sentry/SentryBundle/SentrySymfonyEvents.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php

namespace Sentry\SentryBundle;

/**
* Event names that are triggered to allow for further modification of the
* Raven client during error processing.
*/
class SentrySymfonyEvents
{

/**
* The PRE_CAPTURE event is triggered just before the client captures the
* exception.
*
* @Event("Symfony\Component\EventDispatcher\Event")
*
* @var string
*/
const PRE_CAPTURE = 'sentry.pre_capture';

/**
* The SET_USER_CONTEXT event is triggered on requests where the user is
* authenticated and has authorization.
*
* @Event("Sentry\SentryBundle\Event\SentryUserContextEvent")
*
* @var string
*/
const SET_USER_CONTEXT = 'sentry.set_user_context';
}
Loading