@@ -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+
781959def get_replay_metadata (
782960 * ,
783961 replay_id : str ,
0 commit comments