Skip to content

Commit 3e70867

Browse files
authored
feat: Implement Session Stats API (#22770)
This is a new API for querying sessions and has a query interface similar to discover, and returns timeseries data.
1 parent 6a4de6d commit 3e70867

File tree

7 files changed

+1202
-3
lines changed

7 files changed

+1202
-3
lines changed
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
from __future__ import absolute_import
2+
3+
from contextlib import contextmanager
4+
5+
from rest_framework.response import Response
6+
from rest_framework.exceptions import ParseError
7+
8+
import six
9+
import sentry_sdk
10+
11+
from sentry.api.bases import OrganizationEventsEndpointBase
12+
from sentry.snuba.sessions_v2 import (
13+
InvalidField,
14+
QueryDefinition,
15+
run_sessions_query,
16+
massage_sessions_result,
17+
)
18+
19+
20+
# NOTE: this currently extends `OrganizationEventsEndpointBase` for `handle_query_errors` only, which should ideally be decoupled from the base class.
21+
class OrganizationSessionsEndpoint(OrganizationEventsEndpointBase):
22+
def get(self, request, organization):
23+
with self.handle_query_errors():
24+
with sentry_sdk.start_span(op="sessions.endpoint", description="build_sessions_query"):
25+
query = self.build_sessions_query(request, organization)
26+
27+
with sentry_sdk.start_span(op="sessions.endpoint", description="run_sessions_query"):
28+
result_totals, result_timeseries = run_sessions_query(query)
29+
30+
with sentry_sdk.start_span(
31+
op="sessions.endpoint", description="massage_sessions_result"
32+
):
33+
result = massage_sessions_result(query, result_totals, result_timeseries)
34+
return Response(result, status=200)
35+
36+
def build_sessions_query(self, request, organization):
37+
# validate and default all `project` params.
38+
projects = self.get_projects(request, organization)
39+
project_ids = [p.id for p in projects]
40+
41+
return QueryDefinition(request.GET, project_ids)
42+
43+
@contextmanager
44+
def handle_query_errors(self):
45+
try:
46+
# TODO: this context manager should be decoupled from `OrganizationEventsEndpointBase`?
47+
with super(OrganizationSessionsEndpoint, self).handle_query_errors():
48+
yield
49+
except InvalidField as error:
50+
raise ParseError(detail=six.text_type(error))

src/sentry/api/urls.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@
171171
from .endpoints.organization_join_request import OrganizationJoinRequestEndpoint
172172
from .endpoints.organization_search_details import OrganizationSearchDetailsEndpoint
173173
from .endpoints.organization_searches import OrganizationSearchesEndpoint
174+
from .endpoints.organization_sessions import OrganizationSessionsEndpoint
174175
from .endpoints.organization_sentry_apps import OrganizationSentryAppsEndpoint
175176
from .endpoints.organization_shortid import ShortIdLookupEndpoint
176177
from .endpoints.organization_slugs import SlugsUpdateEndpoint
@@ -963,6 +964,11 @@
963964
OrganizationSearchesEndpoint.as_view(),
964965
name="sentry-api-0-organization-searches",
965966
),
967+
url(
968+
r"^(?P<organization_slug>[^\/]+)/sessions/$",
969+
OrganizationSessionsEndpoint.as_view(),
970+
name="sentry-api-0-organization-sessions",
971+
),
966972
url(
967973
r"^(?P<organization_slug>[^\/]+)/users/issues/$",
968974
OrganizationUserIssuesSearchEndpoint.as_view(),

src/sentry/api/utils.py

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
from datetime import timedelta
44

5+
import math
56
import six
67
from django.utils import timezone
78

89
from sentry.search.utils import parse_datetime_string, InvalidQuery
9-
from sentry.utils.dates import parse_stats_period
10-
10+
from sentry.utils.dates import parse_stats_period, to_timestamp, to_datetime
11+
from sentry.constants import MAX_ROLLUP_POINTS
1112

1213
MAX_STATS_PERIOD = timedelta(days=90)
1314

@@ -83,3 +84,43 @@ def get_date_range_from_params(params, optional=False):
8384
raise InvalidParams("start must be before end")
8485

8586
return start, end
87+
88+
89+
def get_date_range_rollup_from_params(
90+
params,
91+
minimum_interval="1h",
92+
default_interval="",
93+
round_range=False,
94+
max_points=MAX_ROLLUP_POINTS,
95+
):
96+
"""
97+
Similar to `get_date_range_from_params`, but this also parses and validates
98+
an `interval`, as `get_rollup_from_request` would do.
99+
100+
This also optionally rounds the returned range to the given `interval`.
101+
The rounding uses integer arithmetic on unix timestamps, so might yield
102+
unexpected results when the interval is > 1d.
103+
"""
104+
minimum_interval = parse_stats_period(minimum_interval).total_seconds()
105+
interval = parse_stats_period(params.get("interval", default_interval))
106+
interval = minimum_interval if interval is None else interval.total_seconds()
107+
if interval <= 0:
108+
raise InvalidParams("Interval cannot result in a zero duration.")
109+
110+
# round the interval up to the minimum
111+
interval = int(minimum_interval * math.ceil(interval / minimum_interval))
112+
113+
start, end = get_date_range_from_params(params)
114+
date_range = end - start
115+
if date_range.total_seconds() / interval > max_points:
116+
raise InvalidParams(
117+
"Your interval and date range would create too many results. "
118+
"Use a larger interval, or a smaller date range."
119+
)
120+
121+
if round_range:
122+
end_ts = int(interval * math.ceil(to_timestamp(end) / interval))
123+
end = to_datetime(end_ts)
124+
start = end - date_range
125+
126+
return start, end, interval

0 commit comments

Comments
 (0)