views.py 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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. queryset = self.filter_queryset(self.get_queryset())
  106. ids = request.GET.getlist("id")
  107. if ids:
  108. queryset = queryset.filter(id__in=ids)
  109. status = EventStatus.from_string(request.data.get("status"))
  110. queryset.update(status=status)
  111. return Response({"status": status.label})
  112. def serialize_tags(self, rows):
  113. return [
  114. {
  115. "topValues": [
  116. {"name": row[1], "value": row[1], "count": row[2], "key": key}
  117. for row in rows
  118. if row[0] == key
  119. ],
  120. "uniqueValues": len([row[2] for row in rows if row[0] == key]),
  121. "name": key,
  122. "key": key,
  123. "totalValues": sum([row[2] for row in rows if row[0] == key]),
  124. }
  125. for key in {tup[0] for tup in rows}
  126. ]
  127. @action(detail=True, methods=["get"])
  128. def tags(self, request, pk=None):
  129. """
  130. Get statistics about tags
  131. Filter with query param key=<your key>
  132. """
  133. instance = self.get_object()
  134. keys = tuple(request.GET.getlist("key"))
  135. with connection.cursor() as cursor:
  136. if keys:
  137. query = """
  138. SELECT key, value, count(*)
  139. FROM (
  140. SELECT (each(tags)).key, (each(tags)).value
  141. FROM events_event
  142. WHERE issue_id=%s
  143. )
  144. AS stat
  145. WHERE key in %s
  146. GROUP BY key, value
  147. ORDER BY count DESC, value
  148. limit 100;
  149. """
  150. cursor.execute(query, [instance.pk, keys])
  151. else:
  152. query = """
  153. SELECT key, value, count(*)
  154. FROM (
  155. SELECT (each(tags)).key, (each(tags)).value
  156. FROM events_event
  157. WHERE issue_id=%s
  158. )
  159. AS stat
  160. GROUP BY key, value
  161. ORDER BY count DESC, value
  162. limit 100;
  163. """
  164. cursor.execute(query, [instance.pk])
  165. rows = cursor.fetchall()
  166. tags = self.serialize_tags(rows)
  167. return Response(tags)
  168. @action(detail=True, methods=["get"], url_path=r"tags/(?P<tag>[-\w]+)")
  169. def tag_detail(self, request, pk=None, tag=None):
  170. """
  171. Get statistics about specified tag
  172. """
  173. instance = self.get_object()
  174. with connection.cursor() as cursor:
  175. query = """
  176. SELECT key, value, count(*)
  177. FROM (
  178. SELECT (each(tags)).key, (each(tags)).value
  179. FROM events_event
  180. WHERE issue_id=%s
  181. )
  182. AS stat
  183. WHERE key=%s
  184. GROUP BY key, value
  185. ORDER BY count DESC, value;
  186. """
  187. cursor.execute(query, [instance.pk, tag])
  188. rows = cursor.fetchall()
  189. tags = self.serialize_tags(rows)
  190. if not tags:
  191. raise NotFound()
  192. return Response(tags[0])
  193. class EventViewSet(viewsets.ReadOnlyModelViewSet):
  194. queryset = Event.objects.filter(issue__isnull=False)
  195. serializer_class = EventSerializer
  196. permission_classes = [EventPermission]
  197. def get_serializer_class(self):
  198. if self.action in ["retrieve", "latest"]:
  199. return EventDetailSerializer
  200. return super().get_serializer_class()
  201. def get_queryset(self):
  202. if not self.request.user.is_authenticated:
  203. return self.queryset.none()
  204. qs = (
  205. super()
  206. .get_queryset()
  207. .filter(issue__project__team__members__user=self.request.user)
  208. )
  209. if "issue_pk" in self.kwargs:
  210. qs = qs.filter(issue=self.kwargs["issue_pk"])
  211. if "organization_slug" in self.kwargs:
  212. qs = qs.filter(
  213. issue__project__organization__slug=self.kwargs["organization_slug"],
  214. )
  215. if "project_slug" in self.kwargs:
  216. qs = qs.filter(
  217. issue__project__slug=self.kwargs["project_slug"],
  218. )
  219. qs = qs.prefetch_related("tags__key")
  220. return qs
  221. @action(detail=False, methods=["get"])
  222. def latest(self, request, *args, **kwargs):
  223. instance = self.get_queryset().first()
  224. serializer = self.get_serializer(instance)
  225. return Response(serializer.data)
  226. class EventJsonView(views.APIView):
  227. """
  228. Represents a "raw" view of the event
  229. Not significantly different from event API view in usage but format is very different.
  230. Exists mainly for Sentry API compatibility
  231. """
  232. permission_classes = [EventPermission]
  233. def get(self, request, org, issue, event, format=None):
  234. try:
  235. event = (
  236. Event.objects.filter(
  237. pk=event,
  238. issue__project__organization__slug=org,
  239. issue__project__team__members__user=self.request.user,
  240. )
  241. .distinct()
  242. .get()
  243. )
  244. except Event.DoesNotExist:
  245. return HttpResponseNotFound()
  246. return Response(event.event_json())