pyramid.py 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  1. import functools
  2. import os
  3. import sys
  4. import weakref
  5. import sentry_sdk
  6. from sentry_sdk.integrations import Integration, DidNotEnable
  7. from sentry_sdk.integrations._wsgi_common import RequestExtractor
  8. from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
  9. from sentry_sdk.scope import should_send_default_pii
  10. from sentry_sdk.tracing import SOURCE_FOR_STYLE
  11. from sentry_sdk.utils import (
  12. capture_internal_exceptions,
  13. ensure_integration_enabled,
  14. event_from_exception,
  15. reraise,
  16. )
  17. try:
  18. from pyramid.httpexceptions import HTTPException
  19. from pyramid.request import Request
  20. except ImportError:
  21. raise DidNotEnable("Pyramid not installed")
  22. from typing import TYPE_CHECKING
  23. if TYPE_CHECKING:
  24. from pyramid.response import Response
  25. from typing import Any
  26. from sentry_sdk.integrations.wsgi import _ScopedResponse
  27. from typing import Callable
  28. from typing import Dict
  29. from typing import Optional
  30. from webob.cookies import RequestCookies
  31. from webob.request import _FieldStorageWithFile
  32. from sentry_sdk.utils import ExcInfo
  33. from sentry_sdk._types import Event, EventProcessor
  34. if getattr(Request, "authenticated_userid", None):
  35. def authenticated_userid(request):
  36. # type: (Request) -> Optional[Any]
  37. return request.authenticated_userid
  38. else:
  39. # bw-compat for pyramid < 1.5
  40. from pyramid.security import authenticated_userid # type: ignore
  41. TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern")
  42. class PyramidIntegration(Integration):
  43. identifier = "pyramid"
  44. origin = f"auto.http.{identifier}"
  45. transaction_style = ""
  46. def __init__(self, transaction_style="route_name"):
  47. # type: (str) -> None
  48. if transaction_style not in TRANSACTION_STYLE_VALUES:
  49. raise ValueError(
  50. "Invalid value for transaction_style: %s (must be in %s)"
  51. % (transaction_style, TRANSACTION_STYLE_VALUES)
  52. )
  53. self.transaction_style = transaction_style
  54. @staticmethod
  55. def setup_once():
  56. # type: () -> None
  57. from pyramid import router
  58. old_call_view = router._call_view
  59. @functools.wraps(old_call_view)
  60. def sentry_patched_call_view(registry, request, *args, **kwargs):
  61. # type: (Any, Request, *Any, **Any) -> Response
  62. integration = sentry_sdk.get_client().get_integration(PyramidIntegration)
  63. if integration is None:
  64. return old_call_view(registry, request, *args, **kwargs)
  65. _set_transaction_name_and_source(
  66. sentry_sdk.get_current_scope(), integration.transaction_style, request
  67. )
  68. scope = sentry_sdk.get_isolation_scope()
  69. scope.add_event_processor(
  70. _make_event_processor(weakref.ref(request), integration)
  71. )
  72. return old_call_view(registry, request, *args, **kwargs)
  73. router._call_view = sentry_patched_call_view
  74. if hasattr(Request, "invoke_exception_view"):
  75. old_invoke_exception_view = Request.invoke_exception_view
  76. def sentry_patched_invoke_exception_view(self, *args, **kwargs):
  77. # type: (Request, *Any, **Any) -> Any
  78. rv = old_invoke_exception_view(self, *args, **kwargs)
  79. if (
  80. self.exc_info
  81. and all(self.exc_info)
  82. and rv.status_int == 500
  83. and sentry_sdk.get_client().get_integration(PyramidIntegration)
  84. is not None
  85. ):
  86. _capture_exception(self.exc_info)
  87. return rv
  88. Request.invoke_exception_view = sentry_patched_invoke_exception_view
  89. old_wsgi_call = router.Router.__call__
  90. @ensure_integration_enabled(PyramidIntegration, old_wsgi_call)
  91. def sentry_patched_wsgi_call(self, environ, start_response):
  92. # type: (Any, Dict[str, str], Callable[..., Any]) -> _ScopedResponse
  93. def sentry_patched_inner_wsgi_call(environ, start_response):
  94. # type: (Dict[str, Any], Callable[..., Any]) -> Any
  95. try:
  96. return old_wsgi_call(self, environ, start_response)
  97. except Exception:
  98. einfo = sys.exc_info()
  99. _capture_exception(einfo)
  100. reraise(*einfo)
  101. middleware = SentryWsgiMiddleware(
  102. sentry_patched_inner_wsgi_call,
  103. span_origin=PyramidIntegration.origin,
  104. )
  105. return middleware(environ, start_response)
  106. router.Router.__call__ = sentry_patched_wsgi_call
  107. @ensure_integration_enabled(PyramidIntegration)
  108. def _capture_exception(exc_info):
  109. # type: (ExcInfo) -> None
  110. if exc_info[0] is None or issubclass(exc_info[0], HTTPException):
  111. return
  112. event, hint = event_from_exception(
  113. exc_info,
  114. client_options=sentry_sdk.get_client().options,
  115. mechanism={"type": "pyramid", "handled": False},
  116. )
  117. sentry_sdk.capture_event(event, hint=hint)
  118. def _set_transaction_name_and_source(scope, transaction_style, request):
  119. # type: (sentry_sdk.Scope, str, Request) -> None
  120. try:
  121. name_for_style = {
  122. "route_name": request.matched_route.name,
  123. "route_pattern": request.matched_route.pattern,
  124. }
  125. scope.set_transaction_name(
  126. name_for_style[transaction_style],
  127. source=SOURCE_FOR_STYLE[transaction_style],
  128. )
  129. except Exception:
  130. pass
  131. class PyramidRequestExtractor(RequestExtractor):
  132. def url(self):
  133. # type: () -> str
  134. return self.request.path_url
  135. def env(self):
  136. # type: () -> Dict[str, str]
  137. return self.request.environ
  138. def cookies(self):
  139. # type: () -> RequestCookies
  140. return self.request.cookies
  141. def raw_data(self):
  142. # type: () -> str
  143. return self.request.text
  144. def form(self):
  145. # type: () -> Dict[str, str]
  146. return {
  147. key: value
  148. for key, value in self.request.POST.items()
  149. if not getattr(value, "filename", None)
  150. }
  151. def files(self):
  152. # type: () -> Dict[str, _FieldStorageWithFile]
  153. return {
  154. key: value
  155. for key, value in self.request.POST.items()
  156. if getattr(value, "filename", None)
  157. }
  158. def size_of_file(self, postdata):
  159. # type: (_FieldStorageWithFile) -> int
  160. file = postdata.file
  161. try:
  162. return os.fstat(file.fileno()).st_size
  163. except Exception:
  164. return 0
  165. def _make_event_processor(weak_request, integration):
  166. # type: (Callable[[], Request], PyramidIntegration) -> EventProcessor
  167. def pyramid_event_processor(event, hint):
  168. # type: (Event, Dict[str, Any]) -> Event
  169. request = weak_request()
  170. if request is None:
  171. return event
  172. with capture_internal_exceptions():
  173. PyramidRequestExtractor(request).extract_into_event(event)
  174. if should_send_default_pii():
  175. with capture_internal_exceptions():
  176. user_info = event.setdefault("user", {})
  177. user_info.setdefault("id", authenticated_userid(request))
  178. return event
  179. return pyramid_event_processor