feat: add desktop bug report collection
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
||||
opsapp "github.com/geo-platform/tenant-api/internal/ops/app"
|
||||
opsrepo "github.com/geo-platform/tenant-api/internal/ops/repository"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/response"
|
||||
)
|
||||
|
||||
type DesktopBugReportHandler struct {
|
||||
svc *opsapp.DesktopBugReportService
|
||||
}
|
||||
|
||||
func NewDesktopBugReportHandler(a *bootstrap.App) *DesktopBugReportHandler {
|
||||
return &DesktopBugReportHandler{
|
||||
svc: opsapp.NewDesktopBugReportService(
|
||||
opsrepo.NewDesktopBugReportRepository(a.DB),
|
||||
a.ObjectStorage,
|
||||
nil,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *DesktopBugReportHandler) Upload(c *gin.Context) {
|
||||
input, err := parseDesktopBugReportMultipart(c)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
data, err := h.svc.UploadForClient(c.Request.Context(), MustDesktopClient(c.Request.Context()), input)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func (h *DesktopBugReportHandler) UploadPublic(c *gin.Context) {
|
||||
input, err := parseDesktopBugReportMultipart(c)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
data, err := h.svc.UploadPublic(c.Request.Context(), input)
|
||||
if err != nil {
|
||||
response.Error(c, err)
|
||||
return
|
||||
}
|
||||
response.Success(c, data)
|
||||
}
|
||||
|
||||
func parseDesktopBugReportMultipart(c *gin.Context) (opsapp.DesktopBugReportUploadInput, error) {
|
||||
if c.Request.ContentLength > opsapp.MaxDesktopBugMultipartBytes() {
|
||||
return opsapp.DesktopBugReportUploadInput{}, response.ErrBadRequest(40064, "desktop_bug_dump_too_large", "dump 文件不能超过 64MB")
|
||||
}
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, opsapp.MaxDesktopBugMultipartBytes())
|
||||
if err := c.Request.ParseMultipartForm(opsapp.MaxDesktopBugMultipartBytes()); err != nil {
|
||||
if strings.Contains(err.Error(), "request body too large") {
|
||||
return opsapp.DesktopBugReportUploadInput{}, response.ErrBadRequest(40064, "desktop_bug_dump_too_large", "dump 文件不能超过 64MB")
|
||||
}
|
||||
return opsapp.DesktopBugReportUploadInput{}, response.ErrBadRequest(40065, "invalid_desktop_bug_report", "客户端 Bug 上报表单无效")
|
||||
}
|
||||
|
||||
fields := map[string]string{}
|
||||
if c.Request.MultipartForm != nil {
|
||||
for key, values := range c.Request.MultipartForm.Value {
|
||||
if len(values) == 0 {
|
||||
continue
|
||||
}
|
||||
fields[key] = values[0]
|
||||
}
|
||||
}
|
||||
dumpName, dumpContent, err := readDesktopBugDump(c)
|
||||
if err != nil {
|
||||
return opsapp.DesktopBugReportUploadInput{}, err
|
||||
}
|
||||
runtimeSnapshot := []byte(firstNonEmpty(fields["runtime_snapshot"], fields["_runtime_snapshot"]))
|
||||
if len(runtimeSnapshot) > 0 && !json.Valid(runtimeSnapshot) {
|
||||
runtimeSnapshot = []byte("{}")
|
||||
}
|
||||
|
||||
return opsapp.DesktopBugReportUploadInput{
|
||||
ReportType: firstNonEmpty(fields["report_type"], fields["_report_type"]),
|
||||
Severity: firstNonEmpty(fields["severity"], fields["_severity"]),
|
||||
Title: firstNonEmpty(fields["title"], fields["_title"]),
|
||||
Description: firstNonEmpty(fields["description"], fields["_description"]),
|
||||
DesktopClientID: firstNonEmpty(fields["client_id"], fields["_client_id"]),
|
||||
CrashGUID: fields["guid"],
|
||||
CrashReportID: firstNonEmpty(fields["crash_report_id"], fields["id"]),
|
||||
ProcessType: firstNonEmpty(fields["process_type"], fields["ptype"]),
|
||||
ElectronVersion: fields["ver"],
|
||||
AppVersion: firstNonEmpty(fields["app_version"], fields["_version"]),
|
||||
ProductName: firstNonEmpty(fields["_productName"], fields["prod"]),
|
||||
Platform: fields["platform"],
|
||||
OSArch: firstNonEmpty(fields["osarch"], fields["cpu_arch"]),
|
||||
DeviceName: fields["device_name"],
|
||||
FormFields: fields,
|
||||
RuntimeSnapshot: runtimeSnapshot,
|
||||
AppLogTail: firstNonEmpty(fields["app_log_tail"], fields["_app_log_tail"]),
|
||||
DumpFileName: dumpName,
|
||||
DumpContent: dumpContent,
|
||||
UploaderIP: c.ClientIP(),
|
||||
UserAgent: c.Request.UserAgent(),
|
||||
OccurredAt: opsapp.ParseDesktopBugOccurredAt(firstNonEmpty(fields["occurred_at"], fields["_occurred_at"])),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readDesktopBugDump(c *gin.Context) (string, []byte, error) {
|
||||
for _, field := range []string{"upload_file_minidump", "dump", "file"} {
|
||||
fileHeader, err := c.FormFile(field)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
file, err := fileHeader.Open()
|
||||
if err != nil {
|
||||
return "", nil, response.ErrBadRequest(40066, "desktop_bug_dump_open_failed", "dump 文件读取失败")
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
content, err := io.ReadAll(io.LimitReader(file, opsapp.MaxDesktopBugDumpBytes()+1))
|
||||
if err != nil {
|
||||
return "", nil, response.ErrBadRequest(40066, "desktop_bug_dump_read_failed", "dump 文件读取失败")
|
||||
}
|
||||
if int64(len(content)) > opsapp.MaxDesktopBugDumpBytes() {
|
||||
return "", nil, response.ErrBadRequest(40064, "desktop_bug_dump_too_large", "dump 文件不能超过 64MB")
|
||||
}
|
||||
return fileHeader.Filename, content, nil
|
||||
}
|
||||
return "", nil, nil
|
||||
}
|
||||
@@ -1,6 +1,8 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/auth"
|
||||
"github.com/geo-platform/tenant-api/internal/shared/middleware"
|
||||
@@ -34,6 +36,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
|
||||
desktopClientHandler := NewDesktopClientHandler(app)
|
||||
desktopReleaseHandler := NewDesktopReleaseHandler(app)
|
||||
desktopBugReportHandler := NewDesktopBugReportHandler(app)
|
||||
desktopAccountHandler := NewDesktopAccountHandler(app)
|
||||
desktopTaskHandler := NewDesktopTaskHandler(app)
|
||||
desktopDispatchHandler := NewDesktopDispatchHandler(app)
|
||||
@@ -45,6 +48,8 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
app.Engine.GET("/api/desktop/releases/download-url", desktopReleaseHandler.DownloadURLPublic)
|
||||
app.Engine.GET("/api/desktop/releases/updater/:platform/:arch/:channel/:version/:feed", desktopReleaseHandler.UpdaterFeedPublic)
|
||||
app.Engine.GET("/api/desktop/releases/updater/:platform/:arch/:channel/:version", desktopReleaseHandler.UpdaterFeedPublic)
|
||||
publicBugReportRateLimit := middleware.RateLimitByClientIP(10, time.Minute, 42961, "desktop_bug_report_rate_limited", "客户端 Bug 上报过于频繁,请稍后再试")
|
||||
app.Engine.POST("/api/desktop/bug-reports", publicBugReportRateLimit, desktopBugReportHandler.UploadPublic)
|
||||
|
||||
desktopAuth := app.Engine.Group("/api/desktop")
|
||||
desktopAuth.Use(DesktopClientMiddleware(repository.NewDesktopClientRepository(app.DB)))
|
||||
@@ -52,6 +57,7 @@ func RegisterRoutes(app *bootstrap.App) {
|
||||
desktopAuth.POST("/clients/heartbeat", desktopClientHandler.Heartbeat)
|
||||
desktopAuth.POST("/clients/offline", desktopClientHandler.Offline)
|
||||
desktopAuth.POST("/clients/revoke", desktopClientHandler.Revoke)
|
||||
desktopAuth.POST("/clients/bug-reports", desktopBugReportHandler.Upload)
|
||||
desktopAuth.GET("/clients/releases/latest", desktopReleaseHandler.Latest)
|
||||
desktopAuth.GET("/clients/releases/download-url", desktopReleaseHandler.DownloadURL)
|
||||
desktopAuth.GET("/dispatch", desktopDispatchHandler.Dispatch)
|
||||
|
||||
Reference in New Issue
Block a user