-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
feat(anomaly detection):preview chart proxy api endpoint #77813
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
Merged
Merged
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
abe6e77
build out endpoint (placeholder function)
mifu67 0c83da9
remove todo
mifu67 466755d
add example
mifu67 b8415db
add test with placeholder response
mifu67 47fcc7a
fix
mifu67 1e566cb
replace config with typed config dict
mifu67 5820fc8
mypy :cat-scream:
mifu67 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
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.examples.organization_examples import OrganizationExamples | ||
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, | ||
}, | ||
examples=OrganizationExamples.GET_HISTORICAL_ANOMALIES, | ||
) | ||
def _format_historical_data(self, data) -> list[TimeSeriesPoint] | None: | ||
""" | ||
Format EventsStatsData into the format that the Seer API expects. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Mostly as a note to myself, but after my other pr lands we should be able to consolidate these functions and only have one. Mine serializes the data to get it into the format you're expecting here, but I can just do that before calling the function. |
||
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: | ||
return data | ||
|
||
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", | ||
mifu67 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
status=400, | ||
) | ||
|
||
anomalies = get_historical_anomaly_data_from_seer_preview( | ||
current_data, historical_data, project_id, config | ||
) | ||
mifu67 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# 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), | ||
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
54 changes: 54 additions & 0 deletions
54
tests/sentry/api/endpoints/test_organization_historical_anomalies.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import orjson | ||
|
||
from sentry.incidents.models.alert_rule import ( | ||
AlertRuleSeasonality, | ||
AlertRuleSensitivity, | ||
AlertRuleThresholdType, | ||
) | ||
from sentry.seer.anomaly_detection.types import AnomalyDetectionConfig | ||
from sentry.seer.anomaly_detection.utils import translate_direction | ||
from sentry.testutils.cases import APITestCase | ||
from sentry.testutils.helpers.features import with_feature | ||
from sentry.testutils.outbox import outbox_runner | ||
|
||
|
||
class OrganizationEventsAnomaliesEndpointTest(APITestCase): | ||
mifu67 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
endpoint = "sentry-api-0-organization-events-anomalies" | ||
|
||
method = "post" | ||
|
||
@with_feature("organizations:anomaly-detection-alerts") | ||
@with_feature("organizations:incidents") | ||
def test_simple(self): | ||
self.create_team(organization=self.organization, members=[self.user]) | ||
self.login_as(self.user) | ||
config = AnomalyDetectionConfig( | ||
time_period=60, | ||
sensitivity=AlertRuleSensitivity.LOW, | ||
direction=translate_direction(AlertRuleThresholdType.ABOVE.value), | ||
expected_seasonality=AlertRuleSeasonality.AUTO, | ||
) | ||
data = { | ||
mifu67 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"project_id": 1, | ||
"config": config, | ||
"current_data": [[1, {"count": 0.077881957}], [2, {"count": 0.075652768}]], | ||
"historical_data": [[169, {"count": 0.048480431}], [170, {"count": 0.047910238}]], | ||
} | ||
|
||
with outbox_runner(): | ||
resp = self.get_success_response( | ||
self.organization.slug, status_code=200, raw_data=orjson.dumps(data) | ||
) | ||
|
||
assert resp.data == [ | ||
{ | ||
"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, | ||
}, | ||
] |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.