fix(ci): tolerate empty source link canary response
This commit is contained in:
@@ -430,6 +430,53 @@ class AlertChainSmokeMetricTest(unittest.TestCase):
|
|||||||
self.assertIn(["target_incident_id", "INC-20260505-25E744"], tags)
|
self.assertIn(["target_incident_id", "INC-20260505-25E744"], tags)
|
||||||
self.assertEqual(calls[0]["headers"]["X-AwoooP-Operator-Key"], "secret")
|
self.assertEqual(calls[0]["headers"]["X-AwoooP-Operator-Key"], "secret")
|
||||||
|
|
||||||
|
def test_source_link_canary_accepts_empty_2xx_for_downstream_readback(self):
|
||||||
|
def fake_post(url, payload, *, headers=None, timeout=None):
|
||||||
|
self.assertTrue(url.endswith("/api/v1/webhooks/sentry/error"))
|
||||||
|
self.assertEqual(payload["data"]["issue"]["title"], "AwoooPSourceLinkCanary")
|
||||||
|
return alert_chain_smoke_test.HttpGetResult(204, "")
|
||||||
|
|
||||||
|
original_post = alert_chain_smoke_test.http_post_json
|
||||||
|
try:
|
||||||
|
alert_chain_smoke_test.http_post_json = fake_post
|
||||||
|
result = alert_chain_smoke_test.send_source_link_canary(
|
||||||
|
"https://awoooi.example",
|
||||||
|
target_incident_id="INC-20260505-25E744",
|
||||||
|
operator_key="secret",
|
||||||
|
operator_id="gitea-e2e-health",
|
||||||
|
run_ref="run/123",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
alert_chain_smoke_test.http_post_json = original_post
|
||||||
|
|
||||||
|
self.assertTrue(result.passed)
|
||||||
|
self.assertIn("source-correlation smoke must verify readback", result.message)
|
||||||
|
|
||||||
|
def test_source_link_canary_reports_http_error_before_json_parse(self):
|
||||||
|
def fake_post(url, payload, *, headers=None, timeout=None):
|
||||||
|
self.assertTrue(url.endswith("/api/v1/webhooks/sentry/error"))
|
||||||
|
return alert_chain_smoke_test.HttpGetResult(
|
||||||
|
502,
|
||||||
|
"<html><body>bad gateway</body></html>",
|
||||||
|
)
|
||||||
|
|
||||||
|
original_post = alert_chain_smoke_test.http_post_json
|
||||||
|
try:
|
||||||
|
alert_chain_smoke_test.http_post_json = fake_post
|
||||||
|
result = alert_chain_smoke_test.send_source_link_canary(
|
||||||
|
"https://awoooi.example",
|
||||||
|
target_incident_id="INC-20260505-25E744",
|
||||||
|
operator_key="secret",
|
||||||
|
operator_id="gitea-e2e-health",
|
||||||
|
run_ref="run/123",
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
alert_chain_smoke_test.http_post_json = original_post
|
||||||
|
|
||||||
|
self.assertFalse(result.passed)
|
||||||
|
self.assertIn("sentry HTTP 502", result.message)
|
||||||
|
self.assertIn("bad gateway", result.message)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
|||||||
@@ -137,6 +137,22 @@ def _http_error_message(error: Exception) -> str:
|
|||||||
return str(error)
|
return str(error)
|
||||||
|
|
||||||
|
|
||||||
|
def _response_body_preview(text: str, limit: int = 240) -> str:
|
||||||
|
cleaned = " ".join((text or "").split())
|
||||||
|
if not cleaned:
|
||||||
|
return "<empty body>"
|
||||||
|
if len(cleaned) <= limit:
|
||||||
|
return cleaned
|
||||||
|
return f"{cleaned[:limit]}..."
|
||||||
|
|
||||||
|
|
||||||
|
def _response_json(resp: HttpGetResult) -> dict[str, Any] | None:
|
||||||
|
text = (resp.text or "").strip()
|
||||||
|
if not text:
|
||||||
|
return None
|
||||||
|
return resp.json()
|
||||||
|
|
||||||
|
|
||||||
def _api_health_probe_summary(attempt: int) -> str:
|
def _api_health_probe_summary(attempt: int) -> str:
|
||||||
return (
|
return (
|
||||||
f"attempts={attempt}/{API_HEALTH_ATTEMPTS}, "
|
f"attempts={attempt}/{API_HEALTH_ATTEMPTS}, "
|
||||||
@@ -782,18 +798,43 @@ def send_source_provider_upstream_canary(
|
|||||||
},
|
},
|
||||||
timeout=TIMEOUT,
|
timeout=TIMEOUT,
|
||||||
)
|
)
|
||||||
data = resp.json()
|
except (URLError, TimeoutError, OSError) as e:
|
||||||
except (URLError, TimeoutError, OSError, json.JSONDecodeError) as e:
|
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
"Source Provider Upstream Canary",
|
"Source Provider Upstream Canary",
|
||||||
False,
|
False,
|
||||||
f"{provider} upstream canary failed: {_http_error_message(e)}",
|
f"{provider} upstream canary failed: {_http_error_message(e)}",
|
||||||
)
|
)
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
|
try:
|
||||||
|
data = _response_json(resp)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data = None
|
||||||
|
detail = (
|
||||||
|
data.get("detail")
|
||||||
|
if isinstance(data, dict)
|
||||||
|
else _response_body_preview(resp.text)
|
||||||
|
)
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
"Source Provider Upstream Canary",
|
"Source Provider Upstream Canary",
|
||||||
False,
|
False,
|
||||||
f"{provider} HTTP {resp.status_code}: {data.get('detail', resp.text) if isinstance(data, dict) else resp.text}",
|
f"{provider} HTTP {resp.status_code}: {detail}",
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = _response_json(resp)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return CheckResult(
|
||||||
|
"Source Provider Upstream Canary",
|
||||||
|
False,
|
||||||
|
(
|
||||||
|
f"{provider} upstream canary returned non-JSON "
|
||||||
|
f"HTTP {resp.status_code}: {_response_body_preview(resp.text)}"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if data is None:
|
||||||
|
return CheckResult(
|
||||||
|
"Source Provider Upstream Canary",
|
||||||
|
False,
|
||||||
|
f"{provider} upstream canary returned empty HTTP {resp.status_code}",
|
||||||
)
|
)
|
||||||
validation_error = _validate_upstream_canary_response(provider, data)
|
validation_error = _validate_upstream_canary_response(provider, data)
|
||||||
if validation_error:
|
if validation_error:
|
||||||
@@ -850,18 +891,46 @@ def send_source_link_canary(
|
|||||||
},
|
},
|
||||||
timeout=TIMEOUT,
|
timeout=TIMEOUT,
|
||||||
)
|
)
|
||||||
data = resp.json()
|
except (URLError, TimeoutError, OSError) as e:
|
||||||
except (URLError, TimeoutError, OSError, json.JSONDecodeError) as e:
|
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
"Source Link Canary",
|
"Source Link Canary",
|
||||||
False,
|
False,
|
||||||
f"sentry source-link canary failed: {_http_error_message(e)}",
|
f"sentry source-link canary failed: {_http_error_message(e)}",
|
||||||
)
|
)
|
||||||
if resp.status_code >= 400:
|
if resp.status_code >= 400:
|
||||||
|
try:
|
||||||
|
data = _response_json(resp)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
data = None
|
||||||
|
detail = (
|
||||||
|
data.get("detail")
|
||||||
|
if isinstance(data, dict)
|
||||||
|
else _response_body_preview(resp.text)
|
||||||
|
)
|
||||||
return CheckResult(
|
return CheckResult(
|
||||||
"Source Link Canary",
|
"Source Link Canary",
|
||||||
False,
|
False,
|
||||||
f"sentry HTTP {resp.status_code}: {data.get('detail', resp.text) if isinstance(data, dict) else resp.text}",
|
f"sentry HTTP {resp.status_code}: {detail}",
|
||||||
|
)
|
||||||
|
if not (resp.text or "").strip():
|
||||||
|
return CheckResult(
|
||||||
|
"Source Link Canary",
|
||||||
|
True,
|
||||||
|
(
|
||||||
|
"accepted sentry source-link canary post with empty "
|
||||||
|
f"HTTP {resp.status_code}; source-correlation smoke must verify readback"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
data = resp.json()
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
return CheckResult(
|
||||||
|
"Source Link Canary",
|
||||||
|
False,
|
||||||
|
(
|
||||||
|
"sentry source-link canary returned non-JSON "
|
||||||
|
f"HTTP {resp.status_code}: {_response_body_preview(resp.text)}"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
validation_error = _validate_upstream_canary_response("sentry", data)
|
validation_error = _validate_upstream_canary_response("sentry", data)
|
||||||
if validation_error:
|
if validation_error:
|
||||||
|
|||||||
Reference in New Issue
Block a user