views.py 9.6 KB

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