starlette.py 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737
  1. import asyncio
  2. import functools
  3. import warnings
  4. from collections.abc import Set
  5. from copy import deepcopy
  6. from json import JSONDecodeError
  7. import sentry_sdk
  8. from sentry_sdk.consts import OP
  9. from sentry_sdk.integrations import (
  10. DidNotEnable,
  11. Integration,
  12. _DEFAULT_FAILED_REQUEST_STATUS_CODES,
  13. )
  14. from sentry_sdk.integrations._wsgi_common import (
  15. DEFAULT_HTTP_METHODS_TO_CAPTURE,
  16. HttpCodeRangeContainer,
  17. _is_json_content_type,
  18. request_body_within_bounds,
  19. )
  20. from sentry_sdk.integrations.asgi import SentryAsgiMiddleware
  21. from sentry_sdk.scope import should_send_default_pii
  22. from sentry_sdk.tracing import (
  23. SOURCE_FOR_STYLE,
  24. TransactionSource,
  25. )
  26. from sentry_sdk.utils import (
  27. AnnotatedValue,
  28. capture_internal_exceptions,
  29. ensure_integration_enabled,
  30. event_from_exception,
  31. parse_version,
  32. transaction_from_function,
  33. )
  34. from typing import TYPE_CHECKING
  35. if TYPE_CHECKING:
  36. from typing import Any, Awaitable, Callable, Container, Dict, Optional, Tuple, Union
  37. from sentry_sdk._types import Event, HttpStatusCodeRange
  38. try:
  39. import starlette # type: ignore
  40. from starlette import __version__ as STARLETTE_VERSION
  41. from starlette.applications import Starlette # type: ignore
  42. from starlette.datastructures import UploadFile # type: ignore
  43. from starlette.middleware import Middleware # type: ignore
  44. from starlette.middleware.authentication import ( # type: ignore
  45. AuthenticationMiddleware,
  46. )
  47. from starlette.requests import Request # type: ignore
  48. from starlette.routing import Match # type: ignore
  49. from starlette.types import ASGIApp, Receive, Scope as StarletteScope, Send # type: ignore
  50. except ImportError:
  51. raise DidNotEnable("Starlette is not installed")
  52. try:
  53. # Starlette 0.20
  54. from starlette.middleware.exceptions import ExceptionMiddleware # type: ignore
  55. except ImportError:
  56. # Startlette 0.19.1
  57. from starlette.exceptions import ExceptionMiddleware # type: ignore
  58. try:
  59. # Optional dependency of Starlette to parse form data.
  60. try:
  61. # python-multipart 0.0.13 and later
  62. import python_multipart as multipart # type: ignore
  63. except ImportError:
  64. # python-multipart 0.0.12 and earlier
  65. import multipart # type: ignore
  66. except ImportError:
  67. multipart = None
  68. _DEFAULT_TRANSACTION_NAME = "generic Starlette request"
  69. TRANSACTION_STYLE_VALUES = ("endpoint", "url")
  70. class StarletteIntegration(Integration):
  71. identifier = "starlette"
  72. origin = f"auto.http.{identifier}"
  73. transaction_style = ""
  74. def __init__(
  75. self,
  76. transaction_style="url", # type: str
  77. failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Union[Set[int], list[HttpStatusCodeRange], None]
  78. middleware_spans=True, # type: bool
  79. http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: tuple[str, ...]
  80. ):
  81. # type: (...) -> None
  82. if transaction_style not in TRANSACTION_STYLE_VALUES:
  83. raise ValueError(
  84. "Invalid value for transaction_style: %s (must be in %s)"
  85. % (transaction_style, TRANSACTION_STYLE_VALUES)
  86. )
  87. self.transaction_style = transaction_style
  88. self.middleware_spans = middleware_spans
  89. self.http_methods_to_capture = tuple(map(str.upper, http_methods_to_capture))
  90. if isinstance(failed_request_status_codes, Set):
  91. self.failed_request_status_codes = failed_request_status_codes # type: Container[int]
  92. else:
  93. warnings.warn(
  94. "Passing a list or None for failed_request_status_codes is deprecated. "
  95. "Please pass a set of int instead.",
  96. DeprecationWarning,
  97. stacklevel=2,
  98. )
  99. if failed_request_status_codes is None:
  100. self.failed_request_status_codes = _DEFAULT_FAILED_REQUEST_STATUS_CODES
  101. else:
  102. self.failed_request_status_codes = HttpCodeRangeContainer(
  103. failed_request_status_codes
  104. )
  105. @staticmethod
  106. def setup_once():
  107. # type: () -> None
  108. version = parse_version(STARLETTE_VERSION)
  109. if version is None:
  110. raise DidNotEnable(
  111. "Unparsable Starlette version: {}".format(STARLETTE_VERSION)
  112. )
  113. patch_middlewares()
  114. patch_asgi_app()
  115. patch_request_response()
  116. if version >= (0, 24):
  117. patch_templates()
  118. def _enable_span_for_middleware(middleware_class):
  119. # type: (Any) -> type
  120. old_call = middleware_class.__call__
  121. async def _create_span_call(app, scope, receive, send, **kwargs):
  122. # type: (Any, Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]], Any) -> None
  123. integration = sentry_sdk.get_client().get_integration(StarletteIntegration)
  124. if integration is None or not integration.middleware_spans:
  125. return await old_call(app, scope, receive, send, **kwargs)
  126. middleware_name = app.__class__.__name__
  127. # Update transaction name with middleware name
  128. name, source = _get_transaction_from_middleware(app, scope, integration)
  129. if name is not None:
  130. sentry_sdk.get_current_scope().set_transaction_name(
  131. name,
  132. source=source,
  133. )
  134. with sentry_sdk.start_span(
  135. op=OP.MIDDLEWARE_STARLETTE,
  136. name=middleware_name,
  137. origin=StarletteIntegration.origin,
  138. ) as middleware_span:
  139. middleware_span.set_tag("starlette.middleware_name", middleware_name)
  140. # Creating spans for the "receive" callback
  141. async def _sentry_receive(*args, **kwargs):
  142. # type: (*Any, **Any) -> Any
  143. with sentry_sdk.start_span(
  144. op=OP.MIDDLEWARE_STARLETTE_RECEIVE,
  145. name=getattr(receive, "__qualname__", str(receive)),
  146. origin=StarletteIntegration.origin,
  147. ) as span:
  148. span.set_tag("starlette.middleware_name", middleware_name)
  149. return await receive(*args, **kwargs)
  150. receive_name = getattr(receive, "__name__", str(receive))
  151. receive_patched = receive_name == "_sentry_receive"
  152. new_receive = _sentry_receive if not receive_patched else receive
  153. # Creating spans for the "send" callback
  154. async def _sentry_send(*args, **kwargs):
  155. # type: (*Any, **Any) -> Any
  156. with sentry_sdk.start_span(
  157. op=OP.MIDDLEWARE_STARLETTE_SEND,
  158. name=getattr(send, "__qualname__", str(send)),
  159. origin=StarletteIntegration.origin,
  160. ) as span:
  161. span.set_tag("starlette.middleware_name", middleware_name)
  162. return await send(*args, **kwargs)
  163. send_name = getattr(send, "__name__", str(send))
  164. send_patched = send_name == "_sentry_send"
  165. new_send = _sentry_send if not send_patched else send
  166. return await old_call(app, scope, new_receive, new_send, **kwargs)
  167. not_yet_patched = old_call.__name__ not in [
  168. "_create_span_call",
  169. "_sentry_authenticationmiddleware_call",
  170. "_sentry_exceptionmiddleware_call",
  171. ]
  172. if not_yet_patched:
  173. middleware_class.__call__ = _create_span_call
  174. return middleware_class
  175. @ensure_integration_enabled(StarletteIntegration)
  176. def _capture_exception(exception, handled=False):
  177. # type: (BaseException, **Any) -> None
  178. event, hint = event_from_exception(
  179. exception,
  180. client_options=sentry_sdk.get_client().options,
  181. mechanism={"type": StarletteIntegration.identifier, "handled": handled},
  182. )
  183. sentry_sdk.capture_event(event, hint=hint)
  184. def patch_exception_middleware(middleware_class):
  185. # type: (Any) -> None
  186. """
  187. Capture all exceptions in Starlette app and
  188. also extract user information.
  189. """
  190. old_middleware_init = middleware_class.__init__
  191. not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init)
  192. if not_yet_patched:
  193. def _sentry_middleware_init(self, *args, **kwargs):
  194. # type: (Any, Any, Any) -> None
  195. old_middleware_init(self, *args, **kwargs)
  196. # Patch existing exception handlers
  197. old_handlers = self._exception_handlers.copy()
  198. async def _sentry_patched_exception_handler(self, *args, **kwargs):
  199. # type: (Any, Any, Any) -> None
  200. integration = sentry_sdk.get_client().get_integration(
  201. StarletteIntegration
  202. )
  203. exp = args[0]
  204. if integration is not None:
  205. is_http_server_error = (
  206. hasattr(exp, "status_code")
  207. and isinstance(exp.status_code, int)
  208. and exp.status_code in integration.failed_request_status_codes
  209. )
  210. if is_http_server_error:
  211. _capture_exception(exp, handled=True)
  212. # Find a matching handler
  213. old_handler = None
  214. for cls in type(exp).__mro__:
  215. if cls in old_handlers:
  216. old_handler = old_handlers[cls]
  217. break
  218. if old_handler is None:
  219. return
  220. if _is_async_callable(old_handler):
  221. return await old_handler(self, *args, **kwargs)
  222. else:
  223. return old_handler(self, *args, **kwargs)
  224. for key in self._exception_handlers.keys():
  225. self._exception_handlers[key] = _sentry_patched_exception_handler
  226. middleware_class.__init__ = _sentry_middleware_init
  227. old_call = middleware_class.__call__
  228. async def _sentry_exceptionmiddleware_call(self, scope, receive, send):
  229. # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None
  230. # Also add the user (that was eventually set by be Authentication middle
  231. # that was called before this middleware). This is done because the authentication
  232. # middleware sets the user in the scope and then (in the same function)
  233. # calls this exception middelware. In case there is no exception (or no handler
  234. # for the type of exception occuring) then the exception bubbles up and setting the
  235. # user information into the sentry scope is done in auth middleware and the
  236. # ASGI middleware will then send everything to Sentry and this is fine.
  237. # But if there is an exception happening that the exception middleware here
  238. # has a handler for, it will send the exception directly to Sentry, so we need
  239. # the user information right now.
  240. # This is why we do it here.
  241. _add_user_to_sentry_scope(scope)
  242. await old_call(self, scope, receive, send)
  243. middleware_class.__call__ = _sentry_exceptionmiddleware_call
  244. @ensure_integration_enabled(StarletteIntegration)
  245. def _add_user_to_sentry_scope(scope):
  246. # type: (Dict[str, Any]) -> None
  247. """
  248. Extracts user information from the ASGI scope and
  249. adds it to Sentry's scope.
  250. """
  251. if "user" not in scope:
  252. return
  253. if not should_send_default_pii():
  254. return
  255. user_info = {} # type: Dict[str, Any]
  256. starlette_user = scope["user"]
  257. username = getattr(starlette_user, "username", None)
  258. if username:
  259. user_info.setdefault("username", starlette_user.username)
  260. user_id = getattr(starlette_user, "id", None)
  261. if user_id:
  262. user_info.setdefault("id", starlette_user.id)
  263. email = getattr(starlette_user, "email", None)
  264. if email:
  265. user_info.setdefault("email", starlette_user.email)
  266. sentry_scope = sentry_sdk.get_isolation_scope()
  267. sentry_scope.user = user_info
  268. def patch_authentication_middleware(middleware_class):
  269. # type: (Any) -> None
  270. """
  271. Add user information to Sentry scope.
  272. """
  273. old_call = middleware_class.__call__
  274. not_yet_patched = "_sentry_authenticationmiddleware_call" not in str(old_call)
  275. if not_yet_patched:
  276. async def _sentry_authenticationmiddleware_call(self, scope, receive, send):
  277. # type: (Dict[str, Any], Dict[str, Any], Callable[[], Awaitable[Dict[str, Any]]], Callable[[Dict[str, Any]], Awaitable[None]]) -> None
  278. await old_call(self, scope, receive, send)
  279. _add_user_to_sentry_scope(scope)
  280. middleware_class.__call__ = _sentry_authenticationmiddleware_call
  281. def patch_middlewares():
  282. # type: () -> None
  283. """
  284. Patches Starlettes `Middleware` class to record
  285. spans for every middleware invoked.
  286. """
  287. old_middleware_init = Middleware.__init__
  288. not_yet_patched = "_sentry_middleware_init" not in str(old_middleware_init)
  289. if not_yet_patched:
  290. def _sentry_middleware_init(self, cls, *args, **kwargs):
  291. # type: (Any, Any, Any, Any) -> None
  292. if cls == SentryAsgiMiddleware:
  293. return old_middleware_init(self, cls, *args, **kwargs)
  294. span_enabled_cls = _enable_span_for_middleware(cls)
  295. old_middleware_init(self, span_enabled_cls, *args, **kwargs)
  296. if cls == AuthenticationMiddleware:
  297. patch_authentication_middleware(cls)
  298. if cls == ExceptionMiddleware:
  299. patch_exception_middleware(cls)
  300. Middleware.__init__ = _sentry_middleware_init
  301. def patch_asgi_app():
  302. # type: () -> None
  303. """
  304. Instrument Starlette ASGI app using the SentryAsgiMiddleware.
  305. """
  306. old_app = Starlette.__call__
  307. async def _sentry_patched_asgi_app(self, scope, receive, send):
  308. # type: (Starlette, StarletteScope, Receive, Send) -> None
  309. integration = sentry_sdk.get_client().get_integration(StarletteIntegration)
  310. if integration is None:
  311. return await old_app(self, scope, receive, send)
  312. middleware = SentryAsgiMiddleware(
  313. lambda *a, **kw: old_app(self, *a, **kw),
  314. mechanism_type=StarletteIntegration.identifier,
  315. transaction_style=integration.transaction_style,
  316. span_origin=StarletteIntegration.origin,
  317. http_methods_to_capture=(
  318. integration.http_methods_to_capture
  319. if integration
  320. else DEFAULT_HTTP_METHODS_TO_CAPTURE
  321. ),
  322. asgi_version=3,
  323. )
  324. return await middleware(scope, receive, send)
  325. Starlette.__call__ = _sentry_patched_asgi_app
  326. # This was vendored in from Starlette to support Starlette 0.19.1 because
  327. # this function was only introduced in 0.20.x
  328. def _is_async_callable(obj):
  329. # type: (Any) -> bool
  330. while isinstance(obj, functools.partial):
  331. obj = obj.func
  332. return asyncio.iscoroutinefunction(obj) or (
  333. callable(obj) and asyncio.iscoroutinefunction(obj.__call__)
  334. )
  335. def patch_request_response():
  336. # type: () -> None
  337. old_request_response = starlette.routing.request_response
  338. def _sentry_request_response(func):
  339. # type: (Callable[[Any], Any]) -> ASGIApp
  340. old_func = func
  341. is_coroutine = _is_async_callable(old_func)
  342. if is_coroutine:
  343. async def _sentry_async_func(*args, **kwargs):
  344. # type: (*Any, **Any) -> Any
  345. integration = sentry_sdk.get_client().get_integration(
  346. StarletteIntegration
  347. )
  348. if integration is None:
  349. return await old_func(*args, **kwargs)
  350. request = args[0]
  351. _set_transaction_name_and_source(
  352. sentry_sdk.get_current_scope(),
  353. integration.transaction_style,
  354. request,
  355. )
  356. sentry_scope = sentry_sdk.get_isolation_scope()
  357. extractor = StarletteRequestExtractor(request)
  358. info = await extractor.extract_request_info()
  359. def _make_request_event_processor(req, integration):
  360. # type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event]
  361. def event_processor(event, hint):
  362. # type: (Event, Dict[str, Any]) -> Event
  363. # Add info from request to event
  364. request_info = event.get("request", {})
  365. if info:
  366. if "cookies" in info:
  367. request_info["cookies"] = info["cookies"]
  368. if "data" in info:
  369. request_info["data"] = info["data"]
  370. event["request"] = deepcopy(request_info)
  371. return event
  372. return event_processor
  373. sentry_scope._name = StarletteIntegration.identifier
  374. sentry_scope.add_event_processor(
  375. _make_request_event_processor(request, integration)
  376. )
  377. return await old_func(*args, **kwargs)
  378. func = _sentry_async_func
  379. else:
  380. @functools.wraps(old_func)
  381. def _sentry_sync_func(*args, **kwargs):
  382. # type: (*Any, **Any) -> Any
  383. integration = sentry_sdk.get_client().get_integration(
  384. StarletteIntegration
  385. )
  386. if integration is None:
  387. return old_func(*args, **kwargs)
  388. current_scope = sentry_sdk.get_current_scope()
  389. if current_scope.transaction is not None:
  390. current_scope.transaction.update_active_thread()
  391. sentry_scope = sentry_sdk.get_isolation_scope()
  392. if sentry_scope.profile is not None:
  393. sentry_scope.profile.update_active_thread_id()
  394. request = args[0]
  395. _set_transaction_name_and_source(
  396. sentry_scope, integration.transaction_style, request
  397. )
  398. extractor = StarletteRequestExtractor(request)
  399. cookies = extractor.extract_cookies_from_request()
  400. def _make_request_event_processor(req, integration):
  401. # type: (Any, Any) -> Callable[[Event, dict[str, Any]], Event]
  402. def event_processor(event, hint):
  403. # type: (Event, dict[str, Any]) -> Event
  404. # Extract information from request
  405. request_info = event.get("request", {})
  406. if cookies:
  407. request_info["cookies"] = cookies
  408. event["request"] = deepcopy(request_info)
  409. return event
  410. return event_processor
  411. sentry_scope._name = StarletteIntegration.identifier
  412. sentry_scope.add_event_processor(
  413. _make_request_event_processor(request, integration)
  414. )
  415. return old_func(*args, **kwargs)
  416. func = _sentry_sync_func
  417. return old_request_response(func)
  418. starlette.routing.request_response = _sentry_request_response
  419. def patch_templates():
  420. # type: () -> None
  421. # If markupsafe is not installed, then Jinja2 is not installed
  422. # (markupsafe is a dependency of Jinja2)
  423. # In this case we do not need to patch the Jinja2Templates class
  424. try:
  425. from markupsafe import Markup
  426. except ImportError:
  427. return # Nothing to do
  428. from starlette.templating import Jinja2Templates # type: ignore
  429. old_jinja2templates_init = Jinja2Templates.__init__
  430. not_yet_patched = "_sentry_jinja2templates_init" not in str(
  431. old_jinja2templates_init
  432. )
  433. if not_yet_patched:
  434. def _sentry_jinja2templates_init(self, *args, **kwargs):
  435. # type: (Jinja2Templates, *Any, **Any) -> None
  436. def add_sentry_trace_meta(request):
  437. # type: (Request) -> Dict[str, Any]
  438. trace_meta = Markup(
  439. sentry_sdk.get_current_scope().trace_propagation_meta()
  440. )
  441. return {
  442. "sentry_trace_meta": trace_meta,
  443. }
  444. kwargs.setdefault("context_processors", [])
  445. if add_sentry_trace_meta not in kwargs["context_processors"]:
  446. kwargs["context_processors"].append(add_sentry_trace_meta)
  447. return old_jinja2templates_init(self, *args, **kwargs)
  448. Jinja2Templates.__init__ = _sentry_jinja2templates_init
  449. class StarletteRequestExtractor:
  450. """
  451. Extracts useful information from the Starlette request
  452. (like form data or cookies) and adds it to the Sentry event.
  453. """
  454. request = None # type: Request
  455. def __init__(self, request):
  456. # type: (StarletteRequestExtractor, Request) -> None
  457. self.request = request
  458. def extract_cookies_from_request(self):
  459. # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
  460. cookies = None # type: Optional[Dict[str, Any]]
  461. if should_send_default_pii():
  462. cookies = self.cookies()
  463. return cookies
  464. async def extract_request_info(self):
  465. # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
  466. client = sentry_sdk.get_client()
  467. request_info = {} # type: Dict[str, Any]
  468. with capture_internal_exceptions():
  469. # Add cookies
  470. if should_send_default_pii():
  471. request_info["cookies"] = self.cookies()
  472. # If there is no body, just return the cookies
  473. content_length = await self.content_length()
  474. if not content_length:
  475. return request_info
  476. # Add annotation if body is too big
  477. if content_length and not request_body_within_bounds(
  478. client, content_length
  479. ):
  480. request_info["data"] = AnnotatedValue.removed_because_over_size_limit()
  481. return request_info
  482. # Add JSON body, if it is a JSON request
  483. json = await self.json()
  484. if json:
  485. request_info["data"] = json
  486. return request_info
  487. # Add form as key/value pairs, if request has form data
  488. form = await self.form()
  489. if form:
  490. form_data = {}
  491. for key, val in form.items():
  492. is_file = isinstance(val, UploadFile)
  493. form_data[key] = (
  494. val
  495. if not is_file
  496. else AnnotatedValue.removed_because_raw_data()
  497. )
  498. request_info["data"] = form_data
  499. return request_info
  500. # Raw data, do not add body just an annotation
  501. request_info["data"] = AnnotatedValue.removed_because_raw_data()
  502. return request_info
  503. async def content_length(self):
  504. # type: (StarletteRequestExtractor) -> Optional[int]
  505. if "content-length" in self.request.headers:
  506. return int(self.request.headers["content-length"])
  507. return None
  508. def cookies(self):
  509. # type: (StarletteRequestExtractor) -> Dict[str, Any]
  510. return self.request.cookies
  511. async def form(self):
  512. # type: (StarletteRequestExtractor) -> Any
  513. if multipart is None:
  514. return None
  515. # Parse the body first to get it cached, as Starlette does not cache form() as it
  516. # does with body() and json() https://github.com/encode/starlette/discussions/1933
  517. # Calling `.form()` without calling `.body()` first will
  518. # potentially break the users project.
  519. await self.request.body()
  520. return await self.request.form()
  521. def is_json(self):
  522. # type: (StarletteRequestExtractor) -> bool
  523. return _is_json_content_type(self.request.headers.get("content-type"))
  524. async def json(self):
  525. # type: (StarletteRequestExtractor) -> Optional[Dict[str, Any]]
  526. if not self.is_json():
  527. return None
  528. try:
  529. return await self.request.json()
  530. except JSONDecodeError:
  531. return None
  532. def _transaction_name_from_router(scope):
  533. # type: (StarletteScope) -> Optional[str]
  534. router = scope.get("router")
  535. if not router:
  536. return None
  537. for route in router.routes:
  538. match = route.matches(scope)
  539. if match[0] == Match.FULL:
  540. try:
  541. return route.path
  542. except AttributeError:
  543. # routes added via app.host() won't have a path attribute
  544. return scope.get("path")
  545. return None
  546. def _set_transaction_name_and_source(scope, transaction_style, request):
  547. # type: (sentry_sdk.Scope, str, Any) -> None
  548. name = None
  549. source = SOURCE_FOR_STYLE[transaction_style]
  550. if transaction_style == "endpoint":
  551. endpoint = request.scope.get("endpoint")
  552. if endpoint:
  553. name = transaction_from_function(endpoint) or None
  554. elif transaction_style == "url":
  555. name = _transaction_name_from_router(request.scope)
  556. if name is None:
  557. name = _DEFAULT_TRANSACTION_NAME
  558. source = TransactionSource.ROUTE
  559. scope.set_transaction_name(name, source=source)
  560. def _get_transaction_from_middleware(app, asgi_scope, integration):
  561. # type: (Any, Dict[str, Any], StarletteIntegration) -> Tuple[Optional[str], Optional[str]]
  562. name = None
  563. source = None
  564. if integration.transaction_style == "endpoint":
  565. name = transaction_from_function(app.__class__)
  566. source = TransactionSource.COMPONENT
  567. elif integration.transaction_style == "url":
  568. name = _transaction_name_from_router(asgi_scope)
  569. source = TransactionSource.ROUTE
  570. return name, source