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
5 changes: 5 additions & 0 deletions src/sentry/search/eap/uptime_results/attributes.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@
internal_name="status_reason_description",
search_type="string",
),
ResolvedAttribute(
public_alias="assertion_failure_data",
internal_name="assertion_failure_data",
search_type="string",
),
ResolvedAttribute(
public_alias="method",
internal_name="method",
Expand Down
26 changes: 25 additions & 1 deletion src/sentry/uptime/endpoints/project_uptime_alert_checks_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from rest_framework.request import Request
from rest_framework.response import Response
from sentry_kafka_schemas.schema_types.snuba_uptime_results_v1 import (
Assertion,
CheckStatus,
CheckStatusReasonType,
)
Expand Down Expand Up @@ -36,7 +37,7 @@
from sentry.uptime.endpoints.serializers import EapCheckEntrySerializerResponse
from sentry.uptime.models import UptimeSubscription, get_uptime_subscription
from sentry.uptime.types import EapCheckEntry, IncidentStatus
from sentry.utils import snuba_rpc
from sentry.utils import json, snuba_rpc
from sentry.workflow_engine.models import Detector

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -194,6 +195,9 @@ def _transform_row(
(duration_val.val_int // 1000) if duration_val and not duration_val.is_null else 0
)
trace_id = row_dict["sentry.trace_id"].val_str
assertion_failure_data = self._extract_assertion_failure_data(
row_dict.get("assertion_failure_data")
)

return EapCheckEntry(
uptime_check_id=uptime_check_id,
Expand All @@ -213,8 +217,28 @@ def _transform_row(
incident_status=IncidentStatus(row_dict["incident_status"].val_int),
environment=row_dict.get("environment", AttributeValue(val_str="")).val_str,
region=row_dict["region"].val_str,
assertion_failure_data=assertion_failure_data,
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should probably json loads this so that we don't just return a json string from the api?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I can do that, it's just that this will also go as a trace-item attr, which only accepts primitives as values.

Keeps the frontend consumers aligned with what they expect.

Since there's an opportunity here, lets just send the json, instead of a str

)

def _extract_assertion_failure_data(self, val: AttributeValue | None) -> Assertion | None:
"""
Extract assertion failure data from attribute value.

This field is stored in EAP as a JSON-encoded string; return the decoded JSON value.
If missing/null/empty or invalid JSON, return None.
"""
if not val or val.is_null:
return None

raw = val.val_str
if raw == "":
return None

try:
return json.loads(raw)
except (TypeError, ValueError):
return None

def _extract_check_status_reason(
self, check_status_reason_val: AttributeValue | None
) -> CheckStatusReasonType | None:
Expand Down
3 changes: 3 additions & 0 deletions src/sentry/uptime/endpoints/serializers.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Any, Literal, TypedDict, cast, override

from sentry_kafka_schemas.schema_types.snuba_uptime_results_v1 import (
Assertion,
CheckStatus,
CheckStatusReasonType,
)
Expand Down Expand Up @@ -154,6 +155,7 @@ class EapCheckEntrySerializerResponse(TypedDict):
scheduledCheckTime: str
checkStatus: SerializedCheckStatus
checkStatusReason: CheckStatusReasonType | None
assertionFailureData: Assertion | None
httpStatusCode: int | None
durationMs: int
traceId: str
Expand Down Expand Up @@ -185,6 +187,7 @@ def serialize(
"scheduledCheckTime": obj.scheduled_check_time.strftime("%Y-%m-%dT%H:%M:%SZ"),
"checkStatus": check_status,
"checkStatusReason": obj.check_status_reason,
"assertionFailureData": obj.assertion_failure_data,
"httpStatusCode": obj.http_status_code,
"durationMs": obj.duration_ms,
"traceId": obj.trace_id,
Expand Down
13 changes: 12 additions & 1 deletion src/sentry/uptime/grouptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import logging
from dataclasses import dataclass
from datetime import datetime
from typing import override
from typing import Any, override

from django.db.models import Q
from sentry_kafka_schemas.schema_types.uptime_results_v1 import CheckResult, CheckStatus
Expand Down Expand Up @@ -83,6 +83,16 @@ def build_evidence_display(result: CheckResult) -> list[IssueEvidence]:
return evidence_display


def build_evidence_data(result: CheckResult) -> dict[str, Any]:
evidence_data: dict[str, Any] = {}

assertion_failure_data = result.get("assertion_failure_data")
if assertion_failure_data is not None:
evidence_data["assertion_failure_data"] = assertion_failure_data

return evidence_data


def build_event_data(result: CheckResult, detector: Detector) -> EventData:
# Default environment when it hasn't been configured
env = detector.config.get("environment", "prod")
Expand Down Expand Up @@ -218,6 +228,7 @@ def create_occurrence(
issue_title=f"Downtime detected for {uptime_subscription.url}",
subtitle="Your monitored domain is down",
evidence_display=build_evidence_display(result),
evidence_data=build_evidence_data(result),
type=UptimeDomainCheckFailure,
level="error",
culprit="", # TODO: The url?
Expand Down
10 changes: 8 additions & 2 deletions src/sentry/uptime/types.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
import enum
from collections.abc import Sequence
from dataclasses import dataclass
from dataclasses import dataclass, field
from datetime import datetime
from enum import IntEnum
from typing import Any, Literal, Required, TypedDict

from sentry_kafka_schemas.schema_types.uptime_results_v1 import CheckStatus, CheckStatusReasonType
from sentry_kafka_schemas.schema_types.uptime_results_v1 import (
Assertion,
CheckStatus,
CheckStatusReasonType,
)

DATA_SOURCE_UPTIME_SUBSCRIPTION = "uptime_subscription"
"""
Expand Down Expand Up @@ -137,6 +141,8 @@ class EapCheckEntry:
incident_status: IncidentStatus
environment: str
region: str
# Parsed JSON, can be a dict, so exclude it from hashing/comparison.
assertion_failure_data: Assertion | None = field(compare=False)


@dataclass(frozen=True)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@

MOCK_DATETIME = datetime.now(tz=timezone.utc) - timedelta(days=1)

MOCK_ASSERTION_FAILURE_DATA = {
"root": {
"op": "and",
"children": [
{
"op": "not",
"operand": {"op": "json_path", "value": '$.components[?@.status == "operational"]'},
},
],
}
}


class ProjectUptimeAlertCheckIndexBaseTest(UptimeAlertBaseEndpointTest):
__test__ = False
Expand Down Expand Up @@ -83,6 +95,7 @@ def test_get(self) -> None:
"regionName",
"checkStatus",
"checkStatusReason",
"assertionFailureData",
"traceId",
"httpStatusCode",
"incidentStatus",
Expand All @@ -92,6 +105,7 @@ def test_get(self) -> None:
assert most_recent["uptimeCheckId"]
assert most_recent["regionName"] == "Default Region"
assert most_recent["checkStatusReason"] == "failure"
assert most_recent["assertionFailureData"] == MOCK_ASSERTION_FAILURE_DATA

assert any(v for v in response.data if v["checkStatus"] == "failure_incident")
assert any(v for v in response.data if v["checkStatusReason"] is None)
Expand Down Expand Up @@ -202,6 +216,9 @@ def store_uptime_data(
"status_reason_type": "failure" if check_status == "failure" else None,
"region": "default",
"http_status_code": http_status,
"assertion_failure_data": (
MOCK_ASSERTION_FAILURE_DATA if check_status == "failure" else None
),
}
uptime_result = self.create_eap_uptime_result(**create_params)
self.store_uptime_results([uptime_result])
23 changes: 23 additions & 0 deletions tests/sentry/uptime/test_grouptype.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
UptimeDomainCheckFailure,
UptimePacketValue,
build_event_data,
build_evidence_data,
build_evidence_display,
)
from sentry.uptime.models import UptimeSubscription, get_uptime_subscription
Expand Down Expand Up @@ -96,6 +97,28 @@ def test_build_fingerprint(self) -> None:
assert fingerprint == expected_fingerprint


class BuildEvidenceDataTest(UptimeTestCase):
def test_build_evidence_data(self) -> None:
assertion_failure_data = {
"root": {
"op": "and",
"children": [
{
"op": "not",
"operand": {
"op": "json_path",
"value": '$.components[?@.status == "operational"]',
},
},
],
}
}
result = self.create_uptime_result()
assert build_evidence_data(result) == {
"assertion_failure_data": assertion_failure_data,
}


class BuildEvidenceDisplayTest(UptimeTestCase):
def test_build_evidence_display(self) -> None:
result = self.create_uptime_result()
Expand Down
Loading