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

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
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
Expand Up @@ -444,6 +444,10 @@ func mapPageOn(vu moduleVU, p *common.Page) func(common.PageOnEventName, sobek.C
init: prepK6BrowserRegExChecker(rt),
wait: true,
},
common.EventPageRequestCalled: {
mapp: mapRequestEvent,
wait: false,
},
}

return func(eventName common.PageOnEventName, handleEvent sobek.Callable) error {
Expand Down
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
Expand Up @@ -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()
Expand Down
4 changes: 4 additions & 0 deletions internal/js/modules/k6/browser/common/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
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
Expand Up @@ -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)
inancgumus marked this conversation as resolved.
Show resolved Hide resolved
}

// 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)
Expand All @@ -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()
Expand All @@ -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 {
Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -233,7 +234,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)
Expand Down Expand Up @@ -281,7 +282,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)
Expand Down Expand Up @@ -512,6 +513,8 @@ func (m *NetworkManager) onRequest(event *network.EventRequestWillBeSent, interc
m.reqsMu.Unlock()
m.emitRequestMetrics(req)
m.frameManager.requestStarted(req)

m.eventInterceptor.onRequest(req)
inancgumus marked this conversation as resolved.
Show resolved Hide resolved
}

func (m *NetworkManager) onRequestPaused(event *fetch.EventRequestPaused) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
33 changes: 33 additions & 0 deletions internal/js/modules/k6/browser/common/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -1174,6 +1203,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
Expand Down
Loading
Loading