@@ -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+
10741288def 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