fix: flatten React Router structure — no nested Routes, each page is a top-level route
This commit is contained in:
@@ -0,0 +1,370 @@
|
||||
from collections.abc import Set
|
||||
from copy import deepcopy
|
||||
|
||||
import sentry_sdk
|
||||
from sentry_sdk.consts import OP, SPANDATA
|
||||
from sentry_sdk.integrations import (
|
||||
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
|
||||
DidNotEnable,
|
||||
Integration,
|
||||
)
|
||||
from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
|
||||
from sentry_sdk.integrations.logging import ignore_logger
|
||||
from sentry_sdk.scope import should_send_default_pii
|
||||
from sentry_sdk.tracing import SOURCE_FOR_STYLE, TransactionSource
|
||||
from sentry_sdk.tracing_utils import has_span_streaming_enabled
|
||||
from sentry_sdk.utils import (
|
||||
ensure_integration_enabled,
|
||||
event_from_exception,
|
||||
transaction_from_function,
|
||||
)
|
||||
|
||||
try:
|
||||
from litestar import Litestar, Request # type: ignore
|
||||
from litestar.data_extractors import ConnectionDataExtractor # type: ignore
|
||||
from litestar.exceptions import HTTPException # type: ignore
|
||||
from litestar.handlers.base import BaseRouteHandler # type: ignore
|
||||
from litestar.middleware import DefineMiddleware # type: ignore
|
||||
from litestar.routes.http import HTTPRoute # type: ignore
|
||||
except ImportError:
|
||||
raise DidNotEnable("Litestar is not installed")
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import Any, Optional, Union
|
||||
|
||||
from litestar.middleware import MiddlewareProtocol
|
||||
from litestar.types import ( # type: ignore
|
||||
HTTPReceiveMessage,
|
||||
HTTPScope,
|
||||
Message,
|
||||
Middleware,
|
||||
Receive,
|
||||
Send,
|
||||
WebSocketReceiveMessage,
|
||||
)
|
||||
from litestar.types import (
|
||||
Scope as LitestarScope,
|
||||
)
|
||||
from litestar.types.asgi_types import ASGIApp # type: ignore
|
||||
|
||||
from sentry_sdk._types import Event, Hint
|
||||
|
||||
_DEFAULT_TRANSACTION_NAME = "generic Litestar request"
|
||||
|
||||
|
||||
class LitestarIntegration(Integration):
|
||||
identifier = "litestar"
|
||||
origin = f"auto.http.{identifier}"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
failed_request_status_codes: "Set[int]" = _DEFAULT_FAILED_REQUEST_STATUS_CODES,
|
||||
) -> None:
|
||||
self.failed_request_status_codes = failed_request_status_codes
|
||||
|
||||
@staticmethod
|
||||
def setup_once() -> None:
|
||||
patch_app_init()
|
||||
patch_middlewares()
|
||||
patch_http_route_handle()
|
||||
|
||||
# The following line follows the pattern found in other integrations such as `DjangoIntegration.setup_once`.
|
||||
# The Litestar `ExceptionHandlerMiddleware.__call__` catches exceptions and does the following
|
||||
# (among other things):
|
||||
# 1. Logs them, some at least (such as 500s) as errors
|
||||
# 2. Calls after_exception hooks
|
||||
# The `LitestarIntegration`` provides an after_exception hook (see `patch_app_init` below) to create a Sentry event
|
||||
# from an exception, which ends up being called during step 2 above. However, the Sentry `LoggingIntegration` will
|
||||
# by default create a Sentry event from error logs made in step 1 if we do not prevent it from doing so.
|
||||
ignore_logger("litestar")
|
||||
|
||||
|
||||
class SentryLitestarASGIMiddleware(SentryAsgiMiddleware):
|
||||
def __init__(
|
||||
self, app: "ASGIApp", span_origin: str = LitestarIntegration.origin
|
||||
) -> None:
|
||||
super().__init__(
|
||||
app=app,
|
||||
unsafe_context_data=False,
|
||||
transaction_style="endpoint",
|
||||
mechanism_type="asgi",
|
||||
span_origin=span_origin,
|
||||
asgi_version=3,
|
||||
)
|
||||
|
||||
def _capture_request_exception(self, exc: Exception) -> None:
|
||||
"""Avoid catching exceptions from request handlers.
|
||||
|
||||
Those exceptions are already handled in Litestar.after_exception handler.
|
||||
We still catch exceptions from application lifespan handlers.
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def patch_app_init() -> None:
|
||||
"""
|
||||
Replaces the Litestar class's `__init__` function in order to inject `after_exception` handlers and set the
|
||||
`SentryLitestarASGIMiddleware` as the outmost middleware in the stack.
|
||||
See:
|
||||
- https://docs.litestar.dev/2/usage/applications.html#after-exception
|
||||
- https://docs.litestar.dev/2/usage/middleware/using-middleware.html
|
||||
"""
|
||||
old__init__ = Litestar.__init__
|
||||
|
||||
@ensure_integration_enabled(LitestarIntegration, old__init__)
|
||||
def injection_wrapper(self: "Litestar", *args: "Any", **kwargs: "Any") -> None:
|
||||
kwargs["after_exception"] = [
|
||||
exception_handler,
|
||||
*(kwargs.get("after_exception") or []),
|
||||
]
|
||||
|
||||
middleware = kwargs.get("middleware") or []
|
||||
kwargs["middleware"] = [SentryLitestarASGIMiddleware, *middleware]
|
||||
old__init__(self, *args, **kwargs)
|
||||
|
||||
Litestar.__init__ = injection_wrapper
|
||||
|
||||
|
||||
def patch_middlewares() -> None:
|
||||
old_resolve_middleware_stack = BaseRouteHandler.resolve_middleware
|
||||
|
||||
@ensure_integration_enabled(LitestarIntegration, old_resolve_middleware_stack)
|
||||
def resolve_middleware_wrapper(self: "BaseRouteHandler") -> "list[Middleware]":
|
||||
return [
|
||||
enable_span_for_middleware(middleware)
|
||||
for middleware in old_resolve_middleware_stack(self)
|
||||
]
|
||||
|
||||
BaseRouteHandler.resolve_middleware = resolve_middleware_wrapper
|
||||
|
||||
|
||||
def enable_span_for_middleware(middleware: "Middleware") -> "Middleware":
|
||||
if (
|
||||
not hasattr(middleware, "__call__") # noqa: B004
|
||||
or middleware is SentryLitestarASGIMiddleware
|
||||
):
|
||||
return middleware
|
||||
|
||||
if isinstance(middleware, DefineMiddleware):
|
||||
old_call: "ASGIApp" = middleware.middleware.__call__
|
||||
else:
|
||||
old_call = middleware.__call__
|
||||
|
||||
async def _create_span_call(
|
||||
self: "MiddlewareProtocol",
|
||||
scope: "LitestarScope",
|
||||
receive: "Receive",
|
||||
send: "Send",
|
||||
) -> None:
|
||||
client = sentry_sdk.get_client()
|
||||
if client.get_integration(LitestarIntegration) is None:
|
||||
return await old_call(self, scope, receive, send)
|
||||
|
||||
middleware_name = self.__class__.__name__
|
||||
if has_span_streaming_enabled(client.options):
|
||||
if sentry_sdk.traces.get_current_span() is None:
|
||||
return await old_call(self, scope, receive, send)
|
||||
with sentry_sdk.traces.start_span(
|
||||
name=middleware_name,
|
||||
attributes={
|
||||
"sentry.op": OP.MIDDLEWARE_LITESTAR,
|
||||
"sentry.origin": LitestarIntegration.origin,
|
||||
},
|
||||
) as middleware_span:
|
||||
middleware_span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name)
|
||||
|
||||
# Creating spans for the "receive" callback
|
||||
async def _sentry_receive(
|
||||
*args: "Any", **kwargs: "Any"
|
||||
) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]":
|
||||
if client.get_integration(LitestarIntegration) is None:
|
||||
return await receive(*args, **kwargs)
|
||||
if sentry_sdk.traces.get_current_span() is None:
|
||||
return await receive(*args, **kwargs)
|
||||
with sentry_sdk.traces.start_span(
|
||||
name=getattr(receive, "__qualname__", str(receive)),
|
||||
attributes={
|
||||
"sentry.op": OP.MIDDLEWARE_LITESTAR_RECEIVE,
|
||||
"sentry.origin": LitestarIntegration.origin,
|
||||
},
|
||||
) as span:
|
||||
span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name)
|
||||
return await receive(*args, **kwargs)
|
||||
|
||||
receive_name = getattr(receive, "__name__", str(receive))
|
||||
receive_patched = receive_name == "_sentry_receive"
|
||||
new_receive = _sentry_receive if not receive_patched else receive
|
||||
|
||||
# Creating spans for the "send" callback
|
||||
async def _sentry_send(message: "Message") -> None:
|
||||
if client.get_integration(LitestarIntegration) is None:
|
||||
return await send(message)
|
||||
if sentry_sdk.traces.get_current_span() is None:
|
||||
return await send(message)
|
||||
with sentry_sdk.traces.start_span(
|
||||
name=getattr(send, "__qualname__", str(send)),
|
||||
attributes={
|
||||
"sentry.op": OP.MIDDLEWARE_LITESTAR_SEND,
|
||||
"sentry.origin": LitestarIntegration.origin,
|
||||
},
|
||||
) as span:
|
||||
span.set_attribute(SPANDATA.MIDDLEWARE_NAME, middleware_name)
|
||||
return await send(message)
|
||||
|
||||
send_name = getattr(send, "__name__", str(send))
|
||||
send_patched = send_name == "_sentry_send"
|
||||
new_send = _sentry_send if not send_patched else send
|
||||
|
||||
return await old_call(self, scope, new_receive, new_send)
|
||||
else:
|
||||
with sentry_sdk.start_span(
|
||||
op=OP.MIDDLEWARE_LITESTAR,
|
||||
name=middleware_name,
|
||||
origin=LitestarIntegration.origin,
|
||||
) as middleware_span:
|
||||
middleware_span.set_tag("litestar.middleware_name", middleware_name)
|
||||
|
||||
# Creating spans for the "receive" callback
|
||||
async def _sentry_receive(
|
||||
*args: "Any", **kwargs: "Any"
|
||||
) -> "Union[HTTPReceiveMessage, WebSocketReceiveMessage]":
|
||||
if client.get_integration(LitestarIntegration) is None:
|
||||
return await receive(*args, **kwargs)
|
||||
with sentry_sdk.start_span(
|
||||
op=OP.MIDDLEWARE_LITESTAR_RECEIVE,
|
||||
name=getattr(receive, "__qualname__", str(receive)),
|
||||
origin=LitestarIntegration.origin,
|
||||
) as span:
|
||||
span.set_tag("litestar.middleware_name", middleware_name)
|
||||
return await receive(*args, **kwargs)
|
||||
|
||||
receive_name = getattr(receive, "__name__", str(receive))
|
||||
receive_patched = receive_name == "_sentry_receive"
|
||||
new_receive = _sentry_receive if not receive_patched else receive
|
||||
|
||||
# Creating spans for the "send" callback
|
||||
async def _sentry_send(message: "Message") -> None:
|
||||
if client.get_integration(LitestarIntegration) is None:
|
||||
return await send(message)
|
||||
with sentry_sdk.start_span(
|
||||
op=OP.MIDDLEWARE_LITESTAR_SEND,
|
||||
name=getattr(send, "__qualname__", str(send)),
|
||||
origin=LitestarIntegration.origin,
|
||||
) as span:
|
||||
span.set_tag("litestar.middleware_name", middleware_name)
|
||||
return await send(message)
|
||||
|
||||
send_name = getattr(send, "__name__", str(send))
|
||||
send_patched = send_name == "_sentry_send"
|
||||
new_send = _sentry_send if not send_patched else send
|
||||
|
||||
return await old_call(self, scope, new_receive, new_send)
|
||||
|
||||
not_yet_patched = old_call.__name__ not in ["_create_span_call"]
|
||||
|
||||
if not_yet_patched:
|
||||
if isinstance(middleware, DefineMiddleware):
|
||||
middleware.middleware.__call__ = _create_span_call
|
||||
else:
|
||||
middleware.__call__ = _create_span_call
|
||||
|
||||
return middleware
|
||||
|
||||
|
||||
def patch_http_route_handle() -> None:
|
||||
old_handle = HTTPRoute.handle
|
||||
|
||||
async def handle_wrapper(
|
||||
self: "HTTPRoute", scope: "HTTPScope", receive: "Receive", send: "Send"
|
||||
) -> None:
|
||||
if sentry_sdk.get_client().get_integration(LitestarIntegration) is None:
|
||||
return await old_handle(self, scope, receive, send)
|
||||
|
||||
sentry_scope = sentry_sdk.get_isolation_scope()
|
||||
request: "Request[Any, Any]" = scope["app"].request_class(
|
||||
scope=scope, receive=receive, send=send
|
||||
)
|
||||
extracted_request_data = ConnectionDataExtractor(
|
||||
parse_body=True, parse_query=True
|
||||
)(request)
|
||||
body = extracted_request_data.pop("body")
|
||||
|
||||
request_data = await body
|
||||
|
||||
route_handler = scope.get("route_handler")
|
||||
|
||||
func = None
|
||||
if route_handler.name is not None:
|
||||
name = route_handler.name
|
||||
# Accounts for use of type `Ref` in earlier versions of litestar without the need to reference it as a type
|
||||
elif hasattr(route_handler.fn, "value"):
|
||||
func = route_handler.fn.value
|
||||
else:
|
||||
func = route_handler.fn
|
||||
if func is not None:
|
||||
name = transaction_from_function(func)
|
||||
|
||||
source = SOURCE_FOR_STYLE["endpoint"]
|
||||
|
||||
if not name:
|
||||
name = _DEFAULT_TRANSACTION_NAME
|
||||
source = TransactionSource.ROUTE
|
||||
|
||||
sentry_sdk.set_transaction_name(name, source)
|
||||
sentry_scope.set_transaction_name(name, source)
|
||||
|
||||
def event_processor(event: "Event", _: "Hint") -> "Event":
|
||||
request_info = event.get("request", {})
|
||||
request_info["content_length"] = len(scope.get("_body", b""))
|
||||
if should_send_default_pii():
|
||||
request_info["cookies"] = extracted_request_data["cookies"]
|
||||
if request_data is not None:
|
||||
request_info["data"] = request_data
|
||||
|
||||
event["request"] = deepcopy(request_info)
|
||||
return event
|
||||
|
||||
sentry_scope._name = LitestarIntegration.identifier
|
||||
sentry_scope.add_event_processor(event_processor)
|
||||
|
||||
return await old_handle(self, scope, receive, send)
|
||||
|
||||
HTTPRoute.handle = handle_wrapper
|
||||
|
||||
|
||||
def retrieve_user_from_scope(scope: "LitestarScope") -> "Optional[dict[str, Any]]":
|
||||
scope_user = scope.get("user")
|
||||
if isinstance(scope_user, dict):
|
||||
return scope_user
|
||||
if hasattr(scope_user, "asdict"): # dataclasses
|
||||
return scope_user.asdict()
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@ensure_integration_enabled(LitestarIntegration)
|
||||
def exception_handler(exc: Exception, scope: "LitestarScope") -> None:
|
||||
user_info: "Optional[dict[str, Any]]" = None
|
||||
if should_send_default_pii():
|
||||
user_info = retrieve_user_from_scope(scope)
|
||||
if user_info and isinstance(user_info, dict):
|
||||
sentry_scope = sentry_sdk.get_isolation_scope()
|
||||
sentry_scope.set_user(user_info)
|
||||
|
||||
if isinstance(exc, HTTPException):
|
||||
integration = sentry_sdk.get_client().get_integration(LitestarIntegration)
|
||||
if (
|
||||
integration is not None
|
||||
and exc.status_code not in integration.failed_request_status_codes
|
||||
):
|
||||
return
|
||||
|
||||
event, hint = event_from_exception(
|
||||
exc,
|
||||
client_options=sentry_sdk.get_client().options,
|
||||
mechanism={"type": LitestarIntegration.identifier, "handled": False},
|
||||
)
|
||||
|
||||
sentry_sdk.capture_event(event, hint=hint)
|
||||
Reference in New Issue
Block a user