views.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. import json
  2. import logging
  3. import random
  4. import string
  5. import uuid
  6. from urllib.parse import urlparse
  7. from django.conf import settings
  8. from django.core.exceptions import SuspiciousOperation, ValidationError
  9. from django.db import connection
  10. from django.db.models import Exists, OuterRef
  11. from django.db.utils import IntegrityError
  12. from django.http import HttpResponse
  13. from django.test import RequestFactory
  14. from django.views.decorators.csrf import csrf_exempt
  15. from rest_framework import exceptions, permissions, status
  16. from rest_framework.response import Response
  17. from rest_framework.views import APIView
  18. from sentry_sdk import capture_exception, set_context, set_level
  19. from difs.models import DebugInformationFile
  20. from difs.tasks import difs_run_resolve_stacktrace
  21. from performance.serializers import TransactionEventSerializer
  22. from projects.models import Project
  23. from sentry.utils.auth import parse_auth_header
  24. from .negotiation import IgnoreClientContentNegotiation
  25. from .parsers import EnvelopeParser
  26. from .serializers import (
  27. EnvelopeHeaderSerializer,
  28. StoreCSPReportSerializer,
  29. StoreDefaultSerializer,
  30. StoreErrorSerializer,
  31. )
  32. logger = logging.getLogger(__name__)
  33. def test_event_view(request):
  34. """
  35. This view is used only to test event store performance
  36. It requires DEBUG to be True
  37. """
  38. factory = RequestFactory()
  39. request = request = factory.get(
  40. "/api/6/store/?sentry_key=244703e8083f4b16988c376ea46e9a08"
  41. )
  42. with open("events/test_data/py_hi_event.json") as json_file:
  43. data = json.load(json_file)
  44. data["event_id"] = uuid.uuid4()
  45. data["message"] = "".join(
  46. random.choices(string.ascii_uppercase + string.digits, k=8)
  47. )
  48. request.data = data
  49. EventStoreAPIView().post(request, id=6)
  50. return HttpResponse("<html><body></body></html>")
  51. class BaseEventAPIView(APIView):
  52. permission_classes = [permissions.AllowAny]
  53. authentication_classes = []
  54. # content_negotiation_class = IgnoreClientContentNegotiation
  55. http_method_names = ["post", "get"]
  56. @classmethod
  57. def auth_from_request(cls, request):
  58. # Accept both sentry or glitchtip prefix.
  59. # Prefer glitchtip when not using a sentry SDK but support both.
  60. result = {
  61. k: request.GET[k]
  62. for k in request.GET.keys()
  63. if k[:7] == "sentry_" or k[:10] == "glitchtip_"
  64. }
  65. if request.META.get("HTTP_X_SENTRY_AUTH", "")[:7].lower() == "sentry ":
  66. if result:
  67. raise SuspiciousOperation(
  68. "Multiple authentication payloads were detected."
  69. )
  70. result = parse_auth_header(request.META["HTTP_X_SENTRY_AUTH"])
  71. elif request.META.get("HTTP_AUTHORIZATION", "")[:7].lower() == "sentry ":
  72. if result:
  73. raise SuspiciousOperation(
  74. "Multiple authentication payloads were detected."
  75. )
  76. result = parse_auth_header(request.META["HTTP_AUTHORIZATION"])
  77. if not result:
  78. if (
  79. isinstance(request.data, list)
  80. and len(request.data)
  81. and "dsn" in request.data[0]
  82. ):
  83. dsn = urlparse(request.data[0]["dsn"])
  84. if username := dsn.username:
  85. return username
  86. raise exceptions.NotAuthenticated(
  87. "Unable to find authentication information"
  88. )
  89. try:
  90. key = uuid.UUID(result.get("sentry_key", result.get("glitchtip_key")))
  91. except ValueError as err:
  92. raise exceptions.AuthenticationFailed("invalid api key", code=401) from err
  93. return key
  94. def event_preprocess(self, request, project_id):
  95. """
  96. Check auth, prefetch related data to be used after deserialization
  97. Use database function for optimization.
  98. Return context to be used with serializer
  99. """
  100. if settings.EVENT_STORE_DEBUG:
  101. print(json.dumps(request.data))
  102. sentry_key = BaseEventAPIView.auth_from_request(request)
  103. release = request.data.get("release")
  104. if release:
  105. release = release[:256]
  106. environment = request.data.get("environment")
  107. if environment:
  108. environment = environment[:256]
  109. if "/" in environment:
  110. environment = None
  111. with connection.cursor() as cursor:
  112. cursor.execute(
  113. "SELECT * FROM event_preprocess(%s, %s, %s, %s)",
  114. (project_id, sentry_key, release, environment),
  115. )
  116. result = cursor.fetchone()
  117. context = {
  118. "organization_id": result[0],
  119. "has_diffs": result[1],
  120. "is_accepting_events": result[2],
  121. "should_scrub_ip_addresses": result[3],
  122. "release_id": result[4],
  123. "environment_id": result[5],
  124. "project_id": project_id,
  125. }
  126. if not context["organization_id"]:
  127. raise exceptions.AuthenticationFailed({"error": "Invalid api key"})
  128. if not context["is_accepting_events"]:
  129. raise exceptions.Throttled(detail="event rejected due to rate limit")
  130. return context
  131. def get_event_serializer_class(self, data=None):
  132. """Determine event type and return serializer"""
  133. return StoreErrorSerializer
  134. if data is None:
  135. data = []
  136. if "exception" in data and data["exception"]:
  137. return StoreErrorSerializer
  138. if "platform" not in data:
  139. return StoreCSPReportSerializer
  140. return StoreDefaultSerializer
  141. def process_event(self, data, context):
  142. """Determine correct serializer and save event"""
  143. set_context("incoming event", data)
  144. serializer = self.get_event_serializer_class(data)(
  145. data=data,
  146. context=context,
  147. )
  148. try:
  149. serializer.is_valid(raise_exception=True)
  150. except exceptions.ValidationError as err:
  151. set_level("warning")
  152. capture_exception(err)
  153. logger.warning("Invalid event %s", serializer.errors)
  154. return Response()
  155. event = serializer.save()
  156. if event.data.get("exception") is not None and context["has_diffs"]:
  157. difs_run_resolve_stacktrace(event.event_id)
  158. return Response({"id": event.event_id_hex})
  159. class EventStoreAPIView(BaseEventAPIView):
  160. def get(self, request, *args, **kwargs):
  161. return Response({"hello": "there"})
  162. @csrf_exempt
  163. def post(self, request, *args, **kwargs):
  164. if settings.MAINTENANCE_EVENT_FREEZE:
  165. return Response(
  166. {
  167. "message": "Events are not currently being accepted due to database maintenance."
  168. },
  169. status=status.HTTP_503_SERVICE_UNAVAILABLE,
  170. )
  171. project_id = kwargs.get("id")
  172. try:
  173. context = self.event_preprocess(request, project_id)
  174. except exceptions.AuthenticationFailed as err:
  175. # Replace 403 status code with 401 to match OSS Sentry
  176. return Response(err.detail, status=401)
  177. context["request"] = request
  178. return self.process_event(request.data, context)
  179. class CSPStoreAPIView(EventStoreAPIView):
  180. pass
  181. class EnvelopeAPIView(BaseEventAPIView):
  182. parser_classes = [EnvelopeParser]
  183. def get_serializer_class(self):
  184. return TransactionEventSerializer
  185. def post(self, request, *args, **kwargs):
  186. if settings.MAINTENANCE_EVENT_FREEZE:
  187. return Response(
  188. {
  189. "message": "Events are not currently being accepted due to database maintenance."
  190. },
  191. status=status.HTTP_503_SERVICE_UNAVAILABLE,
  192. )
  193. if settings.EVENT_STORE_DEBUG:
  194. print(json.dumps(request.data))
  195. project = self.get_project(request, kwargs.get("id"))
  196. data = request.data
  197. if len(data) < 2:
  198. logger.warning("Envelope has no headers %s", data)
  199. raise exceptions.ValidationError("Envelope has no headers")
  200. event_header_serializer = EnvelopeHeaderSerializer(data=data.pop(0))
  201. event_header_serializer.is_valid(raise_exception=True)
  202. # Multi part envelopes are not yet supported
  203. message_header = data.pop(0)
  204. if message_header.get("type") == "transaction":
  205. serializer = self.get_serializer_class()(
  206. data=data.pop(0), context={"request": self.request, "project": project}
  207. )
  208. try:
  209. serializer.is_valid(raise_exception=True)
  210. except exceptions.ValidationError as err:
  211. logger.warning("Invalid envelope payload", exc_info=True)
  212. raise err
  213. try:
  214. event = serializer.save()
  215. except IntegrityError as err:
  216. logger.warning("Duplicate event id", exc_info=True)
  217. raise exceptions.ValidationError("Duplicate event id") from err
  218. return Response({"id": event.event_id_hex})
  219. elif message_header.get("type") == "event":
  220. event_data = data.pop(0)
  221. return self.process_event(event_data, request, project)
  222. elif message_header.get("type") == "session":
  223. return Response(
  224. {"message": "Session events are not supported at this time."},
  225. status=status.HTTP_501_NOT_IMPLEMENTED,
  226. )
  227. return Response(status=status.HTTP_501_NOT_IMPLEMENTED)