返回 DeepSeek-Reasonix
crash_app.go
根目录 / desktop / crash_app.go
1 package main
2
3 import (
4 "bytes"
5 "context"
6 "encoding/json"
7 "fmt"
8 "net/http"
9 "regexp"
10 "runtime"
11 "strings"
12 )
13
14 // crash_app.go is the crash/feedback/performance reporting surface. Frontend
15 // reports are sent on an explicit user click. Native fatal/lifecycle reports are
16 // queued locally and sent on a later launch only when desktop telemetry is on.
17
18 var crashEndpoint = "https://crash.reasonix.io/v1/report"
19
20 const maxCrashDetailBytes = 16 << 10
21 const maxCrashStackBytes = 8 << 10
22 const maxCrashFieldBytes = 4 << 10
23 const maxCrashBreadcrumbs = 30
24
25 var (
26 userPathSegment = regexp.MustCompile(`(?i)([A-Z]:\\Users\\|/(?:home|Users)/)[^/\\:\s"']+`)
27 emailPattern = regexp.MustCompile(`\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b`)
28 secretKeyValuePattern = regexp.MustCompile(`(?i)\b(api[_-]?key|access[_-]?token|refresh[_-]?token|id[_-]?token|authorization|secret|password|passwd|pwd|token)\b\s*[:=]\s*(?:Bearer\s+)?['"]?[^'"\s,;]+['"]?`)
29 bearerTokenPattern = regexp.MustCompile(`(?i)\bBearer\s+[A-Za-z0-9._~+/=-]{16,}`)
30 explicitKeyPattern = regexp.MustCompile(`\b(?:sk|rk)-(?:proj-)?[A-Za-z0-9_-]{16,}\b`)
31 envIdentifierPattern = regexp.MustCompile(`\b[A-Z][A-Z0-9_]*(?:API[_-]?KEY|ACCESS[_-]?KEY|PRIVATE[_-]?KEY|SECRET|TOKEN|PASSWORD|PASSWD|PWD)[A-Z0-9_]*\b`)
32 jwtPattern = regexp.MustCompile(`\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b`)
33 longHexPattern = regexp.MustCompile(`\b[0-9a-fA-F]{32,}\b`)
34 longBase64Pattern = regexp.MustCompile(`[A-Za-z0-9+/]{40,}={0,2}`)
35 longBase64URLPattern = regexp.MustCompile(`\b[A-Za-z0-9_-]{48,}\b`)
36 )
37
38 func scrubUserPaths(s string) string {
39 return userPathSegment.ReplaceAllString(s, "${1}_")
40 }
41
42 func scrubSensitiveText(s string) string {
43 s = scrubUserPaths(s)
44 s = emailPattern.ReplaceAllString(s, "[redacted-email]")
45 s = bearerTokenPattern.ReplaceAllString(s, "Bearer [redacted]")
46 s = secretKeyValuePattern.ReplaceAllString(s, "${1}=[redacted]")
47 s = envIdentifierPattern.ReplaceAllString(s, "[redacted-env]")
48 s = jwtPattern.ReplaceAllString(s, "[redacted-jwt]")
49 s = explicitKeyPattern.ReplaceAllString(s, "[redacted-key]")
50 s = longHexPattern.ReplaceAllString(s, "[redacted-hex]")
51 s = longBase64Pattern.ReplaceAllString(s, "[redacted-token]")
52 s = longBase64URLPattern.ReplaceAllString(s, "[redacted-token]")
53 return s
54 }
55
56 type crashBreadcrumb struct {
57 T int64 `json:"t,omitempty"`
58 Cat string `json:"cat,omitempty"`
59 Msg string `json:"msg,omitempty"`
60 }
61
62 type crashReport struct {
63 Kind string `json:"kind"`
64 Version string `json:"version"`
65 OS string `json:"os"`
66 Arch string `json:"arch"`
67 Message string `json:"message"`
68 Device deviceInfo `json:"device"`
69 SchemaVersion int `json:"schemaVersion,omitempty"`
70 Source string `json:"source,omitempty"`
71 Label string `json:"label,omitempty"`
72 ErrorType string `json:"errorType,omitempty"`
73 ErrorMessage string `json:"errorMessage,omitempty"`
74 Stack string `json:"stack,omitempty"`
75 ComponentStack string `json:"componentStack,omitempty"`
76 TopFrame string `json:"topFrame,omitempty"`
77 FingerprintHint string `json:"fingerprintHint,omitempty"`
78 BuildCommit string `json:"buildCommit,omitempty"`
79 Channel string `json:"channel,omitempty"`
80 Language string `json:"language,omitempty"`
81 View string `json:"view,omitempty"`
82 Breadcrumbs []crashBreadcrumb `json:"breadcrumbs,omitempty"`
83 OccurredAt string `json:"occurredAt,omitempty"`
84 }
85
86 type frontendCrashPayload struct {
87 SchemaVersion int `json:"schemaVersion"`
88 Kind string `json:"kind"`
89 Source string `json:"source"`
90 Label string `json:"label"`
91 Message string `json:"message"`
92 ErrorType string `json:"errorType"`
93 ErrorMessage string `json:"errorMessage"`
94 Stack string `json:"stack"`
95 ComponentStack string `json:"componentStack"`
96 TopFrame string `json:"topFrame"`
97 FingerprintHint string `json:"fingerprintHint"`
98 BuildCommit string `json:"buildCommit"`
99 Channel string `json:"channel"`
100 Language string `json:"language"`
101 View string `json:"view"`
102 Breadcrumbs []crashBreadcrumb `json:"breadcrumbs"`
103 OccurredAt string `json:"occurredAt"`
104 }
105
106 func normalizeReportKind(kind string) (string, bool) {
107 switch strings.TrimSpace(kind) {
108 case "crash", "exception", "feedback", "performance", "bot":
109 return strings.TrimSpace(kind), true
110 default:
111 return "", false
112 }
113 }
114
115 func clipCrashField(s string, max int) string {
116 if len(s) > max {
117 return s[:max]
118 }
119 return s
120 }
121
122 func sanitizeCrashField(s string, max int) string {
123 return clipCrashField(scrubUserPaths(strings.TrimSpace(s)), max)
124 }
125
126 func sanitizeCrashText(s string, max int) string {
127 return clipCrashField(strings.TrimSpace(scrubSensitiveText(s)), max)
128 }
129
130 func sanitizeBreadcrumbs(in []crashBreadcrumb) []crashBreadcrumb {
131 if len(in) > maxCrashBreadcrumbs {
132 in = in[len(in)-maxCrashBreadcrumbs:]
133 }
134 out := make([]crashBreadcrumb, 0, len(in))
135 for _, b := range in {
136 cat := sanitizeCrashField(b.Cat, 64)
137 msg := sanitizeCrashText(b.Msg, 240)
138 if cat == "" && msg == "" {
139 continue
140 }
141 out = append(out, crashBreadcrumb{T: b.T, Cat: cat, Msg: msg})
142 }
143 return out
144 }
145
146 func baseCrashReport(kind string) crashReport {
147 return crashReport{
148 Kind: kind,
149 Version: version,
150 OS: runtime.GOOS,
151 Arch: runtime.GOARCH,
152 Device: collectDeviceInfo(),
153 Channel: channel,
154 }
155 }
156
157 func topFrameFromStack(stack string) string {
158 for _, line := range strings.Split(stack, "\n") {
159 line = strings.TrimSpace(line)
160 if line == "" {
161 continue
162 }
163 if strings.Contains(line, ".go:") || strings.Contains(line, ".ts:") || strings.Contains(line, ".tsx:") || strings.Contains(line, ".js:") || strings.Contains(line, ".jsx:") {
164 if strings.Contains(line, "/runtime/") || strings.Contains(line, `\runtime\`) || strings.Contains(line, "crash_pending.go") {
165 continue
166 }
167 return sanitizeCrashText(line, 300)
168 }
169 }
170 return ""
171 }
172
173 func nativeResourceContext() string {
174 var m runtime.MemStats
175 runtime.ReadMemStats(&m)
176 mb := func(n uint64) string {
177 return fmt.Sprintf("%.1f MB", float64(n)/1024/1024)
178 }
179 return strings.Join([]string{
180 "go heap alloc: " + mb(m.Alloc),
181 "go heap sys: " + mb(m.HeapSys),
182 "go total sys: " + mb(m.Sys),
183 fmt.Sprintf("goroutines: %d", runtime.NumGoroutine()),
184 fmt.Sprintf("gc cycles: %d", m.NumGC),
185 }, "\n")
186 }
187
188 func appendNativeResourceContext(kind, message string) string {
189 if kind != "performance" {
190 return message
191 }
192 return sanitizeCrashText(message+"\n\n--- native runtime context ---\n"+nativeResourceContext(), maxCrashDetailBytes)
193 }
194
195 func crashReportFromDetail(kind, detail string) (crashReport, error) {
196 rawKind := kind
197 kind, ok := normalizeReportKind(kind)
198 if !ok {
199 return crashReport{}, fmt.Errorf("unknown report kind %q", rawKind)
200 }
201 if strings.TrimSpace(detail) == "" {
202 return crashReport{}, fmt.Errorf("empty report")
203 }
204 r := baseCrashReport(kind)
205
206 var payload frontendCrashPayload
207 if json.Unmarshal([]byte(detail), &payload) == nil && payload.SchemaVersion == 2 {
208 if payloadKind, ok := normalizeReportKind(payload.Kind); ok {
209 r.Kind = payloadKind
210 }
211 r.SchemaVersion = payload.SchemaVersion
212 r.Source = sanitizeCrashField(payload.Source, 32)
213 r.Label = sanitizeCrashField(payload.Label, 64)
214 r.ErrorType = sanitizeCrashField(payload.ErrorType, 128)
215 r.ErrorMessage = sanitizeCrashText(payload.ErrorMessage, maxCrashFieldBytes)
216 r.Stack = sanitizeCrashText(payload.Stack, maxCrashStackBytes)
217 r.ComponentStack = sanitizeCrashText(payload.ComponentStack, maxCrashStackBytes)
218 r.TopFrame = sanitizeCrashText(payload.TopFrame, 300)
219 r.FingerprintHint = sanitizeCrashText(payload.FingerprintHint, 300)
220 r.BuildCommit = sanitizeCrashField(payload.BuildCommit, 64)
221 r.Channel = sanitizeCrashField(payload.Channel, 32)
222 r.Language = sanitizeCrashField(payload.Language, 64)
223 r.View = sanitizeCrashText(payload.View, 200)
224 r.Breadcrumbs = sanitizeBreadcrumbs(payload.Breadcrumbs)
225 r.OccurredAt = sanitizeCrashField(payload.OccurredAt, 64)
226 r.Message = sanitizeCrashText(payload.Message, maxCrashDetailBytes)
227 if r.TopFrame == "" {
228 r.TopFrame = topFrameFromStack(r.Stack)
229 }
230 if r.Message == "" {
231 r.Message = sanitizeCrashText(fmt.Sprintf("[%s]\n\n%s", r.Label, r.ErrorMessage), maxCrashDetailBytes)
232 }
233 if r.Source == "" {
234 r.Source = "frontend"
235 }
236 r.Message = appendNativeResourceContext(r.Kind, r.Message)
237 return r, nil
238 }
239
240 r.SchemaVersion = 1
241 r.Source = "legacy"
242 r.Label = kind
243 r.Message = sanitizeCrashText(detail, maxCrashDetailBytes)
244 r.Message = appendNativeResourceContext(r.Kind, r.Message)
245 return r, nil
246 }
247
248 func (a *App) ReportCrash(kind, detail string) error {
249 r, err := crashReportFromDetail(kind, detail)
250 if err != nil {
251 return err
252 }
253 c, err := httpClient()
254 if err != nil {
255 return err
256 }
257 return postCrashReport(a.reqCtx(), c, crashEndpoint, r)
258 }
259
260 func postCrashReport(ctx context.Context, c *http.Client, endpoint string, r crashReport) error {
261 body, err := json.Marshal(r)
262 if err != nil {
263 return err
264 }
265 req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body))
266 if err != nil {
267 return err
268 }
269 req.Header.Set("Content-Type", "application/json")
270 resp, err := c.Do(req)
271 if err != nil {
272 return err
273 }
274 defer resp.Body.Close()
275 if resp.StatusCode >= 300 {
276 return fmt.Errorf("crash endpoint returned %s", resp.Status)
277 }
278 return nil
279 }
280
280 lines GO