diff --git a/go.mod b/go.mod index a5eab82719f..3d3ed8c8a03 100644 --- a/go.mod +++ b/go.mod @@ -13,7 +13,7 @@ require ( github.com/go-sourcemap/sourcemap v2.1.4-0.20211119122758-180fcef48034+incompatible github.com/golang/protobuf v1.5.2 github.com/gorilla/websocket v1.5.0 - github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f + github.com/grafana/xk6-browser v0.9.0 github.com/grafana/xk6-output-prometheus-remote v0.1.0 github.com/grafana/xk6-redis v0.1.1 github.com/grafana/xk6-timers v0.1.2 diff --git a/go.sum b/go.sum index a6a6c9ce4cc..98f81604bb6 100644 --- a/go.sum +++ b/go.sum @@ -173,8 +173,8 @@ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f h1:/9pTQhJoYlB3vrubQXAODDZEl4pcKdm0sNN+8e0+xtI= -github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f/go.mod h1:W5hLYHj3JT1wivOncQan8OncXBa39rjdRM4qwyXi6nI= +github.com/grafana/xk6-browser v0.9.0 h1:lEPSM/hgzIO+dvLbrf+O6flLgNH3d0i+GVkOzPi9HSs= +github.com/grafana/xk6-browser v0.9.0/go.mod h1:ax6OHARpNEu9hSGYOAI4grAwiRapsNPi9TBQxDYurKw= github.com/grafana/xk6-output-prometheus-remote v0.1.0 h1:yJc09O6TeBYLFfNG/dqBDtvHmM9P1B2ZFTyr0HgvsHY= github.com/grafana/xk6-output-prometheus-remote v0.1.0/go.mod h1:R4o0VbIfbQNNPSGkeeiCBLzwNfG+DEdfKYNsV1oww1Y= github.com/grafana/xk6-redis v0.1.1 h1:rvWnLanRB2qzDwuY6NMBe6PXei3wJ3kjYvfCwRJ+q+8= diff --git a/vendor/github.com/grafana/xk6-browser/browser/mapping.go b/vendor/github.com/grafana/xk6-browser/browser/mapping.go index d50dbe956b9..fe8012c6ed9 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/mapping.go +++ b/vendor/github.com/grafana/xk6-browser/browser/mapping.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "os" "github.com/dop251/goja" @@ -31,11 +32,14 @@ type mapping = map[string]any // See issue #661 for more details. func mapBrowserToGoja(vu moduleVU) *goja.Object { var ( - rt = vu.Runtime() - obj = rt.NewObject() - browserType = chromium.NewBrowserType(vu) + rt = vu.Runtime() + obj = rt.NewObject() + // TODO: Use k6 LookupEnv instead of OS package methods. + // See https://github.com/grafana/xk6-browser/issues/822. + wsURL, isRemoteBrowser = k6ext.IsRemoteBrowser(os.LookupEnv) + browserType = chromium.NewBrowserType(vu) ) - for k, v := range mapBrowserType(vu, browserType) { + for k, v := range mapBrowserType(vu, browserType, wsURL, isRemoteBrowser) { err := obj.Set(k, rt.ToValue(v)) if err != nil { k6common.Throw(rt, fmt.Errorf("mapping: %w", err)) @@ -706,7 +710,7 @@ func mapBrowser(vu moduleVU, b api.Browser) mapping { } // mapBrowserType to the JS module. -func mapBrowserType(vu moduleVU, bt api.BrowserType) mapping { +func mapBrowserType(vu moduleVU, bt api.BrowserType, wsURL string, isRemoteBrowser bool) mapping { rt := vu.Runtime() return mapping{ "connect": func(wsEndpoint string, opts goja.Value) *goja.Object { @@ -718,6 +722,14 @@ func mapBrowserType(vu moduleVU, bt api.BrowserType) mapping { "launchPersistentContext": bt.LaunchPersistentContext, "name": bt.Name, "launch": func(opts goja.Value) *goja.Object { + // If browser is remote, transition from launch + // to connect and avoid storing the browser pid + // as we have no access to it. + if isRemoteBrowser { + m := mapBrowser(vu, bt.Connect(wsURL, opts)) + return rt.ToValue(m).ToObject(rt) + } + b, pid := bt.Launch(opts) // store the pid so we can kill it later on panic. vu.registerPid(pid) diff --git a/vendor/github.com/grafana/xk6-browser/browser/module.go b/vendor/github.com/grafana/xk6-browser/browser/module.go index bdc28ef380d..8c310318967 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/module.go +++ b/vendor/github.com/grafana/xk6-browser/browser/module.go @@ -9,13 +9,11 @@ import ( k6modules "go.k6.io/k6/js/modules" ) -const version = "0.8.1" - type ( // RootModule is the global module instance that will create module // instances for each VU. RootModule struct { - pidRegistry *pidRegistry + PidRegistry *pidRegistry } // JSModule exposes the properties available to the JS script. @@ -39,7 +37,7 @@ var ( // New returns a pointer to a new RootModule instance. func New() *RootModule { return &RootModule{ - pidRegistry: &pidRegistry{}, + PidRegistry: &pidRegistry{}, } } @@ -50,10 +48,9 @@ func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance { mod: &JSModule{ Chromium: mapBrowserToGoja(moduleVU{ VU: vu, - pidRegistry: m.pidRegistry, + pidRegistry: m.PidRegistry, }), Devices: common.GetDevices(), - Version: version, }, } } diff --git a/vendor/github.com/grafana/xk6-browser/common/browser.go b/vendor/github.com/grafana/xk6-browser/common/browser.go index 93ed8a77f89..3f0a845e417 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser.go @@ -144,6 +144,18 @@ func (b *Browser) disposeContext(id cdp.BrowserContextID) error { return nil } +// getDefaultBrowserContextOrByID returns the BrowserContext for the given page ID. +// If the browser context is not found, the default BrowserContext is returned. +func (b *Browser) getDefaultBrowserContextOrByID(id cdp.BrowserContextID) *BrowserContext { + b.contextsMu.RLock() + defer b.contextsMu.RUnlock() + browserCtx := b.defaultContext + if bctx, ok := b.contexts[id]; ok { + browserCtx = bctx + } + return browserCtx +} + func (b *Browser) getPages() []*Page { b.pagesMu.RLock() defer b.pagesMu.RUnlock() @@ -208,112 +220,134 @@ func (b *Browser) initEvents() error { return nil } +// onAttachedToTarget is called when a new page is attached to the browser. func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) { - evti := ev.TargetInfo + b.logger.Debugf("Browser:onAttachedToTarget", "sid:%v tid:%v bctxid:%v", + ev.SessionID, ev.TargetInfo.TargetID, ev.TargetInfo.BrowserContextID) - b.contextsMu.RLock() - browserCtx := b.defaultContext - bctx, ok := b.contexts[evti.BrowserContextID] - if ok { - browserCtx = bctx - } - b.contextsMu.RUnlock() - - b.logger.Debugf("Browser:onAttachedToTarget", "sid:%v tid:%v bctxid:%v bctx nil:%t", - ev.SessionID, evti.TargetID, evti.BrowserContextID, browserCtx == nil) + var ( + targetPage = ev.TargetInfo + browserCtx = b.getDefaultBrowserContextOrByID(targetPage.BrowserContextID) + ) - // We're not interested in the top-level browser target, other targets or DevTools targets right now. - isDevTools := strings.HasPrefix(evti.URL, "devtools://devtools") - if evti.Type == "browser" || evti.Type == "other" || isDevTools { - b.logger.Debugf("Browser:onAttachedToTarget:return", "sid:%v tid:%v (devtools)", ev.SessionID, evti.TargetID) - return + if !b.isAttachedPageValid(ev, browserCtx) { + return // Ignore this page. } - session := b.conn.getSession(ev.SessionID) if session == nil { b.logger.Warnf("Browser:onAttachedToTarget", "session closed before attachToTarget is handled. sid:%v tid:%v", - ev.SessionID, evti.TargetID) + ev.SessionID, targetPage.TargetID) return // ignore } - switch evti.Type { - case "background_page": - p, err := NewPage(b.ctx, session, browserCtx, evti.TargetID, nil, false, b.logger) - if err != nil { - isRunning := atomic.LoadInt64(&b.state) == BrowserStateOpen && b.IsConnected() // b.conn.isConnected() - if _, ok := err.(*websocket.CloseError); !ok && !isRunning { - // If we're no longer connected to browser, then ignore WebSocket errors - b.logger.Debugf("Browser:onAttachedToTarget:background_page:return", "sid:%v tid:%v websocket err:%v", - ev.SessionID, evti.TargetID, err) - return - } - select { - case <-b.ctx.Done(): - b.logger.Debugf("Browser:onAttachedToTarget:background_page:return:<-ctx.Done", - "sid:%v tid:%v err:%v", - ev.SessionID, evti.TargetID, b.ctx.Err()) - return // ignore - default: - k6ext.Panic(b.ctx, "creating a new background page: %w", err) - } - } - - b.pagesMu.Lock() - b.logger.Debugf("Browser:onAttachedToTarget:background_page:addTid", "sid:%v tid:%v", ev.SessionID, evti.TargetID) - b.pages[evti.TargetID] = p - b.pagesMu.Unlock() - - b.sessionIDtoTargetIDMu.Lock() - b.logger.Debugf("Browser:onAttachedToTarget:background_page:addSid", "sid:%v tid:%v", ev.SessionID, evti.TargetID) - b.sessionIDtoTargetID[ev.SessionID] = evti.TargetID - b.sessionIDtoTargetIDMu.Unlock() - case "page": - // Opener is nil for the initial page - var opener *Page + var ( + isPage = targetPage.Type == "page" + opener *Page + ) + // Opener is nil for the initial page. + if isPage { b.pagesMu.RLock() - if t, ok := b.pages[evti.OpenerID]; ok { + if t, ok := b.pages[targetPage.OpenerID]; ok { opener = t } b.pagesMu.RUnlock() + } + p, err := NewPage(b.ctx, session, browserCtx, targetPage.TargetID, opener, isPage, b.logger) + if err != nil && b.isPageAttachmentErrorIgnorable(ev, session, err) { + return // Ignore this page. + } + if err != nil { + k6ext.Panic(b.ctx, "creating a new %s: %w", targetPage.Type, err) + } + b.attachNewPage(p, ev) // Register the page as an active page. + // Emit the page event only for pages, not for background pages. + // Background pages are created by extensions. + if isPage { + browserCtx.emit(EventBrowserContextPage, p) + } +} - b.logger.Debugf("Browser:onAttachedToTarget:page", "sid:%v tid:%v opener nil:%t", ev.SessionID, evti.TargetID, opener == nil) +// attachNewPage registers the page as an active page and attaches the sessionID with the targetID. +func (b *Browser) attachNewPage(p *Page, ev *target.EventAttachedToTarget) { + targetPage := ev.TargetInfo - p, err := NewPage(b.ctx, session, browserCtx, evti.TargetID, opener, true, b.logger) - if err != nil { - isRunning := atomic.LoadInt64(&b.state) == BrowserStateOpen && b.IsConnected() // b.conn.isConnected() - if _, ok := err.(*websocket.CloseError); !ok && !isRunning { - // If we're no longer connected to browser, then ignore WebSocket errors - b.logger.Debugf("Browser:onAttachedToTarget:page:return", "sid:%v tid:%v websocket err:", ev.SessionID, evti.TargetID) - return - } - select { - case <-b.ctx.Done(): - b.logger.Debugf("Browser:onAttachedToTarget:page:return:<-ctx.Done", - "sid:%v tid:%v err:%v", - ev.SessionID, evti.TargetID, b.ctx.Err()) - return // ignore - default: - k6ext.Panic(b.ctx, "creating a new page: %w", err) - } - } + // Register the page as an active page. + b.logger.Debugf("Browser:attachNewPage:addTarget", "sid:%v tid:%v pageType:%s", + ev.SessionID, targetPage.TargetID, targetPage.Type) + b.pagesMu.Lock() + b.pages[targetPage.TargetID] = p + b.pagesMu.Unlock() + + // Attach the sessionID with the targetID so we can communicate with the + // page later. + b.logger.Debugf("Browser:attachNewPage:addSession", "sid:%v tid:%v pageType:%s", + ev.SessionID, targetPage.TargetID, targetPage.Type) + b.sessionIDtoTargetIDMu.Lock() + b.sessionIDtoTargetID[ev.SessionID] = targetPage.TargetID + b.sessionIDtoTargetIDMu.Unlock() +} + +// isAttachedPageValid returns true if the attached page is valid and should be +// added to the browser's pages. It returns false if the attached page is not +// valid and should be ignored. +func (b *Browser) isAttachedPageValid(ev *target.EventAttachedToTarget, browserCtx *BrowserContext) bool { + targetPage := ev.TargetInfo + + // We're not interested in the top-level browser target, other targets or DevTools targets right now. + isDevTools := strings.HasPrefix(targetPage.URL, "devtools://devtools") + if targetPage.Type == "browser" || targetPage.Type == "other" || isDevTools { + b.logger.Debugf("Browser:isAttachedPageValid:return", "sid:%v tid:%v (devtools)", ev.SessionID, targetPage.TargetID) + return false + } + pageType := targetPage.Type + if pageType != "page" && pageType != "background_page" { + b.logger.Warnf( + "Browser:isAttachedPageValid", "sid:%v tid:%v bctxid:%v bctx nil:%t, unknown target type: %q", + ev.SessionID, targetPage.TargetID, targetPage.BrowserContextID, browserCtx == nil, targetPage.Type) + return false + } - b.pagesMu.Lock() - b.logger.Debugf("Browser:onAttachedToTarget:page:addTarget", "sid:%v tid:%v", ev.SessionID, evti.TargetID) - b.pages[evti.TargetID] = p - b.pagesMu.Unlock() + return true +} - b.sessionIDtoTargetIDMu.Lock() - b.logger.Debugf("Browser:onAttachedToTarget:page:sidToTid", "sid:%v tid:%v", ev.SessionID, evti.TargetID) - b.sessionIDtoTargetID[ev.SessionID] = evti.TargetID - b.sessionIDtoTargetIDMu.Unlock() +// isPageAttachmentErrorIgnorable returns true if the error is ignorable. +func (b *Browser) isPageAttachmentErrorIgnorable(ev *target.EventAttachedToTarget, session *Session, err error) bool { + targetPage := ev.TargetInfo - browserCtx.emit(EventBrowserContextPage, p) + // If we're no longer connected to browser, then ignore WebSocket errors. + // This can happen when the browser is closed while the page is being attached. + var ( + isRunning = atomic.LoadInt64(&b.state) == BrowserStateOpen && b.IsConnected() // b.conn.isConnected() + wsErr *websocket.CloseError + ) + if !errors.As(err, &wsErr) && !isRunning { + // If we're no longer connected to browser, then ignore WebSocket errors + b.logger.Debugf("Browser:isPageAttachmentErrorIgnorable:return", + "sid:%v tid:%v pageType:%s websocket err:%v", + ev.SessionID, targetPage.TargetID, targetPage.Type, err) + return true + } + // No need to register the page if the test run is over. + select { + case <-b.ctx.Done(): + b.logger.Debugf("Browser:isPageAttachmentErrorIgnorable:return:<-ctx.Done", + "sid:%v tid:%v pageType:%s err:%v", + ev.SessionID, targetPage.TargetID, targetPage.Type, b.ctx.Err()) + return true default: - b.logger.Warnf( - "Browser:onAttachedToTarget", "sid:%v tid:%v bctxid:%v bctx nil:%t, unknown target type: %q", - ev.SessionID, evti.TargetID, evti.BrowserContextID, browserCtx == nil, evti.Type) } + // Another VU or instance closed the page, and the session is closed. + // This can happen if the page is closed before the attachedToTarget + // event is handled. + if session.Closed() { + b.logger.Debugf("Browser:isPageAttachmentErrorIgnorable:return:session.Done", + "session closed: sid:%v tid:%v pageType:%s err:%v", + ev.SessionID, targetPage.TargetID, targetPage.Type, err) + return true + } + + return false // cannot ignore } // onDetachedFromTarget event can be issued multiple times per target if multiple diff --git a/vendor/github.com/grafana/xk6-browser/common/frame_session.go b/vendor/github.com/grafana/xk6-browser/common/frame_session.go index 2bd3bc2fa92..08508c15f7f 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_session.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_session.go @@ -805,10 +805,9 @@ func (fs *FrameSession) onPageLifecycle(event *cdppage.EventLifecycleEvent) { } eventToMetric := map[string]*k6metrics.Metric{ - "load": fs.k6Metrics.BrowserLoaded, - "DOMContentLoaded": fs.k6Metrics.BrowserDOMContentLoaded, - "firstPaint": fs.k6Metrics.BrowserFirstPaint, - "firstContentfulPaint": fs.k6Metrics.BrowserFirstContentfulPaint, + "load": fs.k6Metrics.BrowserLoaded, + "DOMContentLoaded": fs.k6Metrics.BrowserDOMContentLoaded, + "firstPaint": fs.k6Metrics.BrowserFirstPaint, } if m, ok := eventToMetric[event.Name]; ok { diff --git a/vendor/github.com/grafana/xk6-browser/common/session.go b/vendor/github.com/grafana/xk6-browser/common/session.go index e5c531482f7..8c04272563c 100644 --- a/vendor/github.com/grafana/xk6-browser/common/session.go +++ b/vendor/github.com/grafana/xk6-browser/common/session.go @@ -212,3 +212,13 @@ func (s *Session) ExecuteWithoutExpectationOnReply(ctx context.Context, method s func (s *Session) Done() <-chan struct{} { return s.done } + +// Closed returns true if this session is closed. +func (s *Session) Closed() bool { + select { + case <-s.done: + return true + default: + return false + } +} diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/cloud.go b/vendor/github.com/grafana/xk6-browser/k6ext/cloud.go deleted file mode 100644 index 753e2058b93..00000000000 --- a/vendor/github.com/grafana/xk6-browser/k6ext/cloud.go +++ /dev/null @@ -1,9 +0,0 @@ -package k6ext - -import "os" - -// OnCloud returns true if xk6-browser runs in the cloud. -func OnCloud() bool { - _, ok := os.LookupEnv("K6_CLOUDRUN_INSTANCE_ID") - return ok -} diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/env.go b/vendor/github.com/grafana/xk6-browser/k6ext/env.go new file mode 100644 index 00000000000..cf0fd468bd0 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/k6ext/env.go @@ -0,0 +1,40 @@ +package k6ext + +import ( + "crypto/rand" + "math/big" + "strings" +) + +type envLookupper func(key string) (string, bool) + +// IsRemoteBrowser returns true and the corresponding CDP +// WS URL if this one is set through the K6_BROWSER_WS_URL +// environment variable. Otherwise returns false. +// If K6_BROWSER_WS_URL is set as a comma separated list of +// URLs, this method returns a randomly chosen URL from the list +// so connections are done in a round-robin fashion for all the +// entries in the list. +func IsRemoteBrowser(envLookup envLookupper) (wsURL string, isRemote bool) { + wsURL, isRemote = envLookup("K6_BROWSER_WS_URL") + if !isRemote { + return "", false + } + if !strings.ContainsRune(wsURL, ',') { + return wsURL, isRemote + } + + // If last parts element is a void string, + // because WS URL contained an ending comma, + // remove it + parts := strings.Split(wsURL, ",") + if parts[len(parts)-1] == "" { + parts = parts[:len(parts)-1] + } + + // Choose a random WS URL from the provided list + i, _ := rand.Int(rand.Reader, big.NewInt(int64(len(parts)))) + wsURL = parts[i.Int64()] + + return wsURL, isRemote +} diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go b/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go index d4e86ba4150..3c430f7f5f3 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go @@ -17,10 +17,9 @@ const ( // CustomMetrics are the custom k6 metrics used by xk6-browser. type CustomMetrics struct { - BrowserDOMContentLoaded *k6metrics.Metric - BrowserFirstPaint *k6metrics.Metric - BrowserFirstContentfulPaint *k6metrics.Metric - BrowserLoaded *k6metrics.Metric + BrowserDOMContentLoaded *k6metrics.Metric + BrowserFirstPaint *k6metrics.Metric + BrowserLoaded *k6metrics.Metric WebVitals map[string]*k6metrics.Metric } @@ -61,8 +60,6 @@ func RegisterCustomMetrics(registry *k6metrics.Registry) *CustomMetrics { "browser_dom_content_loaded", k6metrics.Trend, k6metrics.Time), BrowserFirstPaint: registry.MustNewMetric( "browser_first_paint", k6metrics.Trend, k6metrics.Time), - BrowserFirstContentfulPaint: registry.MustNewMetric( - "browser_first_contentful_paint", k6metrics.Trend, k6metrics.Time), BrowserLoaded: registry.MustNewMetric( "browser_loaded", k6metrics.Trend, k6metrics.Time), WebVitals: webVitals, diff --git a/vendor/modules.txt b/vendor/modules.txt index 54a605cfb70..486d3ce6559 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -141,7 +141,7 @@ github.com/google/pprof/profile # github.com/gorilla/websocket v1.5.0 ## explicit; go 1.12 github.com/gorilla/websocket -# github.com/grafana/xk6-browser v0.8.2-0.20230329135657-a01218eaee2f +# github.com/grafana/xk6-browser v0.9.0 ## explicit; go 1.19 github.com/grafana/xk6-browser/api github.com/grafana/xk6-browser/browser