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

Conversation

swquinn
Copy link
Contributor

@swquinn swquinn commented Mar 2, 2017

This commit adds support to the sentry-symfony library for certain
"lifecycle" events that provide, possibly simpler, extension points for
some of the exception capturing process. Specifically two events:
SET_USER_CONTEXT and PRE_CAPTURE.

  • SET_USER_CONTEXT can be used to override the default user context
    that is captured in the ExceptionListener during onKernelRequest

  • PRE_CAPTURE can be used to add tags, or other contents, to the
    client based on the event and the exception pending capture.

I've also added a "Customization" section to the README to make clear
two options developers have to customize the Sentry context.

This commit adds support to the sentry-symfony library for certain
"lifecycle" events that provide, possibly simpler, extension points for
some of the exception capturing process. Specifically two events:
SET_USER_CONTEXT and PRE_CAPTURE.

  - SET_USER_CONTEXT can be used to override the default user context
    that is captured in the ExceptionListener during onKernelRequest

  - PRE_CAPTURE can be used to add tags, or other contents, to the
    client based on the event and the exception pending capture.

I've also added a "Customization" section to the README to make clear
two options developers have to customize the Sentry context.
@swquinn
Copy link
Contributor Author

swquinn commented Mar 2, 2017

In addition, this PR probably meets the request presented in #7 for giving projects a way to configure the user context beyond the default ExceptionListener (which could always just be overriden).

@swquinn swquinn mentioned this pull request Mar 2, 2017
@swquinn
Copy link
Contributor Author

swquinn commented Mar 3, 2017

Thinking more on this now, a few days after opening the PR, the only thing that I might want to change in the events is that PRE_CAPTURE may be too broad since it is triggered for both console and kernel exceptions which could make setting subscribers a little annoying. This could be broken out to PRE_CAPTURE_KERNEL and PRE_CAPTURE_CONSOLE to make writing event subscribers/listeners more friendly.

I'm not really sure how I feel about that. I like the simplicity of just a single PRE_CAPTURE event, but at the same time see its downside.

I guess, worse case scenario with a single PRE_CAPTURE event, a developer could just write:

class MySentryEventSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return array(
            // ...
            SentrySymfonyEvents::PRE_CAPTURE => 'onPreCapture',
        );
    }

    // ...

    public function onPreCapture(Event $event)
    {
        if ($event instanceof GetResponseForExceptionEvent) {
            $this->handlePreCaptureForKernelException($event);
        }
        elseif ($event instanceof ConsoleExceptionEvent) {
            $this->handlePreCaptureForConsoleException($event);
        }
    }
}

Thoughts? Feedback?

@swquinn
Copy link
Contributor Author

swquinn commented Apr 13, 2017

Any idea when this can be merged in?

@@ -41,6 +47,7 @@ class ExceptionListener
public function __construct(
TokenStorageInterface $tokenStorage = null,
AuthorizationCheckerInterface $authorizationChecker = null,
EventDispatcherInterface $dispatcher = null,
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't we take the dispatcher for granted? Why the = null?

Copy link
Contributor Author

@swquinn swquinn Apr 28, 2017

Choose a reason for hiding this comment

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

I'm having trouble remembering why I did this explicitly here. I think it may have been for compatibility cases where someone might have already overridden or defined their own exception listener definition around the ExceptionListener in this code base. I didn't necessarily want to break the signature of someone's code if it didn't explicitly include the $dispatcher.

Although, if you think we should take the presence of the dispatcher for granted, I'm all for removing the nullability of it and the null checks.

Copy link
Contributor Author

@swquinn swquinn Apr 28, 2017

Choose a reason for hiding this comment

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

Upon further inspection of the code, I think I may have also been following precedent since other dependencies (including the Raven client) have the = null. Then again, I guess you're assigning a client if the client is null so the argument of: "It seems like if we're talking about taking the dispatcher for granted, shouldn't we also take the Raven client for granted as well" actually falls over since there should always be a client.

As mentioned I'm happy to make a code change here, just let me know. :)

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree that I would change both arguments; also, the order of the arguments seems strange to me too! But I would wait for @dcramer approval before doing this change.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

You bring up a good point about the order--especially if I were to allow for the nullability of the argument--I probably should have tacked it on as the last argument if I were really doing this to make it as much of a non-breaking change as possible. Before I make further changes, I'll wait for @dcramer's input though. Thanks @Jean85!

Copy link
Member

Choose a reason for hiding this comment

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

i'm lacking a lot of context at this point (been a while since I've touched any of this, and I've never used Symfony). My biggest concerns are always breaking changes and compatibility with various versions of PHP/Symonfy. As long as this doens't cause any concern there, I'll defer ro you both.

Copy link
Contributor

Choose a reason for hiding this comment

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

Ok thanks @dcramer!
If BC is an issue, and we consider this class as part of the public API, we shouldn't change the order of the arguments, and add the new one as the last one. Maybe in the next major bump we can refactor this.

PS: I've sent you and email ;)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Will do! I'll refactor that sometime today and push up the changes. Thanks for your input guys!

@Jean85
Copy link
Contributor

Jean85 commented Apr 28, 2017

I would advise against the dual PRE_CAPTURE events, the piece of code that you posted is good to do that difference, maybe you can append it to the README's changes.

@swquinn
Copy link
Contributor Author

swquinn commented Apr 28, 2017

Sounds good. I'll add the code posted above to the README as a way of handling the difference in exception events. Thanks for the suggestion @Jean85 !

@swquinn
Copy link
Contributor Author

swquinn commented Apr 28, 2017

@Jean85 I added the snippet of code above for handling the difference between the GetResponseForExceptionEvent and ConsoleExceptionEvent with a bit of added context to the README.md, per your suggestion. Thanks!

Copy link
Member

@dcramer dcramer left a comment

Choose a reason for hiding this comment

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

In general this looks good to me.

README.md Outdated
use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInterface;
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;

class MySentryExceptionListener
Copy link
Member

Choose a reason for hiding this comment

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

would it make sense to have the default example extend the builtin?

Copy link
Contributor

Choose a reason for hiding this comment

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

I've given this a longer thought, and something popped up: we should enforce the presence of all the three methods (onKernelRequest, onKernelException, onConsoleException) in any substitute, otherwise the bundle will fail, due to the fact that service.yml wires the listener to all three.

I see two solutions:

  • remove the explicit binding of the methods from the YAML, and use the getSubscribedEvents() to wire the listener
  • create an SentryExceptionListenerInterface and make the injection of the bundle fail if that's not implemented by the %sentry.exception_listener% class

I would prefer the latter, because it's more guided for the end users: it would give and explicit and clear error when the listener is misconfigured, it requires explicit action to disable one of the three behaviors, and it hints toward extending the original class if only a small modification is required.

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 really like this proposal (as I indicated in my previous comment), I think I had considered using an interface before but had hesitated to avoid adding what I thought could have been seen as unnecessary code at the time of the original PR.

Regardless, I've made the first half of the changes here, in this commit: e075f15

It has been a little while since I've done Symfony (I've been bouncing around languages recently because of work), but I'm not sure if I've done something in the service definitions before that would enforce a definition's implementation of an interface. I'll try to search for this today, but if you have the time could you point me to code examples or documentation in Symfony? Thanks!

@@ -41,6 +47,7 @@ class ExceptionListener
public function __construct(
TokenStorageInterface $tokenStorage = null,
AuthorizationCheckerInterface $authorizationChecker = null,
EventDispatcherInterface $dispatcher = null,
Copy link
Member

Choose a reason for hiding this comment

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

i'm lacking a lot of context at this point (been a while since I've touched any of this, and I've never used Symfony). My biggest concerns are always breaking changes and compatibility with various versions of PHP/Symonfy. As long as this doens't cause any concern there, I'll defer ro you both.

swquinn added 2 commits May 4, 2017 13:29
This moves the $dispatcher argument to the end of the constructor
arguments to reduce BC failures.
@swquinn
Copy link
Contributor Author

swquinn commented May 4, 2017

I've refactored the code so that the $dispatcher is declared last in constructor's dependency list as discussed above. I've also made changes to the README.md example, though instead of explicitly showing the example extending the ExceptionListener defined in the library, I made a note of it the possibility of, instead of writing your listener from scratch, you can extend from the default to inherit default behavior where it makes sense.

@Jean85
Copy link
Contributor

Jean85 commented May 5, 2017

Thanks for the hard work! I've found an issue with the replacing of the listener, can you address it? I've explained myself in the inline comment here

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

/**
* The SentryUserContextEvent.
Copy link
Contributor

@Addvilz Addvilz May 5, 2017

Choose a reason for hiding this comment

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

I see these kinds of docblocks a lot. For me, this is just extra lines, as they don't really comment or document anything. We see the class name and I don't think it's any good to repeat that here in comment. You can keep the @author and @package but I would remove these kinds of comment lines that don't really explain anything - across the entire PR. Same with methods below - they just repeat something we already clearly see in the code.

@Jean85 @dcramer thoughts?

Copy link
Contributor

Choose a reason for hiding this comment

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

I agree. Comments should be nearly always become irrelevant thanks to clean code..

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Good call, I cleaned up and removed comments that seemed to be the most unnecessary.

Please see commit: bc1648f

@@ -81,6 +90,11 @@ public function onKernelRequest(GetResponseEvent $event)

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

if (null !== $this->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 can simplify this to !$this->dispatcher. Up to you tho.

Copy link
Contributor

@Jean85 Jean85 May 5, 2017

Choose a reason for hiding this comment

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

It's the opposite, no !. In any case, I don't think it's an important change.

Copy link
Contributor

Choose a reason for hiding this comment

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

Disregard this, it's Friday and I didn't think clearly 👯‍♂️

@@ -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.

@swquinn
Copy link
Contributor Author

swquinn commented May 5, 2017

Just seeing the comments now, haven't had a chance to go through all of them. I'll try to tackle them some time today and make the changes requested. I like the idea of adding the interface as well, to enforce those functions so I'll do that. Thanks for your attention on this!

swquinn added 5 commits May 5, 2017 07:43
The SentryExceptionListenerInterface serves as a contract enforcing the
shape of the exception listener. The shape is important because, for
Sentry exception listeners, it is necessary to capture exceptions
coming from both web requests (GetResponseForExceptionEvents) and the
console (ConsoleExceptionEvents).
Originally the EventDispatcher was added with the intent that it could
be nullable, however a disconnect in how it was coded in the
ExceptionListener and how it was being injected left the code in a sort
of paradox with itself: we were always requiring it, but we were
null-checking it in the code as if it were possible to leave the
dispatcher null. Ultimately, after a few other changes and suggestions
from other participants in the code review it made the most sense just
to require the EventDispatcher and, within the code, make the
assumption that it would always be there rather than checking to see if
there was a null value.
@swquinn
Copy link
Contributor Author

swquinn commented May 5, 2017

I've made a number of changes as requested by both @Jean85 and @Addvilz and I think that I got most of them. I'm doing this from a machine I don't have set up for running Symfony tests right now (let alone PHP), so I hadn't been able to run the style-checker before committing. Sp, I apologize for the various stupid-prone build errors that required a few additional commits towards the end there.

I think the only change I haven't been able to make is one @Jean85 requested which is to make the injection of the bundle fail if the %sentry.exception_listener% class doesn't implement the newly added interface.

I'll try to do some research into how to do this, but its not something I've commonly done--or if it is I'm simply forgetting how to do it--and would appreciate any advice you guys have on handling that!

@@ -81,6 +90,11 @@ public function onKernelRequest(GetResponseEvent $event)

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

if (null !== $this->dispatcher) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Since the dispatcher is always there, this check is now futile, needs to be removed

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Shoot! Missed that line of code. This has been removed in the following commit: 812007b

@@ -0,0 +1,10 @@
<?php
Copy link
Contributor

Choose a reason for hiding this comment

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

This interface is currently unused, it's empty and it has no usages apart from the implementing single class. Maybe we should delete it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm. Good call. I thought I was using that for something else--even if it wasn't in this bundle's code per-se. Although, I could have sworn I was using that as a contract for examples in the README.md file but clearly I'm not.

I guess if we find ourselves in need of a context event interface, we can always add it back in at a later point in time. Before I remove it though, I'm going to see if I can't find why I thought I needed this. Thanks for pointing this out!

Copy link
Contributor

Choose a reason for hiding this comment

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

This still needs to be deleted.

@Jean85
Copy link
Contributor

Jean85 commented May 5, 2017

@swquinn I think I can help you toward the validation: you should change the Configuration class like this:

                ->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()
[...]
    /**
     * @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);
        };
    }

I'm not sure this works, so probably you should write a few tests about this in the ExtensionTest

Per suggestions from others in the PR, and with their help, I've added
in a check that will validate that the exception listener implements
the required interface. In addition, I've updated the tests and added a
new test to verify that this works as expected.
@Jean85
Copy link
Contributor

Jean85 commented May 16, 2017

Thanks @swquinn, you did an astounding job! The only remaining fix is to delete the unused interface. Once you do that, I will merge this.

@swquinn
Copy link
Contributor Author

swquinn commented May 16, 2017

Thanks @Jean85! I made the changes last night but didn't get a chance to comment (it was way too late! Hah!). Thanks also for pointing out that the interface still needed to be deleted. I thought I had already done that.

Also, I noticed that there are conflicts between this branch and master's README.md file. Do you want me to fix that by merging master into this PR's branch? I just want to make sure I'm going about it the right way for this project.

@Jean85
Copy link
Contributor

Jean85 commented May 16, 2017

If you have time to solve the conflicts now, go for it! Otherwise, I will do that later this afternoon before merging.

@swquinn
Copy link
Contributor Author

swquinn commented May 16, 2017

I will try to do this shortly after getting into work this morning! Should be fixed in the next couple of hours. Thanks!

@swquinn
Copy link
Contributor Author

swquinn commented May 16, 2017

Got to the conflict resolution a little later than I thought I would. I merged master in to resolve the conflict with README.md, Travis seems to be running a bit slow though. I'll check back in to verify all the checks completed successfully!

@Jean85 Jean85 merged commit 0d2a008 into getsentry:master May 17, 2017
@Jean85
Copy link
Contributor

Jean85 commented May 17, 2017

That's all green, good!

Thanks again for your contribution, it was a long stretch but it was worthwhile!

Jean85 added a commit that referenced this pull request May 17, 2017
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants