Skip to content

Commit 171b6ec

Browse files
authored
feat(explorer): Add get_issue_details and get_event_details RPCs (#110027)
1 parent 64c8d12 commit 171b6ec

File tree

4 files changed

+760
-43
lines changed

4 files changed

+760
-43
lines changed

src/sentry/seer/endpoints/organization_seer_rpc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,9 @@
5454
execute_trace_table_query,
5555
get_baseline_tag_distribution,
5656
get_comparative_attribute_distributions,
57+
get_event_details,
5758
get_issue_and_event_details_v2,
59+
get_issue_details,
5860
get_log_attributes_for_trace,
5961
get_metric_attributes_for_trace,
6062
get_replay_metadata,
@@ -104,6 +106,8 @@
104106
"execute_trace_table_query": execute_trace_table_query,
105107
"execute_issues_query": map_org_id_param(execute_issues_query),
106108
"get_issue_and_event_details_v2": get_issue_and_event_details_v2,
109+
"get_issue_details": get_issue_details,
110+
"get_event_details": get_event_details,
107111
"get_profile_flamegraph": rpc_get_profile_flamegraph,
108112
"get_replay_metadata": get_replay_metadata,
109113
"get_log_attributes_for_trace": map_org_id_param(get_log_attributes_for_trace),

src/sentry/seer/endpoints/seer_rpc.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,9 @@
9191
execute_trace_table_query,
9292
get_baseline_tag_distribution,
9393
get_comparative_attribute_distributions,
94+
get_event_details,
9495
get_issue_and_event_details_v2,
96+
get_issue_details,
9597
get_log_attributes_for_trace,
9698
get_metric_attributes_for_trace,
9799
get_replay_metadata,
@@ -794,6 +796,8 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s
794796
"get_issues_for_transaction": rpc_get_issues_for_transaction,
795797
"get_trace_waterfall": rpc_get_trace_waterfall,
796798
"get_issue_and_event_details_v2": get_issue_and_event_details_v2,
799+
"get_issue_details": get_issue_details,
800+
"get_event_details": get_event_details,
797801
"get_profile_flamegraph": rpc_get_profile_flamegraph,
798802
"execute_table_query": execute_table_query,
799803
"execute_timeseries_query": execute_timeseries_query,

src/sentry/seer/explorer/tools.py

Lines changed: 215 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1071,6 +1071,220 @@ def get_issue_and_event_response(
10711071
return result
10721072

10731073

1074+
def get_issue_details(
1075+
*,
1076+
organization_id: int,
1077+
issue_id: str,
1078+
start: str | None = None,
1079+
end: str | None = None,
1080+
project_slug: str | None = None,
1081+
) -> dict[str, Any] | None:
1082+
"""
1083+
Get issue-level details for an issue, optionally scoped by time range.
1084+
1085+
Args:
1086+
organization_id: The ID of the organization.
1087+
issue_id: The issue ID (numeric) or qualified short ID (e.g. PROJECT-123).
1088+
start: ISO timestamp for the start of the time range (optional).
1089+
end: ISO timestamp for the end of the time range (optional).
1090+
project_slug: The slug of the project (optional, used to improve numeric ID lookups).
1091+
1092+
Returns:
1093+
Dict with issue metadata, event_timeseries, tags_overview, and user_activity, or None if not found.
1094+
"""
1095+
start_dt, end_dt = get_date_range_from_params({"start": start, "end": end}, optional=True)
1096+
1097+
organization = Organization.objects.get(id=organization_id)
1098+
group: Group
1099+
if issue_id.isdigit():
1100+
project_ids = list(
1101+
Project.objects.filter(
1102+
organization=organization,
1103+
status=ObjectStatus.ACTIVE,
1104+
**({"slug": project_slug} if project_slug else {}),
1105+
).values_list("id", flat=True)
1106+
)
1107+
if not project_ids:
1108+
return None
1109+
1110+
group = Group.objects.get(project_id__in=project_ids, id=int(issue_id))
1111+
else:
1112+
# Note short IDs are already scoped to a project so no need for project filtering.
1113+
group = Group.objects.by_qualified_short_id(organization_id, issue_id)
1114+
1115+
# Get the issue metadata.
1116+
serialized_group = dict(serialize(group, user=None, serializer=GroupSerializer()))
1117+
# Add issueTypeDescription as it provides better context for LLMs. Note the initial type should be BaseGroupSerializerResponse.
1118+
serialized_group["issueTypeDescription"] = group.issue_type.description
1119+
1120+
# Get aggregate tag and event data and activity.
1121+
try:
1122+
tags_overview = get_all_tags_overview(group, start_dt, end_dt)
1123+
except Exception:
1124+
logger.exception(
1125+
"get_issue_details: Failed to get tags overview",
1126+
extra={"organization_id": organization_id, "issue_id": issue_id},
1127+
)
1128+
tags_overview = None
1129+
1130+
try:
1131+
ts_result = _get_issue_event_timeseries(
1132+
group=group,
1133+
organization=organization,
1134+
start=start_dt,
1135+
end=end_dt,
1136+
)
1137+
except Exception:
1138+
logger.exception(
1139+
"get_issue_details: Failed to get event timeseries",
1140+
extra={"organization_id": organization_id, "issue_id": issue_id},
1141+
)
1142+
ts_result = None
1143+
1144+
if ts_result:
1145+
timeseries, timeseries_stats_period, timeseries_interval = ts_result
1146+
else:
1147+
timeseries, timeseries_stats_period, timeseries_interval = None, None, None
1148+
1149+
try:
1150+
activities = Activity.objects.filter(
1151+
group=group,
1152+
type__in=_SEER_EXPLORER_ACTIVITY_TYPES,
1153+
).order_by("-datetime")[:50]
1154+
serialized_activities = serialize(
1155+
list(activities), user=None, serializer=ActivitySerializer()
1156+
)
1157+
except Exception:
1158+
logger.exception(
1159+
"get_issue_details: Failed to get user activity",
1160+
extra={"organization_id": organization_id, "issue_id": issue_id},
1161+
)
1162+
serialized_activities = []
1163+
1164+
return {
1165+
"issue": serialized_group,
1166+
"event_timeseries": timeseries,
1167+
"timeseries_stats_period": timeseries_stats_period,
1168+
"timeseries_interval": timeseries_interval,
1169+
"tags_overview": tags_overview,
1170+
"user_activity": serialized_activities,
1171+
}
1172+
1173+
1174+
def get_event_details(
1175+
*,
1176+
organization_id: int,
1177+
event_id: str | None = None,
1178+
issue_id: str | None = None,
1179+
start: str | None = None,
1180+
end: str | None = None,
1181+
project_slug: str | None = None,
1182+
) -> dict[str, Any] | None:
1183+
"""
1184+
Get event details by event ID, or get the recommended event for an issue, optionally scoped by time range.
1185+
Exactly one of event_id or issue_id must be provided.
1186+
1187+
Args:
1188+
organization_id: The ID of the organization.
1189+
event_id: The UUID of the event (mutually exclusive with issue_id).
1190+
issue_id: The issue ID (numeric) or qualified short ID (mutually exclusive with event_id).
1191+
start: ISO timestamp for the start of the time range to get recommended event for (optional).
1192+
end: ISO timestamp for the end of the time range to get recommended event for (optional).
1193+
project_slug: The slug of the project (optional).
1194+
1195+
Returns:
1196+
Dict with serialized event, event_id, event_trace_id, project_id, project_slug, or None if not found.
1197+
"""
1198+
if bool(event_id) == bool(issue_id):
1199+
raise BadRequest("Either event_id or issue_id must be provided, but not both.")
1200+
1201+
organization = Organization.objects.get(id=organization_id)
1202+
1203+
project_ids = list(
1204+
Project.objects.filter(
1205+
organization=organization,
1206+
status=ObjectStatus.ACTIVE,
1207+
**({"slug": project_slug} if project_slug else {}),
1208+
).values_list("id", flat=True)
1209+
)
1210+
if not project_ids:
1211+
return None
1212+
1213+
event: Event | GroupEvent | None
1214+
group: Group | None
1215+
1216+
if event_id is None:
1217+
start_dt, end_dt = get_date_range_from_params({"start": start, "end": end}, optional=True)
1218+
1219+
# Fetch the group then get a sample event from the time range.
1220+
assert issue_id is not None
1221+
if issue_id.isdigit():
1222+
group = Group.objects.get(project_id__in=project_ids, id=int(issue_id))
1223+
else:
1224+
group = Group.objects.by_qualified_short_id(organization_id, issue_id)
1225+
assert group is not None
1226+
event = _get_recommended_event(group, organization, start_dt, end_dt)
1227+
1228+
else:
1229+
# Fetch the event directly by ID.
1230+
uuid.UUID(event_id) # Raises ValueError if not valid UUID
1231+
if len(project_ids) == 1:
1232+
event = eventstore.backend.get_event_by_id(
1233+
project_id=project_ids[0],
1234+
event_id=event_id,
1235+
tenant_ids={"organization_id": organization_id},
1236+
)
1237+
else:
1238+
# Error events live in Events, occurrence events in IssuePlatform;
1239+
# we don't know which dataset holds this event_id until we query.
1240+
event = None
1241+
for dataset in (Dataset.Events, Dataset.IssuePlatform):
1242+
events_result = eventstore.backend.get_events(
1243+
filter=eventstore.Filter(
1244+
event_ids=[event_id],
1245+
organization_id=organization_id,
1246+
project_ids=project_ids,
1247+
),
1248+
limit=1,
1249+
tenant_ids={"organization_id": organization_id},
1250+
dataset=dataset,
1251+
)
1252+
if events_result:
1253+
event = events_result[0]
1254+
break
1255+
1256+
group = event.group if event else None
1257+
1258+
# Convert Event to GroupEvent so the occurrence (if any) can be lazy-loaded
1259+
# from nodestore via the occurrence_id in snuba_data during serialization.
1260+
if event is not None and group is not None and isinstance(event, Event):
1261+
event = event.for_group(group)
1262+
1263+
if event is None:
1264+
logger.warning(
1265+
"get_event_details: Event not found",
1266+
extra={
1267+
"organization_id": organization_id,
1268+
"project_slug": project_slug,
1269+
"issue_id": issue_id,
1270+
"event_id": event_id,
1271+
"start": start,
1272+
"end": end,
1273+
},
1274+
)
1275+
return None
1276+
1277+
serialized_event = serialize(event, user=None, serializer=EventSerializer())
1278+
1279+
return {
1280+
"event": serialized_event,
1281+
"event_id": event.event_id,
1282+
"event_trace_id": event.trace_id,
1283+
"project_id": event.project_id,
1284+
"project_slug": event.project.slug,
1285+
}
1286+
1287+
10741288
def get_issue_and_event_details_v2(
10751289
*,
10761290
organization_id: int,
@@ -1108,7 +1322,7 @@ def get_issue_and_event_details_v2(
11081322
group = Group.objects.get(project_id__in=project_ids, id=int(issue_id))
11091323
else:
11101324
group = Group.objects.by_qualified_short_id(organization_id, issue_id)
1111-
1325+
assert group is not None
11121326
event = _get_recommended_event(group, organization, start_dt, end_dt)
11131327

11141328
else:

0 commit comments

Comments
 (0)