views.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346
  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"), user_report_count=(Count("userreport"))
  108. )
  109. return qs
  110. def bulk_update(self, request, *args, **kwargs):
  111. """Limited to pagination page count limit"""
  112. queryset = self.filter_queryset(self.get_queryset())
  113. ids = request.GET.getlist("id")
  114. if ids:
  115. queryset = queryset.filter(id__in=ids)
  116. status = EventStatus.from_string(request.data.get("status"))
  117. self.queryset.filter(pk__in=queryset[: self.pagination_class.max_hits]).update(
  118. status=status
  119. )
  120. return Response({"status": status.label})
  121. def bulk_delete(self, request, *args, **kwargs):
  122. """Limited to pagination page count limit"""
  123. queryset = self.filter_queryset(self.get_queryset())
  124. ids = request.GET.getlist("id")
  125. if len(ids) > 0:
  126. queryset = queryset.filter(id__in=ids)
  127. count = self.queryset.filter(
  128. pk__in=queryset[: self.pagination_class.max_hits]
  129. ).delete()
  130. return Response(status=204)
  131. def serialize_tags(self, rows):
  132. return [
  133. {
  134. "topValues": [
  135. {"name": row[1], "value": row[1], "count": row[2], "key": key}
  136. for row in rows
  137. if row[0] == key
  138. ],
  139. "uniqueValues": len([row[2] for row in rows if row[0] == key]),
  140. "name": key,
  141. "key": key,
  142. "totalValues": sum([row[2] for row in rows if row[0] == key]),
  143. }
  144. for key in {tup[0] for tup in rows}
  145. ]
  146. @action(detail=True, methods=["get"])
  147. def tags(self, request, pk=None):
  148. """
  149. Get statistics about tags
  150. Filter with query param key=<your key>
  151. """
  152. instance = self.get_object()
  153. keys = request.GET.getlist("key")
  154. with connection.cursor() as cursor:
  155. if keys:
  156. # The limit 2000000 prevents excessive load times
  157. query = """
  158. SELECT key, value, count(*)
  159. FROM (
  160. SELECT (each(tags)).*
  161. FROM events_event
  162. WHERE issue_id=%s
  163. LIMIT 2000000
  164. )
  165. AS stat(key, value)
  166. WHERE key = ANY(%s)
  167. GROUP BY key, value
  168. ORDER BY count DESC, value
  169. limit 100;
  170. """
  171. cursor.execute(query, [instance.pk, keys])
  172. else:
  173. query = """
  174. SELECT key, value, count(*)
  175. FROM (
  176. SELECT (each(tags)).*
  177. FROM events_event
  178. WHERE issue_id=%s
  179. LIMIT 2000000
  180. )
  181. AS stat(key, value)
  182. GROUP BY key, value
  183. ORDER BY count DESC, value
  184. limit 100;
  185. """
  186. cursor.execute(query, [instance.pk])
  187. rows = cursor.fetchall()
  188. tags = self.serialize_tags(rows)
  189. return Response(tags)
  190. @action(detail=True, methods=["get"], url_path=r"tags/(?P<tag>[-\w]+)")
  191. def tag_detail(self, request, pk=None, tag=None):
  192. """
  193. Get statistics about specified tag
  194. """
  195. instance = self.get_object()
  196. with connection.cursor() as cursor:
  197. query = """
  198. SELECT key, value, count(*)
  199. FROM (
  200. SELECT (each(tags)).key, (each(tags)).value
  201. FROM events_event
  202. WHERE issue_id=%s
  203. )
  204. AS stat
  205. WHERE key=%s
  206. GROUP BY key, value
  207. ORDER BY count DESC, value;
  208. """
  209. cursor.execute(query, [instance.pk, tag])
  210. rows = cursor.fetchall()
  211. tags = self.serialize_tags(rows)
  212. if not tags:
  213. raise NotFound()
  214. return Response(tags[0])
  215. class EventViewSet(viewsets.ReadOnlyModelViewSet):
  216. queryset = Event.objects.filter(issue__isnull=False)
  217. serializer_class = EventSerializer
  218. permission_classes = [EventPermission]
  219. def get_serializer_class(self):
  220. if self.action in ["retrieve", "latest"]:
  221. return EventDetailSerializer
  222. return super().get_serializer_class()
  223. def get_queryset(self):
  224. if not self.request.user.is_authenticated:
  225. return self.queryset.none()
  226. qs = (
  227. super()
  228. .get_queryset()
  229. .filter(issue__project__organization__users=self.request.user)
  230. )
  231. if "issue_pk" in self.kwargs:
  232. qs = qs.filter(issue=self.kwargs["issue_pk"])
  233. if "organization_slug" in self.kwargs:
  234. qs = qs.filter(
  235. issue__project__organization__slug=self.kwargs["organization_slug"],
  236. )
  237. if "project_slug" in self.kwargs:
  238. qs = qs.filter(
  239. issue__project__slug=self.kwargs["project_slug"],
  240. )
  241. qs = (
  242. qs.select_related("issue__project__organization")
  243. .prefetch_related("tags__key")
  244. .defer("issue__tags", "issue__search_vector")
  245. )
  246. return qs
  247. @action(detail=False, methods=["get"])
  248. def latest(self, request, *args, **kwargs):
  249. instance = self.get_queryset().first()
  250. serializer = self.get_serializer(instance)
  251. return Response(serializer.data)
  252. class EventJsonView(views.APIView):
  253. """
  254. Represents a "raw" view of the event
  255. Not significantly different from event API view in usage but format is very different.
  256. Exists mainly for Sentry API compatibility
  257. """
  258. permission_classes = [EventPermission]
  259. def get(self, request, org, issue, event, format=None):
  260. try:
  261. event = (
  262. Event.objects.filter(
  263. pk=event,
  264. issue__project__organization__slug=org,
  265. issue__project__organization__users=self.request.user,
  266. )
  267. .distinct()
  268. .get()
  269. )
  270. except Event.DoesNotExist:
  271. return HttpResponseNotFound()
  272. return Response(event.event_json())
  273. class CommentViewSet(
  274. mixins.CreateModelMixin,
  275. mixins.ListModelMixin,
  276. mixins.UpdateModelMixin,
  277. mixins.DestroyModelMixin,
  278. viewsets.GenericViewSet,
  279. ):
  280. queryset = Comment.objects.all()
  281. serializer_class = CommentSerializer
  282. permission_classes = [EventPermission]
  283. def get_queryset(self):
  284. if not self.request.user.is_authenticated:
  285. return self.queryset.none()
  286. queryset = (
  287. super()
  288. .get_queryset()
  289. .filter(issue__project__organization__users=self.request.user)
  290. )
  291. issue_id = self.kwargs.get("issue_pk")
  292. if issue_id:
  293. queryset = queryset.filter(issue_id=issue_id)
  294. return queryset
  295. def perform_create(self, serializer):
  296. try:
  297. issue = Issue.objects.get(
  298. id=self.kwargs.get("issue_pk"),
  299. project__organization__users=self.request.user,
  300. )
  301. except Issue.DoesNotExist:
  302. raise exceptions.ValidationError("Issue does not exist")
  303. serializer.save(issue=issue, user=self.request.user)