2020-11-12 05:29:43 -06:00
package api
import (
2021-02-04 00:37:28 -06:00
"errors"
2022-09-12 05:04:43 -05:00
"net/http"
2021-02-11 03:00:55 -06:00
"net/url"
2021-02-04 00:37:28 -06:00
"os"
"strings"
2020-11-12 05:29:43 -06:00
"testing"
"time"
2022-01-06 08:28:05 -06:00
"github.com/go-kit/log"
2022-07-08 09:56:30 -05:00
"github.com/go-kit/log/level"
2023-01-30 02:18:26 -06:00
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2021-02-04 00:37:28 -06:00
"github.com/grafana/grafana/pkg/api/frontendlogging"
2021-01-15 07:43:20 -06:00
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/api/routing"
2021-02-04 00:37:28 -06:00
"github.com/grafana/grafana/pkg/plugins"
2023-01-27 01:50:36 -06:00
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
2021-02-04 00:37:28 -06:00
"github.com/grafana/grafana/pkg/setting"
2020-11-12 05:29:43 -06:00
)
2021-02-04 00:37:28 -06:00
type SourceMapReadRecord struct {
dir string
path string
}
2023-08-30 10:46:47 -05:00
type logScenarioFunc func ( c * scenarioContext , logs map [ string ] any , sourceMapReads [ ] SourceMapReadRecord )
2020-11-12 05:29:43 -06:00
2022-06-28 02:25:30 -05:00
func logGrafanaJavascriptAgentEventScenario ( t * testing . T , desc string , event frontendlogging . FrontendGrafanaJavascriptAgentEvent , fn logScenarioFunc ) {
t . Run ( desc , func ( t * testing . T ) {
2023-08-30 10:46:47 -05:00
var logcontent = make ( map [ string ] any )
2022-06-28 02:25:30 -05:00
logcontent [ "logger" ] = "frontend"
2023-08-30 10:46:47 -05:00
newfrontendLogger := log . Logger ( log . LoggerFunc ( func ( keyvals ... any ) error {
2022-06-28 02:25:30 -05:00
for i := 0 ; i < len ( keyvals ) ; i += 2 {
logcontent [ keyvals [ i ] . ( string ) ] = keyvals [ i + 1 ]
}
return nil
} ) )
origHandler := frontendLogger . GetLogger ( )
frontendLogger . Swap ( level . NewFilter ( newfrontendLogger , level . AllowInfo ( ) ) )
sourceMapReads := [ ] SourceMapReadRecord { }
t . Cleanup ( func ( ) {
frontendLogger . Swap ( origHandler )
} )
sc := setupScenarioContext ( t , "/log-grafana-javascript-agent" )
cdnRootURL , e := url . Parse ( "https://storage.googleapis.com/grafana-static-assets" )
require . NoError ( t , e )
cfg := & setting . Cfg {
StaticRootPath : "/staticroot" ,
CDNRootURL : cdnRootURL ,
}
readSourceMap := func ( dir string , path string ) ( [ ] byte , error ) {
sourceMapReads = append ( sourceMapReads , SourceMapReadRecord {
dir : dir ,
path : path ,
} )
if strings . Contains ( path , "error" ) {
return nil , errors . New ( "epic hard drive failure" )
}
if strings . HasSuffix ( path , "foo.js.map" ) {
2022-08-10 08:37:51 -05:00
f , err := os . ReadFile ( "./frontendlogging/test-data/foo.js.map" )
2022-06-28 02:25:30 -05:00
require . NoError ( t , err )
return f , nil
}
return nil , os . ErrNotExist
}
// fake plugin route so we will try to find a source map there
pm := fakePluginStaticRouteResolver {
routes : [ ] * plugins . StaticRoute {
{
Directory : "/usr/local/telepathic-panel" ,
PluginID : "telepathic" ,
} ,
} ,
}
sourceMapStore := frontendlogging . NewSourceMapStore ( cfg , & pm , readSourceMap )
loggingHandler := GrafanaJavascriptAgentLogMessageHandler ( sourceMapStore )
2023-01-27 01:50:36 -06:00
handler := routing . Wrap ( func ( c * contextmodel . ReqContext ) response . Response {
2022-06-28 02:25:30 -05:00
sc . context = c
c . Req . Body = mockRequestBody ( event )
c . Req . Header . Add ( "Content-Type" , "application/json" )
2022-09-12 05:04:43 -05:00
loggingHandler ( nil , c . Context )
return response . Success ( "OK" )
2022-06-28 02:25:30 -05:00
} )
sc . m . Post ( sc . url , handler )
sc . fakeReqWithParams ( "POST" , sc . url , map [ string ] string { } ) . exec ( )
fn ( sc , logcontent , sourceMapReads )
} )
}
func TestFrontendLoggingEndpointGrafanaJavascriptAgent ( t * testing . T ) {
ts , err := time . Parse ( "2006-01-02T15:04:05.000Z" , "2020-10-22T06:29:29.078Z" )
require . NoError ( t , err )
t . Run ( "FrontendLoggingEndpointGrafanaJavascriptAgent" , func ( t * testing . T ) {
user := frontendlogging . User {
Email : "test@example.com" ,
ID : "45" ,
}
meta := frontendlogging . Meta {
User : user ,
Page : frontendlogging . Page {
URL : "http://localhost:3000/dashboard/db/test" ,
} ,
}
errorEvent := frontendlogging . FrontendGrafanaJavascriptAgentEvent {
Meta : meta ,
Exceptions : [ ] frontendlogging . Exception {
{
Type : "UserError" ,
Value : "Please replace user and try again\n at foofn (foo.js:123:23)\n at barfn (bar.js:113:231)" ,
Stacktrace : & frontendlogging . Stacktrace {
Frames : [ ] frontendlogging . Frame { {
Function : "bla" ,
Filename : "http://localhost:3000/public/build/foo.js" ,
Lineno : 20 ,
Colno : 30 ,
} ,
} ,
} ,
Timestamp : ts ,
} ,
} ,
}
logGrafanaJavascriptAgentEventScenario ( t , "Should log received error event" , errorEvent ,
2023-08-30 10:46:47 -05:00
func ( sc * scenarioContext , logs map [ string ] any , sourceMapReads [ ] SourceMapReadRecord ) {
2022-09-12 05:04:43 -05:00
assert . Equal ( t , http . StatusAccepted , sc . resp . Code )
2022-06-28 02:25:30 -05:00
assertContextContains ( t , logs , "logger" , "frontend" )
assertContextContains ( t , logs , "page_url" , errorEvent . Meta . Page . URL )
assertContextContains ( t , logs , "user_email" , errorEvent . Meta . User . Email )
assertContextContains ( t , logs , "user_id" , errorEvent . Meta . User . ID )
assertContextContains ( t , logs , "original_timestamp" , errorEvent . Exceptions [ 0 ] . Timestamp )
assertContextContains ( t , logs , "msg" , ` UserError : Please replace user and try again
at foofn ( foo . js : 123 : 23 )
at barfn ( bar . js : 113 : 231 ) ` )
assert . NotContains ( t , logs , "context" )
} )
logEvent := frontendlogging . FrontendGrafanaJavascriptAgentEvent {
Meta : meta ,
Logs : [ ] frontendlogging . Log { {
Message : "This is a test log message" ,
Timestamp : ts ,
LogLevel : "info" ,
} } ,
}
logGrafanaJavascriptAgentEventScenario ( t , "Should log received log event" , logEvent ,
2023-08-30 10:46:47 -05:00
func ( sc * scenarioContext , logs map [ string ] any , sourceMapReads [ ] SourceMapReadRecord ) {
2022-09-12 05:04:43 -05:00
assert . Equal ( t , http . StatusAccepted , sc . resp . Code )
2022-06-28 02:25:30 -05:00
assert . Len ( t , logs , 11 )
assertContextContains ( t , logs , "logger" , "frontend" )
assertContextContains ( t , logs , "msg" , "This is a test log message" )
assertContextContains ( t , logs , "original_log_level" , frontendlogging . LogLevel ( "info" ) )
assertContextContains ( t , logs , "original_timestamp" , ts )
assert . NotContains ( t , logs , "stacktrace" )
assert . NotContains ( t , logs , "context" )
} )
logEventWithContext := frontendlogging . FrontendGrafanaJavascriptAgentEvent {
Meta : meta ,
Logs : [ ] frontendlogging . Log { {
Message : "This is a test log message" ,
Timestamp : ts ,
LogLevel : "info" ,
Context : map [ string ] string {
"one" : "two" ,
"bar" : "baz" ,
} ,
} } ,
}
logGrafanaJavascriptAgentEventScenario ( t , "Should log received log context" , logEventWithContext ,
2023-08-30 10:46:47 -05:00
func ( sc * scenarioContext , logs map [ string ] any , sourceMapReads [ ] SourceMapReadRecord ) {
2022-09-12 05:04:43 -05:00
assert . Equal ( t , http . StatusAccepted , sc . resp . Code )
2022-06-28 02:25:30 -05:00
assertContextContains ( t , logs , "context_one" , "two" )
assertContextContains ( t , logs , "context_bar" , "baz" )
} )
errorEventForSourceMapping := frontendlogging . FrontendGrafanaJavascriptAgentEvent {
Meta : meta ,
Exceptions : [ ] frontendlogging . Exception {
{
Type : "UserError" ,
Value : "Please replace user and try again" ,
Stacktrace : & frontendlogging . Stacktrace {
Frames : [ ] frontendlogging . Frame {
{
Function : "foofn" ,
Filename : "http://localhost:3000/public/build/moo/foo.js" , // source map found and mapped, core
Lineno : 2 ,
Colno : 5 ,
} ,
{
Function : "foofn" ,
Filename : "http://localhost:3000/public/plugins/telepathic/foo.js" , // plugin, source map found and mapped
Lineno : 3 ,
Colno : 10 ,
} ,
{
Function : "explode" ,
Filename : "http://localhost:3000/public/build/error.js" , // reading source map throws error
Lineno : 3 ,
Colno : 10 ,
} ,
{
Function : "wat" ,
Filename : "http://localhost:3000/public/build/bar.js" , // core, but source map not found on fs
Lineno : 3 ,
Colno : 10 ,
} ,
{
Function : "nope" ,
Filename : "http://localhost:3000/baz.js" , // not core or plugin, wont even attempt to get source map
Lineno : 3 ,
Colno : 10 ,
} ,
{
Function : "fake" ,
Filename : "http://localhost:3000/public/build/../../secrets.txt" , // path will be sanitized
Lineno : 3 ,
Colno : 10 ,
} ,
{
Function : "cdn" ,
Filename : "https://storage.googleapis.com/grafana-static-assets/grafana-oss/pre-releases/7.5.0-11925pre/public/build/foo.js" , // source map found and mapped
Lineno : 3 ,
Colno : 10 ,
} ,
} ,
} ,
Timestamp : ts ,
} ,
} ,
}
logGrafanaJavascriptAgentEventScenario ( t , "Should load sourcemap and transform stacktrace line when possible" , errorEventForSourceMapping ,
2023-08-30 10:46:47 -05:00
func ( sc * scenarioContext , logs map [ string ] any , sourceMapReads [ ] SourceMapReadRecord ) {
2022-09-12 05:04:43 -05:00
assert . Equal ( t , http . StatusAccepted , sc . resp . Code )
2022-06-28 02:25:30 -05:00
assertContextContains ( t , logs , "stacktrace" , ` UserError : Please replace user and try again
2023-05-02 04:10:56 -05:00
at ? ( core | webpack : ///./some_source.ts:2:2)
at ? ( telepathic | webpack : ///./some_source.ts:3:2)
2022-06-28 02:25:30 -05:00
at explode ( http : //localhost:3000/public/build/error.js:3:10)
at wat ( http : //localhost:3000/public/build/bar.js:3:10)
at nope ( http : //localhost:3000/baz.js:3:10)
at fake ( http : //localhost:3000/public/build/../../secrets.txt:3:10)
2023-05-02 04:10:56 -05:00
at ? ( core | webpack : ///./some_source.ts:3:2)`)
2022-06-28 02:25:30 -05:00
assert . Len ( t , sourceMapReads , 6 )
assert . Equal ( t , "/staticroot" , sourceMapReads [ 0 ] . dir )
assert . Equal ( t , "build/moo/foo.js.map" , sourceMapReads [ 0 ] . path )
assert . Equal ( t , "/usr/local/telepathic-panel" , sourceMapReads [ 1 ] . dir )
assert . Equal ( t , "/foo.js.map" , sourceMapReads [ 1 ] . path )
assert . Equal ( t , "/staticroot" , sourceMapReads [ 2 ] . dir )
assert . Equal ( t , "build/error.js.map" , sourceMapReads [ 2 ] . path )
assert . Equal ( t , "/staticroot" , sourceMapReads [ 3 ] . dir )
assert . Equal ( t , "build/bar.js.map" , sourceMapReads [ 3 ] . path )
assert . Equal ( t , "/staticroot" , sourceMapReads [ 4 ] . dir )
assert . Equal ( t , "secrets.txt.map" , sourceMapReads [ 4 ] . path )
assert . Equal ( t , "/staticroot" , sourceMapReads [ 5 ] . dir )
assert . Equal ( t , "build/foo.js.map" , sourceMapReads [ 5 ] . path )
} )
logWebVitals := frontendlogging . FrontendGrafanaJavascriptAgentEvent {
Meta : meta ,
Measurements : [ ] frontendlogging . Measurement { {
Values : map [ string ] float64 {
"CLS" : 1.0 ,
} ,
} ,
} ,
}
logGrafanaJavascriptAgentEventScenario ( t , "Should log web vitals as context" , logWebVitals ,
2023-08-30 10:46:47 -05:00
func ( sc * scenarioContext , logs map [ string ] any , sourceMapReads [ ] SourceMapReadRecord ) {
2022-09-12 05:04:43 -05:00
assert . Equal ( t , http . StatusAccepted , sc . resp . Code )
2022-06-28 02:25:30 -05:00
assertContextContains ( t , logs , "CLS" , float64 ( 1 ) )
} )
} )
}
2023-08-30 10:46:47 -05:00
func assertContextContains ( t * testing . T , logRecord map [ string ] any , label string , value any ) {
2022-01-06 08:28:05 -06:00
assert . Contains ( t , logRecord , label )
assert . Equal ( t , value , logRecord [ label ] )
2020-11-12 05:29:43 -06:00
}