Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ref(outcomes) Add methods to build outcomes queries without django #45730

Merged
merged 4 commits into from Mar 15, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/sentry/api/endpoints/organization_stats_v2.py
Expand Up @@ -198,7 +198,7 @@ def build_outcomes_query(self, request: Request, organization):
if project_ids:
params["project_id"] = project_ids

return QueryDefinition(request.GET, params)
return QueryDefinition.from_query_dict(request.GET, params)

def _get_projects_for_orgstats_query(self, request: Request, organization):
# look at the raw project_id filter passed in, if its empty
Expand Down
39 changes: 15 additions & 24 deletions src/sentry/api/endpoints/project_key_stats.py
@@ -1,5 +1,4 @@
from django.db.models import F
from django.http import QueryDict
from rest_framework.exceptions import ParseError
from rest_framework.request import Request
from rest_framework.response import Response
Expand Down Expand Up @@ -38,37 +37,29 @@ def get(self, request: Request, project, key_id) -> Response:
except ProjectKey.DoesNotExist:
raise ResourceDoesNotExist

# Outcomes queries are coupled to Django's QueryDict :(
query_data = QueryDict(mutable=True)
query_data.setlist("field", ["sum(quantity)"])
query_data.setlist(
"outcome",
[
Outcome.ACCEPTED.api_name(),
Outcome.FILTERED.api_name(),
Outcome.RATE_LIMITED.api_name(),
],
)
query_data["groupBy"] = "outcome"
query_data["category"] = "error"
query_data["key_id"] = key.id

try:
stats_params = self._parse_args(request)
except Exception:
raise ParseError(detail="Invalid request data")

query_data["end"] = stats_params["end"].isoformat()
query_data["start"] = stats_params["start"].isoformat()
query_data["interval"] = request.GET.get("resolution", "1d")

try:
query_definition = QueryDefinition(
query_data,
{"organization_id": project.organization_id},
outcomes_query = QueryDefinition.build(
fields=["sum(quantity)"],
start=stats_params["start"].isoformat(),
end=stats_params["end"].isoformat(),
organization_id=project.organization_id,
outcome=[
Outcome.ACCEPTED.api_name(),
Outcome.FILTERED.api_name(),
Outcome.RATE_LIMITED.api_name(),
],
group_by=["outcome"],
category="error",
key_id=key.id,
interval=request.GET.get("resolution", "1d"),
)
results = massage_outcomes_result(
query_definition, [], run_outcomes_query_timeseries(query_definition)
outcomes_query, [], run_outcomes_query_timeseries(outcomes_query)
)
except Exception:
raise ParseError(detail="Invalid request data")
Expand Down
71 changes: 71 additions & 0 deletions src/sentry/snuba/outcomes.py
@@ -1,3 +1,5 @@
from __future__ import annotations

from abc import ABC, abstractmethod
from typing import Any, Dict, List, Mapping, MutableMapping, Optional, Sequence, Tuple

Expand Down Expand Up @@ -222,6 +224,75 @@ class QueryDefinition:
`fields` and `groupby` definitions as [`ColumnDefinition`] objects.
"""

@classmethod
def build(
cls,
*,
fields: List[str],
start: str,
end: str,
organization_id: Optional[int] = None,
project_ids: Optional[List[int]] = None,
key_id: Optional[int] = None,
interval: Optional[str] = None,
outcome: Optional[List[str]] = None,
group_by: Optional[List[str]] = None,
query: Optional[Mapping[str, Any]] = None,
category: Optional[str] = None,
reason: Optional[str] = None,
allow_minute_resolution: bool = True,
) -> QueryDefinition:
"""
Factory method for building a `QueryDefintion` from python primitives.

Ideally this would be the constructor, but we have cross repository dependencies
that need to be addressed first.
"""
# TODO(mark) this is a workaround because __init__() uses QueryDict
# and I wanted to make smaller changes.
query_dict = QueryDict("", mutable=True)
query_dict.setlist("field", fields)
query_dict["start"] = start
query_dict["end"] = end
if group_by is not None:
query_dict.setlist("groupBy", group_by)
if outcome is not None:
query_dict.setlist("outcome", outcome)

if interval is not None:
query_dict["interval"] = interval
if reason is not None:
query_dict["reason"] = reason
if category is not None:
query_dict["category"] = category
if query is not None:
query_dict["query"] = query
if key_id is not None:
query_dict["key_id"] = key_id

params: MutableMapping[str, Any] = {"organization_id": organization_id}
if project_ids is not None:
params["project_id"] = project_ids

return cls(query_dict, params=params, allow_minute_resolution=allow_minute_resolution)

@classmethod
def from_query_dict(
cls,
query: QueryDict,
params: Mapping[Any, Any],
allow_minute_resolution: Optional[bool] = True,
):
"""
Create a QueryDefinition from a Django request QueryDict

Useful when you want to convert request data into an outcomes.QueryDefinition.
"""
# TODO(mark) Move __init__ logic here so that __init__ can work with python primitives.
return QueryDefinition(
query=query, params=params, allow_minute_resolution=allow_minute_resolution
)

def __init__(
self,
query: QueryDict,
Expand Down