diff --git a/.github/workflows/cd.yaml b/.github/workflows/cd.yaml index 058b29b5..434ec3aa 100644 --- a/.github/workflows/cd.yaml +++ b/.github/workflows/cd.yaml @@ -185,9 +185,7 @@ jobs: run: | docker build --push \ --build-arg NEXT_PUBLIC_API_URL=https://awoooi.wooo.work \ - # 暫時停用前端 Sentry (會觸發區域網路權限對話框) - # TODO: 實作 Sentry Tunnel 後再啟用 - # --build-arg NEXT_PUBLIC_SENTRY_DSN=http://da02d4e5d6542e4d1ed6b2dd6542efeb@192.168.0.110:9000/2 \ + --build-arg NEXT_PUBLIC_SENTRY_DSN=http://da02d4e5d6542e4d1ed6b2dd6542efeb@192.168.0.110:9000/2 \ --tag ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web:${{ steps.tag.outputs.tag }} \ --file apps/web/Dockerfile . echo "✅ Web: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-web:${{ steps.tag.outputs.tag }}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index b9369735..5ade6ee3 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -148,9 +148,8 @@ jobs: - name: Build packages env: NEXT_PUBLIC_API_URL: https://awoooi.wooo.work - # 暫時停用前端 Sentry (會觸發區域網路權限對話框) - # TODO: 實作 Sentry Tunnel 後再啟用 - # NEXT_PUBLIC_SENTRY_DSN: http://da02d4e5d6542e4d1ed6b2dd6542efeb@192.168.0.110:9000/2 + # Sentry DSN (透過 /api/sentry-tunnel 避免區域網路權限問題) + NEXT_PUBLIC_SENTRY_DSN: http://da02d4e5d6542e4d1ed6b2dd6542efeb@192.168.0.110:9000/2 run: pnpm turbo build - name: Upload build artifacts diff --git a/apps/web/sentry.client.config.ts b/apps/web/sentry.client.config.ts index bbbca4e1..c8e02985 100644 --- a/apps/web/sentry.client.config.ts +++ b/apps/web/sentry.client.config.ts @@ -14,6 +14,11 @@ if (process.env.NEXT_PUBLIC_SENTRY_DSN) { Sentry.init({ dsn: process.env.NEXT_PUBLIC_SENTRY_DSN, + // Sentry Tunnel: 避免區域網路權限對話框 + // @see apps/web/src/app/api/sentry-tunnel/route.ts + // @see feedback_sentry_local_network.md + tunnel: '/api/sentry-tunnel', + // 環境標識 environment: process.env.NODE_ENV, diff --git a/apps/web/src/app/api/sentry-tunnel/route.ts b/apps/web/src/app/api/sentry-tunnel/route.ts new file mode 100644 index 00000000..6e0d41c8 --- /dev/null +++ b/apps/web/src/app/api/sentry-tunnel/route.ts @@ -0,0 +1,86 @@ +/** + * Sentry Tunnel API Route + * ======================= + * + * 解決問題: 前端 Sentry DSN 使用內網 IP (192.168.0.110:9000) 會觸發 + * 瀏覽器「存取區域網路上的其他裝置」權限對話框。 + * + * 解決方案: 使用 Next.js API Route 作為 Tunnel,前端透過公網域名 + * (/api/sentry-tunnel) 發送事件,後端再轉發到內網 Sentry Server。 + * + * 參考: https://docs.sentry.io/platforms/javascript/troubleshooting/#dealing-with-ad-blockers + * + * @see feedback_sentry_local_network.md + * @see project_sentry_full_integration.md + */ + +import { NextRequest, NextResponse } from 'next/server'; + +// Sentry Self-Hosted 內網地址 +const SENTRY_HOST = 'http://192.168.0.110:9000'; + +// 允許的 Project IDs (防止濫用) +const ALLOWED_PROJECT_IDS = new Set(['2', '3']); // awoooi-web: 2, awoooi-api: 3 + +export async function POST(request: NextRequest) { + try { + const envelope = await request.text(); + + // 解析 envelope 取得 DSN 資訊 + const [header] = envelope.split('\n'); + const headerObj = JSON.parse(header); + const dsn = headerObj.dsn; + + if (!dsn) { + return NextResponse.json( + { error: 'Missing DSN in envelope header' }, + { status: 400 } + ); + } + + // 從 DSN 解析 Project ID + // DSN 格式: http://@:/ + const projectId = dsn.split('/').pop(); + + if (!projectId || !ALLOWED_PROJECT_IDS.has(projectId)) { + return NextResponse.json( + { error: 'Invalid or unauthorized project ID' }, + { status: 403 } + ); + } + + // 轉發到 Sentry Server + const response = await fetch(`${SENTRY_HOST}/api/${projectId}/envelope/`, { + method: 'POST', + body: envelope, + headers: { + 'Content-Type': 'application/x-sentry-envelope', + }, + }); + + if (!response.ok) { + console.error('[Sentry Tunnel] Forward failed:', response.status, response.statusText); + return NextResponse.json( + { error: 'Failed to forward to Sentry' }, + { status: response.status } + ); + } + + return new NextResponse(null, { status: 200 }); + } catch (error) { + console.error('[Sentry Tunnel] Error:', error); + return NextResponse.json( + { error: 'Internal server error' }, + { status: 500 } + ); + } +} + +// 健康檢查端點 +export async function GET() { + return NextResponse.json({ + status: 'ok', + tunnel: '/api/sentry-tunnel', + target: SENTRY_HOST, + }); +}