From 90ac2f9751a937a04c33c5bb107becddcd52ae8b Mon Sep 17 00:00:00 2001 From: ka3de Date: Fri, 28 Jul 2023 10:48:41 +0200 Subject: [PATCH 1/6] Upgrade xk6-browser to v1.0.0 --- go.mod | 2 +- go.sum | 4 +- .../grafana/xk6-browser/api/browser.go | 2 +- .../grafana/xk6-browser/api/browser_type.go | 14 - .../grafana/xk6-browser/api/frame.go | 8 +- .../grafana/xk6-browser/browser/mapping.go | 84 ++--- .../grafana/xk6-browser/browser/module.go | 56 ++- .../grafana/xk6-browser/browser/modulevu.go | 7 + .../xk6-browser/browser/pidregistry.go | 28 -- .../grafana/xk6-browser/browser/registry.go | 340 ++++++++++++++++++ .../xk6-browser/chromium/browser_type.go | 161 +++++---- .../grafana/xk6-browser/common/browser.go | 83 ++--- .../xk6-browser/common/browser_context.go | 2 +- .../xk6-browser/common/browser_options.go | 202 ++++++----- .../xk6-browser/common/browser_process.go | 11 +- .../grafana/xk6-browser/common/connection.go | 27 +- .../grafana/xk6-browser/common/context.go | 17 +- .../grafana/xk6-browser/common/doc.go | 3 + .../grafana/xk6-browser/common/frame.go | 41 ++- .../xk6-browser/common/frame_manager.go | 23 +- .../xk6-browser/common/frame_session.go | 25 +- .../grafana/xk6-browser/common/hooks.go | 2 +- .../xk6-browser/common/js/injected_script.js | 24 ++ .../xk6-browser/common/network_manager.go | 92 ++--- .../grafana/xk6-browser/common/options.go | 60 ---- .../grafana/xk6-browser/common/page.go | 15 +- .../grafana/xk6-browser/common/response.go | 2 +- .../grafana/xk6-browser/common/session.go | 10 +- .../grafana/xk6-browser/common/types.go | 12 + .../github.com/grafana/xk6-browser/env/env.go | 84 +++++ .../grafana/xk6-browser/k6ext/context.go | 23 ++ .../grafana/xk6-browser/k6ext/env.go | 40 --- .../grafana/xk6-browser/k6ext/metrics.go | 53 +-- .../grafana/xk6-browser/log/logger.go | 19 +- vendor/modules.txt | 3 +- 35 files changed, 1003 insertions(+), 576 deletions(-) delete mode 100644 vendor/github.com/grafana/xk6-browser/api/browser_type.go delete mode 100644 vendor/github.com/grafana/xk6-browser/browser/pidregistry.go create mode 100644 vendor/github.com/grafana/xk6-browser/browser/registry.go create mode 100644 vendor/github.com/grafana/xk6-browser/common/doc.go delete mode 100644 vendor/github.com/grafana/xk6-browser/common/options.go create mode 100644 vendor/github.com/grafana/xk6-browser/env/env.go delete mode 100644 vendor/github.com/grafana/xk6-browser/k6ext/env.go diff --git a/go.mod b/go.mod index 4c0e0265791..4fdc2395beb 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.3 github.com/gorilla/websocket v1.5.0 - github.com/grafana/xk6-browser v0.10.0 + github.com/grafana/xk6-browser v1.0.0 github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1 github.com/grafana/xk6-output-prometheus-remote v0.2.3 github.com/grafana/xk6-redis v0.1.1 diff --git a/go.sum b/go.sum index 97b8a1d1459..f6709ccc9f3 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,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.10.0 h1:Mnx0Ho+mlyFGlV7zW7zXkN0njRglh9JflLV+OzXSaRk= -github.com/grafana/xk6-browser v0.10.0/go.mod h1:ax6OHARpNEu9hSGYOAI4grAwiRapsNPi9TBQxDYurKw= +github.com/grafana/xk6-browser v1.0.0 h1:P0zUsftWSYrY6GnrGOsJEh9+PdPd6/dYkFHPzoG5cUA= +github.com/grafana/xk6-browser v1.0.0/go.mod h1:Ck8IWES3Emu4CMKktQ/oD7l/JFj/1ESnDIbQGpfaoYA= github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1 h1:SdMihJN+fkH6cO/1NeAnVxSVOnJ3ZkZ1v7FJnrcqhog= github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1/go.mod h1:iq6qHN64XgAEmDHKf0OXZ4mvoqF4Udr22fiCIXNpXA0= github.com/grafana/xk6-output-prometheus-remote v0.2.3 h1:ta4wFrO85+29H0papAbeMCavHrBuHDZ4bdKC1Zv8zlo= diff --git a/vendor/github.com/grafana/xk6-browser/api/browser.go b/vendor/github.com/grafana/xk6-browser/api/browser.go index 7b60fca75cd..a111a817c3e 100644 --- a/vendor/github.com/grafana/xk6-browser/api/browser.go +++ b/vendor/github.com/grafana/xk6-browser/api/browser.go @@ -5,7 +5,7 @@ import "github.com/dop251/goja" // Browser is the public interface of a CDP browser. type Browser interface { Close() - Contexts() []BrowserContext + Context() BrowserContext IsConnected() bool NewContext(opts goja.Value) (BrowserContext, error) NewPage(opts goja.Value) (Page, error) diff --git a/vendor/github.com/grafana/xk6-browser/api/browser_type.go b/vendor/github.com/grafana/xk6-browser/api/browser_type.go deleted file mode 100644 index 07f047c3da8..00000000000 --- a/vendor/github.com/grafana/xk6-browser/api/browser_type.go +++ /dev/null @@ -1,14 +0,0 @@ -package api - -import ( - "github.com/dop251/goja" -) - -// BrowserType is the public interface of a CDP browser client. -type BrowserType interface { - Connect(wsEndpoint string, opts goja.Value) Browser - ExecutablePath() string - Launch(opts goja.Value) (_ Browser, browserProcessID int) - LaunchPersistentContext(userDataDir string, opts goja.Value) Browser - Name() string -} diff --git a/vendor/github.com/grafana/xk6-browser/api/frame.go b/vendor/github.com/grafana/xk6-browser/api/frame.go index a8e44acb93e..5bf69b62b9c 100644 --- a/vendor/github.com/grafana/xk6-browser/api/frame.go +++ b/vendor/github.com/grafana/xk6-browser/api/frame.go @@ -1,6 +1,10 @@ package api -import "github.com/dop251/goja" +import ( + "context" + + "github.com/dop251/goja" +) // Frame is the interface of a CDP target frame. type Frame interface { @@ -12,6 +16,8 @@ type Frame interface { Content() string Dblclick(selector string, opts goja.Value) DispatchEvent(selector string, typ string, eventInit goja.Value, opts goja.Value) + // EvaluateWithContext for internal use only + EvaluateWithContext(ctx context.Context, pageFunc goja.Value, args ...goja.Value) (any, error) Evaluate(pageFunc goja.Value, args ...goja.Value) any EvaluateHandle(pageFunc goja.Value, args ...goja.Value) (JSHandle, error) Fill(selector string, value string, opts goja.Value) diff --git a/vendor/github.com/grafana/xk6-browser/browser/mapping.go b/vendor/github.com/grafana/xk6-browser/browser/mapping.go index fe8012c6ed9..2ce97453dff 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/mapping.go +++ b/vendor/github.com/grafana/xk6-browser/browser/mapping.go @@ -4,12 +4,10 @@ import ( "context" "errors" "fmt" - "os" "github.com/dop251/goja" "github.com/grafana/xk6-browser/api" - "github.com/grafana/xk6-browser/chromium" "github.com/grafana/xk6-browser/k6error" "github.com/grafana/xk6-browser/k6ext" @@ -34,12 +32,8 @@ func mapBrowserToGoja(vu moduleVU) *goja.Object { var ( 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, wsURL, isRemoteBrowser) { + for k, v := range mapBrowser(vu) { err := obj.Set(k, rt.ToValue(v)) if err != nil { k6common.Throw(rt, fmt.Errorf("mapping: %w", err)) @@ -678,20 +672,28 @@ func mapBrowserContext(vu moduleVU, bc api.BrowserContext) mapping { } // mapBrowser to the JS module. -func mapBrowser(vu moduleVU, b api.Browser) mapping { +func mapBrowser(vu moduleVU) mapping { rt := vu.Runtime() return mapping{ - "close": b.Close, - "contexts": b.Contexts, - "isConnected": b.IsConnected, - "on": func(event string) *goja.Promise { - return k6ext.Promise(vu.Context(), func() (result any, reason error) { - return b.On(event) //nolint:wrapcheck - }) + "context": func() (api.BrowserContext, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } + return b.Context(), nil + }, + "isConnected": func() (bool, error) { + b, err := vu.browser() + if err != nil { + return false, err + } + return b.IsConnected(), nil }, - "userAgent": b.UserAgent, - "version": b.Version, "newContext": func(opts goja.Value) (*goja.Object, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } bctx, err := b.NewContext(opts) if err != nil { return nil, err //nolint:wrapcheck @@ -699,7 +701,25 @@ func mapBrowser(vu moduleVU, b api.Browser) mapping { m := mapBrowserContext(vu, bctx) return rt.ToValue(m).ToObject(rt), nil }, + "userAgent": func() (string, error) { + b, err := vu.browser() + if err != nil { + return "", err + } + return b.UserAgent(), nil + }, + "version": func() (string, error) { + b, err := vu.browser() + if err != nil { + return "", err + } + return b.Version(), nil + }, "newPage": func(opts goja.Value) (mapping, error) { + b, err := vu.browser() + if err != nil { + return nil, err + } page, err := b.NewPage(opts) if err != nil { return nil, err //nolint:wrapcheck @@ -709,36 +729,6 @@ func mapBrowser(vu moduleVU, b api.Browser) mapping { } } -// mapBrowserType to the JS module. -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 { - b := bt.Connect(wsEndpoint, opts) - m := mapBrowser(vu, b) - return rt.ToValue(m).ToObject(rt) - }, - "executablePath": bt.ExecutablePath, - "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) - m := mapBrowser(vu, b) - return rt.ToValue(m).ToObject(rt) - }, - } -} - func panicIfFatalError(ctx context.Context, err error) { if errors.Is(err, k6error.ErrFatal) { k6ext.Abort(ctx, err.Error()) diff --git a/vendor/github.com/grafana/xk6-browser/browser/module.go b/vendor/github.com/grafana/xk6-browser/browser/module.go index 8c310318967..1f25e3e8d6d 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/module.go +++ b/vendor/github.com/grafana/xk6-browser/browser/module.go @@ -1,10 +1,17 @@ -// Package browser provides an entry point to the browser extension. +// Package browser provides an entry point to the browser module. package browser import ( + "log" + "net/http" + _ "net/http/pprof" //nolint:gosec + "sync" + "github.com/dop251/goja" "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/env" + "github.com/grafana/xk6-browser/k6ext" k6modules "go.k6.io/k6/js/modules" ) @@ -13,14 +20,15 @@ type ( // RootModule is the global module instance that will create module // instances for each VU. RootModule struct { - PidRegistry *pidRegistry + PidRegistry *pidRegistry + remoteRegistry *remoteRegistry + initOnce *sync.Once } // JSModule exposes the properties available to the JS script. JSModule struct { - Chromium *goja.Object - Devices map[string]common.Device - Version string + Browser *goja.Object + Devices map[string]common.Device } // ModuleInstance represents an instance of the JS module. @@ -38,17 +46,27 @@ var ( func New() *RootModule { return &RootModule{ PidRegistry: &pidRegistry{}, + initOnce: &sync.Once{}, } } // NewModuleInstance implements the k6modules.Module interface to return // a new instance for each VU. func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance { + // initialization should be done once per module as it initializes + // globally used values across the whole test run and not just the + // current VU. Since initialization can fail with an error, + // we've had to place it here so that if an error occurs a + // panic can be initiated and safely handled by k6. + m.initOnce.Do(func() { + m.initialize(vu) + }) return &ModuleInstance{ mod: &JSModule{ - Chromium: mapBrowserToGoja(moduleVU{ - VU: vu, - pidRegistry: m.PidRegistry, + Browser: mapBrowserToGoja(moduleVU{ + VU: vu, + pidRegistry: m.PidRegistry, + browserRegistry: newBrowserRegistry(vu, m.remoteRegistry, m.PidRegistry), }), Devices: common.GetDevices(), }, @@ -60,3 +78,25 @@ func (m *RootModule) NewModuleInstance(vu k6modules.VU) k6modules.Instance { func (mi *ModuleInstance) Exports() k6modules.Exports { return k6modules.Exports{Default: mi.mod} } + +// initialize initializes the module instance with a new remote registry +// and debug server, etc. +func (m *RootModule) initialize(vu k6modules.VU) { + var ( + err error + initEnv = vu.InitEnv() + ) + m.remoteRegistry, err = newRemoteRegistry(initEnv.LookupEnv) + if err != nil { + k6ext.Abort(vu.Context(), "failed to create remote registry: %v", err) + } + if _, ok := initEnv.LookupEnv(env.EnableProfiling); ok { + go startDebugServer() + } +} + +func startDebugServer() { + log.Println("Starting http debug server", env.ProfilingServerAddr) + log.Println(http.ListenAndServe(env.ProfilingServerAddr, nil)) //nolint:gosec + // no linted because we don't need to set timeouts for the debug server. +} diff --git a/vendor/github.com/grafana/xk6-browser/browser/modulevu.go b/vendor/github.com/grafana/xk6-browser/browser/modulevu.go index 3c38d2aa39c..bf353c4569f 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/modulevu.go +++ b/vendor/github.com/grafana/xk6-browser/browser/modulevu.go @@ -3,6 +3,7 @@ package browser import ( "context" + "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/k6ext" k6modules "go.k6.io/k6/js/modules" @@ -16,6 +17,12 @@ type moduleVU struct { k6modules.VU *pidRegistry + *browserRegistry +} + +// browser returns the VU browser instance for the current iteration. +func (vu moduleVU) browser() (api.Browser, error) { + return vu.browserRegistry.getBrowser(vu.State().Iteration) } func (vu moduleVU) Context() context.Context { diff --git a/vendor/github.com/grafana/xk6-browser/browser/pidregistry.go b/vendor/github.com/grafana/xk6-browser/browser/pidregistry.go deleted file mode 100644 index a505c53d0d0..00000000000 --- a/vendor/github.com/grafana/xk6-browser/browser/pidregistry.go +++ /dev/null @@ -1,28 +0,0 @@ -package browser - -import "sync" - -// pidRegistry keeps track of the launched browser process IDs. -type pidRegistry struct { - mu sync.RWMutex - ids []int -} - -// registerPid registers the launched browser process ID. -func (r *pidRegistry) registerPid(pid int) { - r.mu.Lock() - defer r.mu.Unlock() - - r.ids = append(r.ids, pid) -} - -// Pids returns the launched browser process IDs. -func (r *pidRegistry) Pids() []int { - r.mu.RLock() - defer r.mu.RUnlock() - - pids := make([]int, len(r.ids)) - copy(pids, r.ids) - - return pids -} diff --git a/vendor/github.com/grafana/xk6-browser/browser/registry.go b/vendor/github.com/grafana/xk6-browser/browser/registry.go new file mode 100644 index 00000000000..b4eb283861f --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/browser/registry.go @@ -0,0 +1,340 @@ +package browser + +import ( + "context" + "crypto/rand" + "encoding/json" + "errors" + "fmt" + "math/big" + "strconv" + "strings" + "sync" + "sync/atomic" + + "github.com/grafana/xk6-browser/api" + "github.com/grafana/xk6-browser/chromium" + "github.com/grafana/xk6-browser/env" + "github.com/grafana/xk6-browser/k6ext" + + k6event "go.k6.io/k6/event" + k6modules "go.k6.io/k6/js/modules" +) + +// errBrowserNotFoundInRegistry indicates that the browser instance +// for the iteration, which should have been initialized as a result +// of the IterStart event, has not been found in the registry. This +// might happen if browser type option is not set in scenario definition. +var errBrowserNotFoundInRegistry = errors.New("browser not found in registry. " + + "make sure to set browser type option in scenario definition in order to use the browser module") + +// pidRegistry keeps track of the launched browser process IDs. +type pidRegistry struct { + mu sync.RWMutex + ids []int +} + +// registerPid registers the launched browser process ID. +func (r *pidRegistry) registerPid(pid int) { + r.mu.Lock() + defer r.mu.Unlock() + + r.ids = append(r.ids, pid) +} + +// Pids returns the launched browser process IDs. +func (r *pidRegistry) Pids() []int { + r.mu.RLock() + defer r.mu.RUnlock() + + pids := make([]int, len(r.ids)) + copy(pids, r.ids) + + return pids +} + +// remoteRegistry contains the details of the remote web browsers. +// At the moment it's the WS URLs. +type remoteRegistry struct { + isRemote bool + wsURLs []string +} + +// newRemoteRegistry will create a new RemoteRegistry. This will +// parse the K6_BROWSER_WS_URL env var to retrieve the defined +// list of WS URLs. +// +// K6_BROWSER_WS_URL can be defined as a single WS URL or a +// comma separated list of URLs. +func newRemoteRegistry(envLookup env.LookupFunc) (*remoteRegistry, error) { + r := &remoteRegistry{} + + isRemote, wsURLs, err := checkForScenarios(envLookup) + if err != nil { + return nil, err + } + if isRemote { + r.isRemote = isRemote + r.wsURLs = wsURLs + return r, nil + } + + r.isRemote, r.wsURLs = checkForBrowserWSURLs(envLookup) + + return r, nil +} + +func checkForBrowserWSURLs(envLookup env.LookupFunc) (bool, []string) { + wsURL, isRemote := envLookup(env.WebSocketURLs) + if !isRemote { + return false, nil + } + + if !strings.ContainsRune(wsURL, ',') { + return true, []string{wsURL} + } + + // 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] + } + + return true, parts +} + +// checkForScenarios will parse the K6_INSTANCE_SCENARIOS env var if +// it has been defined. +func checkForScenarios(envLookup env.LookupFunc) (bool, []string, error) { + scenariosJSON, isRemote := envLookup(env.InstanceScenarios) + if !isRemote { + return false, nil, nil + } + // prevent failing in unquoting empty string. + if scenariosJSON == "" { + return false, nil, nil + } + scenariosJSON, err := strconv.Unquote(scenariosJSON) + if err != nil { + return false, nil, fmt.Errorf("unqouting K6_INSTANCE_SCENARIOS: %w", err) + } + + var scenarios []struct { + ID string `json:"id"` + Browsers []struct { + Handle string `json:"handle"` + } `json:"browsers"` + } + if err := json.Unmarshal([]byte(scenariosJSON), &scenarios); err != nil { + return false, nil, fmt.Errorf("parsing K6_INSTANCE_SCENARIOS: %w", err) + } + + var wsURLs []string + for _, s := range scenarios { + for _, b := range s.Browsers { + if strings.TrimSpace(b.Handle) == "" { + continue + } + wsURLs = append(wsURLs, b.Handle) + } + } + if len(wsURLs) == 0 { + return false, wsURLs, nil + } + + return true, wsURLs, nil +} + +// isRemoteBrowser returns a WS URL and true when a WS URL is defined, +// otherwise it returns an empty string and false. If more than one +// WS URL was registered in newRemoteRegistry, a randomly chosen URL from +// the list in a round-robin fashion is selected and returned. +func (r *remoteRegistry) isRemoteBrowser() (string, bool) { + if !r.isRemote { + return "", false + } + + // Choose a random WS URL from the provided list + i, _ := rand.Int(rand.Reader, big.NewInt(int64(len(r.wsURLs)))) + wsURL := r.wsURLs[i.Int64()] + + return wsURL, true +} + +// browserRegistry stores a single VU browser instances +// indexed per iteration. +type browserRegistry struct { + vu k6modules.VU + + mu sync.RWMutex + m map[int64]api.Browser + + buildFn browserBuildFunc + + stopped atomic.Bool // testing purposes +} + +type browserBuildFunc func(ctx context.Context) (api.Browser, error) + +func newBrowserRegistry(vu k6modules.VU, remote *remoteRegistry, pids *pidRegistry) *browserRegistry { + bt := chromium.NewBrowserType(vu) + builder := func(ctx context.Context) (api.Browser, error) { + var ( + err error + b api.Browser + wsURL, isRemoteBrowser = remote.isRemoteBrowser() + ) + + if isRemoteBrowser { + b, err = bt.Connect(ctx, wsURL) + if err != nil { + return nil, err //nolint:wrapcheck + } + } else { + var pid int + b, pid, err = bt.Launch(ctx) + if err != nil { + return nil, err //nolint:wrapcheck + } + pids.registerPid(pid) + } + + return b, nil + } + + r := &browserRegistry{ + vu: vu, + m: make(map[int64]api.Browser), + buildFn: builder, + } + + exitSubID, exitCh := vu.Events().Global.Subscribe( + k6event.Exit, + ) + go r.handleExitEvent(exitCh) + + iterSubID, eventsCh := vu.Events().Local.Subscribe( + k6event.IterStart, + k6event.IterEnd, + ) + unsubscribe := func() { + vu.Events().Local.Unsubscribe(iterSubID) + vu.Events().Global.Unsubscribe(exitSubID) + } + go r.handleIterEvents(eventsCh, unsubscribe) + + return r +} + +func (r *browserRegistry) handleIterEvents(eventsCh <-chan *k6event.Event, unsubscribeFn func()) { + var ( + ok bool + data k6event.IterData + ctx = context.Background() + vuCtx = k6ext.WithVU(r.vu.Context(), r.vu) + ) + + for e := range eventsCh { + // If browser module is imported in the test, NewModuleInstance will be called for + // every VU. Because on VU init stage we can not distinguish to which scenario it + // belongs or access its options (because state is nil), we have to always subscribe + // to each VU iter events, including VUs that do not make use of the browser in their + // iterations. + // Therefore, if we get an event that does not correspond to a browser iteration, then + // unsubscribe for the VU events and exit the loop in order to reduce unuseful overhead. + if !isBrowserIter(r.vu) { + unsubscribeFn() + r.stop() + e.Done() + return + } + + if data, ok = e.Data.(k6event.IterData); !ok { + e.Done() + k6ext.Abort(vuCtx, "unexpected iteration event data format: %v", e.Data) + // Continue so we don't block the k6 event system producer. + // Test will be aborted by k6, which will previously send the + // 'Exit' event so browser resources cleanup can be guaranteed. + continue + } + + switch e.Type { //nolint:exhaustive + case k6event.IterStart: + b, err := r.buildFn(ctx) + if err != nil { + e.Done() + k6ext.Abort(vuCtx, "error building browser on IterStart: %v", err) + // Continue so we don't block the k6 event system producer. + // Test will be aborted by k6, which will previously send the + // 'Exit' event so browser resources cleanup can be guaranteed. + continue + } + r.setBrowser(data.Iteration, b) + case k6event.IterEnd: + r.deleteBrowser(data.Iteration) + default: + r.vu.State().Logger.Warnf("received unexpected event type: %v", e.Type) + } + + e.Done() + } +} + +func (r *browserRegistry) handleExitEvent(exitCh <-chan *k6event.Event) { + e, ok := <-exitCh + if !ok { + return + } + defer e.Done() + r.clear() +} + +func (r *browserRegistry) setBrowser(id int64, b api.Browser) { + r.mu.Lock() + defer r.mu.Unlock() + + r.m[id] = b +} + +func (r *browserRegistry) getBrowser(id int64) (api.Browser, error) { + r.mu.RLock() + defer r.mu.RUnlock() + + if b, ok := r.m[id]; ok { + return b, nil + } + + return nil, errBrowserNotFoundInRegistry +} + +func (r *browserRegistry) deleteBrowser(id int64) { + r.mu.Lock() + defer r.mu.Unlock() + + if b, ok := r.m[id]; ok { + b.Close() + delete(r.m, id) + } +} + +func (r *browserRegistry) clear() { + r.mu.Lock() + defer r.mu.Unlock() + + for id, b := range r.m { + b.Close() + delete(r.m, id) + } +} + +func (r *browserRegistry) stop() { + r.stopped.Store(true) +} + +func isBrowserIter(vu k6modules.VU) bool { + opts := k6ext.GetScenarioOpts(vu.Context(), vu) + _, ok := opts["type"] // Check if browser type option is set + return ok +} diff --git a/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go b/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go index ba6b9fa9768..33366a2adba 100644 --- a/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go +++ b/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "math/rand" - "os" "os/exec" "path/filepath" "sort" @@ -15,6 +14,7 @@ import ( "github.com/grafana/xk6-browser/api" "github.com/grafana/xk6-browser/common" + "github.com/grafana/xk6-browser/env" "github.com/grafana/xk6-browser/k6ext" "github.com/grafana/xk6-browser/log" "github.com/grafana/xk6-browser/storage" @@ -26,72 +26,71 @@ import ( "github.com/dop251/goja" ) -// Ensure BrowserType implements the api.BrowserType interface. -var _ api.BrowserType = &BrowserType{} - // BrowserType provides methods to launch a Chrome browser instance or connect to an existing one. // It's the entry point for interacting with the browser. type BrowserType struct { // FIXME: This is only exported because testBrowser needs it. Contexts // shouldn't be stored on structs if we can avoid it. - Ctx context.Context - vu k6modules.VU - hooks *common.Hooks - k6Metrics *k6ext.CustomMetrics - execPath string // path to the Chromium executable - randSrc *rand.Rand + Ctx context.Context + vu k6modules.VU + hooks *common.Hooks + k6Metrics *k6ext.CustomMetrics + execPath string // path to the Chromium executable + randSrc *rand.Rand + envLookupper env.LookupFunc } // NewBrowserType registers our custom k6 metrics, creates method mappings on // the goja runtime, and returns a new Chrome browser type. -func NewBrowserType(vu k6modules.VU) api.BrowserType { +func NewBrowserType(vu k6modules.VU) *BrowserType { // NOTE: vu.InitEnv() *must* be called from the script init scope, // otherwise it will return nil. - k6m := k6ext.RegisterCustomMetrics(vu.InitEnv().Registry) - b := BrowserType{ - vu: vu, - hooks: common.NewHooks(), - k6Metrics: k6m, - randSrc: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint: gosec - } + env := vu.InitEnv() - return &b + return &BrowserType{ + vu: vu, + hooks: common.NewHooks(), + k6Metrics: k6ext.RegisterCustomMetrics(env.Registry), + randSrc: rand.New(rand.NewSource(time.Now().UnixNano())), //nolint: gosec + envLookupper: env.LookupEnv, + } } func (b *BrowserType) init( - opts goja.Value, isRemoteBrowser bool, -) (context.Context, *common.LaunchOptions, *log.Logger, error) { - ctx := b.initContext() + ctx context.Context, isRemoteBrowser bool, +) (context.Context, *common.BrowserOptions, *log.Logger, error) { + ctx = b.initContext(ctx) - logger, err := makeLogger(ctx) + logger, err := makeLogger(ctx, b.envLookupper) if err != nil { return nil, nil, nil, fmt.Errorf("error setting up logger: %w", err) } - var launchOpts *common.LaunchOptions + var browserOpts *common.BrowserOptions if isRemoteBrowser { - launchOpts = common.NewRemoteBrowserLaunchOptions() + browserOpts = common.NewRemoteBrowserOptions() } else { - launchOpts = common.NewLaunchOptions() + browserOpts = common.NewLocalBrowserOptions() } - if err = launchOpts.Parse(ctx, logger, opts); err != nil { - return nil, nil, nil, fmt.Errorf("error parsing launch options: %w", err) + opts := k6ext.GetScenarioOpts(b.vu.Context(), b.vu) + if err = browserOpts.Parse(ctx, logger, opts, b.envLookupper); err != nil { + return nil, nil, nil, fmt.Errorf("error parsing browser options: %w", err) } - ctx = common.WithLaunchOptions(ctx, launchOpts) + ctx = common.WithBrowserOptions(ctx, browserOpts) - if err := logger.SetCategoryFilter(launchOpts.LogCategoryFilter); err != nil { + if err := logger.SetCategoryFilter(browserOpts.LogCategoryFilter); err != nil { return nil, nil, nil, fmt.Errorf("error setting category filter: %w", err) } - if launchOpts.Debug { + if browserOpts.Debug { _ = logger.SetLevel("debug") } - return ctx, launchOpts, logger, nil + return ctx, browserOpts, logger, nil } -func (b *BrowserType) initContext() context.Context { - ctx := k6ext.WithVU(b.vu.Context(), b.vu) +func (b *BrowserType) initContext(ctx context.Context) context.Context { + ctx = k6ext.WithVU(ctx, b.vu) ctx = k6ext.WithCustomMetrics(ctx, b.k6Metrics) ctx = common.WithHooks(ctx, b.hooks) ctx = common.WithIterationID(ctx, fmt.Sprintf("%x", b.randSrc.Uint64())) @@ -99,28 +98,28 @@ func (b *BrowserType) initContext() context.Context { } // Connect attaches k6 browser to an existing browser instance. -func (b *BrowserType) Connect(wsEndpoint string, opts goja.Value) api.Browser { - ctx, launchOpts, logger, err := b.init(opts, true) +func (b *BrowserType) Connect(ctx context.Context, wsEndpoint string) (api.Browser, error) { + ctx, browserOpts, logger, err := b.init(ctx, true) if err != nil { - k6ext.Panic(ctx, "initializing browser type: %w", err) + return nil, fmt.Errorf("initializing browser type: %w", err) } - bp, err := b.connect(ctx, wsEndpoint, launchOpts, logger) + bp, err := b.connect(ctx, wsEndpoint, browserOpts, logger) if err != nil { err = &k6ext.UserFriendlyError{ Err: err, - Timeout: launchOpts.Timeout, + Timeout: browserOpts.Timeout, } - k6ext.Panic(ctx, "%w", err) + return nil, fmt.Errorf("%w", err) } - return bp + return bp, nil } func (b *BrowserType) connect( - ctx context.Context, wsURL string, opts *common.LaunchOptions, logger *log.Logger, + ctx context.Context, wsURL string, opts *common.BrowserOptions, logger *log.Logger, ) (*common.Browser, error) { - browserProc, err := b.link(ctx, wsURL, opts, logger) + browserProc, err := b.link(ctx, wsURL, logger) if browserProc == nil { return nil, fmt.Errorf("connecting to browser: %w", err) } @@ -140,10 +139,9 @@ func (b *BrowserType) connect( } func (b *BrowserType) link( - ctx context.Context, wsURL string, - opts *common.LaunchOptions, logger *log.Logger, + ctx context.Context, wsURL string, logger *log.Logger, ) (*common.BrowserProcess, error) { - bProcCtx, bProcCtxCancel := context.WithTimeout(ctx, opts.Timeout) + bProcCtx, bProcCtxCancel := context.WithCancel(ctx) p, err := common.NewRemoteBrowserProcess(bProcCtx, wsURL, bProcCtxCancel, logger) if err != nil { bProcCtxCancel() @@ -155,37 +153,34 @@ func (b *BrowserType) link( // Launch allocates a new Chrome browser process and returns a new api.Browser value, // which can be used for controlling the Chrome browser. -func (b *BrowserType) Launch(opts goja.Value) (_ api.Browser, browserProcessID int) { - ctx, launchOpts, logger, err := b.init(opts, false) +func (b *BrowserType) Launch(ctx context.Context) (_ api.Browser, browserProcessID int, _ error) { + ctx, browserOpts, logger, err := b.init(ctx, false) if err != nil { - k6ext.Panic(ctx, "initializing browser type: %w", err) + return nil, 0, fmt.Errorf("initializing browser type: %w", err) } - bp, pid, err := b.launch(ctx, launchOpts, logger) + bp, pid, err := b.launch(ctx, browserOpts, logger) if err != nil { err = &k6ext.UserFriendlyError{ Err: err, - Timeout: launchOpts.Timeout, + Timeout: browserOpts.Timeout, } - k6ext.Panic(ctx, "%w", err) + return nil, 0, fmt.Errorf("%w", err) } - return bp, pid + return bp, pid, nil } func (b *BrowserType) launch( - ctx context.Context, opts *common.LaunchOptions, logger *log.Logger, + ctx context.Context, opts *common.BrowserOptions, logger *log.Logger, ) (_ *common.Browser, pid int, _ error) { - envs := make([]string, 0, len(opts.Env)) - for k, v := range opts.Env { - envs = append(envs, fmt.Sprintf("%s=%s", k, v)) - } flags, err := prepareFlags(opts, &(b.vu.State()).Options) if err != nil { return nil, 0, fmt.Errorf("%w", err) } + dataDir := &storage.Dir{} - if err := dataDir.Make("", flags["user-data-dir"]); err != nil { + if err := dataDir.Make(b.tmpdir(), flags["user-data-dir"]); err != nil { return nil, 0, fmt.Errorf("%w", err) } flags["user-data-dir"] = dataDir.Dir @@ -204,7 +199,7 @@ func (b *BrowserType) launch( <-c.Done() }(ctx) - browserProc, err := b.allocate(ctx, opts, flags, envs, dataDir, logger) + browserProc, err := b.allocate(ctx, opts, flags, dataDir, logger) if browserProc == nil { return nil, 0, fmt.Errorf("launching browser: %w", err) } @@ -222,6 +217,14 @@ func (b *BrowserType) launch( return browser, browserProc.Pid(), nil } +// tmpdir returns the temporary directory to use for the browser. +// It returns the value of the TMPDIR environment variable if set, +// otherwise it returns an empty string. +func (b *BrowserType) tmpdir() string { + dir, _ := b.envLookupper("TMPDIR") + return dir +} + // LaunchPersistentContext launches the browser with persistent storage. func (b *BrowserType) LaunchPersistentContext(userDataDir string, opts goja.Value) api.Browser { rt := b.vu.Runtime() @@ -236,11 +239,11 @@ func (b *BrowserType) Name() string { // allocate starts a new Chromium browser process and returns it. func (b *BrowserType) allocate( - ctx context.Context, opts *common.LaunchOptions, - flags map[string]any, env []string, dataDir *storage.Dir, + ctx context.Context, opts *common.BrowserOptions, + flags map[string]any, dataDir *storage.Dir, logger *log.Logger, ) (_ *common.BrowserProcess, rerr error) { - bProcCtx, bProcCtxCancel := context.WithTimeout(ctx, opts.Timeout) + bProcCtx, bProcCtxCancel := context.WithCancel(ctx) defer func() { if rerr != nil { bProcCtxCancel() @@ -257,7 +260,7 @@ func (b *BrowserType) allocate( path = b.ExecutablePath() } - return common.NewLocalBrowserProcess(bProcCtx, path, args, env, dataDir, bProcCtxCancel, logger) //nolint: wrapcheck + return common.NewLocalBrowserProcess(bProcCtx, path, args, dataDir, bProcCtxCancel, logger) //nolint: wrapcheck } // ExecutablePath returns the path where the extension expects to find the browser executable. @@ -269,7 +272,7 @@ func (b *BrowserType) ExecutablePath() (execPath string) { b.execPath = execPath }() - for _, path := range [...]string{ + paths := []string{ // Unix-like "headless_shell", "headless-shell", @@ -280,18 +283,19 @@ func (b *BrowserType) ExecutablePath() (execPath string) { "google-chrome-beta", "google-chrome-unstable", "/usr/bin/google-chrome", - // Windows "chrome", "chrome.exe", // in case PATHEXT is misconfigured `C:\Program Files (x86)\Google\Chrome\Application\chrome.exe`, `C:\Program Files\Google\Chrome\Application\chrome.exe`, - filepath.Join(os.Getenv("USERPROFILE"), `AppData\Local\Google\Chrome\Application\chrome.exe`), - // Mac (from https://commondatastorage.googleapis.com/chromium-browser-snapshots/index.html?prefix=Mac/857950/) "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", "/Applications/Chromium.app/Contents/MacOS/Chromium", - } { + } + if userProfile, ok := b.envLookupper("USERPROFILE"); ok { + paths = append(paths, filepath.Join(userProfile, `AppData\Local\Google\Chrome\Application\chrome.exe`)) + } + for _, path := range paths { if _, err := exec.LookPath(path); err == nil { return path } @@ -322,12 +326,12 @@ func parseArgs(flags map[string]any) ([]string, error) { // Force the first page to be blank, instead of the welcome page; // --no-first-run doesn't enforce that. - // args = append(args, "about:blank") + // args = append(args, common.BlankPage) // args = append(args, "--no-startup-window") return args, nil } -func prepareFlags(lopts *common.LaunchOptions, k6opts *k6lib.Options) (map[string]any, error) { +func prepareFlags(lopts *common.BrowserOptions, k6opts *k6lib.Options) (map[string]any, error) { // After Puppeteer's and Playwright's default behavior. f := map[string]any{ "disable-background-networking": true, @@ -354,11 +358,10 @@ func prepareFlags(lopts *common.LaunchOptions, k6opts *k6lib.Options) (map[strin "use-mock-keychain": true, "no-service-autorun": true, - "no-startup-window": true, - "no-default-browser-check": true, - "headless": lopts.Headless, - "auto-open-devtools-for-tabs": lopts.Devtools, - "window-size": fmt.Sprintf("%d,%d", 800, 600), + "no-startup-window": true, + "no-default-browser-check": true, + "headless": lopts.Headless, + "window-size": fmt.Sprintf("%d,%d", 800, 600), } if lopts.Headless { f["hide-scrollbars"] = true @@ -440,12 +443,12 @@ func setFlagsFromK6Options(flags map[string]any, k6opts *k6lib.Options) error { } // makeLogger makes and returns an extension wide logger. -func makeLogger(ctx context.Context) (*log.Logger, error) { +func makeLogger(ctx context.Context, envLookup env.LookupFunc) (*log.Logger, error) { var ( k6Logger = k6ext.GetVU(ctx).State().Logger logger = log.New(k6Logger, common.GetIterationID(ctx)) ) - if el, ok := os.LookupEnv("XK6_BROWSER_LOG"); ok { + if el, ok := envLookup(env.LogLevel); ok { if logger.SetLevel(el) != nil { return nil, fmt.Errorf( "invalid log level %q, should be one of: panic, fatal, error, warn, warning, info, debug, trace", @@ -453,7 +456,7 @@ func makeLogger(ctx context.Context) (*log.Logger, error) { ) } } - if _, ok := os.LookupEnv("XK6_BROWSER_CALLER"); ok { + if _, ok := envLookup(env.LogCaller); ok { logger.ReportCaller() } diff --git a/vendor/github.com/grafana/xk6-browser/common/browser.go b/vendor/github.com/grafana/xk6-browser/common/browser.go index 3f0a845e417..b9d9f4cf4e6 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser.go @@ -25,8 +25,7 @@ import ( // Ensure Browser implements the EventEmitter and Browser interfaces. var ( - _ EventEmitter = &Browser{} - _ api.Browser = &Browser{} + _ api.Browser = &Browser{} ) const ( @@ -36,22 +35,19 @@ const ( // Browser stores a Browser context. type Browser struct { - BaseEventEmitter - ctx context.Context cancelFn context.CancelFunc state int64 browserProc *BrowserProcess - launchOpts *LaunchOptions + browserOpts *BrowserOptions // Connection to the browser to talk CDP protocol. // A *Connection is saved to this field, see: connect(). conn connection - contextsMu sync.RWMutex - contexts map[cdp.BrowserContextID]*BrowserContext + context *BrowserContext defaultContext *BrowserContext // Cancel function to stop event listening @@ -78,10 +74,10 @@ func NewBrowser( ctx context.Context, cancel context.CancelFunc, browserProc *BrowserProcess, - launchOpts *LaunchOptions, + browserOpts *BrowserOptions, logger *log.Logger, ) (*Browser, error) { - b := newBrowser(ctx, cancel, browserProc, launchOpts, logger) + b := newBrowser(ctx, cancel, browserProc, browserOpts, logger) if err := b.connect(); err != nil { return nil, err } @@ -93,17 +89,15 @@ func newBrowser( ctx context.Context, cancelFn context.CancelFunc, browserProc *BrowserProcess, - launchOpts *LaunchOptions, + browserOpts *BrowserOptions, logger *log.Logger, ) *Browser { return &Browser{ - BaseEventEmitter: NewBaseEventEmitter(ctx), ctx: ctx, cancelFn: cancelFn, state: int64(BrowserStateOpen), browserProc: browserProc, - launchOpts: launchOpts, - contexts: make(map[cdp.BrowserContextID]*BrowserContext), + browserOpts: browserOpts, pages: make(map[target.ID]*Page), sessionIDtoTargetID: make(map[target.SessionID]target.ID), vu: k6ext.GetVU(ctx), @@ -137,23 +131,19 @@ func (b *Browser) disposeContext(id cdp.BrowserContextID) error { return fmt.Errorf("disposing browser context ID %s: %w", id, err) } - b.contextsMu.Lock() - defer b.contextsMu.Unlock() - delete(b.contexts, id) + b.context = nil return nil } -// getDefaultBrowserContextOrByID returns the BrowserContext for the given page ID. +// getDefaultBrowserContextOrMatchedID returns the BrowserContext for the given browser context 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) getDefaultBrowserContextOrMatchedID(id cdp.BrowserContextID) *BrowserContext { + if b.context == nil || b.context.id != id { + return b.defaultContext + } + + return b.context } func (b *Browser) getPages() []*Page { @@ -227,7 +217,7 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) { var ( targetPage = ev.TargetInfo - browserCtx = b.getDefaultBrowserContextOrByID(targetPage.BrowserContextID) + browserCtx = b.getDefaultBrowserContextOrMatchedID(targetPage.BrowserContextID) ) if !b.isAttachedPageValid(ev, browserCtx) { @@ -235,7 +225,7 @@ func (b *Browser) onAttachedToTarget(ev *target.EventAttachedToTarget) { } session := b.conn.getSession(ev.SessionID) if session == nil { - b.logger.Warnf("Browser:onAttachedToTarget", + b.logger.Debugf("Browser:onAttachedToTarget", "session closed before attachToTarget is handled. sid:%v tid:%v", ev.SessionID, targetPage.TargetID) return // ignore @@ -377,14 +367,11 @@ func (b *Browser) onDetachedFromTarget(ev *target.EventDetachedFromTarget) { } func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) { - b.contextsMu.RLock() - browserCtx, ok := b.contexts[id] - b.contextsMu.RUnlock() - if !ok { - return nil, fmt.Errorf("missing browser context: %s", id) + if b.context == nil || b.context.id != id { + return nil, fmt.Errorf("missing browser context %s, current context is %s", id, b.context.id) } - ctx, cancel := context.WithTimeout(b.ctx, b.launchOpts.Timeout) + ctx, cancel := context.WithTimeout(b.ctx, b.browserOpts.Timeout) defer cancel() // buffer of one is for sending the target ID whether an event handler @@ -393,7 +380,7 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) { waitForPage, removeEventHandler := createWaitForEventHandler( ctx, - browserCtx, // browser context will emit the following event: + b.context, // browser context will emit the following event: []string{EventBrowserContextPage}, func(e any) bool { tid := <-targetID @@ -408,7 +395,7 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) { defer removeEventHandler() // create a new page. - action := target.CreateTarget("about:blank").WithBrowserContextID(id) + action := target.CreateTarget(BlankPage).WithBrowserContextID(id) tid, err := action.Do(cdp.WithExecutor(ctx, b.conn)) if err != nil { return nil, fmt.Errorf("creating a new blank page: %w", err) @@ -425,7 +412,7 @@ func (b *Browser) newPageInContext(id cdp.BrowserContextID) (*Page, error) { case <-ctx.Done(): err = &k6ext.UserFriendlyError{ Err: ctx.Err(), - Timeout: b.launchOpts.Timeout, + Timeout: b.browserOpts.Timeout, } b.logger.Debugf("Browser:newPageInContext:<-ctx.Done", "tid:%v bctxid:%v err:%v", tid, id, err) } @@ -462,7 +449,7 @@ func (b *Browser) Close() { // If the browser is not being executed remotely, send the Browser.close CDP // command, which triggers the browser process to exit. - if !b.launchOpts.isRemoteBrowser { + if !b.browserOpts.isRemoteBrowser { var closeErr *websocket.CloseError err := cdpbrowser.Close().Do(cdp.WithExecutor(b.ctx, b.conn)) if err != nil && !errors.As(err, &closeErr) { @@ -492,17 +479,9 @@ func (b *Browser) Close() { b.conn.Close() } -// Contexts returns list of browser contexts. -func (b *Browser) Contexts() []api.BrowserContext { - b.contextsMu.RLock() - defer b.contextsMu.RUnlock() - - contexts := make([]api.BrowserContext, 0, len(b.contexts)) - for _, b := range b.contexts { - contexts = append(contexts, b) - } - - return contexts +// Context returns the current browser context or nil. +func (b *Browser) Context() api.BrowserContext { + return b.context } // IsConnected returns whether the WebSocket connection to the browser process @@ -513,6 +492,10 @@ func (b *Browser) IsConnected() bool { // NewContext creates a new incognito-like browser context. func (b *Browser) NewContext(opts goja.Value) (api.BrowserContext, error) { + if b.context != nil { + return nil, errors.New("existing browser context must be closed before creating a new one") + } + action := target.CreateBrowserContext().WithDisposeOnDetach(true) browserContextID, err := action.Do(cdp.WithExecutor(b.ctx, b.conn)) b.logger.Debugf("Browser:NewContext", "bctxid:%v", browserContextID) @@ -525,13 +508,11 @@ func (b *Browser) NewContext(opts goja.Value) (api.BrowserContext, error) { k6ext.Panic(b.ctx, "parsing newContext options: %w", err) } - b.contextsMu.Lock() - defer b.contextsMu.Unlock() browserCtx, err := NewBrowserContext(b.ctx, b, browserContextID, browserCtxOpts, b.logger) if err != nil { return nil, fmt.Errorf("new context: %w", err) } - b.contexts[browserContextID] = browserCtx + b.context = browserCtx return browserCtx, nil } diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_context.go b/vendor/github.com/grafana/xk6-browser/common/browser_context.go index b98e8a6daca..82a4540b8fe 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_context.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_context.go @@ -368,7 +368,7 @@ func (b *BrowserContext) WaitForEvent(event string, optsOrPredicate goja.Value) isCallable bool predicateFn goja.Callable // TODO: Find out whether * time.Second is necessary. - timeout = b.browser.launchOpts.Timeout * time.Second //nolint:durationcheck + timeout = b.browser.browserOpts.Timeout * time.Second //nolint:durationcheck ) if gojaValueExists(optsOrPredicate) { switch optsOrPredicate.ExportType() { diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_options.go b/vendor/github.com/grafana/xk6-browser/common/browser_options.go index 0732090c853..2a0c26282aa 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_options.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_options.go @@ -2,74 +2,51 @@ package common import ( "context" + "errors" + "fmt" + "strconv" + "strings" "time" - "github.com/dop251/goja" - - "github.com/grafana/xk6-browser/k6ext" + "github.com/grafana/xk6-browser/env" "github.com/grafana/xk6-browser/log" -) -const ( - optArgs = "args" - optDebug = "debug" - optDevTools = "devtools" - optEnv = "env" - optExecutablePath = "executablePath" - optHeadless = "headless" - optIgnoreDefaultArgs = "ignoreDefaultArgs" - optLogCategoryFilter = "logCategoryFilter" - optProxy = "proxy" - optSlowMo = "slowMo" - optTimeout = "timeout" + "go.k6.io/k6/lib/types" ) -// ProxyOptions allows configuring a proxy server. -type ProxyOptions struct { - Server string - Bypass string - Username string - Password string -} +// Script variables. +const optType = "type" -// LaunchOptions stores browser launch options. -type LaunchOptions struct { +// BrowserOptions stores browser options. +type BrowserOptions struct { Args []string Debug bool - Devtools bool - Env map[string]string ExecutablePath string Headless bool IgnoreDefaultArgs []string LogCategoryFilter string - Proxy ProxyOptions - SlowMo time.Duration - Timeout time.Duration + // TODO: Do not expose slowMo option by now. + // See https://github.com/grafana/xk6-browser/issues/857. + SlowMo time.Duration + Timeout time.Duration isRemoteBrowser bool // some options will be ignored if browser is in a remote machine } -// LaunchPersistentContextOptions stores browser launch options for persistent context. -type LaunchPersistentContextOptions struct { - LaunchOptions - BrowserContextOptions -} - -// NewLaunchOptions returns a new LaunchOptions. -func NewLaunchOptions() *LaunchOptions { - return &LaunchOptions{ - Env: make(map[string]string), +// NewLocalBrowserOptions returns a new BrowserOptions +// for a browser launched in the local machine. +func NewLocalBrowserOptions() *BrowserOptions { + return &BrowserOptions{ Headless: true, LogCategoryFilter: ".*", Timeout: DefaultTimeout, } } -// NewRemoteBrowserLaunchOptions returns a new LaunchOptions +// NewRemoteBrowserOptions returns a new BrowserOptions // for a browser running in a remote machine. -func NewRemoteBrowserLaunchOptions() *LaunchOptions { - return &LaunchOptions{ - Env: make(map[string]string), +func NewRemoteBrowserOptions() *BrowserOptions { + return &BrowserOptions{ Headless: true, LogCategoryFilter: ".*", Timeout: DefaultTimeout, @@ -77,58 +54,58 @@ func NewRemoteBrowserLaunchOptions() *LaunchOptions { } } -// Parse parses launch options from a JS object. -func (l *LaunchOptions) Parse(ctx context.Context, logger *log.Logger, opts goja.Value) error { //nolint:cyclop - // when opts is nil, we just return the default options without error. - if !gojaValueExists(opts) { - return nil +// Parse parses browser options from a JS object. +func (bo *BrowserOptions) Parse( //nolint:cyclop + ctx context.Context, logger *log.Logger, opts map[string]any, envLookup env.LookupFunc, +) error { + // Parse opts + bt, ok := opts[optType] + // Only 'chromium' is supported by now, so return error + // if type option is not set, or if it's set and its value + // is different than 'chromium' + if !ok { + return errors.New("browser type option must be set") } - var ( - rt = k6ext.Runtime(ctx) - o = opts.ToObject(rt) - defaults = map[string]any{ - optEnv: l.Env, - optHeadless: l.Headless, - optLogCategoryFilter: l.LogCategoryFilter, - optTimeout: l.Timeout, - } - ) - for _, k := range o.Keys() { - if l.shouldIgnoreIfBrowserIsRemote(k) { - logger.Warnf("LaunchOptions", "setting %s option is disallowed when browser is remote", k) + if bt != "chromium" { + return fmt.Errorf("unsupported browser type: %s", bt) + } + + // Parse env + envOpts := [...]string{ + env.BrowserArguments, + env.BrowserEnableDebugging, + env.BrowserExecutablePath, + env.BrowserHeadless, + env.BrowserIgnoreDefaultArgs, + env.LogCategoryFilter, + env.BrowserGlobalTimeout, + } + + for _, e := range envOpts { + ev, ok := envLookup(e) + if !ok || ev == "" { continue } - v := o.Get(k) - if v.Export() == nil { - if dv, ok := defaults[k]; ok { - logger.Warnf("LaunchOptions", "%s was null and set to its default: %v", k, dv) - } + if bo.shouldIgnoreIfBrowserIsRemote(e) { + logger.Warnf("BrowserOptions", "setting %s option is disallowed when browser is remote", e) continue } var err error - switch k { - case optArgs: - err = exportOpt(rt, k, v, &l.Args) - case optDebug: - l.Debug, err = parseBoolOpt(k, v) - case optDevTools: - l.Devtools, err = parseBoolOpt(k, v) - case optEnv: - err = exportOpt(rt, k, v, &l.Env) - case optExecutablePath: - l.ExecutablePath, err = parseStrOpt(k, v) - case optHeadless: - l.Headless, err = parseBoolOpt(k, v) - case optIgnoreDefaultArgs: - err = exportOpt(rt, k, v, &l.IgnoreDefaultArgs) - case optLogCategoryFilter: - l.LogCategoryFilter, err = parseStrOpt(k, v) - case optProxy: - err = exportOpt(rt, k, v, &l.Proxy) - case optSlowMo: - l.SlowMo, err = parseTimeOpt(k, v) - case optTimeout: - l.Timeout, err = parseTimeOpt(k, v) + switch e { + case env.BrowserArguments: + bo.Args = parseListOpt(ev) + case env.BrowserEnableDebugging: + bo.Debug, err = parseBoolOpt(e, ev) + case env.BrowserExecutablePath: + bo.ExecutablePath = ev + case env.BrowserHeadless: + bo.Headless, err = parseBoolOpt(e, ev) + case env.BrowserIgnoreDefaultArgs: + bo.IgnoreDefaultArgs = parseListOpt(ev) + case env.LogCategoryFilter: + bo.LogCategoryFilter = ev + case env.BrowserGlobalTimeout: + bo.Timeout, err = parseTimeOpt(e, ev) } if err != nil { return err @@ -138,21 +115,48 @@ func (l *LaunchOptions) Parse(ctx context.Context, logger *log.Logger, opts goja return nil } -func (l *LaunchOptions) shouldIgnoreIfBrowserIsRemote(opt string) bool { - if !l.isRemoteBrowser { +func (bo *BrowserOptions) shouldIgnoreIfBrowserIsRemote(opt string) bool { + if !bo.isRemoteBrowser { return false } shouldIgnoreIfBrowserIsRemote := map[string]struct{}{ - optArgs: {}, - optDevTools: {}, - optEnv: {}, - optExecutablePath: {}, - optHeadless: {}, - optIgnoreDefaultArgs: {}, - optProxy: {}, + env.BrowserArguments: {}, + env.BrowserExecutablePath: {}, + env.BrowserHeadless: {}, + env.BrowserIgnoreDefaultArgs: {}, } _, ignore := shouldIgnoreIfBrowserIsRemote[opt] return ignore } + +func parseBoolOpt(k, v string) (bool, error) { + b, err := strconv.ParseBool(v) + if err != nil { + return false, fmt.Errorf("%s should be a boolean", k) + } + + return b, nil +} + +func parseTimeOpt(k, v string) (time.Duration, error) { + t, err := types.GetDurationValue(v) + if err != nil { + return time.Duration(0), fmt.Errorf("%s should be a time duration value: %w", k, err) + } + + return t, nil +} + +func parseListOpt(v string) []string { + elems := strings.Split(v, ",") + // If last element is a void string, + // because value contained an ending comma, + // remove it + if elems[len(elems)-1] == "" { + elems = elems[:len(elems)-1] + } + + return elems +} diff --git a/vendor/github.com/grafana/xk6-browser/common/browser_process.go b/vendor/github.com/grafana/xk6-browser/common/browser_process.go index 47e0862aba3..0a82bb566e1 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser_process.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser_process.go @@ -35,10 +35,10 @@ type BrowserProcess struct { // NewLocalBrowserProcess starts a local browser process and // returns a new BrowserProcess instance to interact with it. func NewLocalBrowserProcess( - ctx context.Context, path string, args, env []string, dataDir *storage.Dir, + ctx context.Context, path string, args []string, dataDir *storage.Dir, ctxCancel context.CancelFunc, logger *log.Logger, ) (*BrowserProcess, error) { - cmd, err := execute(ctx, ctxCancel, path, args, env, dataDir, logger) + cmd, err := execute(ctx, path, args, dataDir, logger) if err != nil { return nil, err } @@ -151,7 +151,7 @@ type command struct { } func execute( - ctx context.Context, ctxCancel func(), path string, args, env []string, + ctx context.Context, path string, args []string, dataDir *storage.Dir, logger *log.Logger, ) (command, error) { cmd := exec.CommandContext(ctx, path, args...) @@ -166,11 +166,6 @@ func execute( return command{}, fmt.Errorf("%w", err) } - // Set up environment variable for process - if len(env) > 0 { - cmd.Env = append(os.Environ(), env...) - } - // We must start the cmd before calling cmd.Wait, as otherwise the two // can run into a data race. err = cmd.Start() diff --git a/vendor/github.com/grafana/xk6-browser/common/connection.go b/vendor/github.com/grafana/xk6-browser/common/connection.go index 756ce1e9df2..c1cecf9f3e1 100644 --- a/vendor/github.com/grafana/xk6-browser/common/connection.go +++ b/vendor/github.com/grafana/xk6-browser/common/connection.go @@ -28,6 +28,25 @@ const wsWriteBufferSize = 1 << 20 var _ EventEmitter = &Connection{} var _ cdp.Executor = &Connection{} +// Each connection needs its own msgID. A msgID will be used by the +// connection and associated sessions. When a CDP request is made to +// chrome, it's best to work with unique ids to avoid the Execute +// handlers working with the wrong response, or handlers deadlocking +// when their response is rerouted to the wrong handler. +// +// Use the msgIDGenerator interface to abstract `id` away. +type msgID struct { + id int64 +} + +func (m *msgID) newID() int64 { + return atomic.AddInt64(&m.id, 1) +} + +type msgIDGenerator interface { + newID() int64 +} + type executorEmitter interface { cdp.Executor EventEmitter @@ -112,7 +131,7 @@ type Connection struct { done chan struct{} closing chan struct{} shutdownOnce sync.Once - msgID int64 + msgIDGen msgIDGenerator sessionsMu sync.RWMutex sessions map[target.SessionID]*Session @@ -150,7 +169,7 @@ func NewConnection(ctx context.Context, wsURL string, logger *log.Logger) (*Conn errorCh: make(chan error), done: make(chan struct{}), closing: make(chan struct{}), - msgID: 0, + msgIDGen: &msgID{}, sessions: make(map[target.SessionID]*Session), } @@ -316,7 +335,7 @@ func (c *Connection) recvLoop() { sid, tid := eva.SessionID, eva.TargetInfo.TargetID c.sessionsMu.Lock() - session := NewSession(c.ctx, c, sid, tid, c.logger) + session := NewSession(c.ctx, c, sid, tid, c.logger, c.msgIDGen) c.logger.Debugf("Connection:recvLoop:EventAttachedToTarget", "sid:%v tid:%v wsURL:%q", sid, tid, c.wsURL) c.sessions[sid] = session c.sessionsMu.Unlock() @@ -496,7 +515,7 @@ func (c *Connection) Close(args ...goja.Value) { // Execute implements cdproto.Executor and performs a synchronous send and receive. func (c *Connection) Execute(ctx context.Context, method string, params easyjson.Marshaler, res easyjson.Unmarshaler) error { c.logger.Debugf("connection:Execute", "wsURL:%q method:%q", c.wsURL, method) - id := atomic.AddInt64(&c.msgID, 1) + id := c.msgIDGen.newID() // Setup event handler used to block for response to message being sent. ch := make(chan *cdproto.Message, 1) diff --git a/vendor/github.com/grafana/xk6-browser/common/context.go b/vendor/github.com/grafana/xk6-browser/common/context.go index 8efe8689c74..56f8d643d6f 100644 --- a/vendor/github.com/grafana/xk6-browser/common/context.go +++ b/vendor/github.com/grafana/xk6-browser/common/context.go @@ -7,7 +7,7 @@ import ( type ctxKey int const ( - ctxKeyLaunchOptions ctxKey = iota + ctxKeyBrowserOptions ctxKey = iota ctxKeyHooks ctxKeyIterationID ) @@ -35,16 +35,21 @@ func GetIterationID(ctx context.Context) string { return s } -func WithLaunchOptions(ctx context.Context, opts *LaunchOptions) context.Context { - return context.WithValue(ctx, ctxKeyLaunchOptions, opts) +// WithBrowserOptions adds the browser options to the context. +func WithBrowserOptions(ctx context.Context, opts *BrowserOptions) context.Context { + return context.WithValue(ctx, ctxKeyBrowserOptions, opts) } -func GetLaunchOptions(ctx context.Context) *LaunchOptions { - v := ctx.Value(ctxKeyLaunchOptions) +// GetBrowserOptions returns the browser options attached to the context. +func GetBrowserOptions(ctx context.Context) *BrowserOptions { + v := ctx.Value(ctxKeyBrowserOptions) if v == nil { return nil } - return v.(*LaunchOptions) + if bo, ok := v.(*BrowserOptions); ok { + return bo + } + return nil } // contextWithDoneChan returns a new context that is canceled either diff --git a/vendor/github.com/grafana/xk6-browser/common/doc.go b/vendor/github.com/grafana/xk6-browser/common/doc.go new file mode 100644 index 00000000000..ed51123e2fb --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/common/doc.go @@ -0,0 +1,3 @@ +// Package common contains the implementation of API elements that do not +// depend on the browser type. +package common diff --git a/vendor/github.com/grafana/xk6-browser/common/frame.go b/vendor/github.com/grafana/xk6-browser/common/frame.go index e825e354a8e..dc1bfbfa22d 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame.go @@ -305,7 +305,10 @@ func (f *Frame) onLifecycleEvent(event LifecycleEvent) { f.lifecycleEvents[event] = true f.lifecycleEventsMu.Unlock() - f.emit(EventFrameAddLifecycle, event) + f.emit(EventFrameAddLifecycle, FrameLifecycleEvent{ + URL: f.URL(), + Event: event, + }) } func (f *Frame) onLoadingStarted() { @@ -706,9 +709,11 @@ func (f *Frame) dispatchEvent(selector, typ string, eventInit goja.Value, opts * return nil } -// Evaluate will evaluate provided page function within an execution context. -func (f *Frame) Evaluate(pageFunc goja.Value, args ...goja.Value) any { - f.log.Debugf("Frame:Evaluate", "fid:%s furl:%q", f.ID(), f.URL()) +// EvaluateWithContext will evaluate provided page function within an execution context. +// The passed in context will be used instead of the frame's context. The context must +// be a derivative of one that contains the goja runtime. +func (f *Frame) EvaluateWithContext(ctx context.Context, pageFunc goja.Value, args ...goja.Value) (any, error) { + f.log.Debugf("Frame:EvaluateWithContext", "fid:%s furl:%q", f.ID(), f.URL()) f.waitForExecutionContext(mainWorld) @@ -716,12 +721,24 @@ func (f *Frame) Evaluate(pageFunc goja.Value, args ...goja.Value) any { forceCallable: true, returnByValue: true, } - result, err := f.evaluate(f.ctx, mainWorld, opts, pageFunc, args...) + result, err := f.evaluate(ctx, mainWorld, opts, pageFunc, args...) if err != nil { - k6ext.Panic(f.ctx, "evaluating JS: %v", err) + return nil, fmt.Errorf("evaluating JS: %w", err) } - applySlowMo(f.ctx) + applySlowMo(ctx) + + return result, nil +} + +// Evaluate will evaluate provided page function within an execution context. +func (f *Frame) Evaluate(pageFunc goja.Value, args ...goja.Value) any { + f.log.Debugf("Frame:Evaluate", "fid:%s furl:%q", f.ID(), f.URL()) + + result, err := f.EvaluateWithContext(f.ctx, pageFunc, args...) + if err != nil { + k6ext.Panic(f.ctx, "%v", err) + } return result } @@ -1693,8 +1710,8 @@ func (f *Frame) WaitForLoadState(state string, opts goja.Value) { lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler( timeoutCtx, f, []string{EventFrameAddLifecycle}, func(data any) bool { - if le, ok := data.(LifecycleEvent); ok { - return le == waitUntil + if le, ok := data.(FrameLifecycleEvent); ok { + return le.Event == waitUntil } return false }) @@ -1736,8 +1753,8 @@ func (f *Frame) WaitForNavigation(opts goja.Value) (api.Response, error) { lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler( timeoutCtx, f, []string{EventFrameAddLifecycle}, func(data any) bool { - if le, ok := data.(LifecycleEvent); ok { - return le == parsedOpts.WaitUntil + if le, ok := data.(FrameLifecycleEvent); ok { + return le.Event == parsedOpts.WaitUntil } return false }) @@ -1773,7 +1790,7 @@ func (f *Frame) WaitForNavigation(opts goja.Value) (api.Response, error) { sameDocNav = true break } - // request could be nil if navigating to e.g. about:blank + // request could be nil if navigating to e.g. BlankPage. req := e.newDocument.request if req != nil { req.responseMu.RLock() diff --git a/vendor/github.com/grafana/xk6-browser/common/frame_manager.go b/vendor/github.com/grafana/xk6-browser/common/frame_manager.go index fb34edadd82..aee8c8a07f5 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_manager.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_manager.go @@ -238,9 +238,11 @@ func (m *FrameManager) frameNavigated(frameID cdp.FrameID, parentFrameID cdp.Fra m.ID(), frameID, parentFrameID, documentID, name, url, initial) if frame != nil { + m.framesMu.Unlock() for _, child := range frame.ChildFrames() { m.removeFramesRecursively(child.(*Frame)) } + m.framesMu.Lock() } var mainFrame *Frame @@ -592,10 +594,23 @@ func (m *FrameManager) NavigateFrame(frame *Frame, url string, parsedOpts *Frame lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler( timeoutCtx, frame, []string{EventFrameAddLifecycle}, func(data any) bool { - if le, ok := data.(LifecycleEvent); ok { - return le == parsedOpts.WaitUntil + le, ok := data.(FrameLifecycleEvent) + if !ok { + return false } - return false + // skip the initial blank page if we are navigating to a non-blank page. + // otherwise, we will get a lifecycle event for the initial blank page + // and return prematurely before waiting for the navigation to complete. + if url != BlankPage && le.URL == BlankPage { + m.logger.Debugf( + "FrameManager:NavigateFrame:createWaitForEventPredicateHandler", + "fmid:%d fid:%v furl:%s url:%s waitUntil:%s event.lifecycle:%q event.url:%q skipping %s", + fmid, fid, furl, url, parsedOpts.WaitUntil, le.Event, le.URL, BlankPage, + ) + return false + } + + return le.Event == parsedOpts.WaitUntil }) defer lifecycleEvtCancel() @@ -646,7 +661,7 @@ func (m *FrameManager) NavigateFrame(frame *Frame, url string, parsedOpts *Frame case evt := <-navEvtCh: if e, ok := evt.(*NavigationEvent); ok { req := e.newDocument.request - // Request could be nil in case of navigation to e.g. about:blank + // Request could be nil in case of navigation to e.g. BlankPage. if req != nil { req.responseMu.RLock() resp = req.response 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 969b56ab638..fa954b88d18 100644 --- a/vendor/github.com/grafana/xk6-browser/common/frame_session.go +++ b/vendor/github.com/grafana/xk6-browser/common/frame_session.go @@ -76,6 +76,8 @@ func NewFrameSession( ) (_ *FrameSession, err error) { l.Debugf("NewFrameSession", "sid:%v tid:%v", s.ID(), tid) + k6Metrics := k6ext.GetCustomMetrics(ctx) + fs := FrameSession{ ctx: ctx, // TODO: create cancelable context that can be used to cancel and close all child sessions session: s, @@ -89,7 +91,7 @@ func NewFrameSession( eventCh: make(chan Event), childSessions: make(map[cdp.FrameID]*FrameSession), vu: k6ext.GetVU(ctx), - k6Metrics: k6ext.GetCustomMetrics(ctx), + k6Metrics: k6Metrics, logger: l, serializer: l.ConsoleLogFormatterSerializer(), } @@ -98,7 +100,7 @@ func NewFrameSession( if fs.parent != nil { parentNM = fs.parent.networkManager } - fs.networkManager, err = NewNetworkManager(ctx, s, fs.manager, parentNM) + fs.networkManager, err = NewNetworkManager(ctx, k6Metrics, s, fs.manager, parentNM) if err != nil { l.Debugf("NewFrameSession:NewNetworkManager", "sid:%v tid:%v err:%v", s.ID(), tid, err) @@ -304,11 +306,6 @@ func (fs *FrameSession) parseAndEmitWebVitalMetric(object string) error { return fmt.Errorf("metric not registered %q", wv.Name) } - metricRating, ok := fs.k6Metrics.WebVitals[k6ext.ConcatWebVitalNameRating(wv.Name, wv.Rating)] - if !ok { - return fmt.Errorf("metric not registered %q", k6ext.ConcatWebVitalNameRating(wv.Name, wv.Rating)) - } - value, err := wv.Value.Float64() if err != nil { return fmt.Errorf("value couldn't be parsed %q", wv.Value) @@ -320,19 +317,16 @@ func (fs *FrameSession) parseAndEmitWebVitalMetric(object string) error { tags = tags.With("url", wv.URL) } + tags = tags.With("rating", wv.Rating) + now := time.Now() - k6metrics.PushIfNotDone(fs.ctx, state.Samples, k6metrics.ConnectedSamples{ + k6metrics.PushIfNotDone(fs.vu.Context(), state.Samples, k6metrics.ConnectedSamples{ Samples: []k6metrics.Sample{ { TimeSeries: k6metrics.TimeSeries{Metric: metric, Tags: tags}, Value: value, Time: now, }, - { - TimeSeries: k6metrics.TimeSeries{Metric: metricRating, Tags: tags}, - Value: 1, - Time: now, - }, }, }) @@ -588,7 +582,8 @@ func (fs *FrameSession) navigateFrame(frame *Frame, url, referrer string) (strin func (fs *FrameSession) onConsoleAPICalled(event *cdpruntime.EventConsoleAPICalled) { l := fs.serializer. WithTime(event.Timestamp.Time()). - WithField("source", "browser-console-api") + WithField("source", "browser"). + WithField("browser_source", "console-api") /* accessing the state Group while not on the eventloop is racy if s := fs.vu.State(); s.Group.Path != "" { @@ -1076,7 +1071,7 @@ func (fs *FrameSession) updateViewport() error { // add an inset to viewport depending on the operating system. // this won't add an inset if we're running in headless mode. viewport.calculateInset( - fs.page.browserCtx.browser.launchOpts.Headless, + fs.page.browserCtx.browser.browserOpts.Headless, runtime.GOOS, ) action2 := browser.SetWindowBounds(fs.windowID, &browser.Bounds{ diff --git a/vendor/github.com/grafana/xk6-browser/common/hooks.go b/vendor/github.com/grafana/xk6-browser/common/hooks.go index abfa2200502..dfa2340c2c7 100644 --- a/vendor/github.com/grafana/xk6-browser/common/hooks.go +++ b/vendor/github.com/grafana/xk6-browser/common/hooks.go @@ -30,7 +30,7 @@ func applySlowMo(ctx context.Context) { } func defaultSlowMo(ctx context.Context) { - sm := GetLaunchOptions(ctx).SlowMo + sm := GetBrowserOptions(ctx).SlowMo if sm <= 0 { return } diff --git a/vendor/github.com/grafana/xk6-browser/common/js/injected_script.js b/vendor/github.com/grafana/xk6-browser/common/js/injected_script.js index 4ee98c260eb..63ede878f41 100644 --- a/vendor/github.com/grafana/xk6-browser/common/js/injected_script.js +++ b/vendor/github.com/grafana/xk6-browser/common/js/injected_script.js @@ -223,6 +223,10 @@ class InjectedScript { } result.push({ element, capture }); } + + // Explore the Shadow DOM recursively. + const shadowResults = this._exploreShadowDOM(root.element, selector, index, queryCache, capture); + result.push(...shadowResults); } return this._querySelectorRecursively( @@ -233,6 +237,26 @@ class InjectedScript { ); } + _exploreShadowDOM(root, selector, index, queryCache, capture) { + let result = []; + if (root.shadowRoot) { + const shadowRootResults = this._querySelectorRecursively( + [{ element: root.shadowRoot, capture }], + selector, + index, + queryCache + ); + result = result.concat(shadowRootResults); + } + + for (let i = 0; i < root.children.length; i++) { + const childElement = root.children[i]; + result = result.concat(this._exploreShadowDOM(childElement, selector, index, queryCache, capture)); + } + + return result; + } + // Make sure we target an appropriate node in the DOM before performing an action. _retarget(node, behavior) { let element = diff --git a/vendor/github.com/grafana/xk6-browser/common/network_manager.go b/vendor/github.com/grafana/xk6-browser/common/network_manager.go index 7ca44ebf51e..3f8897e92e0 100644 --- a/vendor/github.com/grafana/xk6-browser/common/network_manager.go +++ b/vendor/github.com/grafana/xk6-browser/common/network_manager.go @@ -2,6 +2,7 @@ package common import ( "context" + "errors" "fmt" "net" "net/url" @@ -34,14 +35,15 @@ var _ EventEmitter = &NetworkManager{} type NetworkManager struct { BaseEventEmitter - ctx context.Context - logger *log.Logger - session session - parent *NetworkManager - frameManager *FrameManager - credentials *Credentials - resolver k6netext.Resolver - vu k6modules.VU + ctx context.Context + logger *log.Logger + session session + parent *NetworkManager + frameManager *FrameManager + credentials *Credentials + resolver k6netext.Resolver + vu k6modules.VU + customMetrics *k6ext.CustomMetrics // TODO: manage inflight requests separately (move them between the two maps // as they transition from inflight -> completed) @@ -59,7 +61,7 @@ type NetworkManager struct { // NewNetworkManager creates a new network manager. func NewNetworkManager( - ctx context.Context, s session, fm *FrameManager, parent *NetworkManager, + ctx context.Context, customMetrics *k6ext.CustomMetrics, s session, fm *FrameManager, parent *NetworkManager, ) (*NetworkManager, error) { vu := k6ext.GetVU(ctx) state := vu.State() @@ -80,6 +82,7 @@ func NewNetworkManager( frameManager: fm, resolver: resolver, vu: vu, + customMetrics: customMetrics, reqIDToRequest: make(map[network.RequestID]*Request), attemptedAuth: make(map[fetch.RequestID]bool), extraHTTPHeaders: make(map[string]string), @@ -153,10 +156,10 @@ func (m *NetworkManager) emitRequestMetrics(req *Request) { tags = tags.With("url", req.URL()) } - k6metrics.PushIfNotDone(m.ctx, state.Samples, k6metrics.ConnectedSamples{ + k6metrics.PushIfNotDone(m.vu.Context(), state.Samples, k6metrics.ConnectedSamples{ Samples: []k6metrics.Sample{ { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.DataSent, Tags: tags}, + TimeSeries: k6metrics.TimeSeries{Metric: m.customMetrics.BrowserDataSent, Tags: tags}, Value: float64(req.Size().Total()), Time: req.wallTime, }, @@ -176,6 +179,7 @@ func (m *NetworkManager) emitResponseMetrics(resp *Response, req *Request) { fromCache, fromPreCache, fromSvcWrk bool url = req.url.String() wallTime = time.Now() + failed float64 ) if resp != nil { status = resp.status @@ -187,6 +191,11 @@ func (m *NetworkManager) emitResponseMetrics(resp *Response, req *Request) { fromSvcWrk = resp.fromServiceWorker wallTime = resp.wallTime url = resp.url + // Assuming that a failure is when status + // is not between 200 and 399 (inclusive). + if status < 200 || status > 399 { + failed = 1 + } } else { m.logger.Debugf("NetworkManager:emitResponseMetrics", "response is nil url:%s method:%s", req.url, req.method) @@ -213,20 +222,15 @@ func (m *NetworkManager) emitResponseMetrics(resp *Response, req *Request) { tags = tags.With("from_prefetch_cache", strconv.FormatBool(fromPreCache)) tags = tags.With("from_service_worker", strconv.FormatBool(fromSvcWrk)) - k6metrics.PushIfNotDone(m.ctx, state.Samples, k6metrics.ConnectedSamples{ + k6metrics.PushIfNotDone(m.vu.Context(), state.Samples, k6metrics.ConnectedSamples{ Samples: []k6metrics.Sample{ { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.HTTPReqs, Tags: tags}, - Value: 1, - Time: wallTime, - }, - { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.HTTPReqDuration, Tags: tags}, + TimeSeries: k6metrics.TimeSeries{Metric: m.customMetrics.BrowserHTTPReqDuration, Tags: tags}, Value: k6metrics.D(wallTime.Sub(req.wallTime)), Time: wallTime, }, { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.DataReceived, Tags: tags}, + TimeSeries: k6metrics.TimeSeries{Metric: m.customMetrics.BrowserDataReceived, Tags: tags}, Value: float64(bodySize), Time: wallTime, }, @@ -234,26 +238,11 @@ func (m *NetworkManager) emitResponseMetrics(resp *Response, req *Request) { }) if resp != nil && resp.timing != nil { - k6metrics.PushIfNotDone(m.ctx, state.Samples, k6metrics.ConnectedSamples{ + k6metrics.PushIfNotDone(m.vu.Context(), state.Samples, k6metrics.ConnectedSamples{ Samples: []k6metrics.Sample{ { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.HTTPReqConnecting, Tags: tags}, - Value: k6metrics.D(time.Duration(resp.timing.ConnectEnd-resp.timing.ConnectStart) * time.Millisecond), - Time: wallTime, - }, - { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.HTTPReqTLSHandshaking, Tags: tags}, - Value: k6metrics.D(time.Duration(resp.timing.SslEnd-resp.timing.SslStart) * time.Millisecond), - Time: wallTime, - }, - { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.HTTPReqSending, Tags: tags}, - Value: k6metrics.D(time.Duration(resp.timing.SendEnd-resp.timing.SendStart) * time.Millisecond), - Time: wallTime, - }, - { - TimeSeries: k6metrics.TimeSeries{Metric: state.BuiltinMetrics.HTTPReqReceiving, Tags: tags}, - Value: k6metrics.D(time.Duration(resp.timing.ReceiveHeadersEnd-resp.timing.SendEnd) * time.Millisecond), + TimeSeries: k6metrics.TimeSeries{Metric: m.customMetrics.BrowserHTTPReqFailed, Tags: tags}, + Value: failed, Time: wallTime, }, }, @@ -441,7 +430,7 @@ func (m *NetworkManager) onRequest(event *network.EventRequestWillBeSent, interc m.frameManager.requestStarted(req) } -func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) { +func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) { //nolint:funlen m.logger.Debugf("NetworkManager:onRequestPaused", "sid:%s url:%v", m.session.ID(), event.Request.URL) defer m.logger.Debugf("NetworkManager:onRequestPaused:return", @@ -453,18 +442,31 @@ func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) { if failErr != nil { action := fetch.FailRequest(event.RequestID, network.ErrorReasonBlockedByClient) if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - m.logger.Errorf("NetworkManager:onRequestPaused", - "interrupting request: %s", err) - } else { - m.logger.Warnf("NetworkManager:onRequestPaused", - "request %s %s was interrupted: %s", event.Request.Method, event.Request.URL, failErr) + // Avoid logging as error when context is canceled. + // Most probably this happens when trying to fail a site's background request + // while the iteration is ending and therefore the browser context is being closed. + if errors.Is(err, context.Canceled) { + m.logger.Debug("NetworkManager:onRequestPaused", "context canceled interrupting request") + } else { + m.logger.Errorf("NetworkManager:onRequestPaused", "interrupting request: %s", err) + } return } + m.logger.Warnf("NetworkManager:onRequestPaused", + "request %s %s was interrupted: %s", event.Request.Method, event.Request.URL, failErr) + + return } action := fetch.ContinueRequest(event.RequestID) if err := action.Do(cdp.WithExecutor(m.ctx, m.session)); err != nil { - m.logger.Errorf("NetworkManager:onRequestPaused", - "continuing request: %s", err) + // Avoid logging as error when context is canceled. + // Most probably this happens when trying to continue a site's background request + // while the iteration is ending and therefore the browser context is being closed. + if errors.Is(err, context.Canceled) { + m.logger.Debug("NetworkManager:onRequestPaused", "context canceled continuing request") + return + } + m.logger.Errorf("NetworkManager:onRequestPaused", "continuing request: %s", err) } }() diff --git a/vendor/github.com/grafana/xk6-browser/common/options.go b/vendor/github.com/grafana/xk6-browser/common/options.go deleted file mode 100644 index 9d4e5399cc3..00000000000 --- a/vendor/github.com/grafana/xk6-browser/common/options.go +++ /dev/null @@ -1,60 +0,0 @@ -package common - -import ( - "fmt" - "reflect" - "time" - - "github.com/dop251/goja" - - "go.k6.io/k6/lib/types" -) - -func parseBoolOpt(key string, val goja.Value) (b bool, err error) { - if val.ExportType().Kind() != reflect.Bool { - return false, fmt.Errorf("%s should be a boolean", key) - } - b, _ = val.Export().(bool) - return b, nil -} - -func parseStrOpt(key string, val goja.Value) (s string, err error) { - if val.ExportType().Kind() != reflect.String { - return "", fmt.Errorf("%s should be a string", key) - } - return val.String(), nil -} - -func parseTimeOpt(key string, val goja.Value) (t time.Duration, err error) { - if t, err = types.GetDurationValue(val.String()); err != nil { - return time.Duration(0), fmt.Errorf("%s should be a time duration value: %w", key, err) - } - return -} - -// exportOpt exports src to dst and dynamically returns an error -// depending on the type if an error occurs. Panics if dst is not -// a pointer and not points to a map, struct, or slice. -func exportOpt[T any](rt *goja.Runtime, key string, src goja.Value, dst T) error { - typ := reflect.TypeOf(dst) - if typ.Kind() != reflect.Pointer { - panic("dst should be a pointer") - } - kind := typ.Elem().Kind() - s, ok := map[reflect.Kind]string{ - reflect.Map: "a map", - reflect.Struct: "an object", - reflect.Slice: "an array of", - }[kind] - if !ok { - panic("dst should be one of: map, struct, slice") - } - if err := rt.ExportTo(src, dst); err != nil { - if kind == reflect.Slice { - s += fmt.Sprintf(" %ss", typ.Elem().Elem()) - } - return fmt.Errorf("%s should be %s: %w", key, s, err) - } - - return nil -} diff --git a/vendor/github.com/grafana/xk6-browser/common/page.go b/vendor/github.com/grafana/xk6-browser/common/page.go index 4f888c66877..40df9f0d404 100644 --- a/vendor/github.com/grafana/xk6-browser/common/page.go +++ b/vendor/github.com/grafana/xk6-browser/common/page.go @@ -425,13 +425,22 @@ func (p *Page) Click(selector string, opts goja.Value) error { func (p *Page) Close(opts goja.Value) error { p.logger.Debugf("Page:Close", "sid:%v", p.sessionID()) + // forcing the pagehide event to trigger web vitals metrics. + v := p.vu.Runtime().ToValue(`() => window.dispatchEvent(new Event('pagehide'))`) + ctx, cancel := context.WithTimeout(p.ctx, p.defaultTimeout()) + defer cancel() + _, err := p.MainFrame().EvaluateWithContext(ctx, v) + if err != nil { + p.logger.Warnf("Page:Close", "failed to hide page: %v", err) + } + add := runtime.RemoveBinding(webVitalBinding) if err := add.Do(cdp.WithExecutor(p.ctx, p.session)); err != nil { return fmt.Errorf("internal error while removing binding from page: %w", err) } action := target.CloseTarget(p.targetID) - err := action.Do(cdp.WithExecutor(p.ctx, p.session)) + err = action.Do(cdp.WithExecutor(p.ctx, p.session)) if err != nil { // When a close target command is sent to the browser via CDP, // the browser will start to cleanup and the first thing it @@ -762,8 +771,8 @@ func (p *Page) Reload(opts goja.Value) api.Response { lifecycleEvtCh, lifecycleEvtCancel := createWaitForEventPredicateHandler( timeoutCtx, p.frameManager.MainFrame(), []string{EventFrameAddLifecycle}, func(data any) bool { - if le, ok := data.(LifecycleEvent); ok { - return le == parsedOpts.WaitUntil + if le, ok := data.(FrameLifecycleEvent); ok { + return le.Event == parsedOpts.WaitUntil } return false }) diff --git a/vendor/github.com/grafana/xk6-browser/common/response.go b/vendor/github.com/grafana/xk6-browser/common/response.go index c02ebc7abfe..13b19e4d803 100644 --- a/vendor/github.com/grafana/xk6-browser/common/response.go +++ b/vendor/github.com/grafana/xk6-browser/common/response.go @@ -179,7 +179,7 @@ func (r *Response) bodySize() int64 { } if err := r.fetchBody(); err != nil { - r.logger.Warnf("Response:bodySize:fetchBody", + r.logger.Debugf("Response:bodySize:fetchBody", "url:%s method:%s err:%s", r.url, r.request.method, err) } diff --git a/vendor/github.com/grafana/xk6-browser/common/session.go b/vendor/github.com/grafana/xk6-browser/common/session.go index 8c04272563c..16417ebaefe 100644 --- a/vendor/github.com/grafana/xk6-browser/common/session.go +++ b/vendor/github.com/grafana/xk6-browser/common/session.go @@ -3,7 +3,6 @@ package common import ( "context" "errors" - "sync/atomic" "github.com/chromedp/cdproto" "github.com/chromedp/cdproto/cdp" @@ -24,7 +23,7 @@ type Session struct { conn *Connection id target.SessionID targetID target.ID - msgID int64 + msgIDGen msgIDGenerator readCh chan *cdproto.Message done chan struct{} closed bool @@ -35,7 +34,7 @@ type Session struct { // NewSession creates a new session. func NewSession( - ctx context.Context, conn *Connection, id target.SessionID, tid target.ID, logger *log.Logger, + ctx context.Context, conn *Connection, id target.SessionID, tid target.ID, logger *log.Logger, msgIDGen msgIDGenerator, ) *Session { s := Session{ BaseEventEmitter: NewBaseEventEmitter(ctx), @@ -44,6 +43,7 @@ func NewSession( targetID: tid, readCh: make(chan *cdproto.Message), done: make(chan struct{}), + msgIDGen: msgIDGen, logger: logger, } @@ -118,7 +118,7 @@ func (s *Session) Execute(ctx context.Context, method string, params easyjson.Ma return ErrTargetCrashed } - id := atomic.AddInt64(&s.msgID, 1) + id := s.msgIDGen.newID() // Setup event handler used to block for response to message being sent. ch := make(chan *cdproto.Message, 1) @@ -186,7 +186,7 @@ func (s *Session) ExecuteWithoutExpectationOnReply(ctx context.Context, method s } } msg := &cdproto.Message{ - ID: atomic.AddInt64(&s.msgID, 1), + ID: s.msgIDGen.newID(), // We use different sessions to send messages to "targets" // (browser, page, frame etc.) in CDP. // diff --git a/vendor/github.com/grafana/xk6-browser/common/types.go b/vendor/github.com/grafana/xk6-browser/common/types.go index a2655cb2e1e..1bbf2d01d21 100644 --- a/vendor/github.com/grafana/xk6-browser/common/types.go +++ b/vendor/github.com/grafana/xk6-browser/common/types.go @@ -15,6 +15,9 @@ import ( "github.com/dop251/goja" ) +// BlankPage represents a blank page. +const BlankPage = "about:blank" + // ColorScheme represents a browser color scheme. type ColorScheme string @@ -217,6 +220,15 @@ func (f *ImageFormat) UnmarshalJSON(b []byte) error { return nil } +// FrameLifecycleEvent is emitted when a frame lifecycle event occurs. +type FrameLifecycleEvent struct { + // URL is the URL of the frame that emitted the event. + URL string + + // Event is the lifecycle event that occurred. + Event LifecycleEvent +} + type LifecycleEvent int const ( diff --git a/vendor/github.com/grafana/xk6-browser/env/env.go b/vendor/github.com/grafana/xk6-browser/env/env.go new file mode 100644 index 00000000000..4029ca122e0 --- /dev/null +++ b/vendor/github.com/grafana/xk6-browser/env/env.go @@ -0,0 +1,84 @@ +// Package env provides types to interact with environment setup. +package env + +import "os" + +// Execution specific. +const ( + // InstanceScenarios is an environment variable that can be used to + // define the extra scenarios details to use when running remotely. + InstanceScenarios = "K6_INSTANCE_SCENARIOS" + + // WebSocketURLs is an environment variable that can be used to + // define the WS URLs to connect to when running remotely. + WebSocketURLs = "K6_BROWSER_WS_URL" + + // BrowserArguments is an environment variable that can be used to + // pass extra arguments to the browser process. + BrowserArguments = "K6_BROWSER_ARGS" + + // BrowserExecutablePath is an environment variable that can be used + // to define the path to the browser to execute. + BrowserExecutablePath = "K6_BROWSER_EXECUTABLE_PATH" + + // BrowserEnableDebugging is an environment variable that can be used to + // define if the browser should be launched with debugging enabled. + BrowserEnableDebugging = "K6_BROWSER_DEBUG" + + // BrowserHeadless is an environment variable that can be used to + // define if the browser should be launched in headless mode. + BrowserHeadless = "K6_BROWSER_HEADLESS" + + // BrowserIgnoreDefaultArgs is an environment variable that can be + // used to define if the browser should ignore default arguments. + BrowserIgnoreDefaultArgs = "K6_BROWSER_IGNORE_DEFAULT_ARGS" + + // BrowserGlobalTimeout is an environment variable that can be used + // to set the global timeout for the browser. + BrowserGlobalTimeout = "K6_BROWSER_TIMEOUT" +) + +// Logging and debugging. +const ( + // EnableProfiling is an environment variable that can be used to + // enable profiling for the browser. It will start up a debugging + // server on ProfilingServerAddr. + EnableProfiling = "K6_BROWSER_ENABLE_PPROF" + + // ProfilingServerAddr is the address of the profiling server. + ProfilingServerAddr = "localhost:6060" + + // LogCaller is an environment variable that can be used to enable + // the caller function information in the browser logs. + LogCaller = "K6_BROWSER_LOG_CALLER" + + // LogLevel is an environment variable that can be used to set the + // log level for the browser logs. + LogLevel = "K6_BROWSER_LOG" + + // LogCategoryFilter is an environment variable that can be used to + // filter the browser logs based on their category. It supports + // regular expressions. + LogCategoryFilter = "K6_BROWSER_LOG_CATEGORY_FILTER" +) + +// LookupFunc defines a function to look up a key from the environment. +type LookupFunc func(key string) (string, bool) + +// EmptyLookup is a LookupFunc that always returns "" and false. +func EmptyLookup(key string) (string, bool) { return "", false } + +// Lookup is a LookupFunc that uses os.LookupEnv. +func Lookup(key string) (string, bool) { return os.LookupEnv(key) } + +// ConstLookup is a LookupFunc that always returns the given value and true +// if the key matches the given key. Otherwise it returns EmptyLookup +// behaviour. Useful for testing. +func ConstLookup(k, v string) LookupFunc { + return func(key string) (string, bool) { + if key == k { + return v, true + } + return EmptyLookup(key) + } +} diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/context.go b/vendor/github.com/grafana/xk6-browser/k6ext/context.go index 9c1de769823..8f763bcdc29 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/context.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/context.go @@ -4,6 +4,7 @@ import ( "context" k6modules "go.k6.io/k6/js/modules" + k6lib "go.k6.io/k6/lib" "github.com/dop251/goja" ) @@ -51,3 +52,25 @@ func GetCustomMetrics(ctx context.Context) *CustomMetrics { func Runtime(ctx context.Context) *goja.Runtime { return GetVU(ctx).Runtime() } + +// GetScenarioName returns the scenario name associated with the given context. +func GetScenarioName(ctx context.Context) string { + ss := k6lib.GetScenarioState(ctx) + if ss == nil { + return "" + } + return ss.Name +} + +// GetScenarioOpts returns the browser options and environment variables associated +// with the given context. +func GetScenarioOpts(ctx context.Context, vu k6modules.VU) map[string]any { + scenario := GetScenarioName(ctx) + if scenario == "" { + return nil + } + if so := vu.State().Options.Scenarios[scenario].GetScenarioOptions(); so != nil { + return so.Browser + } + return nil +} diff --git a/vendor/github.com/grafana/xk6-browser/k6ext/env.go b/vendor/github.com/grafana/xk6-browser/k6ext/env.go deleted file mode 100644 index cf0fd468bd0..00000000000 --- a/vendor/github.com/grafana/xk6-browser/k6ext/env.go +++ /dev/null @@ -1,40 +0,0 @@ -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 d727100918c..a2991b5586f 100644 --- a/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go +++ b/vendor/github.com/grafana/xk6-browser/k6ext/metrics.go @@ -1,8 +1,6 @@ package k6ext import ( - "fmt" - k6metrics "go.k6.io/k6/metrics" ) @@ -13,23 +11,40 @@ const ( webVitalCLS = "CLS" webVitalINP = "INP" webVitalFCP = "FCP" + + fidName = "browser_web_vital_fid" + ttfbName = "browser_web_vital_ttfb" + lcpName = "browser_web_vital_lcp" + clsName = "browser_web_vital_cls" + inpName = "browser_web_vital_inp" + fcpName = "browser_web_vital_fcp" + + browserDataSentName = "browser_data_sent" + browserDataReceivedName = "browser_data_received" + browserHTTPReqDurationName = "browser_http_req_duration" + browserHTTPReqFailedName = "browser_http_req_failed" ) // CustomMetrics are the custom k6 metrics used by xk6-browser. type CustomMetrics struct { WebVitals map[string]*k6metrics.Metric + + BrowserDataSent *k6metrics.Metric + BrowserDataReceived *k6metrics.Metric + BrowserHTTPReqDuration *k6metrics.Metric + BrowserHTTPReqFailed *k6metrics.Metric } // RegisterCustomMetrics creates and registers our custom metrics with the k6 // VU Registry and returns our internal struct pointer. func RegisterCustomMetrics(registry *k6metrics.Registry) *CustomMetrics { wvs := map[string]string{ - webVitalFID: "webvital_first_input_delay", - webVitalTTFB: "webvital_time_to_first_byte", - webVitalLCP: "webvital_largest_content_paint", - webVitalCLS: "webvital_cumulative_layout_shift", - webVitalINP: "webvital_interaction_to_next_paint", - webVitalFCP: "webvital_first_contentful_paint", + webVitalFID: fidName, // first input delay + webVitalTTFB: ttfbName, // time to first byte + webVitalLCP: lcpName, // largest content paint + webVitalCLS: clsName, // cumulative layout shift + webVitalINP: inpName, // interaction to next paint + webVitalFCP: fcpName, // first contentful paint } webVitals := make(map[string]*k6metrics.Metric) @@ -42,24 +57,14 @@ func RegisterCustomMetrics(registry *k6metrics.Registry) *CustomMetrics { } webVitals[k] = registry.MustNewMetric(v, k6metrics.Trend, t) - - webVitals[ConcatWebVitalNameRating(k, "good")] = registry.MustNewMetric( - v+"_good", k6metrics.Counter) - webVitals[ConcatWebVitalNameRating(k, "needs-improvement")] = registry.MustNewMetric( - v+"_needs_improvement", k6metrics.Counter) - webVitals[ConcatWebVitalNameRating(k, "poor")] = registry.MustNewMetric( - v+"_poor", k6metrics.Counter) } + //nolint:lll return &CustomMetrics{ - WebVitals: webVitals, + WebVitals: webVitals, + BrowserDataSent: registry.MustNewMetric(browserDataSentName, k6metrics.Counter, k6metrics.Data), + BrowserDataReceived: registry.MustNewMetric(browserDataReceivedName, k6metrics.Counter, k6metrics.Data), + BrowserHTTPReqDuration: registry.MustNewMetric(browserHTTPReqDurationName, k6metrics.Trend, k6metrics.Time), + BrowserHTTPReqFailed: registry.MustNewMetric(browserHTTPReqFailedName, k6metrics.Rate), } } - -// ConcatWebVitalNameRating can be used -// to create the correct metric key name -// to retrieve the corresponding metric -// from the registry. -func ConcatWebVitalNameRating(name, rating string) string { - return fmt.Sprintf("%s:%s", name, rating) -} diff --git a/vendor/github.com/grafana/xk6-browser/log/logger.go b/vendor/github.com/grafana/xk6-browser/log/logger.go index 374e26c7266..dc17684b1a3 100644 --- a/vendor/github.com/grafana/xk6-browser/log/logger.go +++ b/vendor/github.com/grafana/xk6-browser/log/logger.go @@ -6,7 +6,6 @@ import ( "io/ioutil" "regexp" "runtime" - "strconv" "strings" "sync" "time" @@ -98,9 +97,9 @@ func (l *Logger) Logf(level logrus.Level, category string, msg string, args ...a return } fields := logrus.Fields{ - "category": category, - "elapsed": fmt.Sprintf("%d ms", elapsed), - "goroutine": goRoutineID(), + "source": "browser", + "category": category, + "elapsed": fmt.Sprintf("%d ms", elapsed), } if l.iterID != "" && l.GetLevel() > logrus.InfoLevel { fields["iteration_id"] = l.iterID @@ -161,6 +160,7 @@ func (l *Logger) ConsoleLogFormatterSerializer() *Logger { Out: l.Out, Level: l.Level, Formatter: &consoleLogFormatter{l.Formatter}, + Hooks: l.Hooks, }, } } @@ -176,17 +176,6 @@ func (l *Logger) SetCategoryFilter(filter string) (err error) { return nil } -func goRoutineID() int { - var buf [64]byte - n := runtime.Stack(buf[:], false) - idField := strings.Fields(strings.TrimPrefix(string(buf[:n]), "goroutine "))[0] - id, err := strconv.Atoi(idField) - if err != nil { - panic(fmt.Sprintf("internal error while getting goroutine ID: %v", err)) - } - return id -} - type consoleLogFormatter struct { logrus.Formatter } diff --git a/vendor/modules.txt b/vendor/modules.txt index 32d5b1c3402..c2044281bd6 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -150,13 +150,14 @@ github.com/google/uuid # github.com/gorilla/websocket v1.5.0 ## explicit; go 1.12 github.com/gorilla/websocket -# github.com/grafana/xk6-browser v0.10.0 +# github.com/grafana/xk6-browser v1.0.0 ## explicit; go 1.19 github.com/grafana/xk6-browser/api github.com/grafana/xk6-browser/browser github.com/grafana/xk6-browser/chromium github.com/grafana/xk6-browser/common github.com/grafana/xk6-browser/common/js +github.com/grafana/xk6-browser/env github.com/grafana/xk6-browser/k6error github.com/grafana/xk6-browser/k6ext github.com/grafana/xk6-browser/keyboardlayout From 9e09acb2e9a5646149d9ffb3b67c6193c4415e7b Mon Sep 17 00:00:00 2001 From: ka3de Date: Fri, 21 Jul 2023 09:16:11 +0200 Subject: [PATCH 2/6] Remove K6_BROWSER_ENABLED flag requirement Currently (as of grafana/xk6-browser@v1.0.0) the browser extension requires the definition of the browser type parameter inside the options element for every scenario that wants to use the browser module. Therefore the K6_BROWSER_ENABLED flag has become redundant, as both parameters have to be set, being the scenario one more restrictive. Because we no longer have to parse the environment variable, the module wrapper implementation can be removed completely and use the xk6-browser root module constructor directly instead. --- cmd/tests/cmd_run_test.go | 74 ------------------- js/jsmodules.go | 4 +- .../k6/experimental/browser/rootmodule.go | 58 --------------- 3 files changed, 2 insertions(+), 134 deletions(-) delete mode 100644 js/modules/k6/experimental/browser/rootmodule.go diff --git a/cmd/tests/cmd_run_test.go b/cmd/tests/cmd_run_test.go index 0df7021af5d..4472ae21a1d 100644 --- a/cmd/tests/cmd_run_test.go +++ b/cmd/tests/cmd_run_test.go @@ -1827,80 +1827,6 @@ func BenchmarkReadResponseBody(b *testing.B) { cmd.ExecuteWithGlobalState(ts.GlobalState) } -func TestBrowserPermissions(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - envVarValue string - envVarMsgValue string - expectedExitCode exitcodes.ExitCode - expectedError string - }{ - { - name: "no env var set", - envVarValue: "", - expectedExitCode: 107, - expectedError: "To run browser tests set env var K6_BROWSER_ENABLED=true", - }, - { - name: "env var set but set to false", - envVarValue: "false", - expectedExitCode: 107, - expectedError: "To run browser tests set env var K6_BROWSER_ENABLED=true", - }, - { - name: "env var set but set to 09adsu", - envVarValue: "09adsu", - expectedExitCode: 107, - expectedError: "To run browser tests set env var K6_BROWSER_ENABLED=true", - }, - { - name: "with custom message", - envVarValue: "09adsu", - envVarMsgValue: "Try again later", - expectedExitCode: 107, - expectedError: "Try again later", - }, - { - name: "env var set and set to true", - envVarValue: "true", - expectedExitCode: 0, - expectedError: "", - }, - } - - for _, tt := range tests { - tt := tt - t.Run(tt.name, func(t *testing.T) { - t.Parallel() - script := ` - import { chromium } from 'k6/experimental/browser'; - - export default function() {}; - ` - - ts := getSingleFileTestState(t, script, []string{}, tt.expectedExitCode) - if tt.envVarValue != "" { - ts.Env["K6_BROWSER_ENABLED"] = tt.envVarValue - } - if tt.envVarMsgValue != "" { - ts.Env["K6_BROWSER_ENABLED_MSG"] = tt.envVarMsgValue - } - cmd.ExecuteWithGlobalState(ts.GlobalState) - - loglines := ts.LoggerHook.Drain() - - if tt.expectedError == "" { - require.Len(t, loglines, 0) - return - } - - assert.Contains(t, loglines[0].Message, tt.expectedError) - }) - } -} - func TestUIRenderOutput(t *testing.T) { t.Parallel() diff --git a/js/jsmodules.go b/js/jsmodules.go index c068db47a97..6080ab2608b 100644 --- a/js/jsmodules.go +++ b/js/jsmodules.go @@ -8,7 +8,6 @@ import ( "go.k6.io/k6/js/modules/k6/data" "go.k6.io/k6/js/modules/k6/encoding" "go.k6.io/k6/js/modules/k6/execution" - "go.k6.io/k6/js/modules/k6/experimental/browser" "go.k6.io/k6/js/modules/k6/experimental/tracing" "go.k6.io/k6/js/modules/k6/grpc" "go.k6.io/k6/js/modules/k6/html" @@ -16,6 +15,7 @@ import ( "go.k6.io/k6/js/modules/k6/metrics" "go.k6.io/k6/js/modules/k6/ws" + expBrowser "github.com/grafana/xk6-browser/browser" expGrpc "github.com/grafana/xk6-grpc/grpc" "github.com/grafana/xk6-redis/redis" "github.com/grafana/xk6-timers/timers" @@ -37,7 +37,7 @@ func getInternalJSModules() map[string]interface{} { "k6/experimental/grpc": expGrpc.New(), "k6/experimental/timers": timers.New(), "k6/experimental/tracing": tracing.New(), - "k6/experimental/browser": browser.New(), + "k6/experimental/browser": expBrowser.New(), "k6/net/grpc": grpc.New(), "k6/html": html.New(), "k6/http": http.New(), diff --git a/js/modules/k6/experimental/browser/rootmodule.go b/js/modules/k6/experimental/browser/rootmodule.go deleted file mode 100644 index f0ba4f87260..00000000000 --- a/js/modules/k6/experimental/browser/rootmodule.go +++ /dev/null @@ -1,58 +0,0 @@ -// Package browser contains a RootModule wrapper -// that wraps around the experimental browser -// RootModule. -package browser - -import ( - "errors" - "strconv" - - xk6browser "github.com/grafana/xk6-browser/browser" - "go.k6.io/k6/js/common" - "go.k6.io/k6/js/modules" -) - -type ( - // RootModule is a wrapper around the experimental - // browser RootModule. It will prevent browser test - // runs unless K6_BROWSER_ENABLED env var is set. - RootModule struct { - rm *xk6browser.RootModule - } -) - -// New creates an experimental browser RootModule -// and wraps it around this internal RootModule. -func New() *RootModule { - return &RootModule{ - rm: xk6browser.New(), - } -} - -// NewModuleInstance will check to see if -// K6_BROWSER_ENABLED is set before allowing -// test runs to continue. -func (r *RootModule) NewModuleInstance(vu modules.VU) modules.Instance { - env := vu.InitEnv() - - throwError := func() { - msg := "To run browser tests set env var K6_BROWSER_ENABLED=true" - if m, ok := env.LookupEnv("K6_BROWSER_ENABLED_MSG"); ok && m != "" { - msg = m - } - - common.Throw(vu.Runtime(), errors.New(msg)) - } - - vs, ok := env.LookupEnv("K6_BROWSER_ENABLED") - if !ok { - throwError() - } - - v, err := strconv.ParseBool(vs) - if err != nil || !v { - throwError() - } - - return r.rm.NewModuleInstance(vu) -} From b02dcd23650294cfbc42b8a04489490ee68cacfa Mon Sep 17 00:00:00 2001 From: ankur22 Date: Tue, 1 Aug 2023 12:30:03 +0100 Subject: [PATCH 3/6] Use browser package name instead of expBrowser Resolves: https://github.com/grafana/k6/pull/3235#discussion_r1279239061 --- js/jsmodules.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/js/jsmodules.go b/js/jsmodules.go index 6080ab2608b..f5ec36a0d81 100644 --- a/js/jsmodules.go +++ b/js/jsmodules.go @@ -15,7 +15,7 @@ import ( "go.k6.io/k6/js/modules/k6/metrics" "go.k6.io/k6/js/modules/k6/ws" - expBrowser "github.com/grafana/xk6-browser/browser" + "github.com/grafana/xk6-browser/browser" expGrpc "github.com/grafana/xk6-grpc/grpc" "github.com/grafana/xk6-redis/redis" "github.com/grafana/xk6-timers/timers" @@ -37,7 +37,7 @@ func getInternalJSModules() map[string]interface{} { "k6/experimental/grpc": expGrpc.New(), "k6/experimental/timers": timers.New(), "k6/experimental/tracing": tracing.New(), - "k6/experimental/browser": expBrowser.New(), + "k6/experimental/browser": browser.New(), "k6/net/grpc": grpc.New(), "k6/html": html.New(), "k6/http": http.New(), From b1fbe536fa2af2736a6293069a1430a0ae1af54d Mon Sep 17 00:00:00 2001 From: ankur22 Date: Thu, 3 Aug 2023 14:07:23 +0100 Subject: [PATCH 4/6] Update to v1.0.1 of xk6-browser This includes some fixes for the goroutine leaks that we were seeing when we tried to create the e2e tests for the browser module in k6. --- go.mod | 2 +- go.sum | 4 ++-- .../grafana/xk6-browser/browser/registry.go | 8 +++++--- .../grafana/xk6-browser/chromium/browser_type.go | 14 -------------- .../grafana/xk6-browser/common/browser.go | 2 +- vendor/modules.txt | 2 +- 6 files changed, 10 insertions(+), 22 deletions(-) diff --git a/go.mod b/go.mod index 4fdc2395beb..bf44a38bfa5 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.3 github.com/gorilla/websocket v1.5.0 - github.com/grafana/xk6-browser v1.0.0 + github.com/grafana/xk6-browser v1.0.1 github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1 github.com/grafana/xk6-output-prometheus-remote v0.2.3 github.com/grafana/xk6-redis v0.1.1 diff --git a/go.sum b/go.sum index f6709ccc9f3..077f4381ed5 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,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 v1.0.0 h1:P0zUsftWSYrY6GnrGOsJEh9+PdPd6/dYkFHPzoG5cUA= -github.com/grafana/xk6-browser v1.0.0/go.mod h1:Ck8IWES3Emu4CMKktQ/oD7l/JFj/1ESnDIbQGpfaoYA= +github.com/grafana/xk6-browser v1.0.1 h1:/GvMHhDMbNCEuu6ml/AIaQNtHOpj06GpKfgQJpNzPIg= +github.com/grafana/xk6-browser v1.0.1/go.mod h1:Ck8IWES3Emu4CMKktQ/oD7l/JFj/1ESnDIbQGpfaoYA= github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1 h1:SdMihJN+fkH6cO/1NeAnVxSVOnJ3ZkZ1v7FJnrcqhog= github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1/go.mod h1:iq6qHN64XgAEmDHKf0OXZ4mvoqF4Udr22fiCIXNpXA0= github.com/grafana/xk6-output-prometheus-remote v0.2.3 h1:ta4wFrO85+29H0papAbeMCavHrBuHDZ4bdKC1Zv8zlo= diff --git a/vendor/github.com/grafana/xk6-browser/browser/registry.go b/vendor/github.com/grafana/xk6-browser/browser/registry.go index b4eb283861f..511e26544d7 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/registry.go +++ b/vendor/github.com/grafana/xk6-browser/browser/registry.go @@ -213,8 +213,6 @@ func newBrowserRegistry(vu k6modules.VU, remote *remoteRegistry, pids *pidRegist exitSubID, exitCh := vu.Events().Global.Subscribe( k6event.Exit, ) - go r.handleExitEvent(exitCh) - iterSubID, eventsCh := vu.Events().Local.Subscribe( k6event.IterStart, k6event.IterEnd, @@ -223,6 +221,8 @@ func newBrowserRegistry(vu k6modules.VU, remote *remoteRegistry, pids *pidRegist vu.Events().Local.Unsubscribe(iterSubID) vu.Events().Global.Unsubscribe(exitSubID) } + + go r.handleExitEvent(exitCh, unsubscribe) go r.handleIterEvents(eventsCh, unsubscribe) return r @@ -282,7 +282,9 @@ func (r *browserRegistry) handleIterEvents(eventsCh <-chan *k6event.Event, unsub } } -func (r *browserRegistry) handleExitEvent(exitCh <-chan *k6event.Event) { +func (r *browserRegistry) handleExitEvent(exitCh <-chan *k6event.Event, unsubscribeFn func()) { + defer unsubscribeFn() + e, ok := <-exitCh if !ok { return diff --git a/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go b/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go index 33366a2adba..a994fc753ad 100644 --- a/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go +++ b/vendor/github.com/grafana/xk6-browser/chromium/browser_type.go @@ -185,20 +185,6 @@ func (b *BrowserType) launch( } flags["user-data-dir"] = dataDir.Dir - go func(c context.Context) { - defer func() { - if err := dataDir.Cleanup(); err != nil { - logger.Errorf("BrowserType:Launch", "cleaning up the user data directory: %v", err) - } - }() - // There's a small chance that this might be called - // if the context is closed by the k6 runtime. To - // guarantee the cleanup we would need to orchestrate - // it correctly which https://github.com/grafana/k6/issues/2432 - // will enable once it's complete. - <-c.Done() - }(ctx) - browserProc, err := b.allocate(ctx, opts, flags, dataDir, logger) if browserProc == nil { return nil, 0, fmt.Errorf("launching browser: %w", err) diff --git a/vendor/github.com/grafana/xk6-browser/common/browser.go b/vendor/github.com/grafana/xk6-browser/common/browser.go index b9d9f4cf4e6..a0a0598c09c 100644 --- a/vendor/github.com/grafana/xk6-browser/common/browser.go +++ b/vendor/github.com/grafana/xk6-browser/common/browser.go @@ -453,7 +453,7 @@ func (b *Browser) Close() { var closeErr *websocket.CloseError err := cdpbrowser.Close().Do(cdp.WithExecutor(b.ctx, b.conn)) if err != nil && !errors.As(err, &closeErr) { - k6ext.Panic(b.ctx, "closing the browser: %v", err) + b.logger.Errorf("Browser:Close", "closing the browser: %v", err) } } diff --git a/vendor/modules.txt b/vendor/modules.txt index c2044281bd6..a3b692f6c54 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -150,7 +150,7 @@ github.com/google/uuid # github.com/gorilla/websocket v1.5.0 ## explicit; go 1.12 github.com/gorilla/websocket -# github.com/grafana/xk6-browser v1.0.0 +# github.com/grafana/xk6-browser v1.0.1 ## explicit; go 1.19 github.com/grafana/xk6-browser/api github.com/grafana/xk6-browser/browser From 5eee08d5889362a2eb573d65d29f0bb8be519c3e Mon Sep 17 00:00:00 2001 From: ankur22 Date: Thu, 3 Aug 2023 14:08:38 +0100 Subject: [PATCH 5/6] Add e2e test for browser module in k6 This test ensures that an error occurs and the test ends when the browser module is imported but the options are missing, otherwise when setup correctly the browser test runs. --- cmd/tests/cmd_run_test.go | 62 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/cmd/tests/cmd_run_test.go b/cmd/tests/cmd_run_test.go index 4472ae21a1d..459afcfd3b8 100644 --- a/cmd/tests/cmd_run_test.go +++ b/cmd/tests/cmd_run_test.go @@ -2154,3 +2154,65 @@ func BenchmarkRunEvents(b *testing.B) { } } } + +func TestBrowserPermissions(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + options string + expectedExitCode exitcodes.ExitCode + expectedError string + }{ + { + name: "browser option not set", + options: "", + expectedExitCode: 0, + expectedError: "GoError: browser not found in registry. make sure to set browser type option in scenario definition in order to use the browser module", + }, + { + name: "browser option set", + options: `export const options = { + scenarios: { + browser: { + executor: 'shared-iterations', + options: { + browser: { + type: 'chromium', + }, + }, + }, + }, + }`, + expectedExitCode: 0, + expectedError: "", + }, + } + + for _, tt := range tests { + tt := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + script := fmt.Sprintf(` + import { browser } from 'k6/experimental/browser'; + + %s + + export default function() { + browser.isConnected(); + }; + `, tt.options) + + ts := getSingleFileTestState(t, script, []string{}, tt.expectedExitCode) + cmd.ExecuteWithGlobalState(ts.GlobalState) + loglines := ts.LoggerHook.Drain() + + if tt.expectedError == "" { + require.Len(t, loglines, 0) + return + } + + assert.Contains(t, loglines[0].Message, tt.expectedError) + }) + } +} From 0837ee571671ff8922d541174d3fd79e6a4db943 Mon Sep 17 00:00:00 2001 From: ankur22 Date: Mon, 7 Aug 2023 17:01:45 +0100 Subject: [PATCH 6/6] Update to v1.0.2 of xk6-browser This contains a fix for the race condition where the ctx was being read before the vu was setup. --- go.mod | 2 +- go.sum | 4 ++-- .../grafana/xk6-browser/browser/registry.go | 13 +++++++++---- vendor/modules.txt | 2 +- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/go.mod b/go.mod index bf44a38bfa5..f695973ab7e 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.3 github.com/gorilla/websocket v1.5.0 - github.com/grafana/xk6-browser v1.0.1 + github.com/grafana/xk6-browser v1.0.2 github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1 github.com/grafana/xk6-output-prometheus-remote v0.2.3 github.com/grafana/xk6-redis v0.1.1 diff --git a/go.sum b/go.sum index 077f4381ed5..52bd36a6679 100644 --- a/go.sum +++ b/go.sum @@ -183,8 +183,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 v1.0.1 h1:/GvMHhDMbNCEuu6ml/AIaQNtHOpj06GpKfgQJpNzPIg= -github.com/grafana/xk6-browser v1.0.1/go.mod h1:Ck8IWES3Emu4CMKktQ/oD7l/JFj/1ESnDIbQGpfaoYA= +github.com/grafana/xk6-browser v1.0.2 h1:B9ll8xLH68hfCBy3sTzhmksCxwgJBIcqgPeX3mht6jM= +github.com/grafana/xk6-browser v1.0.2/go.mod h1:LV/ECGBCN3vRN/A4St+Ep9JUpbKJuRsj+6TBihQptGw= github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1 h1:SdMihJN+fkH6cO/1NeAnVxSVOnJ3ZkZ1v7FJnrcqhog= github.com/grafana/xk6-grpc v0.1.3-0.20230717090346-fb49221e0ce1/go.mod h1:iq6qHN64XgAEmDHKf0OXZ4mvoqF4Udr22fiCIXNpXA0= github.com/grafana/xk6-output-prometheus-remote v0.2.3 h1:ta4wFrO85+29H0papAbeMCavHrBuHDZ4bdKC1Zv8zlo= diff --git a/vendor/github.com/grafana/xk6-browser/browser/registry.go b/vendor/github.com/grafana/xk6-browser/browser/registry.go index 511e26544d7..b21e29093bc 100644 --- a/vendor/github.com/grafana/xk6-browser/browser/registry.go +++ b/vendor/github.com/grafana/xk6-browser/browser/registry.go @@ -230,10 +230,9 @@ func newBrowserRegistry(vu k6modules.VU, remote *remoteRegistry, pids *pidRegist func (r *browserRegistry) handleIterEvents(eventsCh <-chan *k6event.Event, unsubscribeFn func()) { var ( - ok bool - data k6event.IterData - ctx = context.Background() - vuCtx = k6ext.WithVU(r.vu.Context(), r.vu) + ok bool + data k6event.IterData + ctx = context.Background() ) for e := range eventsCh { @@ -251,6 +250,12 @@ func (r *browserRegistry) handleIterEvents(eventsCh <-chan *k6event.Event, unsub return } + // The context in the VU is not thread safe. It can + // be safely accessed during an iteration but not + // before one is started. This is why it is being + // accessed and used here. + vuCtx := k6ext.WithVU(r.vu.Context(), r.vu) + if data, ok = e.Data.(k6event.IterData); !ok { e.Done() k6ext.Abort(vuCtx, "unexpected iteration event data format: %v", e.Data) diff --git a/vendor/modules.txt b/vendor/modules.txt index a3b692f6c54..5ae9d4a175c 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -150,7 +150,7 @@ github.com/google/uuid # github.com/gorilla/websocket v1.5.0 ## explicit; go 1.12 github.com/gorilla/websocket -# github.com/grafana/xk6-browser v1.0.1 +# github.com/grafana/xk6-browser v1.0.2 ## explicit; go 1.19 github.com/grafana/xk6-browser/api github.com/grafana/xk6-browser/browser