Skip to content

Commit

Permalink
ref(issue-details): Allow event endpoint results to filter by query, …
Browse files Browse the repository at this point in the history
…date and environment (#81471)

This will allow the new UI to filter the recommended, last, and first
events by queries, dates, and environment. It also ensures the logic
applies to the other issue types.

**todo**
- [x] Update tests
- [x] Add tests for different issue types
  • Loading branch information
leeandher authored and andrewshie-sentry committed Jan 2, 2025
1 parent ca634f7 commit 72a689c
Show file tree
Hide file tree
Showing 4 changed files with 669 additions and 190 deletions.
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
2 changes: 1 addition & 1 deletion tests/sentry/issues/endpoints/test_group_event_details.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,7 +377,7 @@ def test_query_issue_platform_title(self) -> None:
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

0 comments on commit 72a689c

Please sign in to comment.