Files
awoooi/apps/web/src/hooks/useGlobalPulseMetrics.ts
Your Name 6ccdf199ad
All checks were successful
CD Pipeline / tests (push) Successful in 1m23s
Code Review / ai-code-review (push) Successful in 13s
CD Pipeline / build-and-deploy (push) Successful in 4m18s
CD Pipeline / post-deploy-checks (push) Successful in 1m44s
chore(web): 清理 IwoooS D2 註解語氣
2026-06-05 01:11:44 +08:00

239 lines
6.5 KiB
TypeScript

'use client'
/**
* useGlobalPulseMetrics - 全局脈搏真實數據 Hook
* =============================================
* 專案鐵律: 禁止假數據!所有指標必須來自 production 真實資料源
*
* Features:
* - Fail Fast 錯誤處理
* - 自動輪詢 (30 秒)
* - 誠實渲染 (無數據顯示 "--")
* - 專案鐵律: 禁止 Hardcode API URL
*/
import { useState, useEffect, useCallback, useRef } from 'react'
import type { PulseMetric } from '@/components/charts/global-pulse-chart'
// =============================================================================
// Types (對應後端 Response)
// =============================================================================
interface GoldMetricItem {
label: string
value: number | string
unit?: string
trend: number[]
status: 'healthy' | 'warning' | 'critical'
}
interface GoldMetricsResponse {
timestamp: string
service_name: string
metrics: GoldMetricItem[]
raw_data?: Record<string, unknown>
}
interface UseGlobalPulseMetricsOptions {
/** 輪詢間隔 (ms),預設 30000 */
pollInterval?: number
/** 是否啟用自動輪詢,預設 true */
enablePolling?: boolean
/** 服務名稱,預設 "awoooi-api" */
serviceName?: string
}
interface UseGlobalPulseMetricsResult {
/** 指標數據 (格式化給 GlobalPulseChart 使用) */
metrics: PulseMetric[]
/** 是否載入中 */
isLoading: boolean
/** 錯誤訊息 */
error: string | null
/** 最後更新時間 */
lastUpdated: Date | null
/** 手動刷新 */
refresh: () => Promise<void>
/** 是否有真實數據 (非全 0) */
hasRealData: boolean
}
// =============================================================================
// API Helper (專案鐵律: 禁止 Hardcode)
// =============================================================================
const getApiBaseUrl = (): string => {
if (typeof window === 'undefined') return ''
const url = process.env.NEXT_PUBLIC_API_URL
if (!url) {
console.error('[AWOOOI ERROR] Missing NEXT_PUBLIC_API_URL configuration.')
return ''
}
return url
}
// =============================================================================
// Hook Implementation
// =============================================================================
export function useGlobalPulseMetrics(
options: UseGlobalPulseMetricsOptions = {}
): UseGlobalPulseMetricsResult {
const {
pollInterval = 30000,
enablePolling = true,
serviceName = 'awoooi-api',
} = options
// State
const [metrics, setMetrics] = useState<PulseMetric[]>([])
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [lastUpdated, setLastUpdated] = useState<Date | null>(null)
const [hasRealData, setHasRealData] = useState(false)
// Polling timer ref
const pollTimerRef = useRef<NodeJS.Timeout | null>(null)
// ==========================================================================
// Fetch Metrics
// ==========================================================================
const fetchMetrics = useCallback(async () => {
const apiBaseUrl = getApiBaseUrl()
// Fail Fast: 無 API URL 則報錯
if (!apiBaseUrl) {
setError('NEXT_PUBLIC_API_URL 未設定')
setIsLoading(false)
setHasRealData(false)
// 誠實渲染: 顯示 "--"
setMetrics(createEmptyMetrics())
return
}
try {
setError(null)
const response = await fetch(
`${apiBaseUrl}/api/v1/metrics/gold?service_name=${encodeURIComponent(serviceName)}`,
{
headers: { 'Content-Type': 'application/json' },
// 10 秒超時
signal: AbortSignal.timeout(10000),
}
)
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const data: GoldMetricsResponse = await response.json()
// 轉換為 PulseMetric 格式
const pulseMetrics: PulseMetric[] = data.metrics.map((m) => ({
label: m.label,
value: m.value,
unit: m.unit,
trend: m.trend,
status: m.status,
}))
setMetrics(pulseMetrics)
setLastUpdated(new Date())
// 檢查是否有真實數據 (非全 0)
const hasReal = pulseMetrics.some(
(m) => typeof m.value === 'number' && m.value > 0
)
setHasRealData(hasReal)
console.log('[GlobalPulse] Metrics fetched:', pulseMetrics.length, 'items')
} catch (err) {
const message = err instanceof Error ? err.message : 'Unknown error'
console.error('[GlobalPulse] Fetch error:', message)
setError(message)
setHasRealData(false)
// 誠實渲染: 錯誤時顯示 "--"
setMetrics(createEmptyMetrics())
} finally {
setIsLoading(false)
}
}, [serviceName])
// ==========================================================================
// Polling Effect
// ==========================================================================
useEffect(() => {
// Initial fetch
fetchMetrics()
// Start polling if enabled
if (enablePolling && pollInterval > 0) {
pollTimerRef.current = setInterval(fetchMetrics, pollInterval)
}
// Cleanup
return () => {
if (pollTimerRef.current) {
clearInterval(pollTimerRef.current)
pollTimerRef.current = null
}
}
}, [fetchMetrics, enablePolling, pollInterval])
return {
metrics,
isLoading,
error,
lastUpdated,
refresh: fetchMetrics,
hasRealData,
}
}
// =============================================================================
// Helper: Create Empty Metrics (誠實渲染用)
// =============================================================================
function createEmptyMetrics(): PulseMetric[] {
return [
{
label: 'RPS',
value: '--',
unit: 'req/s',
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
status: 'critical',
},
{
label: 'Error Rate',
value: '--',
unit: '%',
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
status: 'critical',
},
{
label: 'P99 Latency',
value: '--',
unit: 'ms',
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
status: 'critical',
},
{
label: 'AI Success',
value: '--',
unit: '%',
trend: [0, 0, 0, 0, 0, 0, 0, 0, 0, 0],
status: 'critical',
},
]
}
// =============================================================================
// Export
// =============================================================================
export default useGlobalPulseMetrics