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
7 changes: 4 additions & 3 deletions src/sentry/replays/usecases/summarize.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
)
from sentry.replays.usecases.reader import fetch_segments_metadata, iter_segment_data
from sentry.search.events.types import SnubaParams
from sentry.seer.sentry_data_models import ReplaySummaryLogsResponse
from sentry.services.eventstore.models import Event
from sentry.snuba.referrer import Referrer
from sentry.utils import json, metrics
Expand Down Expand Up @@ -519,7 +520,7 @@ def rpc_get_replay_summary_logs(
project_id: int,
replay_id: str,
num_segments: int,
) -> dict[str, Any]:
) -> ReplaySummaryLogsResponse:
"""
RPC call for Seer. Downloads a replay's segment data, queries associated errors, and parses this into summary logs.
"""
Expand All @@ -546,7 +547,7 @@ def rpc_get_replay_summary_logs(
# 404s should be handled in the originating Sentry endpoint.
# If the replay is missing here just return an empty response.
if not processed_response:
return {"logs": []}
return ReplaySummaryLogsResponse(logs=[])

error_ids = processed_response[0].get("error_ids", [])
trace_ids = processed_response[0].get("trace_ids", [])
Expand Down Expand Up @@ -611,4 +612,4 @@ def rpc_get_replay_summary_logs(
is_mobile_replay=is_mobile_replay,
replay_start=replay_start,
)
return {"logs": logs}
return ReplaySummaryLogsResponse(logs=logs)
18 changes: 10 additions & 8 deletions src/sentry/seer/agent/snapshot_indexes.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import logging
from typing import Any, TypedDict

from pydantic import ValidationError

from sentry.seer.models import SeerApiError
from sentry.seer.sentry_data_models import AgentExportIndexesResponse
from sentry.seer.signed_seer_api import (
AgentExportIndexesRequest,
SeerViewerContext,
Expand All @@ -12,12 +14,6 @@
logger = logging.getLogger(__name__)


class AgentExportIndexesResponse(TypedDict):
org_id: int
version: int
tables: dict[str, list[dict[str, Any]]]


def export_agent_indexes(*, org_id: int) -> AgentExportIndexesResponse:
"""Export all explorer index rows for an org from Seer's database.

Expand All @@ -31,7 +27,13 @@ def export_agent_indexes(*, org_id: int) -> AgentExportIndexesResponse:
raise SeerApiError("Seer export-indexes request failed", response.status)

try:
return response.json()
return AgentExportIndexesResponse(**response.json())
except JSONDecodeError:
logger.exception("Failed to parse Seer export-indexes response")
Comment thread
sentry-warden[bot] marked this conversation as resolved.
raise SeerApiError("Seer returned invalid JSON response", response.status)
except ValidationError:
logger.exception("Seer export-indexes response failed schema validation")
raise SeerApiError(
"Seer returned a response that did not match the export-indexes schema",
response.status,
)
79 changes: 43 additions & 36 deletions src/sentry/seer/agent/tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@
from sentry.seer.autofix.autofix import get_all_tags_overview
from sentry.seer.seer_setup import get_supported_scm_providers
from sentry.seer.sentry_data_models import (
BaselineTagDistributionEntry,
BaselineTagDistributionResponse,
ComparativeAttributeDistributionsResponse,
EAPTrace,
EmptyResponse,
EventDetailsResponse,
Expand All @@ -67,6 +70,10 @@
GetDsnResponse,
IssueAndEventDetailsResponse,
IssueDetailsResponse,
ProfileFlamegraphErrorResponse,
ProfileFlamegraphMetadata,
ProfileFlamegraphSuccessResponse,
ReplayMetadataResponse,
RepositoryDefinitionResponse,
TraceItemAttributesResponse,
TraceItemEventsResponse,
Expand Down Expand Up @@ -654,7 +661,7 @@ def rpc_get_profile_flamegraph(
organization_id: int,
trace_id: str | None = None,
span_description: str | None = None,
) -> dict[str, Any]:
) -> ProfileFlamegraphSuccessResponse | ProfileFlamegraphErrorResponse:
"""
Fetch and format a profile flamegraph by profile ID (8-char or full 32-char).

Expand Down Expand Up @@ -683,7 +690,7 @@ def rpc_get_profile_flamegraph(
"rpc_get_profile_flamegraph: Organization not found",
extra={"organization_id": organization_id},
)
return {"error": "Organization not found"}
return ProfileFlamegraphErrorResponse(error="Organization not found")

# Get all projects for the organization
projects = list(Project.objects.filter(organization=organization, status=ObjectStatus.ACTIVE))
Expand All @@ -693,7 +700,7 @@ def rpc_get_profile_flamegraph(
"rpc_get_profile_flamegraph: No projects found for organization",
extra={"organization_id": organization_id},
)
return {"error": "No projects found for organization"}
return ProfileFlamegraphErrorResponse(error="No projects found for organization")

# Search up to 90 days back using 14-day sliding windows
now = datetime.now(UTC)
Expand Down Expand Up @@ -794,13 +801,13 @@ def rpc_get_profile_flamegraph(
"rpc_get_profile_flamegraph: Profile not found",
extra={"profile_id": profile_id, "organization_id": organization_id},
)
return {"error": "Profile not found in the last 90 days"}
return ProfileFlamegraphErrorResponse(error="Profile not found in the last 90 days")
if not project_id:
logger.warning(
"rpc_get_profile_flamegraph: Could not find project id for profile",
extra={"profile_id": profile_id, "organization_id": organization_id},
)
return {"error": "Project not found"}
return ProfileFlamegraphErrorResponse(error="Project not found")

logger.info(
"rpc_get_profile_flamegraph: Found profile",
Expand Down Expand Up @@ -828,7 +835,9 @@ def rpc_get_profile_flamegraph(
"rpc_get_profile_flamegraph: Failed to fetch profile data from profiling service",
extra={"profile_id": actual_profile_id, "project_id": project_id},
)
return {"error": "Failed to fetch profile data from profiling service"}
return ProfileFlamegraphErrorResponse(
error="Failed to fetch profile data from profiling service"
)

# Convert to execution tree (returns dicts, not Pydantic models)
execution_tree, selected_thread_id = _convert_profile_to_execution_tree(profile_data)
Expand All @@ -842,19 +851,21 @@ def rpc_get_profile_flamegraph(
"raw_profile_data": profile_data,
},
)
return {"error": "Failed to generate execution tree from profile data"}
return ProfileFlamegraphErrorResponse(
error="Failed to generate execution tree from profile data"
)

return {
"execution_tree": execution_tree,
"metadata": {
"profile_id": actual_profile_id,
"project_id": project_id,
"is_continuous": is_continuous,
"start_ts": min_start_ts,
"end_ts": max_end_ts,
"thread_id": selected_thread_id,
},
}
return ProfileFlamegraphSuccessResponse(
execution_tree=execution_tree,
metadata=ProfileFlamegraphMetadata(
profile_id=actual_profile_id,
project_id=project_id,
is_continuous=is_continuous,
start_ts=min_start_ts,
end_ts=max_end_ts,
thread_id=selected_thread_id,
),
)


def get_repository_definition(
Expand Down Expand Up @@ -1683,7 +1694,7 @@ def get_replay_metadata(
replay_id: str,
organization_id: int,
project_slug: str | None = None,
) -> dict[str, Any] | None:
) -> ReplayMetadataResponse | None:
"""
Get the metadata for a replay through an aggregate replay event query.

Expand Down Expand Up @@ -1780,7 +1791,7 @@ def get_replay_metadata(
result["project_slug"] = next(
filter(lambda x: x[0] == int(result["project_id"]), p_ids_and_slugs)
)[1]
return result
return ReplayMetadataResponse(__root__=result)


def get_trace_item_attributes(
Expand Down Expand Up @@ -2123,7 +2134,7 @@ def get_baseline_tag_distribution(
stats_period: str | None = None,
start: str | None = None,
end: str | None = None,
) -> dict[str, Any] | None:
) -> BaselineTagDistributionResponse:
"""
Get baseline tag distribution for suspect attributes analysis.

Expand Down Expand Up @@ -2158,7 +2169,7 @@ def get_baseline_tag_distribution(
)

if not tag_keys:
return {"baseline_tag_distribution": []}
return BaselineTagDistributionResponse(baseline_tag_distribution=[])

# Use first/last seen if date params are not provided.
start_dt, end_dt = get_group_date_range(group, organization, start_dt, end_dt)
Expand Down Expand Up @@ -2226,15 +2237,11 @@ def get_baseline_tag_distribution(
combined_counts[key] = combined_counts.get(key, 0) + result["count"]

baseline_distribution = [
{
"tag_key": tag_key,
"tag_value": tag_value,
"count": count,
}
BaselineTagDistributionEntry(tag_key=tag_key, tag_value=tag_value, count=count)
for (tag_key, tag_value), count in combined_counts.items()
]

return {"baseline_tag_distribution": baseline_distribution}
return BaselineTagDistributionResponse(baseline_tag_distribution=baseline_distribution)


def get_comparative_attribute_distributions(
Expand All @@ -2251,7 +2258,7 @@ def get_comparative_attribute_distributions(
project_ids: list[int] | None = None,
project_slugs: list[str] | None = None,
sampling_mode: SAMPLING_MODES = "NORMAL",
) -> dict[str, Any] | None:
) -> ComparativeAttributeDistributionsResponse:
"""
Fetch span attribute distributions for a selected time range (minute precision) compared to a baseline (defined by start/end/stats_period params).
The selected range should be smaller and within the larger range. This is not validated.
Expand Down Expand Up @@ -2320,13 +2327,13 @@ def get_comparative_attribute_distributions(
query_2=query_2,
)

return {
"baseline_distribution": distributions_result["cohort_2_distribution"],
"total_baseline": distributions_result["total_cohort_2"],
"outliers_distribution": distributions_result["cohort_1_distribution"],
"total_outliers": distributions_result["total_cohort_1"],
"outliers_function_value": distributions_result["cohort_1_function_value"],
}
return ComparativeAttributeDistributionsResponse(
baseline_distribution=distributions_result["cohort_2_distribution"],
total_baseline=distributions_result["total_cohort_2"],
outliers_distribution=distributions_result["cohort_1_distribution"],
total_outliers=distributions_result["total_cohort_1"],
outliers_function_value=distributions_result["cohort_1_function_value"],
)


def get_dsn(
Expand Down
7 changes: 4 additions & 3 deletions src/sentry/seer/assisted_query/issues_tools.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
FilterKeyValuesResponse,
IssueFilterBuiltInField,
IssueFilterKeysResponse,
IssuesStatsResponse,
TagFilterKeyValue,
)
from sentry.snuba.dataset import Dataset
Expand Down Expand Up @@ -738,7 +739,7 @@ def get_issues_stats(
stats_period: str | None = None,
start: str | None = None,
end: str | None = None,
) -> list[dict[str, Any]] | None:
) -> IssuesStatsResponse | None:
"""
Get stats for specific issues by calling the issues-stats endpoint.

Expand All @@ -765,7 +766,7 @@ def get_issues_stats(
return None

if not issue_ids:
return []
return IssuesStatsResponse(__root__=[])

api_key = ApiKey(organization_id=organization.id, scope_list=API_KEY_SCOPES)

Expand All @@ -788,4 +789,4 @@ def get_issues_stats(
params=params,
)

return resp.data
return IssuesStatsResponse(__root__=resp.data)
6 changes: 3 additions & 3 deletions src/sentry/seer/autofix/autofix_tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from sentry.api.serializers import EventSerializer, serialize
from sentry.seer.agent.utils import _convert_profile_to_execution_tree, fetch_profile_data
from sentry.seer.sentry_data_models import ProfileDetailsResponse
from sentry.seer.sentry_data_models import ErrorEventDetailsResponse, ProfileDetailsResponse
from sentry.services import eventstore


Expand Down Expand Up @@ -29,10 +29,10 @@ def get_profile_details(
return ProfileDetailsResponse(execution_tree=execution_tree)


def get_error_event_details(project_id: int, event_id: str):
def get_error_event_details(project_id: int, event_id: str) -> ErrorEventDetailsResponse | None:
event = eventstore.backend.get_event_by_id(project_id, event_id)
if not event:
return None

serialized_event = serialize(objects=event, user=None, serializer=EventSerializer())
return serialized_event
return ErrorEventDetailsResponse(__root__=serialized_event)
Loading
Loading