Repository: getsentry/sentry-python

Issue:
Integration with Pyramid tweens
The common case in Pyramid is to handle exception with [tweens](https://docs.pylonsproject.org/projects/pyramid/en/latest/glossary.html#term-tween) that seat atop of main Pyramid handling function. With a current integration [approach](https://docs.sentry.io/platforms/python/pyramid/) each exception occurring in main function goes to sentry. Is there a way to track just those errors that were not handled in tweens?


Discussion:
As a quick fix you can ignore errors by class with https://docs.sentry.io/error-reporting/configuration/filtering/?platform=python#before-send

As far as I can tell tweens are just like errorhandlers in Flask. In Flask, we have the following behavior:

* If you register an errorhandler for the exception (`@app.errorhandler(Exception)`), it will swallow the exception and it will not report to Sentry
* If you register an errorhandler for a status code (`@app.errorhandler(500)`), we consider the errorhandler to be just a "pretty error page" and still report the error to sentry.

I wonder if we can figure out a rule like that for Pyramid too. I only had a look at [exception views](https://docs.pylonsproject.org/projects/pyramid-cookbook/en/latest/pylons/exceptions.html#exception-views) and was not sure if there is a way to distinguish those two cases, so I opted for what I felt was "safer", which is reporting to sentry
> I wonder if we can figure out a rule like that for Pyramid too. I only had a look at exception views and was not sure if there is a way to distinguish those two cases, so I opted for what I felt was "safer", which is reporting to sentry

We're seeing a similar issue where exception views for 4xx exceptions are now being sent to Sentry, e.g.:

```python
class OurCustomError(Exception):
    pass


@view_config(context=OurCustomError)
def handle_our_custom_error(request):
    request.response.status_int = 404
    return {}
```

It looks like the current Pyramid integration is recording the exception before the handler is even called. 😞 

@untitaker what do you think about implementing Pyramid integration as a tween in a same way it is done for [pyramid_exclog](https://docs.pylonsproject.org/projects/pyramid_exclog/en/latest/#explicit-tween-configuration)?
Or maybe making the `Router.handle_request` portion of the Pyramid integration optional, so we can explicitly log to Sentry in our exception handling views. 😕 
@rouge8 current Pyramid integration lives inside a main handler but I think it should be placed above `pyramid.tweens.excview_tween_factory`. In that case you don't need to record exceptions in exception views explicitly.
@drnextgis I don't want to make the user explicitly wire up the pyramid integration. I don't understand your suggestion in the sense that I don't know how it would affect behavior. We still have the problem that we don't know which exceptions are "handled" and which are "suppressed".

To be clear, I am worried about a catch-all exception view:

```python
@view_config(context=Exception)
def foo(...):
    ...
```

Is the intention to make a default 500 error page or to really suppress all exceptions? As far as i can tell `pyramid_exclog` behaves the exact same way as the Sentry integration:

* Ignores HTTP exceptions
* Logs everything, even if a matching exception view exists
* Has separate config to ignore exception types (see link I posted at beginning of this thread)

(this is just what I read from the docs you linked)
The intent of catch-all exceptions views are to have a custom 500 error page that doesn't mismatch the design of the site and/or conforms to API error schemas.

When a pyramid user has defined any exception-based view, I think it's reasonable to expect them to do all error handling themselves, including error reporting.

This seems to be the only reasonable alternative at the moment. I'd rather not be carpet bombed with every single 4xx and 5xx error we raise as an exception and manually add them to Sentry filter lists. When we upgraded to the latest SDK our issues dashboard exploded with a lot of non-errors and is now even noisier than before.
It seems to me that sentry should only log unhandled exceptions that propagate out of the app. If people register exception views to deal with exceptions then it feels reasonable that they will `sentry_sdk.capture_exception` themselves if they wish. The simplest way to do this is monkeypatching just the `router.__call__` if that's possible.
I'll change this behavior to what you all suggested in 0.9.0
One quick question @goodspark, you don't see subtypes of `HTTPException` being reported to Sentry by any chance? That would be a hard bug as we intend to explicitly ignore those.

Resolving patch:
diff --git a/sentry_sdk/integrations/pyramid.py b/sentry_sdk/integrations/pyramid.py
--- a/sentry_sdk/integrations/pyramid.py
+++ b/sentry_sdk/integrations/pyramid.py
@@ -59,6 +59,7 @@ def __init__(self, transaction_style="route_name"):
     def setup_once():
         # type: () -> None
         from pyramid.router import Router  # type: ignore
+        from pyramid.request import Request  # type: ignore
 
         old_handle_request = Router.handle_request
 
@@ -66,23 +67,34 @@ def sentry_patched_handle_request(self, request, *args, **kwargs):
             # type: (Any, Request, *Any, **Any) -> Response
             hub = Hub.current
             integration = hub.get_integration(PyramidIntegration)
-            if integration is None:
-                return old_handle_request(self, request, *args, **kwargs)
-
-            with hub.configure_scope() as scope:
-                scope.add_event_processor(
-                    _make_event_processor(weakref.ref(request), integration)
-                )
+            if integration is not None:
+                with hub.configure_scope() as scope:
+                    scope.add_event_processor(
+                        _make_event_processor(weakref.ref(request), integration)
+                    )
 
-            try:
-                return old_handle_request(self, request, *args, **kwargs)
-            except Exception:
-                exc_info = sys.exc_info()
-                _capture_exception(exc_info)
-                reraise(*exc_info)
+            return old_handle_request(self, request, *args, **kwargs)
 
         Router.handle_request = sentry_patched_handle_request
 
+        if hasattr(Request, "invoke_exception_view"):
+            old_invoke_exception_view = Request.invoke_exception_view
+
+            def sentry_patched_invoke_exception_view(self, *args, **kwargs):
+                rv = old_invoke_exception_view(self, *args, **kwargs)
+
+                if (
+                    self.exc_info
+                    and all(self.exc_info)
+                    and rv.status_int == 500
+                    and Hub.current.get_integration(PyramidIntegration) is not None
+                ):
+                    _capture_exception(self.exc_info)
+
+                return rv
+
+            Request.invoke_exception_view = sentry_patched_invoke_exception_view
+
         old_wsgi_call = Router.__call__
 
         def sentry_patched_wsgi_call(self, environ, start_response):
@@ -92,15 +104,23 @@ def sentry_patched_wsgi_call(self, environ, start_response):
             if integration is None:
                 return old_wsgi_call(self, environ, start_response)
 
-            return SentryWsgiMiddleware(lambda *a, **kw: old_wsgi_call(self, *a, **kw))(
+            def sentry_patched_inner_wsgi_call(environ, start_response):
+                try:
+                    return old_wsgi_call(self, environ, start_response)
+                except Exception:
+                    einfo = sys.exc_info()
+                    _capture_exception(einfo)
+                    reraise(*einfo)
+
+            return SentryWsgiMiddleware(sentry_patched_inner_wsgi_call)(
                 environ, start_response
             )
 
         Router.__call__ = sentry_patched_wsgi_call
 
 
-def _capture_exception(exc_info, **kwargs):
-    # type: (ExcInfo, **Any) -> None
+def _capture_exception(exc_info):
+    # type: (ExcInfo) -> None
     if exc_info[0] is None or issubclass(exc_info[0], HTTPException):
         return
     hub = Hub.current


Test patch:
diff --git a/tests/integrations/pyramid/test_pyramid.py b/tests/integrations/pyramid/test_pyramid.py
--- a/tests/integrations/pyramid/test_pyramid.py
+++ b/tests/integrations/pyramid/test_pyramid.py
@@ -1,21 +1,25 @@
 import json
-
-from io import BytesIO
-
 import logging
-
+import pkg_resources
 import pytest
 
-from pyramid.authorization import ACLAuthorizationPolicy
+from io import BytesIO
+
 import pyramid.testing
 
+from pyramid.authorization import ACLAuthorizationPolicy
 from pyramid.response import Response
 
-from werkzeug.test import Client
-
 from sentry_sdk import capture_message, add_breadcrumb
 from sentry_sdk.integrations.pyramid import PyramidIntegration
 
+from werkzeug.test import Client
+
+
+PYRAMID_VERSION = tuple(
+    map(int, pkg_resources.get_distribution("pyramid").version.split("."))
+)
+
 
 def hi(request):
     capture_message("hi")
@@ -219,7 +223,7 @@ def errorhandler(exc, request):
     assert not events
 
 
-def test_errorhandler(
+def test_errorhandler_ok(
     sentry_init, pyramid_config, capture_exceptions, route, get_client
 ):
     sentry_init(integrations=[PyramidIntegration()])
@@ -237,8 +241,36 @@ def errorhandler(exc, request):
     client = get_client()
     client.get("/")
 
+    assert not errors
+
+
+@pytest.mark.skipif(
+    PYRAMID_VERSION < (1, 9),
+    reason="We don't have the right hooks in older Pyramid versions",
+)
+def test_errorhandler_500(
+    sentry_init, pyramid_config, capture_exceptions, route, get_client
+):
+    sentry_init(integrations=[PyramidIntegration()])
+    errors = capture_exceptions()
+
+    @route("/")
+    def index(request):
+        1 / 0
+
+    def errorhandler(exc, request):
+        return Response("bad request", status=500)
+
+    pyramid_config.add_view(errorhandler, context=Exception)
+
+    client = get_client()
+    app_iter, status, headers = client.get("/")
+    assert b"".join(app_iter) == b"bad request"
+    assert status.lower() == "500 internal server error"
+
     error, = errors
-    assert type(error) is Exception
+
+    assert isinstance(error, ZeroDivisionError)
 
 
 def test_error_in_errorhandler(
@@ -262,12 +294,9 @@ def error_handler(err, request):
     with pytest.raises(ZeroDivisionError):
         client.get("/")
 
-    event1, event2 = events
-
-    exception, = event1["exception"]["values"]
-    assert exception["type"] == "ValueError"
+    event, = events
 
-    exception = event2["exception"]["values"][-1]
+    exception = event["exception"]["values"][-1]
     assert exception["type"] == "ZeroDivisionError"