Skip to content

Commit ddf5da4

Browse files
authored
Feat/status (#1022)
* init status endpoints * feat: status health endpoints * discord notifs * cleanup
1 parent 9412307 commit ddf5da4

File tree

8 files changed

+1015
-0
lines changed

8 files changed

+1015
-0
lines changed
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import { NextResponse } from 'next/server'
2+
import { PEANUT_API_URL } from '@/constants'
3+
import { fetchWithSentry } from '@/utils'
4+
5+
/**
6+
* Health check for Peanut API backend
7+
* Tests connectivity to the main peanut-api-ts backend service
8+
*/
9+
export async function GET() {
10+
const startTime = Date.now()
11+
12+
try {
13+
if (!PEANUT_API_URL) {
14+
return NextResponse.json(
15+
{
16+
status: 'unhealthy',
17+
service: 'backend',
18+
timestamp: new Date().toISOString(),
19+
error: 'PEANUT_API_URL not configured',
20+
responseTime: Date.now() - startTime,
21+
},
22+
{ status: 500 }
23+
)
24+
}
25+
26+
// Test backend connectivity by fetching a specific user endpoint
27+
const backendTestStart = Date.now()
28+
const backendResponse = await fetchWithSentry(`${PEANUT_API_URL}/users/username/hugo`, {
29+
method: 'GET',
30+
headers: {
31+
'Content-Type': 'application/json',
32+
},
33+
})
34+
35+
const backendResponseTime = Date.now() - backendTestStart
36+
37+
// Backend is healthy if we get any response (200, 404, etc.) - what matters is connectivity
38+
if (!backendResponse.ok && backendResponse.status >= 500) {
39+
throw new Error(`Backend API returned server error ${backendResponse.status}`)
40+
}
41+
42+
const totalResponseTime = Date.now() - startTime
43+
44+
return NextResponse.json({
45+
status: 'healthy',
46+
service: 'backend',
47+
timestamp: new Date().toISOString(),
48+
responseTime: totalResponseTime,
49+
details: {
50+
apiConnectivity: {
51+
status: 'healthy',
52+
responseTime: backendResponseTime,
53+
httpStatus: backendResponse.status,
54+
apiUrl: PEANUT_API_URL,
55+
testEndpoint: '/users/username/hugo',
56+
message: backendResponse.ok
57+
? 'Backend responding normally'
58+
: backendResponse.status === 404
59+
? 'Backend accessible (user not found as expected)'
60+
: 'Backend accessible',
61+
},
62+
},
63+
})
64+
} catch (error) {
65+
const totalResponseTime = Date.now() - startTime
66+
67+
return NextResponse.json(
68+
{
69+
status: 'unhealthy',
70+
service: 'backend',
71+
timestamp: new Date().toISOString(),
72+
error: error instanceof Error ? error.message : 'Unknown error',
73+
responseTime: totalResponseTime,
74+
},
75+
{ status: 500 }
76+
)
77+
}
78+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { NextResponse } from 'next/server'
2+
3+
/**
4+
* Frontend-only health check endpoint
5+
* Tests that the Next.js API routes are working (does not test external dependencies)
6+
*/
7+
export async function GET() {
8+
try {
9+
const healthData = {
10+
status: 'healthy',
11+
service: 'peanut-ui-frontend',
12+
timestamp: new Date().toISOString(),
13+
version: process.env.npm_package_version || 'unknown',
14+
environment: process.env.NODE_ENV,
15+
uptime: process.uptime(),
16+
}
17+
18+
return NextResponse.json(healthData, { status: 200 })
19+
} catch (error) {
20+
return NextResponse.json(
21+
{
22+
status: 'unhealthy',
23+
service: 'peanut-ui-frontend',
24+
timestamp: new Date().toISOString(),
25+
error: error instanceof Error ? error.message : 'Unknown error',
26+
},
27+
{ status: 500 }
28+
)
29+
}
30+
}
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
import { fetchWithSentry } from '@/utils'
2+
import { NextResponse } from 'next/server'
3+
4+
const JUSTANAME_API_URL = 'https://api.justaname.id'
5+
6+
/**
7+
* Health check for JustAName API
8+
* Tests ENS name resolution functionality
9+
*/
10+
export async function GET() {
11+
const startTime = Date.now()
12+
13+
try {
14+
// Test ENS resolution endpoint with a known Ethereum address (Vitalik's)
15+
const ensTestStart = Date.now()
16+
const testAddress = '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045' // Vitalik's address
17+
18+
const ensResponse = await fetchWithSentry(
19+
`${JUSTANAME_API_URL}/ens/v1/subname/address?address=${testAddress}&chainId=1`,
20+
{
21+
headers: {
22+
Accept: '*/*',
23+
'Content-Type': 'application/json',
24+
},
25+
}
26+
)
27+
28+
const ensResponseTime = Date.now() - ensTestStart
29+
30+
if (!ensResponse.ok) {
31+
throw new Error(`ENS resolution API returned ${ensResponse.status}`)
32+
}
33+
34+
const ensData = await ensResponse.json()
35+
36+
// Validate response structure
37+
if (!ensData?.result) {
38+
throw new Error('Invalid ENS API response structure')
39+
}
40+
41+
// Test a second endpoint - ENS name lookup (if available)
42+
const lookupTestStart = Date.now()
43+
let lookupHealth: any = { status: 'not_tested', message: 'Lookup endpoint not tested' }
44+
45+
try {
46+
// Test reverse ENS lookup if the API supports it
47+
const lookupResponse = await fetchWithSentry(`${JUSTANAME_API_URL}/ens/v1/name/vitalik.eth`, {
48+
headers: {
49+
Accept: '*/*',
50+
'Content-Type': 'application/json',
51+
},
52+
})
53+
54+
const lookupResponseTime = Date.now() - lookupTestStart
55+
56+
lookupHealth = {
57+
status: lookupResponse.ok ? 'healthy' : 'degraded',
58+
responseTime: lookupResponseTime,
59+
httpStatus: lookupResponse.status,
60+
}
61+
} catch (error) {
62+
// If lookup fails, that's okay - not all endpoints may be available
63+
lookupHealth = {
64+
status: 'degraded',
65+
responseTime: Date.now() - lookupTestStart,
66+
error: 'Lookup endpoint unavailable',
67+
}
68+
}
69+
70+
const totalResponseTime = Date.now() - startTime
71+
72+
return NextResponse.json({
73+
status: 'healthy',
74+
service: 'justaname',
75+
timestamp: new Date().toISOString(),
76+
responseTime: totalResponseTime,
77+
details: {
78+
ensResolution: {
79+
status: 'healthy',
80+
responseTime: ensResponseTime,
81+
testAddress,
82+
apiUrl: JUSTANAME_API_URL,
83+
},
84+
ensLookup: lookupHealth,
85+
},
86+
})
87+
} catch (error) {
88+
const totalResponseTime = Date.now() - startTime
89+
90+
return NextResponse.json(
91+
{
92+
status: 'unhealthy',
93+
service: 'justaname',
94+
timestamp: new Date().toISOString(),
95+
error: error instanceof Error ? error.message : 'Unknown error',
96+
responseTime: totalResponseTime,
97+
},
98+
{ status: 500 }
99+
)
100+
}
101+
}

src/app/api/health/mobula/route.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { fetchWithSentry } from '@/utils'
2+
import { NextResponse } from 'next/server'
3+
4+
const MOBULA_API_URL = process.env.MOBULA_API_URL!
5+
const MOBULA_API_KEY = process.env.MOBULA_API_KEY!
6+
7+
/**
8+
* Health check for Mobula API
9+
* Tests both asset price endpoint and portfolio endpoint
10+
*/
11+
export async function GET() {
12+
const startTime = Date.now()
13+
14+
try {
15+
if (!MOBULA_API_KEY) {
16+
return NextResponse.json(
17+
{
18+
status: 'unhealthy',
19+
service: 'mobula',
20+
timestamp: new Date().toISOString(),
21+
error: 'MOBULA_API_KEY not configured',
22+
responseTime: Date.now() - startTime,
23+
},
24+
{ status: 500 }
25+
)
26+
}
27+
28+
// Test 1: Asset price endpoint (using USDC on Ethereum as test)
29+
const priceTestStart = Date.now()
30+
const priceResponse = await fetchWithSentry(
31+
`${MOBULA_API_URL}/api/1/market/data?asset=0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48&blockchain=1`,
32+
{
33+
headers: {
34+
'Content-Type': 'application/json',
35+
authorization: MOBULA_API_KEY,
36+
},
37+
}
38+
)
39+
const priceResponseTime = Date.now() - priceTestStart
40+
41+
if (!priceResponse.ok) {
42+
throw new Error(`Price API returned ${priceResponse.status}`)
43+
}
44+
45+
const priceData = await priceResponse.json()
46+
if (!priceData?.data?.price) {
47+
throw new Error('Invalid price data structure')
48+
}
49+
50+
// Test 2: Portfolio endpoint (using a known address with likely balance)
51+
const portfolioTestStart = Date.now()
52+
const portfolioResponse = await fetchWithSentry(
53+
`${MOBULA_API_URL}/api/1/wallet/portfolio?wallet=0x9647BB6a598c2675310c512e0566B60a5aEE6261`,
54+
{
55+
headers: {
56+
'Content-Type': 'application/json',
57+
authorization: MOBULA_API_KEY,
58+
},
59+
}
60+
)
61+
const portfolioResponseTime = Date.now() - portfolioTestStart
62+
63+
const portfolioHealthy = portfolioResponse.ok
64+
65+
// If portfolio API is down, throw error to return HTTP 500
66+
if (!portfolioHealthy) {
67+
throw new Error(`Portfolio API returned ${portfolioResponse.status}`)
68+
}
69+
70+
const totalResponseTime = Date.now() - startTime
71+
72+
return NextResponse.json({
73+
status: 'healthy',
74+
service: 'mobula',
75+
timestamp: new Date().toISOString(),
76+
responseTime: totalResponseTime,
77+
details: {
78+
priceApi: {
79+
status: 'healthy',
80+
responseTime: priceResponseTime,
81+
testAsset: 'USDC',
82+
price: priceData.data.price,
83+
},
84+
portfolioApi: {
85+
status: 'healthy',
86+
responseTime: portfolioResponseTime,
87+
httpStatus: portfolioResponse.status,
88+
},
89+
},
90+
})
91+
} catch (error) {
92+
const totalResponseTime = Date.now() - startTime
93+
94+
return NextResponse.json(
95+
{
96+
status: 'unhealthy',
97+
service: 'mobula',
98+
timestamp: new Date().toISOString(),
99+
error: error instanceof Error ? error.message : 'Unknown error',
100+
responseTime: totalResponseTime,
101+
},
102+
{ status: 500 }
103+
)
104+
}
105+
}

0 commit comments

Comments
 (0)