Skip to content

Commit 0cfb95d

Browse files
authored
ref(explorer): rpc to support issue/event detail queries with event id only (#103787)
1 parent 1a193b1 commit 0cfb95d

File tree

3 files changed

+267
-42
lines changed

3 files changed

+267
-42
lines changed

src/sentry/seer/endpoints/seer_rpc.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@
9292
from sentry.seer.explorer.tools import (
9393
execute_table_query,
9494
execute_timeseries_query,
95+
get_issue_and_event_details,
9596
get_issue_details,
9697
get_replay_metadata,
9798
get_repository_definition,
@@ -1209,6 +1210,7 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s
12091210
"get_issues_for_transaction": rpc_get_issues_for_transaction,
12101211
"get_trace_waterfall": rpc_get_trace_waterfall,
12111212
"get_issue_details": get_issue_details,
1213+
"get_issue_and_event_details": get_issue_and_event_details,
12121214
"get_profile_flamegraph": rpc_get_profile_flamegraph,
12131215
"execute_table_query": execute_table_query,
12141216
"execute_timeseries_query": execute_timeseries_query,

src/sentry/seer/explorer/tools.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -778,6 +778,184 @@ def get_issue_details(
778778
}
779779

780780

781+
def get_issue_and_event_details(
782+
*,
783+
organization_id: int,
784+
issue_id: str | None,
785+
selected_event: str,
786+
) -> dict[str, Any] | None:
787+
"""
788+
Tool to get details for a Sentry issue and one of its associated events. null issue_id can be passed so the
789+
is issue is looked up from the event. We assume the event is always associated with an issue, otherwise None is returned.
790+
791+
Args:
792+
organization_id: The ID of the organization to query.
793+
issue_id: The issue/group ID (numeric) or short ID (string) to look up. If None, we fill this in with the event's `group` property.
794+
selected_event:
795+
If issue_id is provided, this is the event to return and must exist in the issue - the options are "oldest", "latest", "recommended", or a UUID.
796+
If issue_id is not provided, this must be a UUID.
797+
798+
Returns:
799+
A dict containing:
800+
Issue fields: aside from `issue` these are nullable if an error occurred.
801+
`issue`: Serialized issue details.
802+
`tags_overview`: A summary of all tags in the issue.
803+
`event_timeseries`: Event counts over time for the issue.
804+
`timeseries_stats_period`: The stats period used for the event timeseries.
805+
`timeseries_interval`: The interval used for the event timeseries.
806+
807+
Event fields:
808+
`event`: Serialized event details.
809+
`event_id`: The event ID of the selected event.
810+
`event_trace_id`: The trace ID of the selected event. Nullable.
811+
`project_id`: The event and issue's project ID.
812+
`project_slug`: The event and issue's project slug.
813+
814+
Returns None when the requested event or issue is not found, or an error occurred.
815+
"""
816+
try:
817+
organization = Organization.objects.get(id=organization_id)
818+
except Organization.DoesNotExist:
819+
logger.warning(
820+
"Organization does not exist",
821+
extra={"organization_id": organization_id, "issue_id": issue_id},
822+
)
823+
return None
824+
825+
org_project_ids = list(
826+
Project.objects.filter(organization=organization, status=ObjectStatus.ACTIVE).values_list(
827+
"id", flat=True
828+
)
829+
)
830+
if not org_project_ids:
831+
return None
832+
833+
event: Event | GroupEvent | None = None
834+
group: Group
835+
836+
# Fetch the group object.
837+
if issue_id is None:
838+
# If issue_id is not provided, first find the event. Then use this to fetch the group.
839+
uuid.UUID(selected_event) # Raises ValueError if not valid UUID
840+
# We can't use get_event_by_id since we don't know the exact project yet.
841+
events_result = eventstore.backend.get_events(
842+
filter=eventstore.Filter(
843+
event_ids=[selected_event],
844+
organization_id=organization_id,
845+
project_ids=org_project_ids,
846+
),
847+
limit=1,
848+
tenant_ids={"organization_id": organization_id},
849+
)
850+
if not events_result:
851+
logger.warning(
852+
"Could not find the requested event ID",
853+
extra={
854+
"organization_id": organization_id,
855+
"issue_id": issue_id,
856+
"selected_event": selected_event,
857+
},
858+
)
859+
return None
860+
861+
event = events_result[0]
862+
assert event is not None
863+
if event.group is None:
864+
logger.warning(
865+
"Event is not associated with a group",
866+
extra={"organization_id": organization_id, "event_id": event.event_id},
867+
)
868+
return None
869+
870+
group = event.group
871+
872+
else:
873+
# Fetch the group from issue_id.
874+
try:
875+
if issue_id.isdigit():
876+
group = Group.objects.get(project_id__in=org_project_ids, id=int(issue_id))
877+
else:
878+
group = Group.objects.by_qualified_short_id(organization_id, issue_id)
879+
880+
except Group.DoesNotExist:
881+
logger.warning(
882+
"Requested issue does not exist for organization",
883+
extra={"organization_id": organization_id, "issue_id": issue_id},
884+
)
885+
return None
886+
887+
# Get the issue data, tags overview, and event count timeseries.
888+
serialized_group = dict(serialize(group, user=None, serializer=GroupSerializer()))
889+
# Add issueTypeDescription as it provides better context for LLMs. Note the initial type should be BaseGroupSerializerResponse.
890+
serialized_group["issueTypeDescription"] = group.issue_type.description
891+
892+
try:
893+
tags_overview = get_all_tags_overview(group)
894+
except Exception:
895+
logger.exception(
896+
"Failed to get tags overview for issue",
897+
extra={"organization_id": organization_id, "issue_id": issue_id},
898+
)
899+
tags_overview = None
900+
901+
ts_result = _get_issue_event_timeseries(
902+
organization=organization,
903+
project_id=group.project_id,
904+
issue_short_id=group.qualified_short_id,
905+
first_seen_delta=datetime.now(UTC) - group.first_seen,
906+
)
907+
if ts_result:
908+
timeseries, timeseries_stats_period, timeseries_interval = ts_result
909+
else:
910+
timeseries, timeseries_stats_period, timeseries_interval = None, None, None
911+
912+
# Fetch event from group, if not already fetched.
913+
if event is None:
914+
if selected_event == "oldest":
915+
event = group.get_oldest_event()
916+
elif selected_event == "latest":
917+
event = group.get_latest_event()
918+
elif selected_event == "recommended":
919+
event = group.get_recommended_event()
920+
else:
921+
uuid.UUID(selected_event) # Raises ValueError if not valid UUID
922+
event = eventstore.backend.get_event_by_id(
923+
project_id=group.project_id,
924+
event_id=selected_event,
925+
group_id=group.id,
926+
tenant_ids={"organization_id": organization_id},
927+
)
928+
929+
if event is None:
930+
logger.warning(
931+
"Could not find the selected event.",
932+
extra={
933+
"organization_id": organization_id,
934+
"issue_id": issue_id,
935+
"selected_event": selected_event,
936+
},
937+
)
938+
return None
939+
940+
# Serialize event.
941+
serialized_event: IssueEventSerializerResponse = serialize(
942+
event, user=None, serializer=EventSerializer()
943+
)
944+
945+
return {
946+
"issue": serialized_group,
947+
"event_timeseries": timeseries,
948+
"timeseries_stats_period": timeseries_stats_period,
949+
"timeseries_interval": timeseries_interval,
950+
"tags_overview": tags_overview,
951+
"event": serialized_event,
952+
"event_id": event.event_id,
953+
"event_trace_id": event.trace_id,
954+
"project_id": event.project_id,
955+
"project_slug": event.project.slug,
956+
}
957+
958+
781959
def get_replay_metadata(
782960
*,
783961
replay_id: str,

0 commit comments

Comments
 (0)