views.py 9.3 KB

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