Files
explorer-monorepo/scripts/check-explorer-health.sh
defiQUG 0778c18e59
Some checks failed
Deploy Explorer Live / deploy (push) Failing after 15s
fix(explorer): read SSE stream until event and data lines arrive
The health check stopped after two non-empty lines and missed the data line that follows event: ping on mission-control streams.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 18:05:47 -07:00

240 lines
8.2 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
BASE_URL="${1:-https://explorer.d-bis.org}"
python3 - "$BASE_URL" <<'PY'
import re
import sys
import time
import requests
base = sys.argv[1].rstrip("/")
session = requests.Session()
session.headers.update({"User-Agent": "ExplorerHealthCheck/2.0"})
failed = False
html_checks = [
"/",
"/home",
"/blocks",
"/transactions",
"/addresses",
"/bridge",
"/routes",
"/weth",
"/tokens",
"/pools",
"/watchlist",
"/more",
"/analytics",
"/operator",
"/system",
"/liquidity",
"/wallet",
"/snap/",
"/docs.html",
"/privacy.html",
"/terms.html",
"/acknowledgments.html",
]
json_checks = [
"/api/v2/stats",
"/api/config/token-list",
"/api/config/networks",
"/api/config/capabilities",
"/config/CHAIN138_RPC_CAPABILITIES.json",
"/config/topology-graph.json",
"/config/mission-control-verify.example.json",
"/explorer-api/v1/features",
"/explorer-api/v1/ai/context?q=cUSDT",
"/explorer-api/v1/track1/bridge/status",
"/explorer-api/v1/mission-control/liquidity/token/0x93E66202A11B1772E55407B32B44e5Cd8eda7f22/pools",
"/token-aggregation/api/v1/routes/tree?chainId=138&tokenIn=0x93E66202A11B1772E55407B32B44e5Cd8eda7f22&tokenOut=0x004b63A7B5b0E06f6bB6adb4a5F9f590BF3182D1&amountIn=1000000",
"/token-aggregation/api/v1/routes/matrix",
"/token-aggregation/api/v1/routes/ingestion?fromChainId=138&routeType=swap",
"/token-aggregation/api/v1/routes/partner-payloads?partner=0x&amount=1000000&includeUnsupported=true",
]
asset_checks = [
"/token-icons/cUSDC.png",
"/token-icons/cUSDT.png",
"/token-icons/cXAUC.png",
"/token-icons/cXAUT.png",
]
def mark_failure(message):
global failed
failed = True
print(message)
print("== Public routes ==")
for path in html_checks:
url = base + path
try:
resp = session.get(url, timeout=20, allow_redirects=True)
ctype = resp.headers.get("content-type", "")
print(f"{resp.status_code:>3} {path} [{ctype[:60]}]")
if resp.status_code >= 400:
failed = True
except Exception as exc:
mark_failure(f"ERR {path} [{exc}]")
print("\n== JSON and API surfaces ==")
for path in json_checks:
url = base + path
try:
resp = session.get(url, timeout=20, allow_redirects=True)
ctype = resp.headers.get("content-type", "")
print(f"{resp.status_code:>3} {path} [{ctype[:60]}]")
if resp.status_code >= 400:
failed = True
continue
if "json" not in ctype:
failed = True
print(f" expected JSON content-type, got: {ctype}")
continue
payload = resp.json()
if path == "/api/config/capabilities":
if payload.get("chainId") != 138:
mark_failure(" capabilities JSON does not report chainId 138")
wallet_support = payload.get("walletSupport", {})
if wallet_support.get("walletWatchAsset") is not True:
mark_failure(" capabilities JSON does not advertise walletWatchAsset")
elif path == "/api/config/token-list":
tokens = payload.get("tokens", [])
if not tokens:
mark_failure(" token list is empty")
elif path == "/api/config/networks":
chains = payload.get("chains", [])
if not chains:
mark_failure(" networks payload does not include any chains")
elif path == "/explorer-api/v1/features":
if "features" not in payload:
mark_failure(" features payload is missing the features key")
elif path == "/explorer-api/v1/track1/bridge/status":
data = payload.get("data", {})
relays = data.get("ccip_relays", {})
expected_relays = {
"avax",
"avax_cw",
"avax_to_138",
"bsc",
"mainnet_cw",
"mainnet_weth",
}
missing = sorted(expected_relays - set(relays))
if missing:
mark_failure(f" bridge status is missing relay keys: {', '.join(missing)}")
if data.get("status") not in {"operational", "paused", "degraded"}:
mark_failure(" bridge status payload does not include a recognized overall status")
except Exception as exc:
mark_failure(f"ERR {path} [{exc}]")
print("\n== Static assets ==")
for path in asset_checks:
url = base + path
try:
resp = session.get(url, timeout=20, allow_redirects=True)
ctype = resp.headers.get("content-type", "")
print(f"{resp.status_code:>3} {path} [{ctype[:60]}]")
if resp.status_code >= 400:
failed = True
except Exception as exc:
mark_failure(f"ERR {path} [{exc}]")
print("\n== Mission-control SSE ==")
stream_url = base + "/explorer-api/v1/mission-control/stream"
try:
with session.get(stream_url, timeout=(20, 20), stream=True) as resp:
ctype = resp.headers.get("content-type", "")
print(f"{resp.status_code:>3} /explorer-api/v1/mission-control/stream [{ctype[:60]}]")
if resp.status_code >= 400:
failed = True
else:
saw_event = False
saw_data = False
deadline = time.time() + 25
for raw in resp.iter_lines(decode_unicode=True):
if raw:
if raw.startswith("event:"):
saw_event = True
if raw.startswith("data:"):
saw_data = True
if saw_event and saw_data:
break
if time.time() > deadline:
break
if not saw_event:
mark_failure(" mission-control stream did not emit an event line")
if not saw_data:
mark_failure(" mission-control stream did not emit a data line")
except Exception as exc:
mark_failure(f"ERR /explorer-api/v1/mission-control/stream [{exc}]")
print("\n== Internal href targets from homepage ==")
try:
home = session.get(base + "/", timeout=20).text
hrefs = sorted(set(re.findall(r'href=\"([^\"]+)\"', home)))
for href in hrefs:
if href.startswith("/") and not href.startswith("//"):
resp = session.get(base + href, timeout=20, allow_redirects=True)
print(f"{resp.status_code:>3} {href}")
if resp.status_code >= 400:
failed = True
except Exception as exc:
mark_failure(f"ERR homepage href sweep failed: {exc}")
print("\n== External explorer roots referenced by bridge surfaces ==")
external_roots = [
"https://etherscan.io/",
"https://bscscan.com/",
"https://polygonscan.com/",
"https://subnets.avax.network/c-chain",
"https://basescan.org/",
"https://arbiscan.io/",
"https://optimistic.etherscan.io/",
]
for url in external_roots:
try:
resp = session.get(url, timeout=20, allow_redirects=True)
print(f"{resp.status_code:>3} {url}")
except Exception as exc:
print(f"ERR {url} [{exc}]")
print("\n== Native ETH USD (token-aggregation WETH proxy) ==")
weth = "0xc02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
try:
cg = session.get(
"https://api.coingecko.com/api/v3/simple/price",
params={"ids": "ethereum", "vs_currencies": "usd"},
timeout=15,
).json()
cg_usd = float(cg["ethereum"]["usd"])
ta = session.get(
f"{base}/token-aggregation/api/v1/tokens/{weth}",
params={"chainId": 138},
timeout=20,
).json()
ta_usd = float(ta["token"]["market"]["priceUsd"])
delta_pct = abs(ta_usd - cg_usd) / cg_usd * 100.0
layer = ta.get("token", {}).get("pricing", {}).get("sourceLayer", "")
print(f" coingecko=${cg_usd:.2f} explorer=${ta_usd:.2f} delta={delta_pct:.2f}% layer={layer}")
if abs(ta_usd - 2490.0) < 0.01:
mark_failure(" ETH price still at stale repo snapshot $2490")
if delta_pct > 5.0:
mark_failure(f" ETH price drift {delta_pct:.2f}% exceeds 5% vs CoinGecko")
except Exception as exc:
mark_failure(f" native ETH price check failed: {exc}")
if failed:
sys.exit(1)
PY