From 2e9ad4bf6ff7a27f76687a35e80201d3f56fc9c1 Mon Sep 17 00:00:00 2001 From: Eduard Urbach Date: Mon, 4 Nov 2019 16:34:00 +0900 Subject: [PATCH] Crashes are now saved in the database --- arn/ClientErrorReport.go | 5 ++-- arn/ClientErrorReportAPI.go | 1 + arn/Crash.go | 39 ++++++++++++++++++++++++++ arn/CrashAPI.go | 13 +++++++++ arn/Database.go | 1 + middleware/Recover.go | 22 +++++++++++++-- pages/admin/admin.pixy | 1 + pages/admin/crashes.go | 26 +++++++++++++++++ pages/admin/crashes.pixy | 19 +++++++++++++ pages/database/download.go | 2 ++ pages/index/staffroutes/staffroutes.go | 1 + 11 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 arn/Crash.go create mode 100644 arn/CrashAPI.go create mode 100644 pages/admin/crashes.go create mode 100644 pages/admin/crashes.pixy diff --git a/arn/ClientErrorReport.go b/arn/ClientErrorReport.go index 4c9d6d8a..192242ba 100644 --- a/arn/ClientErrorReport.go +++ b/arn/ClientErrorReport.go @@ -14,7 +14,7 @@ type ClientErrorReport struct { hasCreator } -// StreamClientErrorReports returns a stream of all characters. +// StreamClientErrorReports returns a stream of all client error reports. func StreamClientErrorReports() <-chan *ClientErrorReport { channel := make(chan *ClientErrorReport, nano.ChannelBufferSize) @@ -29,10 +29,9 @@ func StreamClientErrorReports() <-chan *ClientErrorReport { return channel } -// AllClientErrorReports returns a slice of all characters. +// AllClientErrorReports returns a slice of all client error reports. func AllClientErrorReports() []*ClientErrorReport { all := make([]*ClientErrorReport, 0, DB.Collection("ClientErrorReport").Count()) - stream := StreamClientErrorReports() for obj := range stream { diff --git a/arn/ClientErrorReportAPI.go b/arn/ClientErrorReportAPI.go index 146b93c3..73985636 100644 --- a/arn/ClientErrorReportAPI.go +++ b/arn/ClientErrorReportAPI.go @@ -7,6 +7,7 @@ import ( "github.com/aerogo/api" ) +// Force interface implementations var ( _ api.Newable = (*ClientErrorReport)(nil) ) diff --git a/arn/Crash.go b/arn/Crash.go new file mode 100644 index 00000000..83f1f6c2 --- /dev/null +++ b/arn/Crash.go @@ -0,0 +1,39 @@ +package arn + +import "github.com/aerogo/nano" + +// Crash contains data about server crashes. +type Crash struct { + Error string `json:"error"` + Stack string `json:"stack"` + + hasID + hasCreator +} + +// StreamCrashes returns a stream of all crashes. +func StreamCrashes() <-chan *Crash { + channel := make(chan *Crash, nano.ChannelBufferSize) + + go func() { + for obj := range DB.All("Crash") { + channel <- obj.(*Crash) + } + + close(channel) + }() + + return channel +} + +// AllCrashes returns a slice of all crashes. +func AllCrashes() []*Crash { + all := make([]*Crash, 0, DB.Collection("Crash").Count()) + stream := StreamCrashes() + + for obj := range stream { + all = append(all, obj) + } + + return all +} diff --git a/arn/CrashAPI.go b/arn/CrashAPI.go new file mode 100644 index 00000000..faa40e74 --- /dev/null +++ b/arn/CrashAPI.go @@ -0,0 +1,13 @@ +package arn + +import "github.com/aerogo/api" + +// Force interface implementations +var ( + _ api.Savable = (*Crash)(nil) +) + +// Save saves the crash in the database. +func (crash *Crash) Save() { + DB.Set("Crash", crash.ID, crash) +} diff --git a/arn/Database.go b/arn/Database.go index ea1647ef..f01c8b32 100644 --- a/arn/Database.go +++ b/arn/Database.go @@ -25,6 +25,7 @@ var DB = Node.Namespace("arn").RegisterTypes( (*AnimeCharacters)(nil), (*AnimeRelations)(nil), (*AnimeList)(nil), + (*Crash)(nil), (*Character)(nil), (*ClientErrorReport)(nil), (*Company)(nil), diff --git a/middleware/Recover.go b/middleware/Recover.go index 12a25d3b..66c1a726 100644 --- a/middleware/Recover.go +++ b/middleware/Recover.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/arn" ) // Recover recovers from panics and shows them as the response body. @@ -27,11 +28,28 @@ func Recover(next aero.Handler) aero.Handler { } stack := make([]byte, 4096) - length := runtime.Stack(stack, true) + length := runtime.Stack(stack, false) stackString := string(stack[:length]) fmt.Fprint(os.Stderr, stackString) - message := err.Error() + "

" + strings.ReplaceAll(stackString, "\n", "
") + // Save crash in database + crash := &arn.Crash{ + Error: err.Error(), + Stack: stackString, + } + + crash.ID = arn.GenerateID("Crash") + crash.Created = arn.DateTimeUTC() + user := arn.GetUserFromContext(ctx) + + if user != nil { + crash.CreatedBy = user.ID + } + + crash.Save() + + // Send HTML + message := "
" + err.Error() + "

" + strings.ReplaceAll(stackString, "\n", "
") + "
" _ = ctx.Error(http.StatusInternalServerError, message) }() diff --git a/pages/admin/admin.pixy b/pages/admin/admin.pixy index 888a3559..47ed2975 100644 --- a/pages/admin/admin.pixy +++ b/pages/admin/admin.pixy @@ -2,6 +2,7 @@ component AdminTabs .tabs Tab("Server", "server", "/admin") Tab("WebDev", "html5", "/admin/webdev") + Tab("Crashes", "exclamation", "/admin/crashes") Tab("Client errors", "exclamation", "/admin/errors/client") Tab("Registrations", "user-plus", "/admin/registrations") Tab("Purchases", "shopping-cart", "/admin/purchases") diff --git a/pages/admin/crashes.go b/pages/admin/crashes.go new file mode 100644 index 00000000..53d0e279 --- /dev/null +++ b/pages/admin/crashes.go @@ -0,0 +1,26 @@ +package admin + +import ( + "sort" + + "github.com/aerogo/aero" + "github.com/animenotifier/notify.moe/arn" + "github.com/animenotifier/notify.moe/components" +) + +const maxCrashes = 80 + +// Crashes shows client-side errors. +func Crashes(ctx aero.Context) error { + crashes := arn.AllCrashes() + + sort.Slice(crashes, func(i, j int) bool { + return crashes[i].Created > crashes[j].Created + }) + + if len(crashes) > maxCrashes { + crashes = crashes[:maxCrashes] + } + + return ctx.HTML(components.Crashes(crashes)) +} diff --git a/pages/admin/crashes.pixy b/pages/admin/crashes.pixy new file mode 100644 index 00000000..0cefdebf --- /dev/null +++ b/pages/admin/crashes.pixy @@ -0,0 +1,19 @@ +component Crashes(crashes []*arn.Crash) + AdminTabs + h1.mountable Server-side crashes + + table + tbody + each crash in crashes + tr.mountable + td + a(href="/api/crash/" + crash.ID, target="_blank")= crash.Error + td + each line in strings.Split(crash.Stack, "\n") + p= line + td.utc-date(data-date=crash.Created) + td.edit-log-user + if crash.CreatedBy != "" + Avatar(crash.Creator()) + else + span anonymous diff --git a/pages/database/download.go b/pages/database/download.go index 12e8fd70..79f4c14d 100644 --- a/pages/database/download.go +++ b/pages/database/download.go @@ -16,6 +16,8 @@ import ( // privateTypes are types that are not available for download. var privateTypes = []string{ "Analytics", + "Crash", + "ClientErrorReport", "EditLogEntry", "EmailToUser", "FacebookToUser", diff --git a/pages/index/staffroutes/staffroutes.go b/pages/index/staffroutes/staffroutes.go index 4289528b..eae945e8 100644 --- a/pages/index/staffroutes/staffroutes.go +++ b/pages/index/staffroutes/staffroutes.go @@ -77,6 +77,7 @@ func Register(app *aero.Application) { page.Get(app, "/admin", admin.Get) page.Get(app, "/admin/webdev", admin.WebDev) page.Get(app, "/admin/registrations", admin.UserRegistrations) + page.Get(app, "/admin/crashes", admin.Crashes) page.Get(app, "/admin/errors/client", admin.ClientErrors) page.Get(app, "/admin/purchases", admin.PurchaseHistory) page.Get(app, "/admin/payments", admin.PaymentHistory)