Repository: getsentry/sentry-python
Issue: `traces_sampler` receives always "generic ASGI request" as transaction name.

### How do you use Sentry?

Sentry Saas (sentry.io)

### Version

1.11.1

### Steps to Reproduce

I have manually instrumented traces_sampler counted on transaction name in sampling context. At some point with sentry-sdk version upgrade, it stopped work - there is always "generic ASGI request" in transaction name. But at the performance page, transaction has a proper name.

How we initiate sentry FastAPI integration:
```
sentry_sdk.init(
    dsn=SENTRY_DSN,
    environment="dev",
    traces_sampler=sentry_traces_sampler,
    integrations=[
        starlette.StarletteIntegration(transaction_style="endpoint"),
        fastapi.FastApiIntegration(transaction_style="endpoint"),`
    ]
)
```

The traces_sampler function:
```
def sentry_traces_sampler(sampling_context):
    transaction_sampling_mapper = {
        "app.api.api_v1.routers.users.some_name": 0.1,
        "app.api.api_v1.routers.messages.some_name_2": 0.5,
    }
    return transaction_sampling_mapper.get(
        sampling_context["transaction_context"]["name"], 1.0
    )
```


### Expected Result

-

### Actual Result

Possible solution: Give the plain URL of the request instead of "generic ASGI request" as the transaction name to `traces_sampler`.

Changed files:
tests/integrations/asgi/test_asgi.py, sentry_sdk/integrations/fastapi.py, sentry_sdk/integrations/starlette.py

diff --git a/sentry_sdk/integrations/fastapi.py b/sentry_sdk/integrations/fastapi.py
index 17e0576c..43e40aaa 100644
--- a/sentry_sdk/integrations/fastapi.py
+++ b/sentry_sdk/integrations/fastapi.py
@@ -37,7 +37,7 @@ class FastApiIntegration(StarletteIntegration):
         patch_get_request_handler()
 
 
-def _set_transaction_name_and_source(scope, transaction_style, request):
+def _get_transaction_name_and_source(scope, transaction_style, request):
     # type: (Scope, str, Any) -> None
     name = ""
 
@@ -59,7 +59,7 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
     else:
         source = SOURCE_FOR_STYLE[transaction_style]
 
-    scope.set_transaction_name(name, source=source)
+    return name, source
 
 
 def patch_get_request_handler():
@@ -97,10 +97,12 @@ def patch_get_request_handler():
 
             with hub.configure_scope() as sentry_scope:
                 request = args[0]
-
-                _set_transaction_name_and_source(
+                transaction_name, transaction_source = _get_transaction_name_and_source(
                     sentry_scope, integration.transaction_style, request
                 )
+                sentry_scope.set_transaction_name(
+                    transaction_name, source=transaction_source
+                )
 
                 extractor = StarletteRequestExtractor(request)
                 info = await extractor.extract_request_info()
diff --git a/sentry_sdk/integrations/starlette.py b/sentry_sdk/integrations/starlette.py
index b44e8f10..78cd84bf 100644
--- a/sentry_sdk/integrations/starlette.py
+++ b/sentry_sdk/integrations/starlette.py
@@ -6,6 +6,7 @@ from copy import deepcopy
 
 from sentry_sdk._compat import iteritems
 from sentry_sdk._types import TYPE_CHECKING
+from sentry_sdk.api import continue_trace
 from sentry_sdk.consts import OP
 from sentry_sdk.hub import Hub, _should_send_default_pii
 from sentry_sdk.integrations import DidNotEnable, Integration
@@ -13,7 +14,6 @@ from sentry_sdk.integrations._wsgi_common import (
     _is_json_content_type,
     request_body_within_bounds,
 )
-from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
 from sentry_sdk.tracing import SOURCE_FOR_STYLE, TRANSACTION_SOURCE_ROUTE
 from sentry_sdk.utils import (
     AnnotatedValue,
@@ -26,8 +26,6 @@ from sentry_sdk.utils import (
 if TYPE_CHECKING:
     from typing import Any, Awaitable, Callable, Dict, Optional
 
-    from sentry_sdk.scope import Scope as SentryScope
-
 try:
     import starlette  # type: ignore
     from starlette import __version__ as STARLETTE_VERSION
@@ -39,7 +37,7 @@ try:
     )
     from starlette.requests import Request  # type: ignore
     from starlette.routing import Match  # type: ignore
-    from starlette.types import ASGIApp, Receive, Scope as StarletteScope, Send  # type: ignore
+    from starlette.types import ASGIApp  # type: ignore
 except ImportError:
     raise DidNotEnable("Starlette is not installed")
 
@@ -86,13 +84,64 @@ class StarletteIntegration(Integration):
                 "Unparsable Starlette version: {}".format(STARLETTE_VERSION)
             )
 
+        patch_starlette_application()
         patch_middlewares()
-        patch_asgi_app()
         patch_request_response()
 
         if version >= (0, 24):
             patch_templates()
 
+    def _get_headers(self, scope):
+        # type: (Any) -> Dict[str, str]
+        """
+        Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
+        """
+        headers = {}  # type: Dict[str, str]
+        for raw_key, raw_value in scope["headers"]:
+            key = raw_key.decode("latin-1")
+            value = raw_value.decode("latin-1")
+            if key in headers:
+                headers[key] = headers[key] + ", " + value
+            else:
+                headers[key] = value
+        return headers
+
+
+def patch_starlette_application():
+    # type: () -> None
+    def wrap_call_function(func):
+        # type: (Callable[..., Any]) -> Callable[..., Any]
+        @functools.wraps(func)
+        async def __sentry_call__(self, scope, receive, send):
+            # type: (Starlette, Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None
+            hub = Hub.current
+            integration = hub.get_integration(StarletteIntegration)
+            if integration is None:
+                await func(self, scope, receive, send)
+
+            request = Request(scope, receive)
+
+            transaction_name, transaction_source = _get_transaction_name_and_source(
+                integration.transaction_style, request
+            )
+
+            ty = scope["type"]
+            transaction = continue_trace(
+                integration._get_headers(scope),
+                op="{}.server".format(ty),
+                name=transaction_name,
+                source=transaction_source,
+            )
+
+            with hub.start_transaction(
+                transaction, custom_sampling_context={"asgi_scope": scope}
+            ):
+                await func(self, scope, receive, send)
+
+        return __sentry_call__
+
+    Starlette.__call__ = wrap_call_function(Starlette.__call__)
+
 
 def _enable_span_for_middleware(middleware_class):
     # type: (Any) -> type
@@ -312,9 +361,8 @@ def patch_middlewares():
 
         def _sentry_middleware_init(self, cls, **options):
             # type: (Any, Any, Any) -> None
-            if cls == SentryAsgiMiddleware:
-                return old_middleware_init(self, cls, **options)
 
+            # TODO: check if we should return the return value of old_middleware_init at the end of this function?
             span_enabled_cls = _enable_span_for_middleware(cls)
             old_middleware_init(self, span_enabled_cls, **options)
 
@@ -327,29 +375,6 @@ def patch_middlewares():
         Middleware.__init__ = _sentry_middleware_init
 
 
-def patch_asgi_app():
-    # type: () -> None
-    """
-    Instrument Starlette ASGI app using the SentryAsgiMiddleware.
-    """
-    old_app = Starlette.__call__
-
-    async def _sentry_patched_asgi_app(self, scope, receive, send):
-        # type: (Starlette, StarletteScope, Receive, Send) -> None
-        if Hub.current.get_integration(StarletteIntegration) is None:
-            return await old_app(self, scope, receive, send)
-
-        middleware = SentryAsgiMiddleware(
-            lambda *a, **kw: old_app(self, *a, **kw),
-            mechanism_type=StarletteIntegration.identifier,
-        )
-
-        middleware.__call__ = middleware._run_asgi3
-        return await middleware(scope, receive, send)
-
-    Starlette.__call__ = _sentry_patched_asgi_app
-
-
 # This was vendored in from Starlette to support Starlette 0.19.1 because
 # this function was only introduced in 0.20.x
 def _is_async_callable(obj):
@@ -382,9 +407,15 @@ def patch_request_response():
 
                 with hub.configure_scope() as sentry_scope:
                     request = args[0]
-
-                    _set_transaction_name_and_source(
-                        sentry_scope, integration.transaction_style, request
+                    (
+                        transaction_name,
+                        transaction_source,
+                    ) = _get_transaction_name_and_source(
+                        integration.transaction_style, request
+                    )
+                    sentry_scope.set_transaction_name(
+                        name=transaction_name,
+                        source=transaction_source,
                     )
 
                     extractor = StarletteRequestExtractor(request)
@@ -430,11 +461,6 @@ def patch_request_response():
                         sentry_scope.profile.update_active_thread_id()
 
                     request = args[0]
-
-                    _set_transaction_name_and_source(
-                        sentry_scope, integration.transaction_style, request
-                    )
-
                     extractor = StarletteRequestExtractor(request)
                     cookies = extractor.extract_cookies_from_request()
 
@@ -619,27 +645,32 @@ class StarletteRequestExtractor:
         return await self.request.json()
 
 
-def _set_transaction_name_and_source(scope, transaction_style, request):
-    # type: (SentryScope, str, Any) -> None
+def _get_transaction_name_and_source(transaction_style, request):
+    # type: (str, Any) -> None
     name = ""
 
     if transaction_style == "endpoint":
         endpoint = request.scope.get("endpoint")
         if endpoint:
             name = transaction_from_function(endpoint) or ""
+        else:
+            name = request.scope.get("raw_path")
 
     elif transaction_style == "url":
-        router = request.scope["router"]
-        for route in router.routes:
-            match = route.matches(request.scope)
-
-            if match[0] == Match.FULL:
-                if transaction_style == "endpoint":
-                    name = transaction_from_function(match[1]["endpoint"]) or ""
-                    break
-                elif transaction_style == "url":
-                    name = route.path
-                    break
+        router = request.scope.get("router")
+        if router:
+            for route in router.routes:
+                match = route.matches(request.scope)
+
+                if match[0] == Match.FULL:
+                    if transaction_style == "endpoint":
+                        name = transaction_from_function(match[1]["endpoint"]) or ""
+                        break
+                    elif transaction_style == "url":
+                        name = route.path
+                        break
+        else:
+            name = request.scope.get("raw_path")
 
     if not name:
         name = _DEFAULT_TRANSACTION_NAME
@@ -647,4 +678,4 @@ def _set_transaction_name_and_source(scope, transaction_style, request):
     else:
         source = SOURCE_FOR_STYLE[transaction_style]
 
-    scope.set_transaction_name(name, source=source)
+    return name, source
diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py
index d51293af..3a4f7f5b 100644
--- a/tests/integrations/asgi/test_asgi.py
+++ b/tests/integrations/asgi/test_asgi.py
@@ -142,7 +142,7 @@ async def test_capture_transaction(
     (transaction_event,) = events
 
     assert transaction_event["type"] == "transaction"
-    assert transaction_event["transaction"] == "generic ASGI request"
+    assert transaction_event["transaction"] == "http://localhost/"
     assert transaction_event["contexts"]["trace"]["op"] == "http.server"
     assert transaction_event["request"] == {
         "headers": {
@@ -174,7 +174,7 @@ async def test_capture_transaction_with_error(
 
     (error_event, transaction_event) = events
 
-    assert error_event["transaction"] == "generic ASGI request"
+    assert error_event["transaction"] == "http://localhost/"
     assert error_event["contexts"]["trace"]["op"] == "http.server"
     assert error_event["exception"]["values"][0]["type"] == "ZeroDivisionError"
     assert error_event["exception"]["values"][0]["value"] == "division by zero"
@@ -389,7 +389,7 @@ async def test_auto_session_tracking_with_aggregates(
         (
             "/message",
             "url",
-            "generic ASGI request",
+            "http://localhost/message",
             "route",
         ),
         (