Files
Edu/services/api-gateway/internal/log/handler.go
SpecialX f586a0b19e feat(portal-shell): v2.0 P1-P4 token migration + unit tests + prod endpoint + e2e
P1: 31 widget 旧纸感令牌批量迁移到 shadcn 标准(1104 次替换)
- bg-paper→bg-background / bg-surface→bg-card / text-ink→text-foreground
- 保留 button.tsx 中 bg-accent(shadcn 标准 hover 语义令牌)

P2: v2.0 新增组件单元测试补齐(5 文件 81 用例)
- permission-bitmap: 24 用例(含 GRADE_READ 重复去重)
- route-permissions: 26 用例(4 张表优先级 + AND/OR 语义)
- notify: 12 用例(sonner toast 双重性质 vi.hoisted mock)
- use-error-report: 9 用例(jsdom Blob vi.stubGlobal mock)
- plugin-boundary: 10 用例(错误边界 + 骨架变体)

P3: 错误上报端点生产替换(后端 /api/v1/log)
- api-gateway: internal/log/handler.go(slog 结构化日志,64KB 限制,204 返回)
- main.go: 注册 POST /api/v1/log 路由
- useErrorReport: 环境感知端点(prod→/api/v1/log,dev→/api/log)

P4: E2E 测试(3 文件 30 用例)
- streaming: 4 用例(React 19 use() + Suspense,act 包裹 render)
- error-boundaries: 6 用例(三级错误边界层级 L1/L2/L3)
- security-boundaries: 20 用例(L1 角色门禁 + L2 权限点 + L3 数据范围)
- vitest setup: IS_REACT_ACT_ENVIRONMENT + jest-dom matchers

验证:typecheck 0 错误 / lint 0 错误 / build 6 路由 / 206 测试全部通过
2026-07-17 16:49:00 +08:00

111 lines
3.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package log 提供前端错误上报接收端点。
//
// 路由POST /api/v1/log
// 职责:
// - 接收前端 useErrorReport hook 通过 sendBeacon/fetch keepalive 上报的错误
// - 使用 slog 结构化日志记录(后续可接入 OTel/Sentry
// - 返回 204 No ContentsendBeacon 不需要响应体)
//
// 安全:
// - 已通过 api-gateway 的 JWT 鉴权中间件
// - 请求体大小限制 64KB错误日志不需要大 body
// - 限流由全局 RateLimit 中间件保障
//
// 关联portal-shell README v2.0 §5.4 三级错误处理
package log
import (
"encoding/json"
"io"
"log/slog"
"net/http"
"github.com/gin-gonic/gin"
)
// maxLogBodySize 日志请求体上限64KB
const maxLogBodySize int64 = 64 * 1024
// ErrorReportPayload 前端错误上报 payload 结构
// 对齐 packages/hooks/src/use-error-report.ts 的 ErrorReportPayload
type ErrorReportPayload struct {
Level string `json:"level"` // "error" | "warning"
Message string `json:"message"` // 错误消息
Stack string `json:"stack,omitempty"` // 调用栈
Digest string `json:"digest,omitempty"` // Next.js 错误摘要
Path string `json:"path"` // window.location.pathname
UserAgent string `json:"userAgent"` // navigator.userAgent
Timestamp string `json:"timestamp"` // ISO 8601
PluginID string `json:"pluginId,omitempty"` // 插件标识
UserID string `json:"userId,omitempty"` // 用户 ID
Context map[string]interface{} `json:"context,omitempty"` // 额外上下文
}
// HandleReport 接收前端错误上报。
//
// POST /api/v1/log
// Request Body: ErrorReportPayload (JSON)
// Response: 204 No Content
//
// 容错策略:
// - JSON 解析失败 → 400 + 错误消息
// - 其他错误 → 204前端上报失败静默降级不应阻塞
func HandleReport(c *gin.Context) {
// 限制请求体大小
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxLogBodySize)
body, err := io.ReadAll(c.Request.Body)
if err != nil {
slog.Warn("log report: read body failed",
"error", err,
"request_id", c.GetString("request_id"),
)
c.Status(http.StatusNoContent)
return
}
var payload ErrorReportPayload
if err := json.Unmarshal(body, &payload); err != nil {
slog.Warn("log report: invalid JSON",
"error", err,
"request_id", c.GetString("request_id"),
"body_size", len(body),
)
c.JSON(http.StatusBadRequest, gin.H{
"error": "invalid JSON payload",
})
return
}
// 结构化日志记录(后续可替换为 OTel/Sentry exporter
slog.LogAttrs(c.Request.Context(), toSlogLevel(payload.Level),
payload.Message,
slog.String("logger", "frontend"),
slog.String("digest", payload.Digest),
slog.String("path", payload.Path),
slog.String("plugin_id", payload.PluginID),
slog.String("user_id", payload.UserID),
slog.String("user_agent", payload.UserAgent),
slog.String("timestamp", payload.Timestamp),
slog.String("stack", payload.Stack),
slog.Any("context", payload.Context),
slog.String("request_id", c.GetString("request_id")),
)
c.Status(http.StatusNoContent)
}
// toSlogLevel 将前端 level 映射到 slog.Level
func toSlogLevel(level string) slog.Level {
switch level {
case "error":
return slog.LevelError
case "warning":
return slog.LevelWarn
case "info":
return slog.LevelInfo
default:
return slog.LevelError
}
}