Skip to content

Commit 8307ce6

Browse files
cursoragentdobrac
andcommitted
use incident io status page
Co-authored-by: Jakub Dobry <dobrac@users.noreply.github.com>
1 parent 01a780e commit 8307ce6

5 files changed

Lines changed: 145 additions & 29 deletions

File tree

.env.example

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,8 +68,10 @@ NEXT_PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
6868
# NEXT_PUBLIC_INCLUDE_REPORT_ISSUE=0
6969

7070
### Enable dashboard status indicator feature: set to 1 to enable
71-
### When enabled, the E2B status is read from https://status.e2b.dev
71+
### When enabled, the E2B status is read from the incident.io widget API
7272
# NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR=0
73+
# NEXT_PUBLIC_STATUS_PAGE_URL=https://status.e2b.dev
74+
# NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL=https://status.e2b.dev/api/widget
7375

7476
### Set to 1 to use mock data
7577
# NEXT_PUBLIC_MOCK_DATA=0

src/features/dashboard/layouts/status-indicator.server.tsx

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,41 +4,25 @@ import { cacheLife } from 'next/cache'
44
import Link from 'next/link'
55
import { l } from '@/core/shared/clients/logger/logger'
66
import { LiveDot } from '@/ui/live'
7+
import {
8+
type AggregateState,
9+
getStatusPageStateFromWidget,
10+
getStatusPageUrl,
11+
getStatusPageWidgetUrl,
12+
type IncidentIOWidgetResponse,
13+
} from './status-indicator'
714

8-
const STATUS_PAGE_URL = 'https://status.e2b.dev'
9-
const STATUS_PAGE_INDEX_URL = `${STATUS_PAGE_URL}/index.json`
15+
export const STATUS_PAGE_URL = getStatusPageUrl()
16+
const STATUS_PAGE_WIDGET_URL = getStatusPageWidgetUrl(STATUS_PAGE_URL)
1017
const STATUS_PAGE_FETCH_TIMEOUT_MS = 5_000
1118
const STATUS_PAGE_CACHE_SECONDS = 300
1219

13-
type AggregateState =
14-
| 'operational'
15-
| 'degraded'
16-
| 'downtime'
17-
| 'maintenance'
18-
| 'unknown'
19-
20-
interface StatusPageIndexResponse {
21-
data?: {
22-
attributes?: {
23-
aggregate_state?: string
24-
}
25-
}
26-
}
27-
2820
interface StatusUI {
2921
label: string
3022
dotCircleClassName: string
3123
dotClassName: string
3224
}
3325

34-
function toAggregateState(value: string | undefined): AggregateState {
35-
if (value === 'operational') return 'operational'
36-
if (value === 'degraded') return 'degraded'
37-
if (value === 'downtime') return 'downtime'
38-
if (value === 'maintenance') return 'maintenance'
39-
return 'unknown'
40-
}
41-
4226
function getStatusUI(state: AggregateState): StatusUI {
4327
switch (state) {
4428
case 'operational':
@@ -83,7 +67,7 @@ async function getStatusPageState(): Promise<AggregateState> {
8367
})
8468

8569
try {
86-
const response = await fetch(STATUS_PAGE_INDEX_URL, {
70+
const response = await fetch(STATUS_PAGE_WIDGET_URL, {
8771
cache: 'force-cache',
8872
next: { revalidate: STATUS_PAGE_CACHE_SECONDS },
8973
signal: AbortSignal.timeout(STATUS_PAGE_FETCH_TIMEOUT_MS),
@@ -101,8 +85,8 @@ async function getStatusPageState(): Promise<AggregateState> {
10185
return 'unknown'
10286
}
10387

104-
const data = (await response.json()) as StatusPageIndexResponse
105-
return toAggregateState(data.data?.attributes?.aggregate_state)
88+
const data = (await response.json()) as IncidentIOWidgetResponse
89+
return getStatusPageStateFromWidget(data)
10690
} catch {
10791
return 'unknown'
10892
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
export type AggregateState =
2+
| 'operational'
3+
| 'degraded'
4+
| 'downtime'
5+
| 'maintenance'
6+
| 'unknown'
7+
8+
export interface IncidentIOWidgetEvent {
9+
affected_components?: Array<{
10+
status?: string
11+
}>
12+
}
13+
14+
export interface IncidentIOWidgetResponse {
15+
ongoing_incidents?: IncidentIOWidgetEvent[]
16+
in_progress_maintenances?: IncidentIOWidgetEvent[]
17+
scheduled_maintenances?: IncidentIOWidgetEvent[]
18+
}
19+
20+
export function getStatusPageUrl() {
21+
return (process.env.NEXT_PUBLIC_STATUS_PAGE_URL ?? 'https://status.e2b.dev')
22+
.trim()
23+
.replace(/\/+$/, '')
24+
}
25+
26+
export function getStatusPageWidgetUrl(statusPageUrl: string) {
27+
const configuredWidgetUrl =
28+
process.env.NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL?.trim()
29+
30+
if (configuredWidgetUrl) return configuredWidgetUrl
31+
32+
return `${statusPageUrl}/api/widget`
33+
}
34+
35+
function hasEvents(events: IncidentIOWidgetEvent[] | undefined) {
36+
return Array.isArray(events) && events.length > 0
37+
}
38+
39+
function getWorstComponentState(
40+
events: IncidentIOWidgetEvent[] | undefined
41+
): AggregateState | undefined {
42+
const componentStatuses =
43+
events?.flatMap(
44+
(event) =>
45+
event.affected_components?.map((component) => component.status) ?? []
46+
) ?? []
47+
48+
if (componentStatuses.includes('full_outage')) return 'downtime'
49+
if (componentStatuses.includes('partial_outage')) return 'degraded'
50+
if (componentStatuses.includes('degraded_performance')) return 'degraded'
51+
if (componentStatuses.includes('under_maintenance')) return 'maintenance'
52+
53+
return undefined
54+
}
55+
56+
export function getStatusPageStateFromWidget(
57+
data: IncidentIOWidgetResponse
58+
): AggregateState {
59+
if (hasEvents(data.ongoing_incidents)) {
60+
return getWorstComponentState(data.ongoing_incidents) ?? 'degraded'
61+
}
62+
63+
if (hasEvents(data.in_progress_maintenances)) return 'maintenance'
64+
65+
return 'operational'
66+
}

src/lib/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,8 @@ export const clientSchema = z.object({
5555
NEXT_PUBLIC_INCLUDE_ARGUS: z.string().optional(),
5656
NEXT_PUBLIC_INCLUDE_REPORT_ISSUE: z.string().optional(),
5757
NEXT_PUBLIC_INCLUDE_STATUS_INDICATOR: z.string().optional(),
58+
NEXT_PUBLIC_STATUS_PAGE_URL: z.url().optional(),
59+
NEXT_PUBLIC_STATUS_PAGE_WIDGET_URL: z.url().optional(),
5860
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY: z.string().optional(),
5961
NEXT_PUBLIC_SCAN: z.string().optional(),
6062
NEXT_PUBLIC_MOCK_DATA: z.string().optional(),
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import { describe, expect, it } from 'vitest'
2+
import { getStatusPageStateFromWidget } from '@/features/dashboard/layouts/status-indicator'
3+
4+
describe('status-indicator', () => {
5+
it('should report operational when widget has no active events', () => {
6+
expect(
7+
getStatusPageStateFromWidget({
8+
ongoing_incidents: [],
9+
in_progress_maintenances: [],
10+
scheduled_maintenances: [
11+
{
12+
affected_components: [{ status: 'under_maintenance' }],
13+
},
14+
],
15+
})
16+
).toBe('operational')
17+
})
18+
19+
it('should report maintenance for in-progress maintenances', () => {
20+
expect(
21+
getStatusPageStateFromWidget({
22+
ongoing_incidents: [],
23+
in_progress_maintenances: [{}],
24+
})
25+
).toBe('maintenance')
26+
})
27+
28+
it('should report downtime for full outage incidents', () => {
29+
expect(
30+
getStatusPageStateFromWidget({
31+
ongoing_incidents: [
32+
{
33+
affected_components: [
34+
{ status: 'degraded_performance' },
35+
{ status: 'full_outage' },
36+
],
37+
},
38+
],
39+
})
40+
).toBe('downtime')
41+
})
42+
43+
it('should report degraded for partial outage incidents', () => {
44+
expect(
45+
getStatusPageStateFromWidget({
46+
ongoing_incidents: [
47+
{
48+
affected_components: [{ status: 'partial_outage' }],
49+
},
50+
],
51+
})
52+
).toBe('degraded')
53+
})
54+
55+
it('should report degraded when incident has no component status', () => {
56+
expect(
57+
getStatusPageStateFromWidget({
58+
ongoing_incidents: [{}],
59+
})
60+
).toBe('degraded')
61+
})
62+
})

0 commit comments

Comments
 (0)