import shlex import uuid from django.db import connection from django.db.models.expressions import RawSQL from django.shortcuts import get_object_or_404 from rest_framework import viewsets, views, mixins from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.filters import OrderingFilter from rest_framework.exceptions import NotFound from django_filters.rest_framework import DjangoFilterBackend from events.models import Event from .models import Issue, EventStatus from .serializers import ( IssueSerializer, EventSerializer, EventDetailSerializer, ) from .filters import IssueFilter from .permissions import IssuePermission, EventPermission class IssueViewSet( mixins.ListModelMixin, mixins.RetrieveModelMixin, mixins.UpdateModelMixin, mixins.DestroyModelMixin, viewsets.GenericViewSet, ): """ View and bulk update issues. # Bulk updates Submit PUT request to bulk update Issue statuses ## Query Parameters - id (int) — a list of IDs of the issues to be removed. This parameter shall be repeated for each issue. - query (string) — querystring for structured search. Example: "is:unresolved" searches for status=unresolved. """ queryset = Issue.objects.all() serializer_class = IssueSerializer filterset_class = IssueFilter filter_backends = [DjangoFilterBackend, OrderingFilter] permission_classes = [IssuePermission] ordering = ["-last_seen"] ordering_fields = ["last_seen", "created", "count", "priority"] page_size_query_param = "limit" def _get_queryset_base(self): if not self.request.user.is_authenticated: return self.queryset.none() qs = ( super() .get_queryset() .filter(project__organization__users=self.request.user) ) if "organization_slug" in self.kwargs: qs = qs.filter( project__organization__slug=self.kwargs["organization_slug"], ) if "project_slug" in self.kwargs: qs = qs.filter(project__slug=self.kwargs["project_slug"],) return qs def list(self, request, *args, **kwargs): try: event_id = uuid.UUID(self.request.GET.get("query", "")) except ValueError: event_id = None if event_id and self.request.user.is_authenticated: issues = list(self._get_queryset_base().filter(event__event_id=event_id)) if issues: serializer = IssueSerializer( issues, many=True, context={"matching_event_id": event_id.hex} ) return Response(serializer.data, headers={"X-Sentry-Direct-Hit": "1"}) return super().list(request, *args, **kwargs) def get_queryset(self): qs = self._get_queryset_base() queries = shlex.split(self.request.GET.get("query", "")) # First look for structured queries for i, query in enumerate(queries): query_part = query.split(":", 1) if len(query_part) == 2: query_name, query_value = query_part query_value = query_value.strip('"') if query_name == "is": qs = qs.filter(status=EventStatus.from_string(query_value)) elif query_name == "has": qs = qs.filter(tags__has_key=query_value) else: qs = qs.filter(tags__contains={query_name: [query_value]}) if len(query_part) == 1: search_query = " ".join(queries[i:]) qs = qs.filter(search_vector=search_query) # Search queries must be at end of query string, finished when parsing break environments = self.request.query_params.getlist("environment") if environments: qs = qs.filter(tags__environment__has_any_keys=environments) if str(self.request.query_params.get("sort")).endswith("priority"): # Raw SQL must be added when sorting by priority # Inspired by https://stackoverflow.com/a/43788975/443457 qs = qs.annotate( priority=RawSQL( "LOG10(count) + EXTRACT(EPOCH FROM last_seen)/300000", () ) ) qs = ( qs.select_related("project") .defer("search_vector") .prefetch_related("userreport_set") ) return qs def bulk_update(self, request, *args, **kwargs): queryset = self.filter_queryset(self.get_queryset()) ids = request.GET.getlist("id") if ids: queryset = queryset.filter(id__in=ids) status = EventStatus.from_string(request.data.get("status")) queryset.update(status=status) return Response({"status": status.label}) def serialize_tags(self, rows): return [ { "topValues": [ {"name": row[1], "value": row[1], "count": row[2], "key": key} for row in rows if row[0] == key ], "uniqueValues": len([row[2] for row in rows if row[0] == key]), "name": key, "key": key, "totalValues": sum([row[2] for row in rows if row[0] == key]), } for key in {tup[0] for tup in rows} ] @action(detail=True, methods=["get"]) def tags(self, request, pk=None): """ Get statistics about tags Filter with query param key= """ instance = self.get_object() keys = tuple(request.GET.getlist("key")) with connection.cursor() as cursor: if keys: query = """ SELECT key, value, count(*) FROM ( SELECT (each(tags)).key, (each(tags)).value FROM events_event WHERE issue_id=%s ) AS stat WHERE key in %s GROUP BY key, value ORDER BY count DESC, value limit 100; """ cursor.execute(query, [instance.pk, keys]) else: query = """ SELECT key, value, count(*) FROM ( SELECT (each(tags)).key, (each(tags)).value FROM events_event WHERE issue_id=%s ) AS stat GROUP BY key, value ORDER BY count DESC, value limit 100; """ cursor.execute(query, [instance.pk]) rows = cursor.fetchall() tags = self.serialize_tags(rows) return Response(tags) @action(detail=True, methods=["get"], url_path=r"tags/(?P[-\w]+)") def tag_detail(self, request, pk=None, tag=None): """ Get statistics about specified tag """ instance = self.get_object() with connection.cursor() as cursor: query = """ SELECT key, value, count(*) FROM ( SELECT (each(tags)).key, (each(tags)).value FROM events_event WHERE issue_id=%s ) AS stat WHERE key=%s GROUP BY key, value ORDER BY count DESC, value; """ cursor.execute(query, [instance.pk, tag]) rows = cursor.fetchall() tags = self.serialize_tags(rows) if not tags: raise NotFound() return Response(tags[0]) class EventViewSet(viewsets.ReadOnlyModelViewSet): queryset = Event.objects.filter(issue__isnull=False) serializer_class = EventSerializer permission_classes = [EventPermission] def get_serializer_class(self): if self.action in ["retrieve", "latest"]: return EventDetailSerializer return super().get_serializer_class() def get_queryset(self): if not self.request.user.is_authenticated: return self.queryset.none() qs = ( super() .get_queryset() .filter(issue__project__team__members__user=self.request.user) ) if "issue_pk" in self.kwargs: qs = qs.filter(issue=self.kwargs["issue_pk"]) if "organization_slug" in self.kwargs: qs = qs.filter( issue__project__organization__slug=self.kwargs["organization_slug"], ) if "project_slug" in self.kwargs: qs = qs.filter(issue__project__slug=self.kwargs["project_slug"],) qs = qs.prefetch_related("tags__key") return qs @action(detail=False, methods=["get"]) def latest(self, request, *args, **kwargs): instance = self.get_queryset().first() serializer = self.get_serializer(instance) return Response(serializer.data) class EventJsonView(views.APIView): """ Represents a "raw" view of the event Not significantly different from event API view in usage but format is very different. Exists mainly for Sentry API compatibility """ permission_classes = [EventPermission] def get(self, request, org, issue, event, format=None): event = get_object_or_404( Event, pk=event, issue__project__organization__slug=org, issue__project__team__members__user=self.request.user, ) return Response(event.event_json())