import { IS_PLATFORM } from 'common' import z from 'zod' import { InternalServerError } from '@/lib/api/apiHelpers' export type IncidentCache = { affected_regions: Array | null affects_project_creation: boolean /** When true, the banner is shown unconditionally regardless of regions or project state. */ force?: boolean } export type IncidentMetadata = { dashboard_metadata?: { show_banner?: boolean } } export type IncidentInfo = { id: string name: string status: string impact: string active_since: string metadata: IncidentMetadata cache?: IncidentCache | null } const STATUSPAGE_API_URL = 'https://api.statuspage.io/v1' const STATUSPAGE_PAGE_ID = process.env.STATUSPAGE_PAGE_ID const STATUSPAGE_API_KEY = process.env.STATUSPAGE_API_KEY function getIncidentsEndpoint(): string { return `${STATUSPAGE_API_URL}/pages/${STATUSPAGE_PAGE_ID}/incidents/unresolved` } const StatusPageIncidentsSchema = z.array( z.object({ id: z.string(), name: z.string(), status: z.string(), created_at: z.string(), scheduled_for: z.string().nullable(), impact: z.string(), metadata: z .object({ dashboard_metadata: z .object({ show_banner: z.boolean().optional(), }) .optional(), }) .optional() .default({}), }) ) /** * Fetches active incidents from the StatusPage API. * This function is used both by the API route and the AI assistant. * * @returns Array of active incidents * @throws InternalServerError if StatusPage is not configured or returns an error */ export async function getActiveIncidents(): Promise { if (!IS_PLATFORM) { return [] } if (!STATUSPAGE_PAGE_ID) { throw new InternalServerError('StatusPage page ID is not configured') } if (!STATUSPAGE_API_KEY) { throw new InternalServerError('StatusPage API key is not configured') } const response = await fetch(getIncidentsEndpoint(), { headers: { Authorization: `OAuth ${STATUSPAGE_API_KEY}`, Accept: 'application/json', 'Content-Type': 'application/json', }, next: { revalidate: 180 }, signal: AbortSignal.timeout(30_000), }) const responseText = await response.text() if (!response.ok) { const retryAfter = response.headers.get('Retry-After') ?? undefined throw new InternalServerError(`StatusPage API responded with ${response.status}`, { status: response.status, body: responseText, ...(retryAfter !== undefined && { retryAfter }), }) } let incidentsJson: unknown try { incidentsJson = JSON.parse(responseText) } catch (error) { throw new InternalServerError('StatusPage API response could not be parsed as JSON', { error: error instanceof Error ? error.message : error, body: responseText, }) } const result = StatusPageIncidentsSchema.safeParse(incidentsJson) if (!result.success) { throw new InternalServerError('StatusPage API response did not match expected schema', { issues: result.error.issues, }) } const now = Date.now() const activeIncidents = result.data.filter((incident) => { const hasNoScheduledTime = !incident.scheduled_for if (hasNoScheduledTime) { return true } const scheduledTime = Date.parse(incident.scheduled_for!) const isScheduledTimeInvalid = Number.isNaN(scheduledTime) if (isScheduledTimeInvalid) { // Keep the record but note it locally for debugging console.warn('Encountered incident with invalid scheduled_for date', { incidentId: incident.id, scheduled_for: incident.scheduled_for, }) return true } const hasScheduledTimePassed = scheduledTime <= now return hasScheduledTimePassed }) return activeIncidents.map((incident) => ({ id: incident.id, name: incident.name, status: incident.status, impact: incident.impact, active_since: incident.scheduled_for ?? incident.created_at, metadata: incident.metadata, })) }