| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | package frontendlogging | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | import ( | 
					
						
							|  |  |  | 	"io/ioutil" | 
					
						
							|  |  |  | 	"net/http" | 
					
						
							|  |  |  | 	"net/url" | 
					
						
							|  |  |  | 	"os" | 
					
						
							|  |  |  | 	"path/filepath" | 
					
						
							|  |  |  | 	"strings" | 
					
						
							|  |  |  | 	"sync" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	sourcemap "github.com/go-sourcemap/sourcemap" | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	"github.com/getsentry/sentry-go" | 
					
						
							| 
									
										
										
										
											2021-03-18 20:53:01 +08:00
										 |  |  | 	"github.com/grafana/grafana/pkg/plugins" | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | 	"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 { | 
					
						
							| 
									
										
										
										
											2021-03-17 23:06:10 +08:00
										 |  |  | 	sync.Mutex | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | 	cache         map[string]*sourceMap | 
					
						
							|  |  |  | 	cfg           *setting.Cfg | 
					
						
							|  |  |  | 	readSourceMap ReadSourceMapFn | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | 	routeResolver plugins.StaticRouteResolver | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | func NewSourceMapStore(cfg *setting.Cfg, routeResolver plugins.StaticRouteResolver, readSourceMap ReadSourceMapFn) *SourceMapStore { | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | 	return &SourceMapStore{ | 
					
						
							|  |  |  | 		cache:         make(map[string]*sourceMap), | 
					
						
							|  |  |  | 		cfg:           cfg, | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | 		routeResolver: routeResolver, | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | 		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 | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-02-11 17:00:55 +08:00
										 |  |  | 	// determine if source comes from grafana core, locally or CDN, look in public build dir on fs
 | 
					
						
							| 
									
										
										
										
											2021-03-18 20:53:01 +08:00
										 |  |  | 	if strings.HasPrefix(u.Path, "/public/build/") || (store.cfg.CDNRootURL != nil && | 
					
						
							|  |  |  | 		strings.HasPrefix(sourceURL, store.cfg.CDNRootURL.String()) && strings.Contains(u.Path, "/public/build/")) { | 
					
						
							| 
									
										
										
										
											2021-02-11 17:00:55 +08:00
										 |  |  | 		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 | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | 		// if source comes from a plugin, look in plugin dir
 | 
					
						
							|  |  |  | 	} else if strings.HasPrefix(u.Path, "/public/plugins/") { | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | 		for _, route := range store.routeResolver.Routes() { | 
					
						
							|  |  |  | 			pluginPrefix := filepath.Join("/public/plugins/", route.PluginID) | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | 			if strings.HasPrefix(u.Path, pluginPrefix) { | 
					
						
							|  |  |  | 				return &sourceMapLocation{ | 
					
						
							|  |  |  | 					dir:      route.Directory, | 
					
						
							|  |  |  | 					path:     u.Path[len(pluginPrefix):] + ".map", | 
					
						
							| 
									
										
										
										
											2021-11-01 17:53:33 +08:00
										 |  |  | 					pluginID: route.PluginID, | 
					
						
							| 
									
										
										
										
											2021-02-04 14:37:28 +08:00
										 |  |  | 				}, 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 | 
					
						
							|  |  |  | } |