views.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. import shlex
  2. import uuid
  3. from django.db import connection
  4. from django.db.models import Count
  5. from django.db.models.expressions import RawSQL
  6. from django.http import HttpResponseNotFound
  7. from django_filters.rest_framework import DjangoFilterBackend
  8. from rest_framework import exceptions, mixins, views, viewsets
  9. from rest_framework.decorators import action
  10. from rest_framework.exceptions import NotFound
  11. from rest_framework.filters import OrderingFilter
  12. from rest_framework.response import Response
  13. from events.models import Event
  14. from .filters import IssueFilter
  15. from .models import Comment, EventStatus, Issue
  16. from .permissions import EventPermission, IssuePermission
  17. from .serializers import (
  18. CommentSerializer,
  19. EventDetailSerializer,
  20. EventSerializer,
  21. IssueSerializer,
  22. )
  23. class IssueViewSet(
  24. mixins.ListModelMixin,
  25. mixins.RetrieveModelMixin,
  26. mixins.UpdateModelMixin,
  27. mixins.DestroyModelMixin,
  28. viewsets.GenericViewSet,
  29. ):
  30. """
  31. View and bulk update issues.
  32. # Bulk updates
  33. Submit PUT request to bulk update Issue statuses
  34. ## Query Parameters
  35. - id (int) — a list of IDs of the issues to be removed. This parameter shall be repeated for each issue.
  36. - query (string) — querystring for structured search. Example: "is:unresolved" searches for status=unresolved.
  37. """
  38. queryset = Issue.objects.all()
  39. serializer_class = IssueSerializer
  40. filterset_class = IssueFilter
  41. filter_backends = [DjangoFilterBackend, OrderingFilter]
  42. permission_classes = [IssuePermission]
  43. ordering = ["-last_seen"]
  44. ordering_fields = ["last_seen", "created", "count", "priority"]
  45. page_size_query_param = "limit"
  46. def _get_queryset_base(self):
  47. if not self.request.user.is_authenticated:
  48. return self.queryset.none()
  49. qs = (
  50. super()
  51. .get_queryset()
  52. .filter(project__organization__users=self.request.user)
  53. )
  54. if "organization_slug" in self.kwargs:
  55. qs = qs.filter(
  56. project__organization__slug=self.kwargs["organization_slug"],
  57. )
  58. if "project_slug" in self.kwargs:
  59. qs = qs.filter(
  60. project__slug=self.kwargs["project_slug"],
  61. )
  62. return qs
  63. def list(self, request, *args, **kwargs):
  64. try:
  65. event_id = uuid.UUID(self.request.GET.get("query", ""))
  66. except ValueError:
  67. event_id = None
  68. if event_id and self.request.user.is_authenticated:
  69. issues = list(self._get_queryset_base().filter(event__event_id=event_id))
  70. if issues:
  71. serializer = IssueSerializer(
  72. issues, many=True, context={"matching_event_id": event_id.hex}
  73. )
  74. return Response(serializer.data, headers={"X-Sentry-Direct-Hit": "1"})
  75. return super().list(request, *args, **kwargs)
  76. def get_queryset(self):
  77. qs = self._get_queryset_base()
  78. queries = shlex.split(self.request.GET.get("query", ""))
  79. # First look for structured queries
  80. for i, query in enumerate(queries):
  81. query_part = query.split(":", 1)
  82. if len(query_part) == 2:
  83. query_name, query_value = query_part
  84. query_value = query_value.strip('"')
  85. if query_name == "is":
  86. qs = qs.filter(status=EventStatus.from_string(query_value))
  87. elif query_name == "has":
  88. qs = qs.filter(tags__has_key=query_value)
  89. else:
  90. qs = qs.filter(tags__contains={query_name: [query_value]})
  91. if len(query_part) == 1:
  92. search_query = " ".join(queries[i:])
  93. qs = qs.filter(search_vector=search_query)
  94. # Search queries must be at end of query string, finished when parsing
  95. break
  96. if str(self.request.query_params.get("sort")).endswith("priority"):
  97. # Raw SQL must be added when sorting by priority
  98. # Inspired by https://stackoverflow.com/a/43788975/443457
  99. qs = qs.annotate(
  100. priority=RawSQL(
  101. "LOG10(count) + EXTRACT(EPOCH FROM last_seen)/300000", ()
  102. )
  103. )
  104. qs = qs.select_related("project").defer("search_vector", "tags")
  105. if self.action in ["retrieve"]:
  106. qs = qs.annotate(
  107. num_comments=Count("comment", distinct=True),
  108. user_report_count=(Count("userreport", distinct=True)),
  109. )
  110. return qs
  111. def bulk_update(self, request, *args, **kwargs):
  112. """Limited to pagination page count limit"""
  113. queryset = self.filter_queryset(self.get_queryset())
  114. ids = request.GET.getlist("id")
  115. if ids:
  116. queryset = queryset.filter(id__in=ids)
  117. status = EventStatus.from_string(request.data.get("status"))
  118. self.queryset.filter(pk__in=queryset[: self.pagination_class.max_hits]).update(
  119. status=status
  120. )
  121. return Response({"status": status.label})
  122. def bulk_delete(self, request, *args, **kwargs):
  123. """Limited to pagination page count limit"""
  124. queryset = self.filter_queryset(self.get_queryset())
  125. ids = request.GET.getlist("id")
  126. if len(ids) > 0:
  127. queryset = queryset.filter(id__in=ids)
  128. count = self.queryset.filter(
  129. pk__in=queryset[: self.pagination_class.max_hits]
  130. ).delete()
  131. return Response(status=204)
  132. def serialize_tags(self, rows):
  133. return [
  134. {
  135. "topValues": [
  136. {"name": row[1], "value": row[1], "count": row[2], "key": key}
  137. for row in rows
  138. if row[0] == key
  139. ],
  140. "uniqueValues": len([row[2] for row in rows if row[0] == key]),
  141. "name": key,
  142. "key": key,
  143. "totalValues": sum([row[2] for row in rows if row[0] == key]),
  144. }
  145. for key in {tup[0] for tup in rows}
  146. ]
  147. @action(detail=True, methods=["get"])
  148. def tags(self, request, pk=None):
  149. """
  150. Get statistics about tags
  151. Filter with query param key=<your key>
  152. """
  153. instance = self.get_object()
  154. keys = request.GET.getlist("key")
  155. with connection.cursor() as cursor:
  156. if keys:
  157. # The limit 2000000 prevents excessive load times
  158. query = """
  159. SELECT key, value, count(*)
  160. FROM (
  161. SELECT (each(tags)).*
  162. FROM events_event
  163. WHERE issue_id=%s
  164. LIMIT 2000000
  165. )
  166. AS stat(key, value)
  167. WHERE key = ANY(%s)
  168. GROUP BY key, value
  169. ORDER BY count DESC, value
  170. limit 100;
  171. """
  172. cursor.execute(query, [instance.pk, keys])
  173. else:
  174. query = """
  175. SELECT key, value, count(*)
  176. FROM (
  177. SELECT (each(tags)).*
  178. FROM events_event
  179. WHERE issue_id=%s
  180. LIMIT 2000000
  181. )
  182. AS stat(key, value)
  183. GROUP BY key, value
  184. ORDER BY count DESC, value
  185. limit 100;
  186. """
  187. cursor.execute(query, [instance.pk])
  188. rows = cursor.fetchall()
  189. tags = self.serialize_tags(rows)
  190. return Response(tags)
  191. @action(detail=True, methods=["get"], url_path=r"tags/(?P<tag>[-\w]+)")
  192. def tag_detail(self, request, pk=None, tag=None):
  193. """
  194. Get statistics about specified tag
  195. """
  196. instance = self.get_object()
  197. with connection.cursor() as cursor:
  198. query = """
  199. SELECT key, value, count(*)
  200. FROM (
  201. SELECT (each(tags)).key, (each(tags)).value
  202. FROM events_event
  203. WHERE issue_id=%s
  204. )
  205. AS stat
  206. WHERE key=%s
  207. GROUP BY key, value
  208. ORDER BY count DESC, value;
  209. """
  210. cursor.execute(query, [instance.pk, tag])
  211. rows = cursor.fetchall()
  212. tags = self.serialize_tags(rows)
  213. if not tags:
  214. raise NotFound()
  215. return Response(tags[0])
  216. class EventViewSet(viewsets.ReadOnlyModelViewSet):
  217. queryset = Event.objects.filter(issue__isnull=False)
  218. serializer_class = EventSerializer
  219. permission_classes = [EventPermission]
  220. def get_serializer_class(self):
  221. if self.action in ["retrieve", "latest"]:
  222. return EventDetailSerializer
  223. return super().get_serializer_class()
  224. def get_queryset(self):
  225. if not self.request.user.is_authenticated:
  226. return self.queryset.none()
  227. qs = (
  228. super()
  229. .get_queryset()
  230. .filter(issue__project__organization__users=self.request.user)
  231. )
  232. if "issue_pk" in self.kwargs:
  233. qs = qs.filter(issue=self.kwargs["issue_pk"])
  234. if "organization_slug" in self.kwargs:
  235. qs = qs.filter(
  236. issue__project__organization__slug=self.kwargs["organization_slug"],
  237. )
  238. if "project_slug" in self.kwargs:
  239. qs = qs.filter(
  240. issue__project__slug=self.kwargs["project_slug"],
  241. )
  242. qs = (
  243. qs.select_related("issue__project__organization")
  244. .prefetch_related("tags__key")
  245. .defer("issue__tags", "issue__search_vector")
  246. )
  247. return qs
  248. @action(detail=False, methods=["get"])
  249. def latest(self, request, *args, **kwargs):
  250. instance = self.get_queryset().first()
  251. serializer = self.get_serializer(instance)
  252. return Response(serializer.data)
  253. class EventJsonView(views.APIView):
  254. """
  255. Represents a "raw" view of the event
  256. Not significantly different from event API view in usage but format is very different.
  257. Exists mainly for Sentry API compatibility
  258. """
  259. permission_classes = [EventPermission]
  260. def get(self, request, org, issue, event, format=None):
  261. try:
  262. event = (
  263. Event.objects.filter(
  264. pk=event,
  265. issue__project__organization__slug=org,
  266. issue__project__organization__users=self.request.user,
  267. )
  268. .distinct()
  269. .get()
  270. )
  271. except Event.DoesNotExist:
  272. return HttpResponseNotFound()
  273. return Response(event.event_json())
  274. class CommentViewSet(
  275. mixins.CreateModelMixin,
  276. mixins.ListModelMixin,
  277. mixins.UpdateModelMixin,
  278. mixins.DestroyModelMixin,
  279. viewsets.GenericViewSet,
  280. ):
  281. queryset = Comment.objects.all()
  282. serializer_class = CommentSerializer
  283. permission_classes = [EventPermission]
  284. def get_queryset(self):
  285. if not self.request.user.is_authenticated:
  286. return self.queryset.none()
  287. queryset = (
  288. super()
  289. .get_queryset()
  290. .filter(issue__project__organization__users=self.request.user)
  291. )
  292. issue_id = self.kwargs.get("issue_pk")
  293. if issue_id:
  294. queryset = queryset.filter(issue_id=issue_id)
  295. return queryset
  296. def perform_create(self, serializer):
  297. try:
  298. issue = Issue.objects.get(
  299. id=self.kwargs.get("issue_pk"),
  300. project__organization__users=self.request.user,
  301. )
  302. except Issue.DoesNotExist:
  303. raise exceptions.ValidationError("Issue does not exist")
  304. serializer.save(issue=issue, user=self.request.user)