import type { CurrentStreak, TrendPoint } from "../shared/api"; export interface IncidentAnalysis { incidentCount: number; longestOutage: null | number; mttr: null | number; } export interface MetricCheckpoint { durationMs: null | number; matched: boolean; timestamp: string; } export function analyzeIncidentSequence(checkpoints: MetricCheckpoint[], from: string, to: string): IncidentAnalysis { const sorted = sortCheckpoints(checkpoints); const fromTime = new Date(from).getTime(); const toTime = new Date(to).getTime(); const recoveredDurations: number[] = []; let incidentCount = 0; let longestOutage: null | number = null; let outageStart: null | number = null; let outageStartedAtWindowBoundary = false; let previousMatched: boolean | null = null; for (const checkpoint of sorted) { const timestamp = new Date(checkpoint.timestamp).getTime(); if (!checkpoint.matched) { if (previousMatched !== false) { incidentCount++; outageStart = previousMatched === null ? fromTime : timestamp; outageStartedAtWindowBoundary = previousMatched === null; } } else if (previousMatched === false && outageStart !== null) { const duration = Math.max(0, timestamp - outageStart); longestOutage = maxNullable(longestOutage, duration); if (!outageStartedAtWindowBoundary) { recoveredDurations.push(duration); } outageStart = null; outageStartedAtWindowBoundary = false; } previousMatched = checkpoint.matched; } if (previousMatched === false && outageStart !== null) { const duration = Math.max(0, toTime - outageStart); longestOutage = maxNullable(longestOutage, duration); } return { incidentCount, longestOutage, mttr: calculateAverageDuration(recoveredDurations), }; } export function buildHourlyTrend(checkpoints: MetricCheckpoint[]): TrendPoint[] { const buckets = new Map< string, { downChecks: number; durations: number[]; totalChecks: number; upChecks: number; } >(); for (const checkpoint of checkpoints) { const bucketStart = getUtcHourStart(checkpoint.timestamp); const bucket = buckets.get(bucketStart) ?? { downChecks: 0, durations: [], totalChecks: 0, upChecks: 0 }; bucket.totalChecks++; if (checkpoint.matched) { bucket.upChecks++; if (checkpoint.durationMs !== null) { bucket.durations.push(checkpoint.durationMs); } } else { bucket.downChecks++; } buckets.set(bucketStart, bucket); } return [...buckets.entries()] .sort(([left], [right]) => left.localeCompare(right)) .map(([bucketStart, bucket]) => ({ availability: calculateAvailability(bucket.upChecks, bucket.totalChecks), avgDurationMs: calculateAverageDuration(bucket.durations), bucketStart, downChecks: bucket.downChecks, maxDurationMs: bucket.durations.length > 0 ? Math.max(...bucket.durations) : null, minDurationMs: bucket.durations.length > 0 ? Math.min(...bucket.durations) : null, totalChecks: bucket.totalChecks, upChecks: bucket.upChecks, })); } export function calculateAvailability(upChecks: number, totalChecks: number): number { if (totalChecks <= 0) return 0; return roundToTwo((upChecks / totalChecks) * 100); } export function calculateAverageDuration(durations: number[]): null | number { if (durations.length === 0) return null; const total = durations.reduce((sum, duration) => sum + duration, 0); return roundToTwo(total / durations.length); } export function calculateCurrentStreak(checkpoints: MetricCheckpoint[], limit?: number): CurrentStreak | null { const sorted = sortCheckpoints(checkpoints); const latest = sorted.at(-1); if (!latest) return null; let count = 0; for (let index = sorted.length - 1; index >= 0; index--) { const checkpoint = sorted[index]; if (checkpoint?.matched !== latest.matched) break; count++; } return { ...(limit !== undefined && count >= limit ? { capped: true } : {}), count, up: latest.matched, }; } export function calculatePercentile(durations: number[], percentile: number): null | number { if (durations.length === 0) return null; const sorted = [...durations].sort((a, b) => a - b); const index = Math.min(sorted.length - 1, Math.max(0, Math.ceil((sorted.length * percentile) / 100) - 1)); return sorted[index] ?? null; } function getUtcHourStart(timestamp: string): string { const date = new Date(timestamp); date.setUTCMinutes(0, 0, 0); return date.toISOString(); } function maxNullable(left: null | number, right: number): number { return left === null ? right : Math.max(left, right); } function roundToTwo(value: number): number { return Math.round(value * 100) / 100; } function sortCheckpoints(checkpoints: MetricCheckpoint[]): MetricCheckpoint[] { return [...checkpoints].sort((left, right) => left.timestamp.localeCompare(right.timestamp)); }