views.py 9.4 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(project__slug=self.kwargs["project_slug"],)
  58. return qs
  59. def list(self, request, *args, **kwargs):
  60. try:
  61. event_id = uuid.UUID(self.request.GET.get("query", ""))
  62. except ValueError:
  63. event_id = None
  64. if event_id and self.request.user.is_authenticated:
  65. issues = list(self._get_queryset_base().filter(event__event_id=event_id))
  66. if issues:
  67. serializer = IssueSerializer(
  68. issues, many=True, context={"matching_event_id": event_id.hex}
  69. )
  70. return Response(serializer.data, headers={"X-Sentry-Direct-Hit": "1"})
  71. return super().list(request, *args, **kwargs)
  72. def get_queryset(self):
  73. qs = self._get_queryset_base()
  74. queries = shlex.split(self.request.GET.get("query", ""))
  75. # First look for structured queries
  76. for i, query in enumerate(queries):
  77. query_part = query.split(":", 1)
  78. if len(query_part) == 2:
  79. query_name, query_value = query_part
  80. query_value = query_value.strip('"')
  81. if query_name == "is":
  82. qs = qs.filter(status=EventStatus.from_string(query_value))
  83. elif query_name == "has":
  84. qs = qs.filter(tags__has_key=query_value)
  85. else:
  86. qs = qs.filter(tags__contains={query_name: [query_value]})
  87. if len(query_part) == 1:
  88. search_query = " ".join(queries[i:])
  89. qs = qs.filter(search_vector=search_query)
  90. # Search queries must be at end of query string, finished when parsing
  91. break
  92. environments = self.request.query_params.getlist("environment")
  93. if environments:
  94. qs = qs.filter(tags__environment__has_any_keys=environments)
  95. if str(self.request.query_params.get("sort")).endswith("priority"):
  96. # Raw SQL must be added when sorting by priority
  97. # Inspired by https://stackoverflow.com/a/43788975/443457
  98. qs = qs.annotate(
  99. priority=RawSQL(
  100. "LOG10(count) + EXTRACT(EPOCH FROM last_seen)/300000", ()
  101. )
  102. )
  103. qs = (
  104. qs.select_related("project")
  105. .defer("search_vector")
  106. .prefetch_related("userreport_set")
  107. )
  108. return qs
  109. def bulk_update(self, request, *args, **kwargs):
  110. queryset = self.filter_queryset(self.get_queryset())
  111. ids = request.GET.getlist("id")
  112. if ids:
  113. queryset = queryset.filter(id__in=ids)
  114. status = EventStatus.from_string(request.data.get("status"))
  115. queryset.update(status=status)
  116. return Response({"status": status.label})
  117. def serialize_tags(self, rows):
  118. return [
  119. {
  120. "topValues": [
  121. {"name": row[1], "value": row[1], "count": row[2], "key": key}
  122. for row in rows
  123. if row[0] == key
  124. ],
  125. "uniqueValues": len([row[2] for row in rows if row[0] == key]),
  126. "name": key,
  127. "key": key,
  128. "totalValues": sum([row[2] for row in rows if row[0] == key]),
  129. }
  130. for key in {tup[0] for tup in rows}
  131. ]
  132. @action(detail=True, methods=["get"])
  133. def tags(self, request, pk=None):
  134. """
  135. Get statistics about tags
  136. Filter with query param key=<your key>
  137. """
  138. instance = self.get_object()
  139. keys = tuple(request.GET.getlist("key"))
  140. with connection.cursor() as cursor:
  141. if keys:
  142. query = """
  143. SELECT key, value, count(*)
  144. FROM (
  145. SELECT (each(tags)).key, (each(tags)).value
  146. FROM events_event
  147. WHERE issue_id=%s
  148. )
  149. AS stat
  150. WHERE key in %s
  151. GROUP BY key, value
  152. ORDER BY count DESC, value
  153. limit 100;
  154. """
  155. cursor.execute(query, [instance.pk, keys])
  156. else:
  157. query = """
  158. SELECT key, value, count(*)
  159. FROM (
  160. SELECT (each(tags)).key, (each(tags)).value
  161. FROM events_event
  162. WHERE issue_id=%s
  163. )
  164. AS stat
  165. GROUP BY key, value
  166. ORDER BY count DESC, value
  167. limit 100;
  168. """
  169. cursor.execute(query, [instance.pk])
  170. rows = cursor.fetchall()
  171. tags = self.serialize_tags(rows)
  172. return Response(tags)
  173. @action(detail=True, methods=["get"], url_path=r"tags/(?P<tag>[-\w]+)")
  174. def tag_detail(self, request, pk=None, tag=None):
  175. """
  176. Get statistics about specified tag
  177. """
  178. instance = self.get_object()
  179. with connection.cursor() as cursor:
  180. query = """
  181. SELECT key, value, count(*)
  182. FROM (
  183. SELECT (each(tags)).key, (each(tags)).value
  184. FROM events_event
  185. WHERE issue_id=%s
  186. )
  187. AS stat
  188. WHERE key=%s
  189. GROUP BY key, value
  190. ORDER BY count DESC, value;
  191. """
  192. cursor.execute(query, [instance.pk, tag])
  193. rows = cursor.fetchall()
  194. tags = self.serialize_tags(rows)
  195. if not tags:
  196. raise NotFound()
  197. return Response(tags[0])
  198. class EventViewSet(viewsets.ReadOnlyModelViewSet):
  199. queryset = Event.objects.filter(issue__isnull=False)
  200. serializer_class = EventSerializer
  201. permission_classes = [EventPermission]
  202. def get_serializer_class(self):
  203. if self.action in ["retrieve", "latest"]:
  204. return EventDetailSerializer
  205. return super().get_serializer_class()
  206. def get_queryset(self):
  207. if not self.request.user.is_authenticated:
  208. return self.queryset.none()
  209. qs = (
  210. super()
  211. .get_queryset()
  212. .filter(issue__project__team__members__user=self.request.user)
  213. )
  214. if "issue_pk" in self.kwargs:
  215. qs = qs.filter(issue=self.kwargs["issue_pk"])
  216. if "organization_slug" in self.kwargs:
  217. qs = qs.filter(
  218. issue__project__organization__slug=self.kwargs["organization_slug"],
  219. )
  220. if "project_slug" in self.kwargs:
  221. qs = qs.filter(issue__project__slug=self.kwargs["project_slug"],)
  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. event = get_object_or_404(
  238. Event,
  239. pk=event,
  240. issue__project__organization__slug=org,
  241. issue__project__team__members__user=self.request.user,
  242. )
  243. return Response(event.event_json())