Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(anomaly detection):preview chart proxy api endpoint #77813

Draft
wants to merge 2 commits into
base: master
Choose a base branch
from
Draft
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
96 changes: 96 additions & 0 deletions src/sentry/api/endpoints/organization_events_anomalies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
from drf_spectacular.utils import extend_schema
from rest_framework.request import Request
from rest_framework.response import Response

from sentry import features
from sentry.api.api_owners import ApiOwner
from sentry.api.api_publish_status import ApiPublishStatus
from sentry.api.base import region_silo_endpoint
from sentry.api.bases.organization_events import OrganizationEventsV2EndpointBase
from sentry.api.exceptions import ResourceDoesNotExist
from sentry.api.paginator import OffsetPaginator
from sentry.api.serializers.base import serialize
from sentry.apidocs.constants import (
RESPONSE_BAD_REQUEST,
RESPONSE_FORBIDDEN,
RESPONSE_NOT_FOUND,
RESPONSE_UNAUTHORIZED,
)
from sentry.apidocs.parameters import GlobalParams
from sentry.apidocs.utils import inline_sentry_response_serializer
from sentry.models.organization import Organization
from sentry.seer.anomaly_detection.get_historical_anomalies import (
get_historical_anomaly_data_from_seer_preview,
)
from sentry.seer.anomaly_detection.types import DetectAnomaliesResponse, TimeSeriesPoint


@region_silo_endpoint
class OrganizationEventsAnomaliesEndpoint(OrganizationEventsV2EndpointBase):
owner = ApiOwner.ALERTS_NOTIFICATIONS
publish_status = {
"POST": ApiPublishStatus.EXPERIMENTAL,
}

@extend_schema(
operation_id="Identify anomalies in historical data",
parameters=[GlobalParams.ORG_ID_OR_SLUG],
responses={
200: inline_sentry_response_serializer(
"ListAlertRuleAnomalies", DetectAnomaliesResponse
),
400: RESPONSE_BAD_REQUEST,
401: RESPONSE_UNAUTHORIZED,
403: RESPONSE_FORBIDDEN,
404: RESPONSE_NOT_FOUND,
},
)
def _format_historical_data(self, data) -> list[TimeSeriesPoint]:
"""
Format EventsStatsData into the format that the Seer API expects.
EventsStatsData is a list of lists with this format:
[epoch timestamp, {'count': count}]
Convert the data to this format:
list[TimeSeriesPoint]
"""
if data is None:
# TODO: figure out error handling here
pass
formatted_data: list[TimeSeriesPoint] = []
for datum in data:
ts_point = TimeSeriesPoint(timestamp=datum[0], value=datum[1].get("count", 0))
formatted_data.append(ts_point)
return formatted_data

def post(self, request: Request, organization: Organization) -> Response:
"""
Return a list of anomalies for a time series of historical event data.
"""
if not features.has("organizations:anomaly-detection-alerts", organization):
raise ResourceDoesNotExist("Your organization does not have access to this feature.")

historical_data = self._format_historical_data(request.data.get("historical_data"))
current_data = self._format_historical_data(request.data.get("current_data"))

config = request.data.get("config")
project_id = request.data.get("project_id")

if project_id is None or not config or not historical_data or not current_data:
return Response(
"Unable to get historical anomaly data: missing required argument(s) project, start, and/or end",
status=400,
)

anomalies = get_historical_anomaly_data_from_seer_preview(
project_id, config, historical_data, current_data
)
# NOTE: returns None if there's a problem with the Seer response
if anomalies is None:
return Response("Unable to get historical anomaly data", status=400)
# NOTE: returns empty list if there is not enough event data
return self.paginate(
request=request,
queryset=anomalies,
paginator_cls=OffsetPaginator,
on_results=lambda x: serialize(x, request.user),
)
6 changes: 6 additions & 0 deletions src/sentry/api/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from sentry.api.endpoints.issues.related_issues import RelatedIssuesEndpoint
from sentry.api.endpoints.org_auth_token_details import OrgAuthTokenDetailsEndpoint
from sentry.api.endpoints.org_auth_tokens import OrgAuthTokensEndpoint
from sentry.api.endpoints.organization_events_anomalies import OrganizationEventsAnomaliesEndpoint
from sentry.api.endpoints.organization_events_root_cause_analysis import (
OrganizationEventsRootCauseAnalysisEndpoint,
)
Expand Down Expand Up @@ -1415,6 +1416,11 @@ def create_group_urls(name_prefix: str) -> list[URLPattern | URLResolver]:
OrganizationEventsStatsEndpoint.as_view(),
name="sentry-api-0-organization-events-stats",
),
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/events/anomalies/$",
OrganizationEventsAnomaliesEndpoint.as_view(),
name="sentry-api-0-organization-events-anomalies",
),
re_path(
r"^(?P<organization_id_or_slug>[^\/]+)/project-templates/$",
OrganizationProjectTemplatesIndexEndpoint.as_view(),
Expand Down
31 changes: 30 additions & 1 deletion src/sentry/seer/anomaly_detection/get_historical_anomalies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@
from sentry.incidents.models.alert_rule import AlertRule, AlertRuleStatus
from sentry.models.project import Project
from sentry.net.http import connection_from_url
from sentry.seer.anomaly_detection.types import AnomalyDetectionConfig, DetectAnomaliesRequest
from sentry.seer.anomaly_detection.types import (
AnomalyDetectionConfig,
DetectAnomaliesRequest,
TimeSeriesPoint,
)
from sentry.seer.anomaly_detection.utils import (
fetch_historical_data,
format_historical_data,
Expand All @@ -28,6 +32,31 @@
)


def get_historical_anomaly_data_from_seer_preview(
current_data: list[TimeSeriesPoint],
historical_data: list[TimeSeriesPoint],
project_id: int,
config: AnomalyDetectionConfig,
) -> list | None:
"""
Send current and historical timeseries data to Seer and return anomaly detection response on the current timeseries.

Dummy function. TODO: write out the Seer request logic.
"""
return [
{
"anomaly": {"anomaly_score": -0.38810767243044786, "anomaly_type": "none"},
"timestamp": 169,
"value": 0.048480431,
},
{
"anomaly": {"anomaly_score": -0.3890542800124323, "anomaly_type": "none"},
"timestamp": 170,
"value": 0.047910238,
},
]


def get_historical_anomaly_data_from_seer(
alert_rule: AlertRule, project: Project, start_string: str, end_string: str
) -> list | None:
Expand Down
Loading