feat(sentry): Implement Sentry Tunnel to avoid local network permission dialog
- Add /api/sentry-tunnel API Route (Next.js) - Update sentry.client.config.ts with tunnel option - Re-enable NEXT_PUBLIC_SENTRY_DSN in CI/CD workflows Resolves: #45 Sentry Tunnel See: feedback_sentry_local_network.md Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
4
.github/workflows/cd.yaml
vendored
4
.github/workflows/cd.yaml
vendored
@@ -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 }}"
|
||||
|
||||
5
.github/workflows/ci.yaml
vendored
5
.github/workflows/ci.yaml
vendored
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
86
apps/web/src/app/api/sentry-tunnel/route.ts
Normal file
86
apps/web/src/app/api/sentry-tunnel/route.ts
Normal file
@@ -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://<key>@<host>:<port>/<project_id>
|
||||
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,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user