views.py 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. import json
  2. import logging
  3. import random
  4. import string
  5. import uuid
  6. from django.conf import settings
  7. from django.core.exceptions import SuspiciousOperation, ValidationError
  8. from django.db.models import Exists, OuterRef
  9. from django.db.utils import IntegrityError
  10. from django.http import HttpResponse
  11. from django.test import RequestFactory
  12. from rest_framework import exceptions, permissions, status
  13. from rest_framework.response import Response
  14. from rest_framework.views import APIView
  15. from sentry_sdk import capture_exception, set_context, set_level
  16. from difs.models import DebugInformationFile
  17. from difs.tasks import difs_run_resolve_stacktrace
  18. from performance.serializers import TransactionEventSerializer
  19. from projects.models import Project
  20. from sentry.utils.auth import parse_auth_header
  21. from .negotiation import IgnoreClientContentNegotiation
  22. from .parsers import EnvelopeParser
  23. from .serializers import (
  24. EnvelopeHeaderSerializer,
  25. StoreCSPReportSerializer,
  26. StoreDefaultSerializer,
  27. StoreErrorSerializer,
  28. )
  29. logger = logging.getLogger(__name__)
  30. def test_event_view(request):
  31. """
  32. This view is used only to test event store performance
  33. It requires DEBUG to be True
  34. """
  35. factory = RequestFactory()
  36. request = request = factory.get(
  37. "/api/6/store/?sentry_key=244703e8083f4b16988c376ea46e9a08"
  38. )
  39. with open("events/test_data/py_hi_event.json") as json_file:
  40. data = json.load(json_file)
  41. data["event_id"] = uuid.uuid4()
  42. data["message"] = "".join(
  43. random.choices(string.ascii_uppercase + string.digits, k=8)
  44. )
  45. request.data = data
  46. EventStoreAPIView().post(request, id=6)
  47. return HttpResponse("<html><body></body></html>")
  48. class BaseEventAPIView(APIView):
  49. permission_classes = [permissions.AllowAny]
  50. authentication_classes = []
  51. content_negotiation_class = IgnoreClientContentNegotiation
  52. http_method_names = ["post"]
  53. @classmethod
  54. def auth_from_request(cls, request):
  55. # Accept both sentry or glitchtip prefix.
  56. # Prefer glitchtip when not using a sentry SDK but support both.
  57. result = {
  58. k: request.GET[k]
  59. for k in request.GET.keys()
  60. if k[:7] == "sentry_" or k[:10] == "glitchtip_"
  61. }
  62. if request.META.get("HTTP_X_SENTRY_AUTH", "")[:7].lower() == "sentry ":
  63. if result:
  64. raise SuspiciousOperation(
  65. "Multiple authentication payloads were detected."
  66. )
  67. result = parse_auth_header(request.META["HTTP_X_SENTRY_AUTH"])
  68. elif request.META.get("HTTP_AUTHORIZATION", "")[:7].lower() == "sentry ":
  69. if result:
  70. raise SuspiciousOperation(
  71. "Multiple authentication payloads were detected."
  72. )
  73. result = parse_auth_header(request.META["HTTP_AUTHORIZATION"])
  74. if not result:
  75. raise exceptions.NotAuthenticated(
  76. "Unable to find authentication information"
  77. )
  78. return result.get("sentry_key", result.get("glitchtip_key"))
  79. def get_project(self, request, project_id):
  80. sentry_key = BaseEventAPIView.auth_from_request(request)
  81. difs_subquery = DebugInformationFile.objects.filter(project_id=OuterRef("pk"))
  82. try:
  83. project = (
  84. Project.objects.filter(id=project_id, projectkey__public_key=sentry_key)
  85. .annotate(has_difs=Exists(difs_subquery))
  86. .select_related("organization")
  87. .only("id", "first_event", "organization__is_accepting_events")
  88. .first()
  89. )
  90. except ValidationError as err:
  91. raise exceptions.AuthenticationFailed({"error": "Invalid api key"}) from err
  92. if not project:
  93. if Project.objects.filter(id=project_id).exists():
  94. raise exceptions.AuthenticationFailed({"error": "Invalid api key"})
  95. raise exceptions.ValidationError("Invalid project_id: %s" % project_id)
  96. if not project.organization.is_accepting_events:
  97. raise exceptions.Throttled(detail="event rejected due to rate limit")
  98. return project
  99. def get_event_serializer_class(self, data=None):
  100. """Determine event type and return serializer"""
  101. if data is None:
  102. data = []
  103. if "exception" in data and data["exception"]:
  104. return StoreErrorSerializer
  105. if "platform" not in data:
  106. return StoreCSPReportSerializer
  107. return StoreDefaultSerializer
  108. def process_event(self, data, request, project):
  109. set_context("incoming event", data)
  110. serializer = self.get_event_serializer_class(data)(
  111. data=data, context={"request": self.request, "project": project}
  112. )
  113. try:
  114. serializer.is_valid(raise_exception=True)
  115. except exceptions.ValidationError as err:
  116. set_level("warning")
  117. capture_exception(err)
  118. logger.warning("Invalid event %s", serializer.errors)
  119. return Response()
  120. event = serializer.save()
  121. if event.data.get("exception") is not None and project.has_difs:
  122. difs_run_resolve_stacktrace(event.event_id)
  123. return Response({"id": event.event_id_hex})
  124. class EventStoreAPIView(BaseEventAPIView):
  125. def post(self, request, *args, **kwargs):
  126. if settings.EVENT_STORE_DEBUG:
  127. print(json.dumps(request.data))
  128. try:
  129. project = self.get_project(request, kwargs.get("id"))
  130. except exceptions.AuthenticationFailed as err:
  131. # Replace 403 status code with 401 to match OSS Sentry
  132. return Response(err.detail, status=401)
  133. return self.process_event(request.data, request, project)
  134. class CSPStoreAPIView(EventStoreAPIView):
  135. pass
  136. class EnvelopeAPIView(BaseEventAPIView):
  137. parser_classes = [EnvelopeParser]
  138. def get_serializer_class(self):
  139. return TransactionEventSerializer
  140. def post(self, request, *args, **kwargs):
  141. if settings.EVENT_STORE_DEBUG:
  142. print(json.dumps(request.data))
  143. project = self.get_project(request, kwargs.get("id"))
  144. data = request.data
  145. if len(data) < 2:
  146. logger.warning("Envelope has no headers %s", data)
  147. raise exceptions.ValidationError("Envelope has no headers")
  148. event_header_serializer = EnvelopeHeaderSerializer(data=data.pop(0))
  149. event_header_serializer.is_valid(raise_exception=True)
  150. # Multi part envelopes are not yet supported
  151. message_header = data.pop(0)
  152. if message_header.get("type") == "transaction":
  153. serializer = self.get_serializer_class()(
  154. data=data.pop(0), context={"request": self.request, "project": project}
  155. )
  156. serializer.is_valid(raise_exception=True)
  157. try:
  158. event = serializer.save()
  159. except IntegrityError as err:
  160. logger.warning("Duplicate event id", exc_info=True)
  161. raise exceptions.ValidationError("Duplicate event id") from err
  162. return Response({"id": event.event_id_hex})
  163. elif message_header.get("type") == "event":
  164. event_data = data.pop(0)
  165. return self.process_event(event_data, request, project)
  166. elif message_header.get("type") == "session":
  167. return Response(
  168. {"message": "Session events are not supported at this time."},
  169. status=status.HTTP_501_NOT_IMPLEMENTED,
  170. )
  171. return Response(status=status.HTTP_501_NOT_IMPLEMENTED)