views.py 9.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  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 ValidationError
  9. from django.db.models import Exists, OuterRef
  10. from django.db.models.expressions import RawSQL
  11. from django.db.utils import IntegrityError
  12. from django.http import HttpResponse
  13. from django.test import RequestFactory
  14. from rest_framework import exceptions, permissions, status
  15. from rest_framework.response import Response
  16. from rest_framework.views import APIView
  17. from sentry_sdk import capture_exception, set_context, set_level
  18. from difs.models import DebugInformationFile
  19. from difs.tasks import difs_run_resolve_stacktrace
  20. from glitchtip.exceptions import ServiceUnavailableException
  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"]
  56. def check_status(self):
  57. if settings.MAINTENANCE_EVENT_FREEZE:
  58. raise ServiceUnavailableException(
  59. {
  60. "message": "Events are not currently being accepted due to maintenance."
  61. },
  62. )
  63. if settings.EVENT_STORE_DEBUG:
  64. print(json.dumps(self.request.data))
  65. @classmethod
  66. def auth_from_request(cls, request):
  67. # Accept both sentry or glitchtip prefix.
  68. for k in request.GET.keys():
  69. if k in ["sentry_key", "glitchtip_key"]:
  70. return request.GET[k]
  71. if auth_header := request.META.get(
  72. "HTTP_X_SENTRY_AUTH", request.META.get("HTTP_AUTHORIZATION")
  73. ):
  74. result = parse_auth_header(auth_header)
  75. return result.get("sentry_key", result.get("glitchtip_key"))
  76. if isinstance(request.data, list):
  77. if data_first := next(iter(request.data), None):
  78. if isinstance(data_first, dict):
  79. dsn = urlparse(data_first.get("dsn"))
  80. if username := dsn.username:
  81. return username
  82. raise exceptions.NotAuthenticated("Unable to find authentication information")
  83. def get_project(self, request, project_id):
  84. sentry_key = BaseEventAPIView.auth_from_request(request)
  85. difs_subquery = DebugInformationFile.objects.filter(project_id=OuterRef("pk"))
  86. if isinstance(request.data, list) and len(request.data) > 1:
  87. data = request.data[2]
  88. else:
  89. data = request.data
  90. try:
  91. project = (
  92. Project.objects.filter(id=project_id, projectkey__public_key=sentry_key)
  93. .annotate(
  94. has_difs=Exists(difs_subquery),
  95. release_id=RawSQL(
  96. "select releases_release.id from releases_release inner join releases_releaseproject on releases_releaseproject.release_id = releases_release.id and releases_releaseproject.project_id=%s where version=%s limit 1",
  97. [project_id, data.get("release")],
  98. ),
  99. environment_id=RawSQL(
  100. "select environments_environment.id from environments_environment inner join environments_environmentproject on environments_environmentproject.environment_id = environments_environment.id and environments_environmentproject.project_id=%s where environments_environment.name=%s limit 1",
  101. [project_id, data.get("environment")],
  102. ),
  103. )
  104. .select_related("organization")
  105. .only(
  106. "id",
  107. "first_event",
  108. "slug",
  109. "organization__is_accepting_events",
  110. "organization__slug",
  111. )
  112. .first()
  113. )
  114. except ValidationError as err:
  115. raise exceptions.AuthenticationFailed({"error": "Invalid api key"}) from err
  116. if not project:
  117. if Project.objects.filter(id=project_id).exists():
  118. raise exceptions.AuthenticationFailed({"error": "Invalid api key"})
  119. raise exceptions.ValidationError("Invalid project_id: %s" % project_id)
  120. if not project.organization.is_accepting_events:
  121. raise exceptions.Throttled(detail="event rejected due to rate limit")
  122. return project
  123. def get_event_serializer_class(self, data=None):
  124. """Determine event type and return serializer"""
  125. if data is None:
  126. data = []
  127. if "exception" in data and data["exception"]:
  128. return StoreErrorSerializer
  129. if "platform" not in data:
  130. return StoreCSPReportSerializer
  131. return StoreDefaultSerializer
  132. def process_event(self, data, request, project):
  133. set_context("incoming event", data)
  134. serializer = self.get_event_serializer_class(data)(
  135. data=data, context={"request": self.request, "project": project}
  136. )
  137. try:
  138. serializer.is_valid(raise_exception=True)
  139. except exceptions.ValidationError as err:
  140. set_level("warning")
  141. capture_exception(err)
  142. logger.warning("Invalid event %s", serializer.errors)
  143. return Response()
  144. event = serializer.save()
  145. if event.data.get("exception") is not None and project.has_difs:
  146. difs_run_resolve_stacktrace(event.event_id)
  147. return Response({"id": event.event_id_hex})
  148. class EventStoreAPIView(BaseEventAPIView):
  149. def post(self, request, *args, **kwargs):
  150. self.check_status()
  151. try:
  152. project = self.get_project(request, kwargs.get("id"))
  153. except exceptions.AuthenticationFailed as err:
  154. # Replace 403 status code with 401 to match OSS Sentry
  155. return Response(err.detail, status=401)
  156. return self.process_event(request.data, request, project)
  157. class CSPStoreAPIView(EventStoreAPIView):
  158. pass
  159. class EnvelopeAPIView(BaseEventAPIView):
  160. parser_classes = [EnvelopeParser]
  161. def get_serializer_class(self):
  162. return TransactionEventSerializer
  163. def post(self, request, *args, **kwargs):
  164. self.check_status()
  165. project = self.get_project(request, kwargs.get("id"))
  166. data = request.data
  167. if len(data) < 2:
  168. logger.warning("Envelope has no headers %s", data)
  169. raise exceptions.ValidationError("Envelope has no headers")
  170. event_header_serializer = EnvelopeHeaderSerializer(data=data.pop(0))
  171. event_header_serializer.is_valid(raise_exception=True)
  172. # Multi part envelopes are not yet supported
  173. message_header = data.pop(0)
  174. if message_header.get("type") == "transaction":
  175. serializer = self.get_serializer_class()(
  176. data=data.pop(0), context={"request": self.request, "project": project}
  177. )
  178. try:
  179. serializer.is_valid(raise_exception=True)
  180. except exceptions.ValidationError as err:
  181. logger.warning("Invalid envelope payload", exc_info=True)
  182. raise err
  183. try:
  184. event = serializer.save()
  185. except IntegrityError as err:
  186. logger.warning("Duplicate event id", exc_info=True)
  187. raise exceptions.ValidationError("Duplicate event id") from err
  188. return Response({"id": event.event_id_hex})
  189. elif message_header.get("type") == "event":
  190. event_data = data.pop(0)
  191. return self.process_event(event_data, request, project)
  192. elif message_header.get("type") == "session":
  193. return Response(
  194. {
  195. "message": "Attempted to record a session event, which are not supported. This is safe to ignore. You may be able to suppress this message by disabling auto session tracking in your SDK. See https://gitlab.com/glitchtip/glitchtip-backend/-/issues/206"
  196. },
  197. status=status.HTTP_501_NOT_IMPLEMENTED,
  198. )
  199. return Response(status=status.HTTP_501_NOT_IMPLEMENTED)