api.py 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192
  1. from datetime import timedelta
  2. from asgiref.sync import sync_to_async
  3. from django.db import connection
  4. from django.http import Http404
  5. from ninja import Query, Router
  6. from apps.projects.models import Project
  7. from glitchtip.api.authentication import AuthHttpRequest
  8. from glitchtip.api.permissions import has_permission
  9. from .schema import StatsV2Schema
  10. router = Router()
  11. EVENT_TIME_SERIES_SQL = """
  12. SELECT gs.ts, sum(event_stat.count)
  13. FROM generate_series(%s, %s, %s::interval) gs (ts)
  14. LEFT JOIN projects_issueeventprojecthourlystatistic event_stat
  15. ON event_stat.date >= gs.ts AND event_stat.date < gs.ts + interval '1 hour'
  16. WHERE event_stat.project_id = ANY(%s) or event_stat is null
  17. GROUP BY gs.ts ORDER BY gs.ts;
  18. """
  19. TRANSACTION_TIME_SERIES_SQL = """
  20. SELECT gs.ts, sum(transaction_stat.count)
  21. FROM generate_series(%s, %s, %s::interval) gs (ts)
  22. LEFT JOIN projects_transactioneventprojecthourlystatistic transaction_stat
  23. ON transaction_stat.date >= gs.ts AND transaction_stat.date < gs.ts + interval '1 hour'
  24. WHERE transaction_stat.project_id = ANY(%s) or transaction_stat is null
  25. GROUP BY gs.ts ORDER BY gs.ts;
  26. """
  27. @sync_to_async
  28. def get_timeseries(category, start, end, interval, project_ids):
  29. if category == "error":
  30. with connection.cursor() as cursor:
  31. cursor.execute(
  32. EVENT_TIME_SERIES_SQL,
  33. [start, end, interval, project_ids],
  34. )
  35. return cursor.fetchall()
  36. else:
  37. with connection.cursor() as cursor:
  38. cursor.execute(
  39. TRANSACTION_TIME_SERIES_SQL,
  40. [start, end, interval, project_ids],
  41. )
  42. return cursor.fetchall()
  43. @router.get("organizations/{slug:organization_slug}/stats_v2/")
  44. @has_permission(["org:read", "org:write", "org:admin"])
  45. async def stats_v2(
  46. request: AuthHttpRequest, organization_slug: str, filters: Query[StatsV2Schema]
  47. ):
  48. """
  49. Reverse engineered stats v2 endpoint. Endpoint in sentry not documented.
  50. Appears similar to documented sessions endpoint.
  51. Used by the Sentry Grafana integration.
  52. Used to return time series statistics.
  53. Submit query params start, end, and interval (defaults to 1h)
  54. Limits results to 1000 intervals. For example if using hours, max days would be 41
  55. """
  56. start = filters.start.replace(microsecond=0, second=0, minute=0)
  57. end = (filters.end + timedelta(hours=1)).replace(microsecond=0, second=0, minute=0)
  58. field = filters.field
  59. interval = filters.interval
  60. category = filters.category
  61. # Get projects that are authorized, filtered by organization, and selected by user
  62. # Intentionally separate SQL call to simplify raw SQL
  63. projects = Project.objects.filter(
  64. organization__slug=organization_slug,
  65. organization__users=request.auth.user_id,
  66. )
  67. if filters.project:
  68. projects = projects.filter(pk__in=filters.project)
  69. project_ids = [id async for id in projects.values_list("id", flat=True)]
  70. if not project_ids:
  71. raise Http404()
  72. series = await get_timeseries(category, start, end, interval, project_ids)
  73. return {
  74. "intervals": [row[0] for row in series],
  75. "groups": [
  76. {
  77. "series": {field: [row[1] for row in series]},
  78. }
  79. ],
  80. }