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(issue-details): Allow event endpoint results to filter by query, date and environment #81471

Merged
merged 13 commits into from
Dec 19, 2024
104 changes: 63 additions & 41 deletions src/sentry/issues/endpoints/group_event_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
from django.contrib.auth.models import AnonymousUser
from drf_spectacular.types import OpenApiTypes
from drf_spectacular.utils import OpenApiParameter, extend_schema
from rest_framework.exceptions import ParseError
from rest_framework.request import Request
from rest_framework.response import Response
from snuba_sdk import Condition, Or
from snuba_sdk import Column, Condition, Op, Or
from snuba_sdk.legacy import is_condition, parse_condition

from sentry import eventstore
Expand All @@ -19,6 +20,7 @@
from sentry.api.helpers.group_index import parse_and_convert_issue_search_query
from sentry.api.helpers.group_index.validators import ValidationError
from sentry.api.serializers import EventSerializer, serialize
from sentry.api.utils import get_date_range_from_params
from sentry.apidocs.constants import (
RESPONSE_BAD_REQUEST,
RESPONSE_FORBIDDEN,
Expand All @@ -29,6 +31,7 @@
from sentry.apidocs.parameters import GlobalParams, IssueParams
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.eventstore.models import Event, GroupEvent
from sentry.exceptions import InvalidParams
from sentry.issues.endpoints.project_event_details import (
GroupEventDetailsResponse,
wrap_event_response,
Expand All @@ -49,7 +52,7 @@

def issue_search_query_to_conditions(
query: str, group: Group, user: User | AnonymousUser, environments: Sequence[Environment]
) -> Sequence[Condition]:
) -> list[Condition]:
from sentry.utils.snuba import resolve_column, resolve_conditions

dataset = (
Expand Down Expand Up @@ -159,56 +162,75 @@ def get(self, request: Request, group: Group, event_id: str) -> Response:
"""
Retrieves the details of an issue event.
"""
environments = [e for e in get_environments(request, group.project.organization)]
organization = group.project.organization
environments = [e for e in get_environments(request, organization)]
environment_names = [e.name for e in environments]

try:
start, end = get_date_range_from_params(request.GET, optional=True)
except InvalidParams:
raise ParseError(detail="Invalid date range")

query = request.GET.get("query")
try:
conditions: list[Condition] = (
issue_search_query_to_conditions(query, group, request.user, environments)
if query
else []
)
except ValidationError:
raise ParseError(detail="Invalid event query")
except Exception:
logging.exception(
"group_event_details.parse_query",
extra={"query": query, "group": group.id, "organization": organization.id},
)
raise ParseError(detail="Unable to parse query")

if environments:
conditions.append(Condition(Column("environment"), Op.IN, environment_names))

metric = "api.endpoints.group_event_details.get"
error_response = {"detail": "Unable to apply query. Change or remove it and try again."}

event: Event | GroupEvent | None = None

if event_id == "latest":
with metrics.timer("api.endpoints.group_event_details.get", tags={"type": "latest"}):
event: Event | GroupEvent | None = group.get_latest_event_for_environments(
environment_names
)
with metrics.timer(metric, tags={"type": "latest", "query": bool(query)}):
try:
event = group.get_latest_event(conditions=conditions, start=start, end=end)
except ValueError:
return Response(error_response, status=400)

elif event_id == "oldest":
with metrics.timer("api.endpoints.group_event_details.get", tags={"type": "oldest"}):
event = group.get_oldest_event_for_environments(environment_names)
with metrics.timer(metric, tags={"type": "oldest", "query": bool(query)}):
try:
event = group.get_oldest_event(conditions=conditions, start=start, end=end)
except ValueError:
return Response(error_response, status=400)

elif event_id == "recommended":
query = request.GET.get("query")
if query:
with metrics.timer(
"api.endpoints.group_event_details.get",
tags={"type": "helpful", "query": True},
):
try:
conditions = issue_search_query_to_conditions(
query, group, request.user, environments
)
event = group.get_recommended_event_for_environments(
environments, conditions
)
except ValidationError:
return Response(status=400)
except Exception:
logging.exception(
"group_event_details:get_helpful",
)
return Response(status=500)
else:
with metrics.timer(
"api.endpoints.group_event_details.get",
tags={"type": "helpful", "query": False},
):
event = group.get_recommended_event_for_environments(environments)
with metrics.timer(metric, tags={"type": "helpful", "query": bool(query)}):
try:
event = group.get_recommended_event(conditions=conditions, start=start, end=end)
except ValueError:
return Response(error_response, status=400)

else:
with metrics.timer("api.endpoints.group_event_details.get", tags={"type": "event"}):
with metrics.timer(metric, tags={"type": "event"}):
event = eventstore.backend.get_event_by_id(
group.project.id, event_id, group_id=group.id
project_id=group.project.id, event_id=event_id, group_id=group.id
)
# TODO: Remove `for_group` check once performance issues are moved to the issue platform

if event is not None and hasattr(event, "for_group") and event.group:
if isinstance(event, Event) and event.group:
event = event.for_group(event.group)

if event is None:
return Response({"detail": "Event not found"}, status=404)
return Response(
{
"detail": "Event not found. The event ID may be incorrect, or it's age exceeded the retention period."
},
status=404,
)

collapse = request.GET.getlist("collapse", [])
if "stacktraceOnly" in collapse:
Expand Down
158 changes: 113 additions & 45 deletions src/sentry/models/group.py
Original file line number Diff line number Diff line change
Expand Up @@ -229,24 +229,33 @@ class EventOrdering(Enum):
]


def get_oldest_or_latest_event_for_environments(
ordering: EventOrdering, environments: Sequence[str], group: Group
def get_oldest_or_latest_event(
group: Group,
ordering: EventOrdering,
conditions: Sequence[Condition] | None = None,
start: datetime | None = None,
end: datetime | None = None,
) -> GroupEvent | None:
conditions = []

if len(environments) > 0:
conditions.append(["environment", "IN", environments])

if group.issue_category == GroupCategory.ERROR:
dataset = Dataset.Events
else:
dataset = Dataset.IssuePlatform

_filter = eventstore.Filter(
conditions=conditions, project_ids=[group.project_id], group_ids=[group.id]
)
events = eventstore.backend.get_events(
filter=_filter,
all_conditions = [
Condition(Column("project_id"), Op.IN, [group.project.id]),
Condition(Column("group_id"), Op.IN, [group.id]),
]

if conditions:
all_conditions.extend(conditions)

events = eventstore.backend.get_events_snql(
organization_id=group.project.organization_id,
group_id=group.id,
start=start,
end=end,
conditions=all_conditions,
limit=1,
orderby=ordering.value,
referrer="Group.get_latest",
Expand All @@ -260,32 +269,32 @@ def get_oldest_or_latest_event_for_environments(
return None


def get_recommended_event_for_environments(
environments: Sequence[Environment],
def get_recommended_event(
group: Group,
conditions: Sequence[Condition] | None = None,
start: datetime | None = None,
end: datetime | None = None,
) -> GroupEvent | None:
if group.issue_category == GroupCategory.ERROR:
dataset = Dataset.Events
else:
dataset = Dataset.IssuePlatform

all_conditions = []
if len(environments) > 0:
all_conditions.append(
Condition(Column("environment"), Op.IN, [e.name for e in environments])
)
all_conditions.append(Condition(Column("project_id"), Op.IN, [group.project.id]))
all_conditions.append(Condition(Column("group_id"), Op.IN, [group.id]))
all_conditions = [
Condition(Column("project_id"), Op.IN, [group.project.id]),
Condition(Column("group_id"), Op.IN, [group.id]),
]

if conditions:
all_conditions.extend(conditions)

end = group.last_seen + timedelta(minutes=1)
start = end - timedelta(days=7)
default_end = group.last_seen + timedelta(minutes=1)
default_start = default_end - timedelta(days=7)

expired, _ = outside_retention_with_modified_start(
start, end, Organization(group.project.organization_id)
start=start if start else default_start,
end=end if end else default_end,
organization=Organization(group.project.organization_id),
)

if expired:
Expand All @@ -294,8 +303,8 @@ def get_recommended_event_for_environments(
events = eventstore.backend.get_events_snql(
organization_id=group.project.organization_id,
group_id=group.id,
start=start,
end=end,
start=start if start else default_start,
end=end if end else default_end,
conditions=all_conditions,
limit=1,
orderby=EventOrdering.RECOMMENDED.value,
Expand Down Expand Up @@ -764,46 +773,105 @@ def get_share_id(self):
# Otherwise it has not been shared yet.
return None

def get_latest_event(self) -> GroupEvent | None:
if not hasattr(self, "_latest_event"):
self._latest_event = self.get_latest_event_for_environments()

return self._latest_event
def get_latest_event(
self,
conditions: Sequence[Condition] | None = None,
start: datetime | None = None,
end: datetime | None = None,
) -> GroupEvent | None:
"""
Returns the latest/newest event given the conditions and time range.
If no event is found, returns None.
"""
return get_oldest_or_latest_event(
group=self,
ordering=EventOrdering.LATEST,
conditions=conditions,
start=start,
end=end,
)

def get_latest_event_for_environments(
self, environments: Sequence[str] = ()
) -> GroupEvent | None:
return get_oldest_or_latest_event_for_environments(
EventOrdering.LATEST,
environments,
self,
"""
Legacy special case of `self.get_latest_event` for environments and no date range.
Kept for compatability, but it's advised to use `self.get_latest_event` directly.
"""
conditions = (
[Condition(Column("environment"), Op.IN, environments)] if len(environments) > 0 else []
)
return self.get_latest_event(conditions=conditions)

def get_oldest_event(
self,
conditions: Sequence[Condition] | None = None,
start: datetime | None = None,
end: datetime | None = None,
) -> GroupEvent | None:
"""
Returns the oldest event given the conditions and time range.
If no event is found, returns None.
"""
return get_oldest_or_latest_event(
group=self,
ordering=EventOrdering.OLDEST,
conditions=conditions,
start=start,
end=end,
)

def get_oldest_event_for_environments(
self, environments: Sequence[str] = ()
) -> GroupEvent | None:
return get_oldest_or_latest_event_for_environments(
EventOrdering.OLDEST,
environments,
self,
"""
Legacy special case of `self.get_oldest_event` for environments and no date range.
Kept for compatability, but it's advised to use `self.get_oldest_event` directly.
"""
conditions = (
[Condition(Column("environment"), Op.IN, environments)] if len(environments) > 0 else []
)
return self.get_oldest_event(conditions=conditions)

def get_recommended_event_for_environments(
def get_recommended_event(
self,
environments: Sequence[Environment] = (),
conditions: Sequence[Condition] | None = None,
start: datetime | None = None,
end: datetime | None = None,
) -> GroupEvent | None:
maybe_event = get_recommended_event_for_environments(
environments,
self,
conditions,
"""
Returns a recommended event given the conditions and time range.
If a helpful recommendation is not found, it will fallback to the latest event.
If neither are found, returns None.
"""
maybe_event = get_recommended_event(
group=self,
conditions=conditions,
start=start,
end=end,
)
return (
maybe_event
if maybe_event
else self.get_latest_event_for_environments([env.name for env in environments])
else self.get_latest_event(conditions=conditions, start=start, end=end)
)

def get_recommended_event_for_environments(
self,
environments: Sequence[Environment] = (),
conditions: Sequence[Condition] | None = None,
) -> GroupEvent | None:
"""
Legacy special case of `self.get_recommended_event` for environments and no date range.
Kept for compatability, but it's advised to use `self.get_recommended_event` directly.
"""
all_conditions: list[Condition] = list(conditions) if conditions else []
if len(environments) > 0:
all_conditions.append(
Condition(Column("environment"), Op.IN, [e.name for e in environments])
)
return self.get_recommended_event(conditions=all_conditions)

def get_suspect_commit(self) -> Commit | None:
from sentry.models.groupowner import GroupOwner, GroupOwnerType

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def test_query_issue_platform_title(self):
issue_title = "king of england"
occurrence, group_info = self.process_occurrence(
project_id=self.project.id,
title=issue_title,
issue_title=issue_title,
event_data={"level": "info"},
)

Expand Down
Loading
Loading