mirror of
https://github.com/supabase/supabase.git
synced 2026-05-09 00:10:05 +08:00
147 lines
4.0 KiB
TypeScript
147 lines
4.0 KiB
TypeScript
import { IS_PLATFORM } from 'common'
|
|
import z from 'zod'
|
|
|
|
import { InternalServerError } from '@/lib/api/apiHelpers'
|
|
|
|
export type IncidentCache = {
|
|
affected_regions: Array<string> | 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<IncidentInfo[]> {
|
|
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,
|
|
}))
|
|
}
|