Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add page.on('request') #4290

Merged
merged 12 commits into from
Feb 19, 2025
4 changes: 4 additions & 0 deletions internal/js/modules/k6/browser/browser/page_mapping.go
Original file line number Diff line number Diff line change
@@ -585,6 +585,10 @@ func mapPageOn(vu moduleVU, p *common.Page) func(common.PageOnEventName, sobek.C
init: prepK6BrowserRegExChecker(rt),
wait: true,
},
common.EventPageRequestCalled: {
mapp: mapRequestEvent,
wait: false,
},
}
pageOnEvent, ok := pageOnEvents[eventName]
if !ok {
6 changes: 6 additions & 0 deletions internal/js/modules/k6/browser/browser/request_mapping.go
Original file line number Diff line number Diff line change
@@ -7,6 +7,12 @@ import (
"go.k6.io/k6/internal/js/modules/k6/browser/k6ext"
)

func mapRequestEvent(vu moduleVU, event common.PageOnEvent) mapping {
r := event.Request

return mapRequest(vu, r)
}

// mapRequest to the JS module.
func mapRequest(vu moduleVU, r *common.Request) mapping {
rt := vu.Runtime()
4 changes: 4 additions & 0 deletions internal/js/modules/k6/browser/common/http.go
Original file line number Diff line number Diff line change
@@ -361,6 +361,10 @@ type resourceTiming struct {

// Timing returns the request timing information.
func (r *Request) Timing() *resourceTiming {
if r.response == nil {
return nil
}

timing := r.response.timing

return &resourceTiming{
35 changes: 19 additions & 16 deletions internal/js/modules/k6/browser/common/network_manager.go
Original file line number Diff line number Diff line change
@@ -43,24 +43,25 @@ func (c Credentials) IsEmpty() bool {
return c == (Credentials{})
}

type metricInterceptor interface {
type eventInterceptor interface {
urlTagName(urlTag string, method string) (string, bool)
onRequest(request *Request)
}

// NetworkManager manages all frames in HTML document.
type NetworkManager struct {
BaseEventEmitter

ctx context.Context
logger *log.Logger
session session
parent *NetworkManager
frameManager *FrameManager
credentials Credentials
resolver k6netext.Resolver
vu k6modules.VU
customMetrics *k6ext.CustomMetrics
mi metricInterceptor
ctx context.Context
logger *log.Logger
session session
parent *NetworkManager
frameManager *FrameManager
credentials Credentials
resolver k6netext.Resolver
vu k6modules.VU
customMetrics *k6ext.CustomMetrics
eventInterceptor eventInterceptor

// TODO: manage inflight requests separately (move them between the two maps
// as they transition from inflight -> completed)
@@ -84,7 +85,7 @@ func NewNetworkManager(
s session,
fm *FrameManager,
parent *NetworkManager,
mi metricInterceptor,
ei eventInterceptor,
) (*NetworkManager, error) {
vu := k6ext.GetVU(ctx)
state := vu.State()
@@ -110,7 +111,7 @@ func NewNetworkManager(
attemptedAuth: make(map[fetch.RequestID]bool),
extraHTTPHeaders: make(map[string]string),
networkProfile: NewNetworkProfile(),
mi: mi,
eventInterceptor: ei,
}
m.initEvents()
if err := m.initDomains(); err != nil {
@@ -178,7 +179,7 @@ func (m *NetworkManager) emitRequestMetrics(req *Request) {
tags = tags.With("method", req.method)
}
if state.Options.SystemTags.Has(k6metrics.TagURL) {
tags = handleURLTag(m.mi, req.URL(), req.method, tags)
tags = handleURLTag(m.eventInterceptor, req.URL(), req.method, tags)
}
tags = tags.With("resource_type", req.ResourceType())

@@ -232,7 +233,7 @@ func (m *NetworkManager) emitResponseMetrics(resp *Response, req *Request) {
tags = tags.With("method", req.method)
}
if state.Options.SystemTags.Has(k6metrics.TagURL) {
tags = handleURLTag(m.mi, url, req.method, tags)
tags = handleURLTag(m.eventInterceptor, url, req.method, tags)
}
if state.Options.SystemTags.Has(k6metrics.TagIP) {
tags = tags.With("ip", ipAddress)
@@ -280,7 +281,7 @@ func (m *NetworkManager) emitResponseMetrics(resp *Response, req *Request) {
// handleURLTag will check if the url tag needs to be grouped by testing
// against user supplied regex. If there's a match a user supplied name will
// be used instead of the url for the url tag, otherwise the url will be used.
func handleURLTag(mi metricInterceptor, url string, method string, tags *k6metrics.TagSet) *k6metrics.TagSet {
func handleURLTag(mi eventInterceptor, url string, method string, tags *k6metrics.TagSet) *k6metrics.TagSet {
if newTagName, urlMatched := mi.urlTagName(url, method); urlMatched {
tags = tags.With("url", newTagName)
tags = tags.With("name", newTagName)
@@ -511,6 +512,8 @@ func (m *NetworkManager) onRequest(event *network.EventRequestWillBeSent, interc
m.reqsMu.Unlock()
m.emitRequestMetrics(req)
m.frameManager.requestStarted(req)

m.eventInterceptor.onRequest(req)
}

func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) {
Original file line number Diff line number Diff line change
@@ -209,12 +209,14 @@ func TestOnRequestPausedBlockedIPs(t *testing.T) {
}
}

type MetricInterceptorMock struct{}
type EventInterceptorMock struct{}

func (m *MetricInterceptorMock) urlTagName(_ string, _ string) (string, bool) {
func (m *EventInterceptorMock) urlTagName(_ string, _ string) (string, bool) {
return "", false
}

func (m *EventInterceptorMock) onRequest(request *Request) {}

func TestNetworkManagerEmitRequestResponseMetricsTimingSkew(t *testing.T) {
t.Parallel()

@@ -277,7 +279,7 @@ func TestNetworkManagerEmitRequestResponseMetricsTimingSkew(t *testing.T) {

var (
vu = k6test.NewVU(t)
nm = &NetworkManager{ctx: vu.Context(), vu: vu, customMetrics: k6m, mi: &MetricInterceptorMock{}}
nm = &NetworkManager{ctx: vu.Context(), vu: vu, customMetrics: k6m, eventInterceptor: &EventInterceptorMock{}}
)
vu.ActivateVU()

33 changes: 33 additions & 0 deletions internal/js/modules/k6/browser/common/page.go
Original file line number Diff line number Diff line change
@@ -42,6 +42,9 @@ const (

// EventPageMetricCalled represents the page.on('metric') event.
EventPageMetricCalled PageOnEventName = "metric"

// EventPageRequestCalled represents the page.on('request') event.
EventPageRequestCalled PageOnEventName = "request"
)

// MediaType represents the type of media to emulate.
@@ -485,6 +488,32 @@ func (p *Page) urlTagName(url string, method string) (string, bool) {
return newTagName, urlMatched
}

func (p *Page) onRequest(request *Request) {
if !hasPageOnHandler(p, EventPageRequestCalled) {
return
}

p.eventHandlersMu.RLock()
defer p.eventHandlersMu.RUnlock()
for _, h := range p.eventHandlers[EventPageRequestCalled] {
err := func() error {
// Handlers can register other handlers, so we need to
// unlock the mutex before calling the next handler.
p.eventHandlersMu.RUnlock()
defer p.eventHandlersMu.RLock()
Comment on lines +502 to +503
Copy link
Contributor

@codebien codebien Jan 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to lock again? Isn't the lock before already safe-guarding for it?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this looks a bit odd. The reason behind this is to allow the handler to be able to add more handlers. This is a behaviour that Playwright exhibits and also documents, so it's something we're replicating.

page.on('request', async () => {
    page.on('response', async () => {
        // Do something with the request and response data...
    })
})

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@codebien I asked the same question before for another related page.on method. Here's a little bit more detail @ankur22 previously answered: grafana/xk6-browser#1456 (comment)


// Call and wait for the handler to complete.
return h(PageOnEvent{
Request: request,
})
}()
if err != nil {
p.logger.Warnf("onRequest", "handler returned an error: %v", err)
return
}
}
}

func (p *Page) onConsoleAPICalled(event *runtime.EventConsoleAPICalled) {
if !hasPageOnHandler(p, EventPageConsoleAPICalled) {
return
@@ -1169,6 +1198,10 @@ type PageOnEvent struct {

// Metric is the metric event event.
Metric *MetricEvent

// Request is the read only request that is about to be sent from the
// browser to the WuT.
Request *Request
}

// On subscribes to a page event for which the given handler will be executed
Loading