-
-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathincident-status.ts
More file actions
145 lines (127 loc) · 3.95 KB
/
Copy pathincident-status.ts
File metadata and controls
145 lines (127 loc) · 3.95 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
import { IS_PLATFORM } from 'common'
import { InternalServerError } from 'lib/api/apiHelpers'
import z from 'zod'
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,
}))
}