feat(sase): deploy regression Telegram alert

Closes the alert side of the deploy-regression view added in Faz 2b.
The dashboard table already flagged regressed deploys; this commit
pushes a Telegram when one happens, so MTTD doesn't depend on the
founder checking the dashboard.

Detection (panel)
- detectVinRegressions() pulls the last 20 Coolify deploys for the
  Sase.tr app, filters to those whose post-window has elapsed (≥30min
  since finishedAt) and isn't too old (≤180min since finishedAt), and
  reuses analyzeDeployRegressions to compute the 30min before/after
  success-rate slices. A row is flagged when:
    - both before and after have ≥5 samples, and
    - success rate dropped ≥10pp (severity 'high'; ≥15pp → 'critical').
- Returns a RegressionHit per flagged deploy with a 24h dedupe TTL
  keyed on deploymentUuid so each deploy alerts exactly once ever
  (regardless of how often the 5-min cron checks).

Endpoint
- GET /api/internal/vin-anomaly-check now returns
  { ok, current, baseline, anomalies, regressions }.

Worker
- sendTelegram() accepts an optional dedupeTtlSeconds override so
  per-call long-TTL dedupes (like deploy alerts) don't have to go
  through the global env default.
- New alertVinRegression() formats severity icon + before/after %
  + deploy commit/timestamp + dashboard link.
- runVinAnomalyDetect now also walks the regressions array and
  fires Telegram for each. Returns { anomalies, regressions,
  alertsFired, alertsDeduped }; pipeline log prints when either
  count is non-zero.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Semih
2026-05-18 13:33:07 +03:00
parent b5ba8919b8
commit a10996f6c5
5 changed files with 170 additions and 13 deletions

View File

@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { timingSafeEqual } from "node:crypto";
import { detectVinAnomalies } from "@/lib/sase/vin-anomaly";
import { detectVinAnomalies, detectVinRegressions } from "@/lib/sase/vin-anomaly";
export const dynamic = "force-dynamic";
@@ -20,6 +20,9 @@ export async function GET(req: Request) {
if (!validToken(req)) {
return NextResponse.json({ ok: false, error: "unauthorized" }, { status: 401 });
}
const result = await detectVinAnomalies();
return NextResponse.json({ ok: true, ...result });
const [anomalyResult, regressions] = await Promise.all([
detectVinAnomalies(),
detectVinRegressions(),
]);
return NextResponse.json({ ok: true, ...anomalyResult, regressions });
}