views.py 7.0 KB

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