Files
grafana/pkg/api/frontendlogging/source_maps.go
Emil Tullstedt 656e270bd9 Chore: Upgrade Go to 1.19.2 (#56857)
We also need to upgrade the linter together with the Go version, all the changes should relate to either fixing linting problems or upgrading the Go version used to build Grafana.
2022-10-13 14:53:51 +02:00

168 lines
4.6 KiB
Go

package frontendlogging
import (
"io/ioutil" //nolint:staticcheck // No need to change in v8.
"net/http"
"net/url"
"os"
"path/filepath"
"strings"
"sync"
sourcemap "github.com/go-sourcemap/sourcemap"
"github.com/getsentry/sentry-go"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
)
type sourceMapLocation struct {
dir string
path string
pluginID string
}
type sourceMap struct {
consumer *sourcemap.Consumer
pluginID string
}
type ReadSourceMapFn func(dir string, path string) ([]byte, error)
func ReadSourceMapFromFS(dir string, path string) ([]byte, error) {
file, err := http.Dir(dir).Open(path)
if err != nil {
return nil, err
}
defer func() {
if err := file.Close(); err != nil {
logger.Error("Failed to close source map file", "err", err)
}
}()
return ioutil.ReadAll(file)
}
type SourceMapStore struct {
sync.Mutex
cache map[string]*sourceMap
cfg *setting.Cfg
readSourceMap ReadSourceMapFn
routeResolver plugins.StaticRouteResolver
}
func NewSourceMapStore(cfg *setting.Cfg, routeResolver plugins.StaticRouteResolver, readSourceMap ReadSourceMapFn) *SourceMapStore {
return &SourceMapStore{
cache: make(map[string]*sourceMap),
cfg: cfg,
routeResolver: routeResolver,
readSourceMap: readSourceMap,
}
}
/* guessSourceMapLocation will attempt to guess location of a source map on fs.
* it does not read the source file or make any web requests,
* just assumes that a [source filename].map file might exist in the same dir as the source file
* and only considers sources coming from grafana core or plugins`
*/
func (store *SourceMapStore) guessSourceMapLocation(sourceURL string) (*sourceMapLocation, error) {
u, err := url.Parse(sourceURL)
if err != nil {
return nil, err
}
// determine if source comes from grafana core, locally or CDN, look in public build dir on fs
if strings.HasPrefix(u.Path, "/public/build/") || (store.cfg.CDNRootURL != nil &&
strings.HasPrefix(sourceURL, store.cfg.CDNRootURL.String()) && strings.Contains(u.Path, "/public/build/")) {
pathParts := strings.SplitN(u.Path, "/public/build/", 2)
if len(pathParts) == 2 {
return &sourceMapLocation{
dir: store.cfg.StaticRootPath,
path: filepath.Join("build", pathParts[1]+".map"),
pluginID: "",
}, nil
}
// if source comes from a plugin, look in plugin dir
} else if strings.HasPrefix(u.Path, "/public/plugins/") {
for _, route := range store.routeResolver.Routes() {
pluginPrefix := filepath.Join("/public/plugins/", route.PluginID)
if strings.HasPrefix(u.Path, pluginPrefix) {
return &sourceMapLocation{
dir: route.Directory,
path: u.Path[len(pluginPrefix):] + ".map",
pluginID: route.PluginID,
}, nil
}
}
}
return nil, nil
}
func (store *SourceMapStore) getSourceMap(sourceURL string) (*sourceMap, error) {
store.Lock()
defer store.Unlock()
if smap, ok := store.cache[sourceURL]; ok {
return smap, nil
}
sourceMapLocation, err := store.guessSourceMapLocation(sourceURL)
if err != nil {
return nil, err
}
if sourceMapLocation == nil {
// Cache nil value for sourceURL, since we want to flag that we couldn't guess the map location and not try again
store.cache[sourceURL] = nil
return nil, nil
}
path := strings.ReplaceAll(sourceMapLocation.path, "../", "") // just in case
b, err := store.readSourceMap(sourceMapLocation.dir, path)
if err != nil {
if os.IsNotExist(err) {
// Cache nil value for sourceURL, since we want to flag that it wasn't found in the filesystem and not try again
store.cache[sourceURL] = nil
return nil, nil
}
return nil, err
}
consumer, err := sourcemap.Parse(sourceURL+".map", b)
if err != nil {
return nil, err
}
smap := &sourceMap{
consumer: consumer,
pluginID: sourceMapLocation.pluginID,
}
store.cache[sourceURL] = smap
return smap, nil
}
func (store *SourceMapStore) resolveSourceLocation(frame sentry.Frame) (*sentry.Frame, error) {
smap, err := store.getSourceMap(frame.Filename)
if err != nil {
return nil, err
}
if smap == nil {
return nil, nil
}
file, function, line, col, ok := smap.consumer.Source(frame.Lineno, frame.Colno)
if !ok {
return nil, nil
}
// unfortunately in many cases go-sourcemap fails to determine the original function name.
// not a big issue as long as file, line and column are correct
if len(function) == 0 {
function = "?"
}
module := "core"
if len(smap.pluginID) > 0 {
module = smap.pluginID
}
return &sentry.Frame{
Filename: file,
Lineno: line,
Colno: col,
Function: function,
Module: module,
}, nil
}