feat(web): show source provider freshness on alerts
This commit is contained in:
@@ -747,8 +747,13 @@
|
||||
"sourceCoverageSentry": "Sentry refs",
|
||||
"sourceCoverageSigNoz": "SigNoz refs",
|
||||
"sourceCoverageRatio": "source refs coverage {ratio} / total {total}",
|
||||
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}",
|
||||
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}"
|
||||
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}, latest {latest} ({age})",
|
||||
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}, latest {latest} ({age})",
|
||||
"sourceCoverageFreshness": "{provider} latest {latest} ({age})",
|
||||
"sourceCoverageFresh": "fresh",
|
||||
"sourceCoverageStaleHours": "stale {hours}h",
|
||||
"sourceCoverageStaleDays": "stale {days}d",
|
||||
"sourceCoverageNoEvents": "no events"
|
||||
},
|
||||
"navSection": {
|
||||
"aiCore": "AI Core",
|
||||
|
||||
@@ -748,8 +748,13 @@
|
||||
"sourceCoverageSentry": "Sentry refs",
|
||||
"sourceCoverageSigNoz": "SigNoz refs",
|
||||
"sourceCoverageRatio": "source refs 覆蓋率 {ratio} / total {total}",
|
||||
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}",
|
||||
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}"
|
||||
"sourceCoverageProvider": "{provider}: total {total}, missing {missing}, Sentry {sentry}, SigNoz {signoz}, latest {latest} ({age})",
|
||||
"sourceCoverageProviderWindow": "{provider} window: total {total}, with refs {withRefs}, missing {missing}, latest {latest} ({age})",
|
||||
"sourceCoverageFreshness": "{provider} latest {latest} ({age})",
|
||||
"sourceCoverageFresh": "fresh",
|
||||
"sourceCoverageStaleHours": "stale {hours}h",
|
||||
"sourceCoverageStaleDays": "stale {days}d",
|
||||
"sourceCoverageNoEvents": "no events"
|
||||
},
|
||||
"navSection": {
|
||||
"aiCore": "AI 核心",
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Bell, BellOff, RefreshCw, AlertTriangle, AlertCircle, Info, ChevronLeft
|
||||
const SEVERITY_ORDER: Record<string, number> = { P0: 0, P1: 1, P2: 2, P3: 3 }
|
||||
const ALERTS_PAGE_SIZE = 50
|
||||
const SOURCE_COVERAGE_LIMIT = 100
|
||||
const SOURCE_FRESHNESS_STALE_HOURS = 24
|
||||
|
||||
const SEVERITY_STYLE: Record<string, { bg: string; text: string }> = {
|
||||
P0: { bg: 'bg-status-critical/10', text: 'text-status-critical' },
|
||||
@@ -91,6 +92,7 @@ interface SourceCoverageProvider {
|
||||
sentry_ref_total: number
|
||||
signoz_ref_total: number
|
||||
alert_ref_total: number
|
||||
latest_received_at?: string | null
|
||||
}
|
||||
|
||||
interface SourceCoverageResponse {
|
||||
@@ -103,6 +105,38 @@ function percentLabel(value: number, total: number) {
|
||||
return `${Math.round((value / total) * 100)}%`
|
||||
}
|
||||
|
||||
function parseApiTimestamp(value?: string | null) {
|
||||
if (!value) return null
|
||||
const normalized = /(?:z|[+-]\d{2}:?\d{2})$/i.test(value) ? value : `${value}Z`
|
||||
const date = new Date(normalized)
|
||||
return Number.isNaN(date.getTime()) ? null : date
|
||||
}
|
||||
|
||||
function formatTimestamp(value: string | null | undefined, locale: string, emptyLabel: string) {
|
||||
const date = parseApiTimestamp(value)
|
||||
if (!date) return emptyLabel
|
||||
return new Intl.DateTimeFormat(locale, {
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false,
|
||||
}).format(date)
|
||||
}
|
||||
|
||||
function freshnessAgeHours(value?: string | null) {
|
||||
const date = parseApiTimestamp(value)
|
||||
if (!date) return null
|
||||
return Math.max(0, (Date.now() - date.getTime()) / 3_600_000)
|
||||
}
|
||||
|
||||
function freshnessToneClass(ageHours: number | null) {
|
||||
if (ageHours === null || ageHours > SOURCE_FRESHNESS_STALE_HOURS) {
|
||||
return 'border-status-warning/30 bg-status-warning/5 text-status-warning'
|
||||
}
|
||||
return 'border-status-healthy/30 bg-status-healthy/5 text-status-healthy'
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// Page
|
||||
// =============================================================================
|
||||
@@ -206,6 +240,17 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
const topCoverageProviders = sourceCoverage?.providers?.slice(0, 3) ?? []
|
||||
const sentryCoverageSummary = providerCoverage.sentry?.summary
|
||||
const signozCoverageSummary = providerCoverage.signoz?.summary
|
||||
const emptyFreshnessLabel = tAlerts('sourceCoverageNoEvents')
|
||||
const formatFreshnessAge = (value?: string | null) => {
|
||||
const ageHours = freshnessAgeHours(value)
|
||||
if (ageHours === null) return emptyFreshnessLabel
|
||||
if (ageHours <= SOURCE_FRESHNESS_STALE_HOURS) return tAlerts('sourceCoverageFresh')
|
||||
if (ageHours < 48) return tAlerts('sourceCoverageStaleHours', { hours: Math.floor(ageHours) })
|
||||
return tAlerts('sourceCoverageStaleDays', { days: Math.floor(ageHours / 24) })
|
||||
}
|
||||
const coverageFreshnessAge = freshnessAgeHours(coverageSummary?.latest_received_at)
|
||||
const sentryFreshnessAge = freshnessAgeHours(sentryCoverageSummary?.latest_received_at)
|
||||
const signozFreshnessAge = freshnessAgeHours(signozCoverageSummary?.latest_received_at)
|
||||
|
||||
return (
|
||||
<AppLayout locale={params.locale}>
|
||||
@@ -289,10 +334,23 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
total: coverageSummary?.source_count ?? 0,
|
||||
})}
|
||||
</span>
|
||||
<span className={cn(
|
||||
'rounded border px-2 py-1',
|
||||
freshnessToneClass(coverageFreshnessAge)
|
||||
)}>
|
||||
{tAlerts('sourceCoverageFreshness', {
|
||||
provider: 'overall',
|
||||
latest: formatTimestamp(coverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||
age: formatFreshnessAge(coverageSummary?.latest_received_at),
|
||||
})}
|
||||
</span>
|
||||
{topCoverageProviders.map(provider => (
|
||||
<span
|
||||
key={provider.provider}
|
||||
className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1"
|
||||
className={cn(
|
||||
'rounded border px-2 py-1',
|
||||
freshnessToneClass(freshnessAgeHours(provider.latest_received_at))
|
||||
)}
|
||||
>
|
||||
{tAlerts('sourceCoverageProvider', {
|
||||
provider: provider.provider,
|
||||
@@ -300,23 +358,35 @@ export default function AlertsPage({ params }: { params: { locale: string } }) {
|
||||
missing: provider.missing_source_refs_total,
|
||||
sentry: provider.sentry_ref_total,
|
||||
signoz: provider.signoz_ref_total,
|
||||
latest: formatTimestamp(provider.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||
age: formatFreshnessAge(provider.latest_received_at),
|
||||
})}
|
||||
</span>
|
||||
))}
|
||||
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
|
||||
<span className={cn(
|
||||
'rounded border px-2 py-1',
|
||||
freshnessToneClass(sentryFreshnessAge)
|
||||
)}>
|
||||
{tAlerts('sourceCoverageProviderWindow', {
|
||||
provider: 'sentry',
|
||||
total: sentryCoverageSummary?.source_count ?? 0,
|
||||
withRefs: sentryCoverageSummary?.with_source_refs_total ?? 0,
|
||||
missing: sentryCoverageSummary?.missing_source_refs_total ?? 0,
|
||||
latest: formatTimestamp(sentryCoverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||
age: formatFreshnessAge(sentryCoverageSummary?.latest_received_at),
|
||||
})}
|
||||
</span>
|
||||
<span className="rounded border border-nothing-gray-200 bg-nothing-gray-50 px-2 py-1">
|
||||
<span className={cn(
|
||||
'rounded border px-2 py-1',
|
||||
freshnessToneClass(signozFreshnessAge)
|
||||
)}>
|
||||
{tAlerts('sourceCoverageProviderWindow', {
|
||||
provider: 'signoz',
|
||||
total: signozCoverageSummary?.source_count ?? 0,
|
||||
withRefs: signozCoverageSummary?.with_source_refs_total ?? 0,
|
||||
missing: signozCoverageSummary?.missing_source_refs_total ?? 0,
|
||||
latest: formatTimestamp(signozCoverageSummary?.latest_received_at, params.locale, emptyFreshnessLabel),
|
||||
age: formatFreshnessAge(signozCoverageSummary?.latest_received_at),
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user