Skip to content

Commit 0d2a008

Browse files
authored
Merge pull request #47 from swquinn/add-support-for-event-subscribers
Add support for subscription to certain life cycle events
2 parents dda5e60 + 78ebf11 commit 0d2a008

File tree

11 files changed

+407
-9
lines changed

11 files changed

+407
-9
lines changed

README.md

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ sentry:
6262
dsn: "https://public:secret@sentry.example.com/1"
6363
```
6464
65-
6665
## Configuration
6766
6867
The following can be configured via ``app/config/config.yml``:
@@ -131,6 +130,138 @@ sentry:
131130
error_types: E_ALL & ~E_DEPRECATED & ~E_NOTICE
132131
```
133132
133+
134+
## Customization
135+
136+
It is possible to customize the configuration of the user context, as well
137+
as modify the client immediately before an exception is captured by wiring
138+
up an event subscriber to the events that are emitted by the default
139+
configured `ExceptionListener` (alternatively, you can also just defined
140+
your own custom exception listener).
141+
142+
### Create a Custom ExceptionListener
143+
144+
You can always replace the default `ExceptionListener` with your own custom
145+
listener. To do this, assign a different class to the `exception_listener`
146+
property in your Sentry configuration, e.g.:
147+
148+
```yaml
149+
sentry:
150+
exception_listener: AppBundle\EventListener\MySentryExceptionListener
151+
```
152+
153+
... and then define the custom `ExceptionListener`, e.g.:
154+
155+
```php
156+
// src/AppBundle/EventSubscriber/MySentryEventListener.php
157+
namespace AppBundle\EventSubscriber;
158+
159+
use Sentry\SentryBundle\EventListener\SentryExceptionListenerInterface;
160+
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
161+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
162+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
163+
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
164+
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
165+
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
166+
167+
class MySentryExceptionListener implements SentryExceptionListenerInterface
168+
{
169+
// ...
170+
171+
public function __construct(TokenStorageInterface $tokenStorage = null, AuthorizationCheckerInterface $authorizationChecker = null, \Raven_Client $client = null, array $skipCapture, EventDispatcherInterface $dispatcher = null)
172+
{
173+
// ...
174+
}
175+
176+
public function onKernelRequest(GetResponseEvent $event)
177+
{
178+
// ...
179+
}
180+
181+
public function onKernelException(GetResponseForExceptionEvent $event)
182+
{
183+
// ...
184+
}
185+
186+
public function onConsoleException(ConsoleExceptionEvent $event)
187+
{
188+
// ...
189+
}
190+
}
191+
```
192+
193+
As a side note, while the above demonstrates a custom exception listener that
194+
does not extend anything you could choose to extend the default
195+
`ExceptionListener` and only override the functionality that you want to.
196+
197+
### Add an EventSubscriber for Sentry Events
198+
199+
Create a new class, e.g. `MySentryEventSubscriber`:
200+
201+
```php
202+
// src/AppBundle/EventSubscriber/MySentryEventListener.php
203+
namespace AppBundle\EventSubscriber;
204+
205+
use Sentry\SentryBundle\Event\SentryUserContextEvent;
206+
use Sentry\SentryBundle\SentrySymfonyEvents;
207+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
208+
209+
class MySentryEventSubscriber implements EventSubscriberInterface
210+
{
211+
/** @var \Raven_Client */
212+
protected $client;
213+
214+
public function __construct(\Raven_Client $client)
215+
{
216+
$this->client = $client;
217+
}
218+
219+
public static function getSubscribedEvents()
220+
{
221+
// return the subscribed events, their methods and priorities
222+
return array(
223+
SentrySymfonyEvents::PRE_CAPTURE => 'onPreCapture',
224+
SentrySymfonyEvents::SET_USER_CONTEXT => 'onSetUserContext'
225+
);
226+
}
227+
228+
public function onSetUserContext(SentryUserContextEvent $event)
229+
{
230+
// ...
231+
}
232+
233+
public function onPreCapture(Event $event)
234+
{
235+
if ($event instanceof GetResponseForExceptionEvent) {
236+
// ...
237+
}
238+
elseif ($event instanceof ConsoleExceptionEvent) {
239+
// ...
240+
}
241+
}
242+
}
243+
```
244+
245+
In the example above, if you subscribe to the `PRE_CAPTURE` event you may
246+
get an event object that caters more toward a response to a web request (e.g.
247+
`GetResponseForExceptionEvent`) or one for actions taken at the command line
248+
(e.g. `ConsoleExceptionEvent`). Depending on what and how the code was
249+
invoked, and whether or not you need to distinguish between these events
250+
during pre-capture, it might be best to test for the type of the event (as is
251+
demonstrated above) before you do any relevant processing of the object.
252+
253+
To configure the above add the following configuration to your services
254+
definitions:
255+
256+
```yaml
257+
app.my_sentry_event_subscriber:
258+
class: AppBundle\EventSubscriber\MySentryEventSubscriber
259+
arguments:
260+
- '@sentry.client'
261+
tags:
262+
- { name: kernel.event_subscriber }
263+
```
264+
134265
[Last stable image]: https://poser.pugx.org/sentry/sentry-symfony/version.svg
135266
[Last unstable image]: https://poser.pugx.org/sentry/sentry-symfony/v/unstable.svg
136267
[Master build image]: https://travis-ci.org/getsentry/sentry-symfony.svg?branch=master

src/Sentry/SentryBundle/DependencyInjection/Configuration.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ public function getConfigTreeBuilder()
4444
->end()
4545
->scalarNode('exception_listener')
4646
->defaultValue('Sentry\SentryBundle\EventListener\ExceptionListener')
47+
->validate()
48+
->ifTrue($this->getExceptionListenerInvalidationClosure())
49+
->thenInvalid('The "sentry.exception_listener" parameter should be a FQCN of a class implementing the SentryExceptionListenerInterface interface')
50+
->end()
4751
->end()
4852
->arrayNode('skip_capture')
4953
->treatNullLike(array())
@@ -72,4 +76,18 @@ public function getConfigTreeBuilder()
7276

7377
return $treeBuilder;
7478
}
79+
80+
/**
81+
* @return \Closure
82+
*/
83+
private function getExceptionListenerInvalidationClosure()
84+
{
85+
return function ($value) {
86+
$implements = class_implements($value);
87+
if ($implements === false) {
88+
return true;
89+
}
90+
return !in_array('Sentry\SentryBundle\EventListener\SentryExceptionListenerInterface', $implements, true);
91+
};
92+
}
7593
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Sentry\SentryBundle\Event;
4+
5+
use Symfony\Component\EventDispatcher\Event;
6+
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
7+
8+
class SentryUserContextEvent extends Event
9+
{
10+
private $authenticationToken;
11+
12+
public function __construct(TokenInterface $authenticationToken)
13+
{
14+
$this->authenticationToken = $authenticationToken;
15+
}
16+
17+
public function getAuthenticationToken()
18+
{
19+
return $this->authenticationToken;
20+
}
21+
}

src/Sentry/SentryBundle/EventListener/ExceptionListener.php

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,24 @@
33
namespace Sentry\SentryBundle\EventListener;
44

55
use Sentry\SentryBundle;
6+
use Sentry\SentryBundle\Event\SentryUserContextEvent;
67
use Sentry\SentryBundle\SentrySymfonyClient;
8+
use Sentry\SentryBundle\SentrySymfonyEvents;
9+
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
10+
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
711
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
812
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
913
use Symfony\Component\HttpKernel\HttpKernelInterface;
1014
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
1115
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
1216
use Symfony\Component\Security\Core\Authorization\Voter\AuthenticatedVoter;
1317
use Symfony\Component\Security\Core\User\UserInterface;
14-
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
1518

1619
/**
1720
* Class ExceptionListener
1821
* @package Sentry\SentryBundle\EventListener
1922
*/
20-
class ExceptionListener
23+
class ExceptionListener implements SentryExceptionListenerInterface
2124
{
2225
/** @var TokenStorageInterface */
2326
private $tokenStorage;
@@ -28,6 +31,9 @@ class ExceptionListener
2831
/** @var \Raven_Client */
2932
protected $client;
3033

34+
/** @var EventDispatcherInterface */
35+
protected $eventDispatcher;
36+
3137
/** @var string[] */
3238
protected $skipCapture;
3339

@@ -37,19 +43,22 @@ class ExceptionListener
3743
* @param AuthorizationCheckerInterface $authorizationChecker
3844
* @param \Raven_Client $client
3945
* @param array $skipCapture
46+
* @param EventDispatcherInterface $dispatcher
4047
*/
4148
public function __construct(
4249
TokenStorageInterface $tokenStorage = null,
4350
AuthorizationCheckerInterface $authorizationChecker = null,
4451
\Raven_Client $client = null,
45-
array $skipCapture
52+
array $skipCapture,
53+
EventDispatcherInterface $dispatcher
4654
) {
4755
if (!$client) {
4856
$client = new SentrySymfonyClient();
4957
}
5058

5159
$this->tokenStorage = $tokenStorage;
5260
$this->authorizationChecker = $authorizationChecker;
61+
$this->dispatcher = $dispatcher;
5362
$this->client = $client;
5463
$this->skipCapture = $skipCapture;
5564
}
@@ -81,6 +90,9 @@ public function onKernelRequest(GetResponseEvent $event)
8190

8291
if (null !== $token && $this->authorizationChecker->isGranted(AuthenticatedVoter::IS_AUTHENTICATED_REMEMBERED)) {
8392
$this->setUserValue($token->getUser());
93+
94+
$contextEvent = new SentryUserContextEvent($token);
95+
$this->dispatcher->dispatch(SentrySymfonyEvents::SET_USER_CONTEXT, $contextEvent);
8496
}
8597
}
8698

@@ -90,11 +102,12 @@ public function onKernelRequest(GetResponseEvent $event)
90102
public function onKernelException(GetResponseForExceptionEvent $event)
91103
{
92104
$exception = $event->getException();
93-
105+
94106
if ($this->shouldExceptionCaptureBeSkipped($exception)) {
95107
return;
96108
}
97109

110+
$this->dispatcher->dispatch(SentrySymfonyEvents::PRE_CAPTURE, $event);
98111
$this->client->captureException($exception);
99112
}
100113

@@ -105,7 +118,7 @@ public function onConsoleException(ConsoleExceptionEvent $event)
105118
{
106119
$command = $event->getCommand();
107120
$exception = $event->getException();
108-
121+
109122
if ($this->shouldExceptionCaptureBeSkipped($exception)) {
110123
return;
111124
}
@@ -117,9 +130,10 @@ public function onConsoleException(ConsoleExceptionEvent $event)
117130
),
118131
);
119132

133+
$this->dispatcher->dispatch(SentrySymfonyEvents::PRE_CAPTURE, $event);
120134
$this->client->captureException($exception, $data);
121135
}
122-
136+
123137
private function shouldExceptionCaptureBeSkipped(\Exception $exception)
124138
{
125139
foreach ($this->skipCapture as $className) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
<?php
2+
3+
namespace Sentry\SentryBundle\EventListener;
4+
5+
use Symfony\Component\Console\Event\ConsoleExceptionEvent;
6+
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
7+
use Symfony\Component\HttpKernel\Event\GetResponseForExceptionEvent;
8+
9+
interface SentryExceptionListenerInterface
10+
{
11+
12+
/**
13+
* Used to capture information from the request before any possible error
14+
* event is encountered by listening on core.request.
15+
*
16+
* Most commonly used for assigning the username to the security context
17+
* used by Sentry for each request.
18+
*
19+
* @param GetResponseEvent $event
20+
*/
21+
public function onKernelRequest(GetResponseEvent $event);
22+
23+
/**
24+
* When an exception occurs as part of a web request, this method will be
25+
* triggered for capturing the error.
26+
*
27+
* @param GetResponseForExceptionEvent $event
28+
*/
29+
public function onKernelException(GetResponseForExceptionEvent $event);
30+
31+
/**
32+
* When an exception occurs on the command line, this method will be
33+
* triggered for capturing the error.
34+
*
35+
* @param ConsoleExceptionEvent $event
36+
*/
37+
public function onConsoleException(ConsoleExceptionEvent $event);
38+
}

src/Sentry/SentryBundle/Resources/config/services.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ services:
1717
- '@?security.authorization_checker'
1818
- '@sentry.client'
1919
- '%sentry.skip_capture%'
20+
- '@event_dispatcher'
2021
tags:
2122
- { name: kernel.event_listener, event: kernel.request, method: onKernelRequest }
2223
- { name: kernel.event_listener, event: kernel.exception, method: onKernelException }
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
3+
namespace Sentry\SentryBundle;
4+
5+
/**
6+
* Event names that are triggered to allow for further modification of the
7+
* Raven client during error processing.
8+
*/
9+
class SentrySymfonyEvents
10+
{
11+
12+
/**
13+
* The PRE_CAPTURE event is triggered just before the client captures the
14+
* exception.
15+
*
16+
* @Event("Symfony\Component\EventDispatcher\Event")
17+
*
18+
* @var string
19+
*/
20+
const PRE_CAPTURE = 'sentry.pre_capture';
21+
22+
/**
23+
* The SET_USER_CONTEXT event is triggered on requests where the user is
24+
* authenticated and has authorization.
25+
*
26+
* @Event("Sentry\SentryBundle\Event\SentryUserContextEvent")
27+
*
28+
* @var string
29+
*/
30+
const SET_USER_CONTEXT = 'sentry.set_user_context';
31+
}

0 commit comments

Comments
 (0)