Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/sentry/seer/endpoints/seer_rpc.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@
from sentry.seer.explorer.tools import (
execute_table_query,
execute_timeseries_query,
get_issue_and_event_details,
get_issue_details,
get_replay_metadata,
get_repository_definition,
Expand Down Expand Up @@ -1209,6 +1210,7 @@ def check_repository_integrations_status(*, repository_integrations: list[dict[s
"get_issues_for_transaction": rpc_get_issues_for_transaction,
"get_trace_waterfall": rpc_get_trace_waterfall,
"get_issue_details": get_issue_details,
"get_issue_and_event_details": get_issue_and_event_details,
"get_profile_flamegraph": rpc_get_profile_flamegraph,
"execute_table_query": execute_table_query,
"execute_timeseries_query": execute_timeseries_query,
Expand Down
178 changes: 178 additions & 0 deletions src/sentry/seer/explorer/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -725,6 +725,184 @@ def get_issue_details(
}


def get_issue_and_event_details(
*,
organization_id: int,
issue_id: str | None,
selected_event: str,
) -> dict[str, Any] | None:
"""
Tool to get details for a Sentry issue and one of its associated events. null issue_id can be passed so the
is issue is looked up from the event. We assume the event is always associated with an issue, otherwise None is returned.

Args:
organization_id: The ID of the organization to query.
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.
selected_event:
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.
If issue_id is not provided, this must be a UUID.

Returns:
A dict containing:
Issue fields: aside from `issue` these are nullable if an error occurred.
`issue`: Serialized issue details.
`tags_overview`: A summary of all tags in the issue.
`event_timeseries`: Event counts over time for the issue.
`timeseries_stats_period`: The stats period used for the event timeseries.
`timeseries_interval`: The interval used for the event timeseries.

Event fields:
`event`: Serialized event details.
`event_id`: The event ID of the selected event.
`event_trace_id`: The trace ID of the selected event. Nullable.
`project_id`: The event and issue's project ID.
`project_slug`: The event and issue's project slug.

Returns None when the requested event or issue is not found, or an error occurred.
"""
try:
organization = Organization.objects.get(id=organization_id)
except Organization.DoesNotExist:
logger.warning(
"Organization does not exist",
extra={"organization_id": organization_id, "issue_id": issue_id},
)
return None

org_project_ids = list(
Project.objects.filter(organization=organization, status=ObjectStatus.ACTIVE).values_list(
"id", flat=True
)
)
if not org_project_ids:
return None

event: Event | GroupEvent | None = None
group: Group

# Fetch the group object.
if issue_id is None:
# If issue_id is not provided, first find the event. Then use this to fetch the group.
uuid.UUID(selected_event) # Raises ValueError if not valid UUID
# We can't use get_event_by_id since we don't know the exact project yet.
events_result = eventstore.backend.get_events(
filter=eventstore.Filter(
event_ids=[selected_event],
organization_id=organization_id,
project_ids=org_project_ids,
),
limit=1,
tenant_ids={"organization_id": organization_id},
)
if not events_result:
logger.warning(
"Could not find the requested event ID",
extra={
"organization_id": organization_id,
"issue_id": issue_id,
"selected_event": selected_event,
},
)
return None

event = events_result[0]
assert event is not None
if event.group is None:
logger.warning(
"Event is not associated with a group",
extra={"organization_id": organization_id, "event_id": event.event_id},
)
return None

group = event.group

else:
# Fetch the group from issue_id.
try:
if issue_id.isdigit():
group = Group.objects.get(project_id__in=org_project_ids, id=int(issue_id))
else:
group = Group.objects.by_qualified_short_id(organization_id, issue_id)

except Group.DoesNotExist:
logger.warning(
"Requested issue does not exist for organization",
extra={"organization_id": organization_id, "issue_id": issue_id},
)
return None

# Get the issue data, tags overview, and event count timeseries.
serialized_group = dict(serialize(group, user=None, serializer=GroupSerializer()))
# Add issueTypeDescription as it provides better context for LLMs. Note the initial type should be BaseGroupSerializerResponse.
serialized_group["issueTypeDescription"] = group.issue_type.description

try:
tags_overview = get_all_tags_overview(group)
except Exception:
logger.exception(
"Failed to get tags overview for issue",
extra={"organization_id": organization_id, "issue_id": issue_id},
)
tags_overview = None

ts_result = _get_issue_event_timeseries(
organization=organization,
project_id=group.project_id,
issue_short_id=group.qualified_short_id,
first_seen_delta=datetime.now(UTC) - group.first_seen,
)
if ts_result:
timeseries, timeseries_stats_period, timeseries_interval = ts_result
else:
timeseries, timeseries_stats_period, timeseries_interval = None, None, None

# Fetch event from group, if not already fetched.
if event is None:
if selected_event == "oldest":
event = group.get_oldest_event()
elif selected_event == "latest":
event = group.get_latest_event()
elif selected_event == "recommended":
event = group.get_recommended_event()
else:
uuid.UUID(selected_event) # Raises ValueError if not valid UUID
event = eventstore.backend.get_event_by_id(
project_id=group.project_id,
event_id=selected_event,
group_id=group.id,
tenant_ids={"organization_id": organization_id},
)

if event is None:
logger.warning(
"Could not find the selected event.",
extra={
"organization_id": organization_id,
"issue_id": issue_id,
"selected_event": selected_event,
},
)
return None

# Serialize event.
serialized_event: IssueEventSerializerResponse = serialize(
event, user=None, serializer=EventSerializer()
)

return {
"issue": serialized_group,
"event_timeseries": timeseries,
"timeseries_stats_period": timeseries_stats_period,
"timeseries_interval": timeseries_interval,
"tags_overview": tags_overview,
"event": serialized_event,
"event_id": event.event_id,
"event_trace_id": event.trace_id,
"project_id": event.project_id,
"project_slug": event.project.slug,
}


def get_replay_metadata(
*,
replay_id: str,
Expand Down
Loading
Loading