Repository: internetarchive/openlibrary

Issue:
Add sentry transactions to covers server
<!-- What issue does this PR close? -->
Closes #8699 . Feature. Let us monitor covers.openlibrary.org performance via sentry.

### Technical
- Note I couldn't get it to do the path regex, so it just displays the class name. That's sufficient.
- Corresponding olsystem change: https://github.com/internetarchive/olsystem/pull/199

### Testing
- Tested by enabling locally into our sentry, and it worked.
- Patch deployed it to ol-covers and ol-web, and both worked!
- See covers in sentry here: https://sentry.archive.org/organizations/ia-ux/performance/?project=10

### Screenshot
![image](https://github.com/internetarchive/openlibrary/assets/6251786/e3c4a453-372b-411e-b401-70be560f698d)


### Stakeholders
<!-- @ tag stakeholders of this bug -->


<!-- Attribution Disclaimer: By proposing this pull request, I affirm to have made a best-effort and exercised my discretion to make sure relevant sections of this code which substantially leverage code suggestions, code generation, or code snippets from sources (e.g. Stack Overflow, GitHub) have been annotated with basic attribution so reviewers & contributors may have confidence and access to the correct context to evaluate and use this code. -->



Discussion:

Resolving patch:
diff --git a/openlibrary/coverstore/code.py b/openlibrary/coverstore/code.py
--- a/openlibrary/coverstore/code.py
+++ b/openlibrary/coverstore/code.py
@@ -239,17 +239,9 @@ def notfound():
             ):
                 return read_file(config.default_image)
             elif is_valid_url(i.default):
-                raise web.seeother(i.default)
+                return web.seeother(i.default)
             else:
-                raise web.notfound("")
-
-        def redirect(id):
-            size_part = size and ("-" + size) or ""
-            url = f"/{category}/id/{id}{size_part}.jpg"
-
-            if query := web.ctx.env.get('QUERY_STRING'):
-                url += '?' + query
-            raise web.found(url)
+                return web.notfound("")
 
         if key == 'isbn':
             value = value.replace("-", "").strip()  # strip hyphens from ISBN
@@ -257,7 +249,7 @@ def redirect(id):
         elif key == 'ia':
             url = self.get_ia_cover_url(value, size)
             if url:
-                raise web.found(url)
+                return web.found(url)
             else:
                 value = None  # notfound or redirect to default. handled later.
         elif key != 'id':
@@ -269,7 +261,7 @@ def redirect(id):
         # redirect to archive.org cluster for large size and original images whenever possible
         if size in ("L", "") and self.is_cover_in_cluster(value):
             url = zipview_url_from_id(int(value), size)
-            raise web.found(url)
+            return web.found(url)
 
         # covers_0008 batches [_00, _82] are tar'd / zip'd in archive.org items
         if isinstance(value, int) or value.isnumeric():  # noqa: SIM102
@@ -281,7 +273,7 @@ def redirect(id):
                 item_file = f"{pid}{'-' + size.upper() if size else ''}"
                 path = f"{item_id}/{item_tar}/{item_file}.jpg"
                 protocol = web.ctx.protocol
-                raise web.found(f"{protocol}://archive.org/download/{path}")
+                return web.found(f"{protocol}://archive.org/download/{path}")
 
         d = self.get_details(value, size.lower())
         if not d:
@@ -291,7 +283,7 @@ def redirect(id):
         if key == 'id':
             etag = f"{d.id}-{size.lower()}"
             if not web.modified(trim_microsecond(d.created), etag=etag):
-                raise web.notmodified()
+                return web.notmodified()
 
             web.header('Cache-Control', 'public')
             # this image is not going to expire in next 100 years.
@@ -306,14 +298,14 @@ def redirect(id):
             from openlibrary.coverstore import archive
 
             if d.id >= 8_820_000 and d.uploaded and '.zip' in d.filename:
-                raise web.found(
+                return web.found(
                     archive.Cover.get_cover_url(
                         d.id, size=size, protocol=web.ctx.protocol
                     )
                 )
             return read_image(d, size)
         except OSError:
-            raise web.notfound()
+            return web.notfound()
 
     def get_ia_cover_url(self, identifier, size="M"):
         url = "https://archive.org/metadata/%s/metadata" % identifier
diff --git a/openlibrary/plugins/openlibrary/sentry.py b/openlibrary/plugins/openlibrary/sentry.py
--- a/openlibrary/plugins/openlibrary/sentry.py
+++ b/openlibrary/plugins/openlibrary/sentry.py
@@ -1,6 +1,6 @@
 import infogami
 from infogami.utils import delegate
-from openlibrary.utils.sentry import Sentry, SentryProcessor
+from openlibrary.utils.sentry import Sentry, InfogamiSentryProcessor
 
 sentry: Sentry | None = None
 
@@ -12,4 +12,4 @@ def setup():
     if sentry.enabled:
         sentry.init()
         delegate.add_exception_hook(lambda: sentry.capture_exception_webpy())
-        delegate.app.add_processor(SentryProcessor())
+        delegate.app.add_processor(InfogamiSentryProcessor(delegate.app))
diff --git a/openlibrary/utils/sentry.py b/openlibrary/utils/sentry.py
--- a/openlibrary/utils/sentry.py
+++ b/openlibrary/utils/sentry.py
@@ -76,6 +76,7 @@ def capture_exception():
             return _internalerror()
 
         app.internalerror = capture_exception
+        app.add_processor(WebPySentryProcessor(app))
 
     def capture_exception_webpy(self):
         with sentry_sdk.push_scope() as scope:
@@ -97,12 +98,48 @@ def to_sentry_name(self) -> str:
         )
 
 
-class SentryProcessor:
+class WebPySentryProcessor:
+    def __init__(self, app: web.application):
+        self.app = app
+
+    def find_route_name(self) -> str:
+        handler, groups = self.app._match(self.app.mapping, web.ctx.path)
+        web.debug('ROUTE HANDLER', handler, groups)
+        return handler or '<other>'
+
+    def __call__(self, handler):
+        route_name = self.find_route_name()
+        hub = sentry_sdk.Hub.current
+        with sentry_sdk.Hub(hub) as hub:
+            with hub.configure_scope() as scope:
+                scope.clear_breadcrumbs()
+                scope.add_event_processor(add_web_ctx_to_event)
+
+            environ = dict(web.ctx.env)
+            # Don't forward cookies to Sentry
+            if 'HTTP_COOKIE' in environ:
+                del environ['HTTP_COOKIE']
+
+            transaction = Transaction.continue_from_environ(
+                environ,
+                op="http.server",
+                name=route_name,
+                source=TRANSACTION_SOURCE_ROUTE,
+            )
+
+            with hub.start_transaction(transaction):
+                try:
+                    return handler()
+                finally:
+                    transaction.set_http_status(int(web.ctx.status.split()[0]))
+
+
+class InfogamiSentryProcessor(WebPySentryProcessor):
     """
     Processor to profile the webpage and send a transaction to Sentry.
     """
 
-    def __call__(self, handler):
+    def find_route_name(self) -> str:
         def find_type() -> tuple[str, str] | None:
             return next(
                 (
@@ -134,27 +171,4 @@ def find_route() -> InfogamiRoute:
 
             return result
 
-        route = find_route()
-        hub = sentry_sdk.Hub.current
-        with sentry_sdk.Hub(hub) as hub:
-            with hub.configure_scope() as scope:
-                scope.clear_breadcrumbs()
-                scope.add_event_processor(add_web_ctx_to_event)
-
-            environ = dict(web.ctx.env)
-            # Don't forward cookies to Sentry
-            if 'HTTP_COOKIE' in environ:
-                del environ['HTTP_COOKIE']
-
-            transaction = Transaction.continue_from_environ(
-                environ,
-                op="http.server",
-                name=route.to_sentry_name(),
-                source=TRANSACTION_SOURCE_ROUTE,
-            )
-
-            with hub.start_transaction(transaction):
-                try:
-                    return handler()
-                finally:
-                    transaction.set_http_status(int(web.ctx.status.split()[0]))
+        return find_route().to_sentry_name()


Test patch: