feat: move sentry configuration from frontend to api
This commit is contained in:
parent
1899f16207
commit
a0e770438d
@ -40,8 +40,6 @@ service:
|
||||
enabletaskcomments: true
|
||||
# Whether totp is enabled. In most cases you want to leave that enabled.
|
||||
enabletotp: true
|
||||
# If not empty, enables logging of crashes and unhandled errors in sentry.
|
||||
sentrydsn: ''
|
||||
# If not empty, this will enable `/test/{table}` endpoints which allow to put any content in the database.
|
||||
# Used to reset the db before frontend tests. Because this is quite a dangerous feature allowing for lots of harm,
|
||||
# each request made to this endpoint needs to provide an `Authorization: <token>` header with the token from below. <br/>
|
||||
@ -61,6 +59,18 @@ service:
|
||||
# You probably don't need to set this value, it was created specifically for usage on [try](https://try.vikunja.io).
|
||||
demomode: false
|
||||
|
||||
sentry:
|
||||
# If set to true, enables anonymous error tracking of api errors via Sentry. This allows us to gather more
|
||||
# information about errors in order to debug and fix it.
|
||||
enabled: false
|
||||
# Configure the Sentry dsn used for api error tracking. Only used when Sentry is enabled for the api.
|
||||
dsn: "https://440eedc957d545a795c17bbaf477497c@o1047380.ingest.sentry.io/4504254983634944"
|
||||
# If set to true, enables anonymous error tracking of frontend errors via Sentry. This allows us to gather more
|
||||
# information about errors in order to debug and fix it.
|
||||
frontendenabled: false
|
||||
# Configure the Sentry dsn used for frontend error tracking. Only used when Sentry is enabled for the frontend.
|
||||
frontenddsn: "https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480"
|
||||
|
||||
database:
|
||||
# Database type to use. Supported types are mysql, postgres and sqlite.
|
||||
type: "sqlite"
|
||||
|
@ -23,10 +23,6 @@
|
||||
// It has to be the full url, including the last /api/v1 part and port.
|
||||
// You can change this if your api is not reachable on the same port as the frontend.
|
||||
window.API_URL = 'http://localhost:3456/api/v1'
|
||||
// Enable error tracking with sentry. If this is set to true, will send anonymized data to
|
||||
// our sentry instance to notify us of potential problems.
|
||||
window.SENTRY_ENABLED = false
|
||||
window.SENTRY_DSN = 'https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480'
|
||||
// If enabled, allows the user to nest projects infinitely, instead of the default 2 levels.
|
||||
// This setting might change in the future or be removed completely.
|
||||
window.PROJECT_INFINITE_NESTING_ENABLED = false
|
||||
|
@ -18,8 +18,8 @@ import {getBrowserLanguage, i18n, setLanguage} from './i18n'
|
||||
declare global {
|
||||
interface Window {
|
||||
API_URL: string;
|
||||
SENTRY_ENABLED: boolean;
|
||||
SENTRY_DSN: string;
|
||||
SENTRY_ENABLED?: boolean;
|
||||
SENTRY_DSN?: string;
|
||||
PROJECT_INFINITE_NESTING_ENABLED: boolean;
|
||||
ALLOW_ICON_CHANGES: boolean;
|
||||
CUSTOM_LOGO_URL?: string;
|
||||
|
@ -8,7 +8,7 @@ export default async function setupSentry(app: App, router: Router) {
|
||||
|
||||
Sentry.init({
|
||||
app,
|
||||
dsn: window.SENTRY_DSN,
|
||||
dsn: window.SENTRY_DSN ?? '',
|
||||
release: import.meta.env.VITE_PLUGIN_SENTRY_CONFIG.release,
|
||||
dist: import.meta.env.VITE_PLUGIN_SENTRY_CONFIG.dist,
|
||||
integrations: [
|
||||
|
@ -58,12 +58,16 @@ const (
|
||||
ServiceTimeZone Key = `service.timezone`
|
||||
ServiceEnableTaskComments Key = `service.enabletaskcomments`
|
||||
ServiceEnableTotp Key = `service.enabletotp`
|
||||
ServiceSentryDsn Key = `service.sentrydsn`
|
||||
ServiceTestingtoken Key = `service.testingtoken`
|
||||
ServiceEnableEmailReminders Key = `service.enableemailreminders`
|
||||
ServiceEnableUserDeletion Key = `service.enableuserdeletion`
|
||||
ServiceMaxAvatarSize Key = `service.maxavatarsize`
|
||||
|
||||
SentryEnabled Key = `sentry.enabled`
|
||||
SentryDsn Key = `sentry.dsn`
|
||||
SentryFrontendEnabled Key = `sentry.frontendenabled`
|
||||
SentryFrontendDsn Key = `sentry.frontenddsn`
|
||||
|
||||
AuthLocalEnabled Key = `auth.local.enabled`
|
||||
AuthOpenIDEnabled Key = `auth.openid.enabled`
|
||||
AuthOpenIDProviders Key = `auth.openid.providers`
|
||||
@ -306,6 +310,10 @@ func InitDefaultConfig() {
|
||||
ServiceMaxAvatarSize.setDefault(1024)
|
||||
ServiceDemoMode.setDefault(false)
|
||||
|
||||
// Sentry
|
||||
SentryDsn.setDefault("https://440eedc957d545a795c17bbaf477497c@o1047380.ingest.sentry.io/4504254983634944")
|
||||
SentryFrontendDsn.setDefault("https://85694a2d757547cbbc90cd4b55c5a18d@o1047380.ingest.sentry.io/6024480")
|
||||
|
||||
// Auth
|
||||
AuthLocalEnabled.setDefault(true)
|
||||
AuthOpenIDEnabled.setDefault(false)
|
||||
|
@ -114,39 +114,7 @@ func NewEcho() *echo.Echo {
|
||||
// panic recover
|
||||
e.Use(middleware.Recover())
|
||||
|
||||
if config.ServiceSentryDsn.GetString() != "" {
|
||||
if err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: config.ServiceSentryDsn.GetString(),
|
||||
AttachStacktrace: true,
|
||||
Release: version.Version,
|
||||
}); err != nil {
|
||||
log.Criticalf("Sentry init failed: %s", err)
|
||||
}
|
||||
defer sentry.Flush(5 * time.Second)
|
||||
|
||||
e.Use(sentryecho.New(sentryecho.Options{
|
||||
Repanic: true,
|
||||
}))
|
||||
|
||||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||
// Only capture errors not already handled by echo
|
||||
var herr *echo.HTTPError
|
||||
if errors.As(err, &herr) && herr.Code > 403 {
|
||||
hub := sentryecho.GetHubFromContext(c)
|
||||
if hub != nil {
|
||||
hub.WithScope(func(scope *sentry.Scope) {
|
||||
scope.SetExtra("url", c.Request().URL)
|
||||
hub.CaptureException(err)
|
||||
})
|
||||
} else {
|
||||
sentry.CaptureException(err)
|
||||
log.Debugf("Could not add context for sending error '%s' to sentry", err.Error())
|
||||
}
|
||||
log.Debugf("Error '%s' sent to sentry", err.Error())
|
||||
}
|
||||
e.DefaultHTTPErrorHandler(err, c)
|
||||
}
|
||||
}
|
||||
setupSentry(e)
|
||||
|
||||
// Validation
|
||||
e.Validator = &CustomValidator{}
|
||||
@ -162,6 +130,44 @@ func NewEcho() *echo.Echo {
|
||||
return e
|
||||
}
|
||||
|
||||
func setupSentry(e *echo.Echo) {
|
||||
if !config.SentryEnabled.GetBool() {
|
||||
return
|
||||
}
|
||||
|
||||
if err := sentry.Init(sentry.ClientOptions{
|
||||
Dsn: config.SentryDsn.GetString(),
|
||||
AttachStacktrace: true,
|
||||
Release: version.Version,
|
||||
}); err != nil {
|
||||
log.Criticalf("Sentry init failed: %s", err)
|
||||
}
|
||||
defer sentry.Flush(5 * time.Second)
|
||||
|
||||
e.Use(sentryecho.New(sentryecho.Options{
|
||||
Repanic: true,
|
||||
}))
|
||||
|
||||
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||
// Only capture errors not already handled by echo
|
||||
var herr *echo.HTTPError
|
||||
if errors.As(err, &herr) && herr.Code > 403 {
|
||||
hub := sentryecho.GetHubFromContext(c)
|
||||
if hub != nil {
|
||||
hub.WithScope(func(scope *sentry.Scope) {
|
||||
scope.SetExtra("url", c.Request().URL)
|
||||
hub.CaptureException(err)
|
||||
})
|
||||
} else {
|
||||
sentry.CaptureException(err)
|
||||
log.Debugf("Could not add context for sending error '%s' to sentry", err.Error())
|
||||
}
|
||||
log.Debugf("Error '%s' sent to sentry", err.Error())
|
||||
}
|
||||
e.DefaultHTTPErrorHandler(err, c)
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterRoutes registers all routes for the application
|
||||
func RegisterRoutes(e *echo.Echo) {
|
||||
|
||||
|
@ -2,6 +2,7 @@ package routes
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"code.vikunja.io/api/pkg/config"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@ -13,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"text/template"
|
||||
|
||||
"code.vikunja.io/api/frontend"
|
||||
|
||||
@ -22,20 +24,88 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
indexFile = `index.html`
|
||||
rootPath = `dist/`
|
||||
cacheControlMax = `max-age=315360000, public, max-age=31536000, s-maxage=31536000, immutable`
|
||||
cacheControlNone = `public, max-age=0, s-maxage=0, must-revalidate`
|
||||
indexFile = `index.html`
|
||||
rootPath = `dist/`
|
||||
cacheControlMax = `max-age=315360000, public, max-age=31536000, s-maxage=31536000, immutable`
|
||||
cacheControlNone = `public, max-age=0, s-maxage=0, must-revalidate`
|
||||
configScriptTagTemplate = `
|
||||
<script>
|
||||
window.SENTRY_ENABLED = {{ .SENTRY_ENABLED }}
|
||||
window.SENTRY_DSN = '{{ .SENTRY_DSN }}'
|
||||
window.ALLOW_ICON_CHANGES = {{ .ALLOW_ICON_CHANGES }}
|
||||
window.CUSTOM_LOGO_URL = '{{ .CUSTOM_LOGO_URL }}'
|
||||
</script>`
|
||||
)
|
||||
|
||||
// Because the files are embedded into the final binary, we can be absolutely sure the etag will never change
|
||||
// and we can cache its generation pretty heavily.
|
||||
var etagCache map[string]string
|
||||
var etagLock sync.Mutex
|
||||
var scriptConfigString string
|
||||
var scriptConfigStringLock sync.Mutex
|
||||
|
||||
func init() {
|
||||
etagCache = make(map[string]string)
|
||||
etagLock = sync.Mutex{}
|
||||
scriptConfigStringLock = sync.Mutex{}
|
||||
}
|
||||
|
||||
func serveIndexFile(c echo.Context, assetFs http.FileSystem) (err error) {
|
||||
index, err := assetFs.Open(path.Join(rootPath, indexFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer index.Close()
|
||||
|
||||
if scriptConfigString == "" {
|
||||
|
||||
scriptConfigStringLock.Lock()
|
||||
defer scriptConfigStringLock.Unlock()
|
||||
|
||||
// replace config variables
|
||||
tmpl, err := template.New("config").Parse(configScriptTagTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var tplOutput bytes.Buffer
|
||||
data := make(map[string]string)
|
||||
|
||||
data["SENTRY_ENABLED"] = "false"
|
||||
if config.SentryFrontendEnabled.GetBool() {
|
||||
data["SENTRY_ENABLED"] = "true"
|
||||
}
|
||||
data["SENTRY_DSN"] = config.SentryFrontendDsn.GetString()
|
||||
data["ALLOW_ICON_CHANGES"] = "true" // TODO
|
||||
data["CUSTOM_LOGO_URL"] = "" // TODO
|
||||
|
||||
err = tmpl.Execute(&tplOutput, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
scriptConfig := tplOutput.String()
|
||||
|
||||
buf := bytes.Buffer{}
|
||||
_, err = buf.ReadFrom(index)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scriptConfigString = strings.ReplaceAll(buf.String(), `<div id="app"></div>`, `<div id="app"></div>`+scriptConfig)
|
||||
}
|
||||
|
||||
reader := strings.NewReader(scriptConfigString)
|
||||
|
||||
info, err := index.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
etag, err := generateEtag(index, info.Name())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return serveFile(c, reader, info, etag)
|
||||
}
|
||||
|
||||
// Copied from echo's middleware.StaticWithConfig simplified and adjusted for caching
|
||||
@ -71,10 +141,8 @@ func static() echo.MiddlewareFunc {
|
||||
return err
|
||||
}
|
||||
|
||||
file, err = assetFs.Open(path.Join(rootPath, indexFile))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Handle all other requests with the index file
|
||||
return serveIndexFile(c, assetFs)
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
@ -85,24 +153,7 @@ func static() echo.MiddlewareFunc {
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
index, err := assetFs.Open(path.Join(name, indexFile))
|
||||
if err != nil {
|
||||
return next(c)
|
||||
}
|
||||
|
||||
defer index.Close()
|
||||
|
||||
info, err = index.Stat()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
etag, err := generateEtag(index, name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return serveFile(c, index, info, etag)
|
||||
return serveIndexFile(c, assetFs)
|
||||
}
|
||||
|
||||
etag, err := generateEtag(file, name)
|
||||
@ -133,7 +184,7 @@ func generateEtag(file http.File, name string) (etag string, err error) {
|
||||
}
|
||||
|
||||
// copied from http.serveContent
|
||||
func getMimeType(name string, file http.File) (mineType string, err error) {
|
||||
func getMimeType(name string, file io.ReadSeeker) (mineType string, err error) {
|
||||
mineType = mime.TypeByExtension(filepath.Ext(name))
|
||||
if mineType == "" {
|
||||
// read a chunk to decide between utf-8 text and binary
|
||||
@ -149,7 +200,7 @@ func getMimeType(name string, file http.File) (mineType string, err error) {
|
||||
return mineType, nil
|
||||
}
|
||||
|
||||
func getCacheControlHeader(info os.FileInfo, file http.File) (header string, err error) {
|
||||
func getCacheControlHeader(info os.FileInfo, file io.ReadSeeker) (header string, err error) {
|
||||
// Don't cache service worker and related files
|
||||
if info.Name() == "robots.txt" ||
|
||||
info.Name() == "sw.js" ||
|
||||
@ -187,7 +238,7 @@ func getCacheControlHeader(info os.FileInfo, file http.File) (header string, err
|
||||
return cacheControlNone, nil
|
||||
}
|
||||
|
||||
func serveFile(c echo.Context, file http.File, info os.FileInfo, etag string) error {
|
||||
func serveFile(c echo.Context, file io.ReadSeeker, info os.FileInfo, etag string) error {
|
||||
|
||||
c.Response().Header().Set("Server", "Vikunja")
|
||||
c.Response().Header().Set("Vary", "Accept-Encoding")
|
||||
|
Loading…
x
Reference in New Issue
Block a user