starlette.py 26 KB

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