Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/sentry/api/endpoints/organization_sessions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
from __future__ import absolute_import

from contextlib import contextmanager

from rest_framework.response import Response
from rest_framework.exceptions import ParseError

import six
import sentry_sdk

from sentry.api.bases import OrganizationEventsEndpointBase
from sentry.snuba.sessions_v2 import (
InvalidField,
QueryDefinition,
run_sessions_query,
massage_sessions_result,
)


# NOTE: this currently extends `OrganizationEventsEndpointBase` for `handle_query_errors` only, which should ideally be decoupled from the base class.
class OrganizationSessionsEndpoint(OrganizationEventsEndpointBase):
def get(self, request, organization):
with self.handle_query_errors():
with sentry_sdk.start_span(op="sessions.endpoint", description="build_sessions_query"):
query = self.build_sessions_query(request, organization)

with sentry_sdk.start_span(op="sessions.endpoint", description="run_sessions_query"):
result_totals, result_timeseries = run_sessions_query(query)

with sentry_sdk.start_span(
op="sessions.endpoint", description="massage_sessions_result"
):
result = massage_sessions_result(query, result_totals, result_timeseries)
return Response(result, status=200)

def build_sessions_query(self, request, organization):
# validate and default all `project` params.
projects = self.get_projects(request, organization)
project_ids = [p.id for p in projects]

return QueryDefinition(request.GET, project_ids)

@contextmanager
def handle_query_errors(self):
try:
# TODO: this context manager should be decoupled from `OrganizationEventsEndpointBase`?
with super(OrganizationSessionsEndpoint, self).handle_query_errors():
yield
except InvalidField as error:
raise ParseError(detail=six.text_type(error))
6 changes: 6 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@
from .endpoints.organization_join_request import OrganizationJoinRequestEndpoint
from .endpoints.organization_search_details import OrganizationSearchDetailsEndpoint
from .endpoints.organization_searches import OrganizationSearchesEndpoint
from .endpoints.organization_sessions import OrganizationSessionsEndpoint
from .endpoints.organization_sentry_apps import OrganizationSentryAppsEndpoint
from .endpoints.organization_shortid import ShortIdLookupEndpoint
from .endpoints.organization_slugs import SlugsUpdateEndpoint
Expand Down Expand Up @@ -963,6 +964,11 @@
OrganizationSearchesEndpoint.as_view(),
name="sentry-api-0-organization-searches",
),
url(
r"^(?P<organization_slug>[^\/]+)/sessions/$",
OrganizationSessionsEndpoint.as_view(),
name="sentry-api-0-organization-sessions",
),
url(
r"^(?P<organization_slug>[^\/]+)/users/issues/$",
OrganizationUserIssuesSearchEndpoint.as_view(),
Expand Down
45 changes: 43 additions & 2 deletions src/sentry/api/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

from datetime import timedelta

import math
import six
from django.utils import timezone

from sentry.search.utils import parse_datetime_string, InvalidQuery
from sentry.utils.dates import parse_stats_period

from sentry.utils.dates import parse_stats_period, to_timestamp, to_datetime
from sentry.constants import MAX_ROLLUP_POINTS

MAX_STATS_PERIOD = timedelta(days=90)

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

return start, end


def get_date_range_rollup_from_params(
params,
minimum_interval="1h",
default_interval="",
round_range=False,
max_points=MAX_ROLLUP_POINTS,
):
"""
Similar to `get_date_range_from_params`, but this also parses and validates
an `interval`, as `get_rollup_from_request` would do.

This also optionally rounds the returned range to the given `interval`.
The rounding uses integer arithmetic on unix timestamps, so might yield
unexpected results when the interval is > 1d.
"""
minimum_interval = parse_stats_period(minimum_interval).total_seconds()
interval = parse_stats_period(params.get("interval", default_interval))
interval = minimum_interval if interval is None else interval.total_seconds()
if interval <= 0:
raise InvalidParams("Interval cannot result in a zero duration.")

# round the interval up to the minimum
interval = int(minimum_interval * math.ceil(interval / minimum_interval))

start, end = get_date_range_from_params(params)
date_range = end - start
if date_range.total_seconds() / interval > max_points:
raise InvalidParams(
"Your interval and date range would create too many results. "
"Use a larger interval, or a smaller date range."
)

if round_range:
end_ts = int(interval * math.ceil(to_timestamp(end) / interval))
end = to_datetime(end_ts)
start = end - date_range

return start, end, interval
Loading