pyramid.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  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: "Request") -> "Optional[Any]":
  36. return request.authenticated_userid
  37. else:
  38. # bw-compat for pyramid < 1.5
  39. from pyramid.security import authenticated_userid # type: ignore
  40. TRANSACTION_STYLE_VALUES = ("route_name", "route_pattern")
  41. class PyramidIntegration(Integration):
  42. identifier = "pyramid"
  43. origin = f"auto.http.{identifier}"
  44. transaction_style = ""
  45. def __init__(self, transaction_style: str = "route_name") -> None:
  46. if transaction_style not in TRANSACTION_STYLE_VALUES:
  47. raise ValueError(
  48. "Invalid value for transaction_style: %s (must be in %s)"
  49. % (transaction_style, TRANSACTION_STYLE_VALUES)
  50. )
  51. self.transaction_style = transaction_style
  52. @staticmethod
  53. def setup_once() -> None:
  54. from pyramid import router
  55. old_call_view = router._call_view
  56. @functools.wraps(old_call_view)
  57. def sentry_patched_call_view(
  58. registry: "Any", request: "Request", *args: "Any", **kwargs: "Any"
  59. ) -> "Response":
  60. integration = sentry_sdk.get_client().get_integration(PyramidIntegration)
  61. if integration is None:
  62. return old_call_view(registry, request, *args, **kwargs)
  63. _set_transaction_name_and_source(
  64. sentry_sdk.get_current_scope(), integration.transaction_style, request
  65. )
  66. scope = sentry_sdk.get_isolation_scope()
  67. scope.add_event_processor(
  68. _make_event_processor(weakref.ref(request), integration)
  69. )
  70. return old_call_view(registry, request, *args, **kwargs)
  71. router._call_view = sentry_patched_call_view
  72. if hasattr(Request, "invoke_exception_view"):
  73. old_invoke_exception_view = Request.invoke_exception_view
  74. def sentry_patched_invoke_exception_view(
  75. self: "Request", *args: "Any", **kwargs: "Any"
  76. ) -> "Any":
  77. rv = old_invoke_exception_view(self, *args, **kwargs)
  78. if (
  79. self.exc_info
  80. and all(self.exc_info)
  81. and rv.status_int == 500
  82. and sentry_sdk.get_client().get_integration(PyramidIntegration)
  83. is not None
  84. ):
  85. _capture_exception(self.exc_info)
  86. return rv
  87. Request.invoke_exception_view = sentry_patched_invoke_exception_view
  88. old_wsgi_call = router.Router.__call__
  89. @ensure_integration_enabled(PyramidIntegration, old_wsgi_call)
  90. def sentry_patched_wsgi_call(
  91. self: "Any", environ: "Dict[str, str]", start_response: "Callable[..., Any]"
  92. ) -> "_ScopedResponse":
  93. def sentry_patched_inner_wsgi_call(
  94. environ: "Dict[str, Any]", start_response: "Callable[..., Any]"
  95. ) -> "Any":
  96. try:
  97. return old_wsgi_call(self, environ, start_response)
  98. except Exception:
  99. einfo = sys.exc_info()
  100. _capture_exception(einfo)
  101. reraise(*einfo)
  102. middleware = SentryWsgiMiddleware(
  103. sentry_patched_inner_wsgi_call,
  104. span_origin=PyramidIntegration.origin,
  105. )
  106. return middleware(environ, start_response)
  107. router.Router.__call__ = sentry_patched_wsgi_call
  108. @ensure_integration_enabled(PyramidIntegration)
  109. def _capture_exception(exc_info: "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(
  119. scope: "sentry_sdk.Scope", transaction_style: str, request: "Request"
  120. ) -> None:
  121. try:
  122. name_for_style = {
  123. "route_name": request.matched_route.name,
  124. "route_pattern": request.matched_route.pattern,
  125. }
  126. scope.set_transaction_name(
  127. name_for_style[transaction_style],
  128. source=SOURCE_FOR_STYLE[transaction_style],
  129. )
  130. except Exception:
  131. pass
  132. class PyramidRequestExtractor(RequestExtractor):
  133. def url(self) -> str:
  134. return self.request.path_url
  135. def env(self) -> "Dict[str, str]":
  136. return self.request.environ
  137. def cookies(self) -> "RequestCookies":
  138. return self.request.cookies
  139. def raw_data(self) -> str:
  140. return self.request.text
  141. def form(self) -> "Dict[str, str]":
  142. return {
  143. key: value
  144. for key, value in self.request.POST.items()
  145. if not getattr(value, "filename", None)
  146. }
  147. def files(self) -> "Dict[str, _FieldStorageWithFile]":
  148. return {
  149. key: value
  150. for key, value in self.request.POST.items()
  151. if getattr(value, "filename", None)
  152. }
  153. def size_of_file(self, postdata: "_FieldStorageWithFile") -> int:
  154. file = postdata.file
  155. try:
  156. return os.fstat(file.fileno()).st_size
  157. except Exception:
  158. return 0
  159. def _make_event_processor(
  160. weak_request: "Callable[[], Request]", integration: "PyramidIntegration"
  161. ) -> "EventProcessor":
  162. def pyramid_event_processor(event: "Event", hint: "Dict[str, Any]") -> "Event":
  163. request = weak_request()
  164. if request is None:
  165. return event
  166. with capture_internal_exceptions():
  167. PyramidRequestExtractor(request).extract_into_event(event)
  168. if should_send_default_pii():
  169. with capture_internal_exceptions():
  170. user_info = event.setdefault("user", {})
  171. user_info.setdefault("id", authenticated_userid(request))
  172. return event
  173. return pyramid_event_processor