Files
geo/server/internal/ops/app/desktop_bug_report.go
T

686 lines
23 KiB
Go

package app
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/google/uuid"
"github.com/geo-platform/tenant-api/internal/ops/domain"
"github.com/geo-platform/tenant-api/internal/ops/repository"
"github.com/geo-platform/tenant-api/internal/shared/objectstorage"
"github.com/geo-platform/tenant-api/internal/shared/response"
tenantrepo "github.com/geo-platform/tenant-api/internal/tenant/repository"
)
const (
ActionDesktopBugReportCreate = "desktop_bug_report.create"
ActionDesktopBugReportUpdate = "desktop_bug_report.update"
)
const (
DesktopBugReportTypeCrash = "crash"
DesktopBugReportTypeBug = "bug"
)
const (
DesktopBugReportStatusNew = "new"
DesktopBugReportStatusTriaged = "triaged"
DesktopBugReportStatusInProgress = "in_progress"
DesktopBugReportStatusResolved = "resolved"
DesktopBugReportStatusIgnored = "ignored"
)
const (
DesktopBugReportSeverityLow = "low"
DesktopBugReportSeverityMedium = "medium"
DesktopBugReportSeverityHigh = "high"
DesktopBugReportSeverityCritical = "critical"
)
const maxDesktopBugDumpBytes = 64 * 1024 * 1024
const maxDesktopBugMultipartBytes = maxDesktopBugDumpBytes + 4*1024*1024
type DesktopBugReportService struct {
reports *repository.DesktopBugReportRepository
storage objectstorage.Client
audits *AuditService
}
func NewDesktopBugReportService(
reports *repository.DesktopBugReportRepository,
storage objectstorage.Client,
audits *AuditService,
) *DesktopBugReportService {
return &DesktopBugReportService{reports: reports, storage: storage, audits: audits}
}
type DesktopBugReportUploadInput struct {
ReportType string
Severity string
Title string
Description string
TenantID *int64
WorkspaceID *int64
UserID *int64
DesktopClientID string
CrashGUID string
CrashReportID string
ProcessType string
ElectronVersion string
AppVersion string
ProductName string
Platform string
OSArch string
DeviceName string
FormFields map[string]string
RuntimeSnapshot []byte
AppLogTail string
DumpFileName string
DumpContent []byte
UploaderIP string
UserAgent string
OccurredAt time.Time
}
type DesktopBugReportListInput struct {
Keyword string
Status string
Severity string
ReportType string
Platform string
Process string
TenantID *int64
ClientID string
Page int
Size int
}
type DesktopBugReportUpdateInput struct {
Status string
Severity string
ResolutionNote *string
}
type DesktopBugReportListResult struct {
Items []DesktopBugReportView `json:"items"`
Total int64 `json:"total"`
Page int `json:"page"`
Size int `json:"size"`
}
type DesktopBugReportUploadResult struct {
ID string `json:"id"`
DumpObjectKey *string `json:"dump_object_key,omitempty"`
}
type DesktopBugReportDownloadURLResult struct {
DownloadURL string `json:"download_url"`
}
type DesktopBugReportView struct {
ID string `json:"id"`
ReportType string `json:"report_type"`
Status string `json:"status"`
Severity string `json:"severity"`
TenantID *int64 `json:"tenant_id,omitempty"`
TenantName *string `json:"tenant_name,omitempty"`
WorkspaceID *int64 `json:"workspace_id,omitempty"`
WorkspaceName *string `json:"workspace_name,omitempty"`
UserID *int64 `json:"user_id,omitempty"`
UserName *string `json:"user_name,omitempty"`
UserPhone *string `json:"user_phone,omitempty"`
UserEmail *string `json:"user_email,omitempty"`
DesktopClientID *string `json:"desktop_client_id,omitempty"`
CrashGUID *string `json:"crash_guid,omitempty"`
CrashReportID *string `json:"crash_report_id,omitempty"`
ProcessType *string `json:"process_type,omitempty"`
ElectronVersion *string `json:"electron_version,omitempty"`
AppVersion *string `json:"app_version,omitempty"`
ProductName *string `json:"product_name,omitempty"`
Platform *string `json:"platform,omitempty"`
OSArch *string `json:"os_arch,omitempty"`
DeviceName *string `json:"device_name,omitempty"`
Title string `json:"title"`
Description *string `json:"description,omitempty"`
DumpObjectKey *string `json:"dump_object_key,omitempty"`
DumpFileName *string `json:"dump_file_name,omitempty"`
DumpSizeBytes *int64 `json:"dump_size_bytes,omitempty"`
DumpSHA256 *string `json:"dump_sha256,omitempty"`
FormFields json.RawMessage `json:"form_fields"`
RuntimeSnapshot json.RawMessage `json:"runtime_snapshot"`
AppLogTail *string `json:"app_log_tail,omitempty"`
ResolutionNote *string `json:"resolution_note,omitempty"`
UploaderIP *string `json:"uploader_ip,omitempty"`
UserAgent *string `json:"user_agent,omitempty"`
OccurredAt time.Time `json:"occurred_at"`
ResolvedAt *time.Time `json:"resolved_at,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (s *DesktopBugReportService) UploadPublic(ctx context.Context, in DesktopBugReportUploadInput) (*DesktopBugReportUploadResult, error) {
in.TenantID = nil
in.WorkspaceID = nil
in.UserID = nil
in.DesktopClientID = ""
in.FormFields = dropPublicDesktopBugIdentityFields(in.FormFields)
return s.Upload(ctx, in)
}
func (s *DesktopBugReportService) Upload(ctx context.Context, in DesktopBugReportUploadInput) (*DesktopBugReportUploadResult, error) {
if s == nil || s.reports == nil {
return nil, response.ErrServiceUnavailable(50361, "desktop_bug_report_unavailable", "desktop bug report service is unavailable")
}
normalized, err := normalizeDesktopBugReportUploadInput(in)
if err != nil {
return nil, err
}
var dumpObjectKey *string
var dumpSHA256 *string
var dumpSize *int64
if len(normalized.DumpContent) > 0 {
if err := s.ensureStorageReady(); err != nil {
return nil, err
}
sum := sha256.Sum256(normalized.DumpContent)
sha256Hex := hex.EncodeToString(sum[:])
size := int64(len(normalized.DumpContent))
objectKey := buildDesktopBugDumpObjectKey(normalized, sha256Hex)
contentType := detectDesktopBugDumpContentType(normalized.DumpFileName, normalized.DumpContent)
if err := s.storage.PutBytes(ctx, objectKey, normalized.DumpContent, contentType); err != nil {
appErr := response.ErrInternal(50061, "desktop_bug_dump_upload_failed", "failed to upload dump to object storage")
appErr.Cause = err
return nil, appErr
}
dumpObjectKey = &objectKey
dumpSHA256 = &sha256Hex
dumpSize = &size
}
formFields, _ := json.Marshal(normalized.FormFields)
report, err := s.reports.Create(ctx, repository.CreateDesktopBugReportInput{
ID: uuid.NewString(),
ReportType: normalized.ReportType,
Status: DesktopBugReportStatusNew,
Severity: normalized.Severity,
TenantID: normalized.TenantID,
WorkspaceID: normalized.WorkspaceID,
UserID: normalized.UserID,
DesktopClientID: optionalUUIDString(normalized.DesktopClientID),
CrashGUID: optionalText(normalized.CrashGUID),
CrashReportID: optionalText(normalized.CrashReportID),
ProcessType: optionalText(normalized.ProcessType),
ElectronVersion: optionalText(normalized.ElectronVersion),
AppVersion: optionalText(normalized.AppVersion),
ProductName: optionalText(normalized.ProductName),
Platform: optionalText(normalized.Platform),
OSArch: optionalText(normalized.OSArch),
DeviceName: optionalText(normalized.DeviceName),
Title: normalized.Title,
Description: optionalText(normalized.Description),
DumpObjectKey: dumpObjectKey,
DumpFileName: optionalText(normalized.DumpFileName),
DumpSizeBytes: dumpSize,
DumpSHA256: dumpSHA256,
FormFields: formFields,
RuntimeSnapshot: normalized.RuntimeSnapshot,
AppLogTail: optionalText(normalized.AppLogTail),
UploaderIP: optionalText(normalized.UploaderIP),
UserAgent: optionalText(normalized.UserAgent),
OccurredAt: normalized.OccurredAt,
})
if err != nil {
return nil, err
}
return &DesktopBugReportUploadResult{ID: report.ID, DumpObjectKey: report.DumpObjectKey}, nil
}
func (s *DesktopBugReportService) UploadForClient(
ctx context.Context,
client *tenantrepo.DesktopClient,
in DesktopBugReportUploadInput,
) (*DesktopBugReportUploadResult, error) {
if client == nil {
return nil, response.ErrUnauthorized(40108, "desktop_client_missing", "desktop client context is required")
}
tenantID := client.TenantID
workspaceID := client.WorkspaceID
userID := client.UserID
in.TenantID = &tenantID
in.WorkspaceID = &workspaceID
in.UserID = &userID
in.DesktopClientID = client.ID.String()
if strings.TrimSpace(in.DeviceName) == "" && client.DeviceName != nil {
in.DeviceName = *client.DeviceName
}
if strings.TrimSpace(in.Platform) == "" && client.OS != nil {
in.Platform = *client.OS
}
if strings.TrimSpace(in.OSArch) == "" && client.CPUArch != nil {
in.OSArch = *client.CPUArch
}
if strings.TrimSpace(in.AppVersion) == "" && client.ClientVersion != nil {
in.AppVersion = *client.ClientVersion
}
return s.Upload(ctx, in)
}
func (s *DesktopBugReportService) List(ctx context.Context, in DesktopBugReportListInput) (*DesktopBugReportListResult, error) {
page := in.Page
if page <= 0 {
page = 1
}
size := in.Size
if size <= 0 || size > 200 {
size = 20
}
items, total, err := s.reports.List(ctx, repository.DesktopBugReportFilter{
Keyword: strings.TrimSpace(in.Keyword),
Status: normalizeDesktopBugStatus(in.Status),
Severity: normalizeDesktopBugSeverity(in.Severity),
ReportType: normalizeDesktopBugReportType(in.ReportType),
Platform: normalizeDesktopBugToken(in.Platform),
Process: normalizeDesktopBugToken(in.Process),
TenantID: in.TenantID,
ClientID: normalizeUUIDString(in.ClientID),
Limit: size,
Offset: (page - 1) * size,
})
if err != nil {
return nil, err
}
views := make([]DesktopBugReportView, 0, len(items))
for i := range items {
views = append(views, desktopBugReportView(&items[i]))
}
return &DesktopBugReportListResult{Items: views, Total: total, Page: page, Size: size}, nil
}
func (s *DesktopBugReportService) Get(ctx context.Context, id string) (*DesktopBugReportView, error) {
report, err := s.reports.GetByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrDesktopBugReportNotFound) {
return nil, response.ErrNotFound(40461, "desktop_bug_report_not_found", "客户端 Bug 记录不存在")
}
return nil, err
}
view := desktopBugReportView(report)
return &view, nil
}
func (s *DesktopBugReportService) Update(ctx context.Context, actor *Actor, id string, in DesktopBugReportUpdateInput) (*DesktopBugReportView, error) {
status := normalizeDesktopBugStatus(in.Status)
if !isAllowedDesktopBugStatus(status) {
return nil, response.ErrBadRequest(40061, "invalid_desktop_bug_status", "状态必须是 new、triaged、in_progress、resolved 或 ignored")
}
severity := normalizeDesktopBugSeverity(in.Severity)
if !isAllowedDesktopBugSeverity(severity) {
return nil, response.ErrBadRequest(40062, "invalid_desktop_bug_severity", "严重级别必须是 low、medium、high 或 critical")
}
report, err := s.reports.UpdateState(ctx, id, repository.UpdateDesktopBugReportStateInput{
Status: status,
Severity: severity,
ResolutionNote: trimOptionalDesktopBugString(in.ResolutionNote, 4000),
})
if err != nil {
if errors.Is(err, repository.ErrDesktopBugReportNotFound) {
return nil, response.ErrNotFound(40461, "desktop_bug_report_not_found", "客户端 Bug 记录不存在")
}
return nil, err
}
s.audit(ctx, actor, ActionDesktopBugReportUpdate, report)
view := desktopBugReportView(report)
return &view, nil
}
func (s *DesktopBugReportService) ResolveDumpDownloadURL(ctx context.Context, id string) (*DesktopBugReportDownloadURLResult, error) {
report, err := s.reports.GetByID(ctx, id)
if err != nil {
if errors.Is(err, repository.ErrDesktopBugReportNotFound) {
return nil, response.ErrNotFound(40461, "desktop_bug_report_not_found", "客户端 Bug 记录不存在")
}
return nil, err
}
if report.DumpObjectKey == nil || strings.TrimSpace(*report.DumpObjectKey) == "" {
return nil, response.ErrNotFound(40462, "desktop_bug_dump_not_found", "该记录没有 dump 文件")
}
if err := s.ensureStorageReady(); err != nil {
return nil, err
}
downloadURL, err := s.storage.DownloadURL(*report.DumpObjectKey)
if err != nil {
appErr := response.ErrInternal(50062, "desktop_bug_dump_url_failed", "failed to generate dump download URL")
appErr.Cause = err
return nil, appErr
}
return &DesktopBugReportDownloadURLResult{DownloadURL: downloadURL}, nil
}
func (s *DesktopBugReportService) ensureStorageReady() error {
if s == nil || s.storage == nil {
return response.ErrServiceUnavailable(50303, "object_storage_unavailable", "object storage is not configured")
}
if err := s.storage.Validate(); err != nil {
return response.ErrServiceUnavailable(50303, "object_storage_unavailable", "object storage is not configured")
}
return nil
}
func normalizeDesktopBugReportUploadInput(in DesktopBugReportUploadInput) (DesktopBugReportUploadInput, error) {
in.ReportType = normalizeDesktopBugReportType(in.ReportType)
if in.ReportType == "" {
in.ReportType = DesktopBugReportTypeCrash
}
if !isAllowedDesktopBugReportType(in.ReportType) {
return DesktopBugReportUploadInput{}, response.ErrBadRequest(40063, "invalid_desktop_bug_report_type", "上报类型必须是 crash 或 bug")
}
in.Severity = normalizeDesktopBugSeverity(in.Severity)
if in.Severity == "" {
if in.ReportType == DesktopBugReportTypeCrash {
in.Severity = DesktopBugReportSeverityHigh
} else {
in.Severity = DesktopBugReportSeverityMedium
}
}
if !isAllowedDesktopBugSeverity(in.Severity) {
return DesktopBugReportUploadInput{}, response.ErrBadRequest(40062, "invalid_desktop_bug_severity", "严重级别必须是 low、medium、high 或 critical")
}
in.Title = limitDesktopBugText(firstNonEmptyString(
in.Title,
in.FormFields["_title"],
in.FormFields["title"],
defaultDesktopBugTitle(in),
), 200)
in.Description = limitDesktopBugText(firstNonEmptyString(in.Description, in.FormFields["_description"], in.FormFields["description"]), 4000)
in.CrashGUID = limitDesktopBugText(firstNonEmptyString(in.CrashGUID, in.FormFields["guid"]), 200)
in.CrashReportID = limitDesktopBugText(firstNonEmptyString(in.CrashReportID, in.FormFields["crash_report_id"], in.FormFields["id"]), 200)
in.ProcessType = limitDesktopBugText(firstNonEmptyString(in.ProcessType, in.FormFields["process_type"], in.FormFields["ptype"]), 80)
in.ElectronVersion = limitDesktopBugText(firstNonEmptyString(in.ElectronVersion, in.FormFields["ver"]), 80)
in.AppVersion = limitDesktopBugText(firstNonEmptyString(in.AppVersion, in.FormFields["app_version"], in.FormFields["_version"]), 80)
in.ProductName = limitDesktopBugText(firstNonEmptyString(in.ProductName, in.FormFields["_productName"], in.FormFields["prod"]), 120)
in.Platform = limitDesktopBugText(firstNonEmptyString(in.Platform, in.FormFields["platform"]), 80)
in.OSArch = limitDesktopBugText(firstNonEmptyString(in.OSArch, in.FormFields["osarch"], in.FormFields["cpu_arch"]), 80)
in.DeviceName = limitDesktopBugText(firstNonEmptyString(in.DeviceName, in.FormFields["device_name"]), 200)
in.DesktopClientID = normalizeUUIDString(firstNonEmptyString(in.DesktopClientID, in.FormFields["client_id"]))
if len(in.DumpContent) > maxDesktopBugDumpBytes {
return DesktopBugReportUploadInput{}, response.ErrBadRequest(40064, "desktop_bug_dump_too_large", "dump 文件不能超过 64MB")
}
in.DumpFileName = sanitizeDesktopBugFileName(in.DumpFileName)
if len(in.DumpContent) > 0 && in.DumpFileName == "" {
in.DumpFileName = "upload_file_minidump.dmp"
}
in.AppLogTail = limitDesktopBugText(in.AppLogTail, 20000)
if len(in.RuntimeSnapshot) == 0 || !json.Valid(in.RuntimeSnapshot) {
in.RuntimeSnapshot = []byte("{}")
}
in.FormFields = sanitizeDesktopBugFormFields(in.FormFields)
if in.OccurredAt.IsZero() {
in.OccurredAt = time.Now().UTC()
}
return in, nil
}
func defaultDesktopBugTitle(in DesktopBugReportUploadInput) string {
if in.ReportType == DesktopBugReportTypeBug {
return "用户反馈"
}
process := firstNonEmptyString(in.ProcessType, in.FormFields["process_type"], in.FormFields["ptype"], "unknown")
platform := firstNonEmptyString(in.Platform, in.FormFields["platform"], "unknown")
return fmt.Sprintf("客户端崩溃:%s / %s", process, platform)
}
func sanitizeDesktopBugFormFields(fields map[string]string) map[string]string {
out := make(map[string]string)
for key, value := range fields {
normalizedKey := strings.TrimSpace(key)
if normalizedKey == "" || len(normalizedKey) > 120 {
continue
}
out[normalizedKey] = limitDesktopBugText(value, 2000)
}
return out
}
func dropPublicDesktopBugIdentityFields(fields map[string]string) map[string]string {
if len(fields) == 0 {
return fields
}
out := make(map[string]string, len(fields))
for key, value := range fields {
switch strings.ToLower(strings.TrimSpace(key)) {
case "tenant_id", "_tenant_id", "workspace_id", "_workspace_id", "user_id", "_user_id",
"client_id", "_client_id", "desktop_client_id", "_desktop_client_id":
continue
default:
out[key] = value
}
}
return out
}
func buildDesktopBugDumpObjectKey(in DesktopBugReportUploadInput, sha256Hex string) string {
datePrefix := time.Now().UTC().Format("2006/01/02")
reportKind := in.ReportType
if reportKind == "" {
reportKind = DesktopBugReportTypeCrash
}
clientID := normalizeUUIDString(in.DesktopClientID)
if clientID == "" {
clientID = "anonymous"
}
ext := strings.ToLower(filepath.Ext(in.DumpFileName))
if ext == "" || len(ext) > 10 {
ext = ".dmp"
}
return fmt.Sprintf("desktop-bug-reports/%s/%s/%s/%s%s", reportKind, datePrefix, clientID, sha256Hex, ext)
}
func detectDesktopBugDumpContentType(fileName string, content []byte) string {
lower := strings.ToLower(fileName)
switch {
case strings.HasSuffix(lower, ".dmp"):
return "application/vnd.microsoft.portable-executable"
case strings.HasSuffix(lower, ".gz"):
return "application/gzip"
case strings.HasSuffix(lower, ".zip"):
return "application/zip"
default:
if len(content) > 0 {
return http.DetectContentType(content)
}
return "application/octet-stream"
}
}
func desktopBugReportView(item *domain.DesktopBugReport) DesktopBugReportView {
return DesktopBugReportView{
ID: item.ID,
ReportType: item.ReportType,
Status: item.Status,
Severity: item.Severity,
TenantID: item.TenantID,
TenantName: item.TenantName,
WorkspaceID: item.WorkspaceID,
WorkspaceName: item.WorkspaceName,
UserID: item.UserID,
UserName: item.UserName,
UserPhone: item.UserPhone,
UserEmail: item.UserEmail,
DesktopClientID: item.DesktopClientID,
CrashGUID: item.CrashGUID,
CrashReportID: item.CrashReportID,
ProcessType: item.ProcessType,
ElectronVersion: item.ElectronVersion,
AppVersion: item.AppVersion,
ProductName: item.ProductName,
Platform: item.Platform,
OSArch: item.OSArch,
DeviceName: item.DeviceName,
Title: item.Title,
Description: item.Description,
DumpObjectKey: item.DumpObjectKey,
DumpFileName: item.DumpFileName,
DumpSizeBytes: item.DumpSizeBytes,
DumpSHA256: item.DumpSHA256,
FormFields: item.FormFields,
RuntimeSnapshot: item.RuntimeSnapshot,
AppLogTail: item.AppLogTail,
ResolutionNote: item.ResolutionNote,
UploaderIP: item.UploaderIP,
UserAgent: item.UserAgent,
OccurredAt: item.OccurredAt,
ResolvedAt: item.ResolvedAt,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
}
}
func (s *DesktopBugReportService) audit(ctx context.Context, actor *Actor, action string, item *domain.DesktopBugReport) {
if actor == nil || s.audits == nil || item == nil {
return
}
event := actor.audit(action, "desktop_bug_report", 0, map[string]any{
"id": item.ID,
"report_type": item.ReportType,
"status": item.Status,
"severity": item.Severity,
"dump_object_key": item.DumpObjectKey,
})
event.TargetID = item.ID
_ = s.audits.Append(ctx, event)
}
func normalizeDesktopBugReportType(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeDesktopBugStatus(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeDesktopBugSeverity(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func normalizeDesktopBugToken(value string) string {
return strings.ToLower(strings.TrimSpace(value))
}
func isAllowedDesktopBugReportType(value string) bool {
return value == DesktopBugReportTypeCrash || value == DesktopBugReportTypeBug
}
func isAllowedDesktopBugStatus(value string) bool {
switch value {
case DesktopBugReportStatusNew, DesktopBugReportStatusTriaged, DesktopBugReportStatusInProgress, DesktopBugReportStatusResolved, DesktopBugReportStatusIgnored:
return true
default:
return false
}
}
func isAllowedDesktopBugSeverity(value string) bool {
switch value {
case DesktopBugReportSeverityLow, DesktopBugReportSeverityMedium, DesktopBugReportSeverityHigh, DesktopBugReportSeverityCritical:
return true
default:
return false
}
}
func optionalText(value string) *string {
value = strings.TrimSpace(value)
if value == "" {
return nil
}
return &value
}
func optionalUUIDString(value string) *string {
value = normalizeUUIDString(value)
if value == "" {
return nil
}
return &value
}
func normalizeUUIDString(value string) string {
value = strings.TrimSpace(value)
if _, err := uuid.Parse(value); err != nil {
return ""
}
return value
}
func limitDesktopBugText(value string, limit int) string {
value = strings.TrimSpace(value)
if value == "" || limit <= 0 {
return ""
}
runes := []rune(value)
if len(runes) <= limit {
return value
}
return string(runes[:limit])
}
func trimOptionalDesktopBugString(value *string, limit int) *string {
if value == nil {
return nil
}
trimmed := limitDesktopBugText(*value, limit)
if trimmed == "" {
return nil
}
return &trimmed
}
func sanitizeDesktopBugFileName(value string) string {
value = strings.TrimSpace(filepath.Base(value))
if value == "" || value == "." || value == "/" || value == "\\" {
return ""
}
value = strings.Map(func(r rune) rune {
if r == '\\' || r == '/' || r == '\r' || r == '\n' {
return -1
}
return r
}, value)
return limitDesktopBugText(value, 200)
}
var desktopBugISODatePattern = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T`)
func ParseDesktopBugOccurredAt(value string) time.Time {
value = strings.TrimSpace(value)
if value == "" || !desktopBugISODatePattern.MatchString(value) {
return time.Time{}
}
parsed, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
return time.Time{}
}
return parsed.UTC()
}
func MaxDesktopBugMultipartBytes() int64 {
return maxDesktopBugMultipartBytes
}
func MaxDesktopBugDumpBytes() int64 {
return maxDesktopBugDumpBytes
}