views.py 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. from datetime import timedelta
  2. from django.db import connection
  3. from rest_framework import views
  4. from rest_framework.response import Response
  5. from rest_framework.status import HTTP_400_BAD_REQUEST, HTTP_404_NOT_FOUND
  6. from organizations_ext.permissions import OrganizationPermission
  7. from projects.models import Project
  8. from .serializers import StatsV2Serializer
  9. EVENT_TIME_SERIES_SQL = """
  10. SELECT gs.ts, count(event.created)
  11. FROM generate_series(%s, %s, %s::interval) gs (ts)
  12. LEFT JOIN events_event event
  13. ON event.created >= gs.ts AND event.created < gs.ts + interval '1 hour'
  14. LEFT JOIN issues_issue issue
  15. ON event.issue_id = issue.id or event is null
  16. WHERE issue.project_id IN %s
  17. GROUP BY gs.ts ORDER BY gs.ts;
  18. """
  19. class StatsV2View(views.APIView):
  20. """
  21. Reverse engineered stats v2 endpoint. Endpoint in sentry not documented.
  22. Appears similar to documented sessions endpoint.
  23. Used by the Sentry Grafana integration.
  24. Used to return time series statistics.
  25. Submit query params start, end, and interval (defaults to 1h)
  26. Limits results to 1000 intervals. For example if using hours, max days would be 41
  27. """
  28. permission_classes = [OrganizationPermission]
  29. def get(self, *args, **kwargs):
  30. query_params = self.request.query_params
  31. data = {
  32. "category": query_params.get("category"),
  33. "project": query_params.getlist("project"),
  34. "field": query_params.get("field"),
  35. "start": query_params.get("start"),
  36. "end": query_params.get("end"),
  37. }
  38. if query_params.get("interval"):
  39. data["interval"] = query_params.get("interval")
  40. serializer = StatsV2Serializer(data=data)
  41. serializer.is_valid(raise_exception=True)
  42. category = serializer.validated_data["category"]
  43. start = serializer.validated_data["start"].replace(
  44. microsecond=0, second=0, minute=0
  45. )
  46. end = (serializer.validated_data["end"] + timedelta(hours=1)).replace(
  47. microsecond=0, second=0, minute=0
  48. )
  49. field = serializer.validated_data["field"]
  50. interval = serializer.validated_data["interval"]
  51. # Get projects that are authorized, filtered by organization, and selected by user
  52. # Intentionally separate SQL call to simplify raw SQL
  53. projects = Project.objects.filter(
  54. organization__slug=self.kwargs.get("organization_slug"),
  55. organization__users=self.request.user,
  56. )
  57. if serializer.validated_data.get("project"):
  58. projects = projects.filter(pk__in=serializer.validated_data["project"])
  59. project_ids = tuple(projects.values_list("id", flat=True))
  60. if not project_ids:
  61. return Response(status=HTTP_404_NOT_FOUND)
  62. if category == "error":
  63. with connection.cursor() as cursor:
  64. cursor.execute(
  65. EVENT_TIME_SERIES_SQL, [start, end, interval, project_ids],
  66. )
  67. series = cursor.fetchall()
  68. else:
  69. return Response(status=HTTP_400_BAD_REQUEST)
  70. data = {
  71. "intervals": [row[0] for row in series],
  72. "groups": [{"series": {field: [row[1] for row in series]},}],
  73. }
  74. return Response(data)