Skip to content

Commit 16d37eb

Browse files
committed
review: update monitor to capture ids from cdp to group request data
1 parent fd4d4d3 commit 16d37eb

8 files changed

Lines changed: 431 additions & 79 deletions

File tree

server/lib/cdpmonitor/cdp_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,15 @@ func newComputedMonitor(t *testing.T) (*Monitor, *eventCollector) {
345345
}
346346

347347
// navigateMonitor sends a Page.frameNavigated to reset computed state.
348+
// It ensures session "s1" has a page-like computedState before navigating,
349+
// mirroring what handleAttachedToTarget would do in production.
348350
func navigateMonitor(m *Monitor, url string) {
351+
m.sessionsMu.Lock()
352+
if _, ok := m.sessions["s1"]; !ok {
353+
m.sessions["s1"] = targetInfo{targetID: "test-target", targetType: targetTypePage}
354+
m.computedStates["s1"] = newComputedState(m.publish)
355+
}
356+
m.sessionsMu.Unlock()
349357
m.handleFrameNavigated(cdpPageFrameNavigatedParams{
350358
Frame: cdpPageFrame{ID: "f1", URL: url},
351359
}, "s1")
@@ -366,7 +374,7 @@ func simulateRequest(m *Monitor, id string) {
366374
// simulateFinished stores minimal state and sends Network.loadingFinished.
367375
func simulateFinished(m *Monitor, id string) {
368376
m.pendReqMu.Lock()
369-
m.pendingRequests[id] = networkReqState{method: "GET", url: "https://example.com/" + id}
377+
m.pendingRequests[id] = networkReqState{sessionID: "s1", method: "GET", url: "https://example.com/" + id}
370378
m.pendReqMu.Unlock()
371379
m.handleLoadingFinished(context.Background(), cdpNetworkLoadingFinishedParams{RequestID: id}, "s1")
372380
}

server/lib/cdpmonitor/computed.go

Lines changed: 90 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cdpmonitor
22

33
import (
44
"encoding/json"
5+
"fmt"
56
"sync"
67
"time"
78

@@ -23,11 +24,23 @@ type computedState struct {
2324
mu sync.Mutex
2425
publish PublishFunc
2526

27+
// dead is set by stop(). Timer callbacks check it under mu and bail,
28+
// preventing orphaned events after a session is detached or cleared.
29+
dead bool
30+
2631
// navSeq is incremented on every resetOnNavigation. AfterFunc callbacks
2732
// capture their navSeq at creation and bail if it has changed, preventing
2833
// stale timers from publishing events for a previous navigation.
2934
navSeq int
3035

36+
// navCtx is the navigation identity stamped at the last Page.frameNavigated.
37+
// navData and navMeta are its precomputed JSON payload and Source.Metadata.
38+
// Maps are replaced (not mutated) on each reset, so in-flight events holding
39+
// a pointer to old navMeta are safe.
40+
navCtx navContext
41+
navData json.RawMessage
42+
navMeta map[string]string
43+
3144
// network_idle: 500 ms debounce after all pending requests finish.
3245
netPending int
3346
netTimer *time.Timer
@@ -47,8 +60,14 @@ type computedState struct {
4760
}
4861

4962
// newComputedState creates a fresh computedState backed by the given publish func.
63+
// navData is initialized to {} and navMeta to an empty map so events emitted
64+
// before the first frameNavigated carry consistent empty payloads rather than null.
5065
func newComputedState(publish PublishFunc) *computedState {
51-
return &computedState{publish: publish}
66+
return &computedState{
67+
publish: publish,
68+
navData: json.RawMessage(`{}`),
69+
navMeta: make(map[string]string),
70+
}
5271
}
5372

5473
func stopTimer(t *time.Timer) {
@@ -63,17 +82,45 @@ func stopTimer(t *time.Timer) {
6382
}
6483
}
6584

85+
// stop marks the state machine dead and cancels pending timers. Called when the
86+
// owning session detaches or the monitor reconnects. Any AfterFunc goroutine
87+
// already running will check dead under mu and discard its result.
88+
func (s *computedState) stop() {
89+
s.mu.Lock()
90+
s.dead = true
91+
stopTimer(s.netTimer)
92+
stopTimer(s.layoutTimer)
93+
s.mu.Unlock()
94+
}
95+
6696
// resetOnNavigation resets all state machines. Called on Page.frameNavigated.
6797
// Increments navSeq so any AfterFunc callbacks already running will discard their results.
68-
// inflight is the number of in-flight requests from other sessions
69-
// (e.g. subframes) that were not cleared by the navigation; netPending is set
70-
// to this value instead of zero so that their eventual loadingFinished events
71-
// decrement correctly.
72-
func (s *computedState) resetOnNavigation(inflight int) {
98+
// inflight seeds netPending; callers pass 0 because each session only tracks its
99+
// own requests and starts fresh on navigation.
100+
func (s *computedState) resetOnNavigation(inflight int, ctx navContext) {
73101
s.mu.Lock()
74102
defer s.mu.Unlock()
75103

76104
s.navSeq++
105+
s.navCtx = ctx
106+
var err error
107+
s.navData, err = json.Marshal(map[string]any{
108+
"session_id": ctx.sessionID,
109+
"target_id": ctx.targetID,
110+
"target_type": ctx.targetType,
111+
"frame_id": ctx.frameID,
112+
"loader_id": ctx.loaderID,
113+
"url": ctx.url,
114+
"nav_seq": s.navSeq,
115+
})
116+
if err != nil {
117+
panic(fmt.Sprintf("cdpmonitor: navData marshal: %v", err))
118+
}
119+
s.navMeta = map[string]string{
120+
MetadataKeyCDPSessionID: ctx.sessionID,
121+
MetadataKeyTargetID: ctx.targetID,
122+
MetadataKeyTargetType: ctx.targetType,
123+
}
77124

78125
stopTimer(s.netTimer)
79126
s.netTimer = nil
@@ -97,6 +144,9 @@ func (s *computedState) resetOnNavigation(inflight int) {
97144
func (s *computedState) onRequest() {
98145
s.mu.Lock()
99146
defer s.mu.Unlock()
147+
if s.dead {
148+
return
149+
}
100150
s.netPending++
101151
// A new request invalidates any pending network_idle timer
102152
stopTimer(s.netTimer)
@@ -107,6 +157,9 @@ func (s *computedState) onRequest() {
107157
func (s *computedState) onLoadingFinished() {
108158
s.mu.Lock()
109159
defer s.mu.Unlock()
160+
if s.dead {
161+
return
162+
}
110163

111164
s.netPending--
112165
if s.netPending < 0 {
@@ -123,11 +176,16 @@ func (s *computedState) onLoadingFinished() {
123176

124177
// startNetIdleTimer arms the network_idle debounce timer. Must be called with s.mu held.
125178
func (s *computedState) startNetIdleTimer() {
179+
if s.dead {
180+
return
181+
}
126182
stopTimer(s.netTimer)
127183
navSeq := s.navSeq
184+
navData := s.navData
185+
navMeta := s.navMeta
128186
s.netTimer = time.AfterFunc(networkIdleDebounce, func() {
129187
s.mu.Lock()
130-
if s.navSeq != navSeq || s.netFired || s.netPending > 0 {
188+
if s.dead || s.navSeq != navSeq || s.netFired || s.netPending > 0 {
131189
s.mu.Unlock()
132190
return
133191
}
@@ -137,8 +195,11 @@ func (s *computedState) startNetIdleTimer() {
137195
Ts: time.Now().UnixMicro(),
138196
Type: EventNetworkIdle,
139197
Category: events.CategoryNetwork,
140-
Source: events.Source{Kind: events.KindCDP},
141-
Data: json.RawMessage(`{}`),
198+
Source: events.Source{
199+
Kind: events.KindCDP,
200+
Metadata: navMeta,
201+
},
202+
Data: navData,
142203
}}
143204
evs = append(evs, s.pendingNavigationSettled()...)
144205
s.mu.Unlock()
@@ -152,6 +213,9 @@ func (s *computedState) startNetIdleTimer() {
152213
func (s *computedState) onPageLoad() {
153214
s.mu.Lock()
154215
defer s.mu.Unlock()
216+
if s.dead {
217+
return
218+
}
155219
s.pageLoadSeen = true
156220
if s.layoutFired {
157221
return
@@ -166,7 +230,7 @@ func (s *computedState) onPageLoad() {
166230
func (s *computedState) onLayoutShift() {
167231
s.mu.Lock()
168232
defer s.mu.Unlock()
169-
if s.layoutFired || !s.pageLoadSeen {
233+
if s.dead || s.layoutFired || !s.pageLoadSeen {
170234
return
171235
}
172236
// Reset the timer to 1s from now.
@@ -178,18 +242,23 @@ func (s *computedState) onLayoutShift() {
178242
// emitLayoutSettled is called from the layout timer's AfterFunc goroutine.
179243
func (s *computedState) emitLayoutSettled(navSeq int) {
180244
s.mu.Lock()
181-
if s.navSeq != navSeq || s.layoutFired || !s.pageLoadSeen {
245+
if s.dead || s.navSeq != navSeq || s.layoutFired || !s.pageLoadSeen {
182246
s.mu.Unlock()
183247
return
184248
}
185249
s.layoutFired = true
186250
s.navLayoutSettled = true
251+
navData := s.navData
252+
navMeta := s.navMeta
187253
evs := []events.Event{{
188254
Ts: time.Now().UnixMicro(),
189255
Type: EventLayoutSettled,
190256
Category: events.CategoryPage,
191-
Source: events.Source{Kind: events.KindCDP},
192-
Data: json.RawMessage(`{}`),
257+
Source: events.Source{
258+
Kind: events.KindCDP,
259+
Metadata: navMeta,
260+
},
261+
Data: navData,
193262
}}
194263
evs = append(evs, s.pendingNavigationSettled()...)
195264
s.mu.Unlock()
@@ -212,14 +281,20 @@ func (s *computedState) onDOMContentLoaded() {
212281
// pendingNavigationSettled returns a navigation_settled event if all three
213282
// conditions are met. Must be called with s.mu held.
214283
func (s *computedState) pendingNavigationSettled() []events.Event {
284+
if s.dead {
285+
return nil
286+
}
215287
if s.navDOMLoaded && s.navNetIdle && s.navLayoutSettled && !s.navFired {
216288
s.navFired = true
217289
return []events.Event{{
218290
Ts: time.Now().UnixMicro(),
219291
Type: EventNavigationSettled,
220292
Category: events.CategoryPage,
221-
Source: events.Source{Kind: events.KindCDP},
222-
Data: json.RawMessage(`{}`),
293+
Source: events.Source{
294+
Kind: events.KindCDP,
295+
Metadata: s.navMeta,
296+
},
297+
Data: s.navData,
223298
}}
224299
}
225300
return nil

server/lib/cdpmonitor/computed_test.go

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cdpmonitor
22

33
import (
4+
"encoding/json"
45
"testing"
56
"time"
67

78
"github.com/kernel/kernel-images/server/lib/events"
89
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
911
)
1012

1113
// newTestComputed creates a computedState with an eventCollector for testing.
@@ -19,7 +21,7 @@ func newTestComputed(t *testing.T) (*computedState, *eventCollector) {
1921
func TestNetworkIdle(t *testing.T) {
2022
t.Run("debounce_500ms", func(t *testing.T) {
2123
cs, ec := newTestComputed(t)
22-
cs.resetOnNavigation(0)
24+
cs.resetOnNavigation(0, navContext{})
2325

2426
cs.onRequest()
2527
cs.onRequest()
@@ -37,7 +39,7 @@ func TestNetworkIdle(t *testing.T) {
3739

3840
t.Run("timer_reset_on_new_request", func(t *testing.T) {
3941
cs, ec := newTestComputed(t)
40-
cs.resetOnNavigation(0)
42+
cs.resetOnNavigation(0, navContext{})
4143

4244
cs.onRequest()
4345
cs.onLoadingFinished()
@@ -55,7 +57,7 @@ func TestNetworkIdle(t *testing.T) {
5557
func TestLayoutSettled(t *testing.T) {
5658
t.Run("debounce_1s_after_page_load", func(t *testing.T) {
5759
cs, ec := newTestComputed(t)
58-
cs.resetOnNavigation(0)
60+
cs.resetOnNavigation(0, navContext{})
5961

6062
t0 := time.Now()
6163
cs.onPageLoad()
@@ -67,7 +69,7 @@ func TestLayoutSettled(t *testing.T) {
6769

6870
t.Run("layout_shift_before_page_load_ignored", func(t *testing.T) {
6971
cs, ec := newTestComputed(t)
70-
cs.resetOnNavigation(0)
72+
cs.resetOnNavigation(0, navContext{})
7173

7274
// layout_shift before page_load should be ignored; layout_settled must
7375
// still fire after page_load's 1s debounce.
@@ -81,7 +83,7 @@ func TestLayoutSettled(t *testing.T) {
8183

8284
t.Run("layout_shift_resets_timer", func(t *testing.T) {
8385
cs, ec := newTestComputed(t)
84-
cs.resetOnNavigation(0)
86+
cs.resetOnNavigation(0, navContext{})
8587
cs.onPageLoad()
8688

8789
time.Sleep(600 * time.Millisecond)
@@ -96,7 +98,7 @@ func TestLayoutSettled(t *testing.T) {
9698
func TestNavigationSettled(t *testing.T) {
9799
t.Run("fires_when_all_three_flags_set", func(t *testing.T) {
98100
cs, ec := newTestComputed(t)
99-
cs.resetOnNavigation(0)
101+
cs.resetOnNavigation(0, navContext{})
100102

101103
cs.onDOMContentLoaded()
102104
cs.onRequest()
@@ -109,15 +111,81 @@ func TestNavigationSettled(t *testing.T) {
109111

110112
t.Run("interrupted_by_new_navigation", func(t *testing.T) {
111113
cs, ec := newTestComputed(t)
112-
cs.resetOnNavigation(0)
114+
cs.resetOnNavigation(0, navContext{})
113115

114116
cs.onDOMContentLoaded()
115117
cs.onRequest()
116118
cs.onLoadingFinished()
117119

118120
// Interrupt before layout_settled fires.
119-
cs.resetOnNavigation(0)
121+
cs.resetOnNavigation(0, navContext{})
120122

121123
ec.assertNone(t, "navigation_settled", 1500*time.Millisecond)
122124
})
123125
}
126+
127+
func TestNavDataMetadata(t *testing.T) {
128+
ctx := navContext{
129+
sessionID: "s1",
130+
targetID: "t1",
131+
targetType: "page",
132+
frameID: "f1",
133+
loaderID: "l1",
134+
url: "https://example.com",
135+
}
136+
137+
t.Run("layout_settled_carries_navData_and_navMeta", func(t *testing.T) {
138+
cs, ec := newTestComputed(t)
139+
cs.resetOnNavigation(0, ctx)
140+
cs.onPageLoad()
141+
142+
ev := ec.waitFor(t, "layout_settled", 3*time.Second)
143+
assert.Equal(t, events.CategoryPage, ev.Category)
144+
assert.Equal(t, "s1", ev.Source.Metadata[MetadataKeyCDPSessionID])
145+
assert.Equal(t, "t1", ev.Source.Metadata[MetadataKeyTargetID])
146+
assert.Equal(t, "page", ev.Source.Metadata[MetadataKeyTargetType])
147+
var data map[string]any
148+
require.NoError(t, json.Unmarshal(ev.Data, &data))
149+
assert.Equal(t, "s1", data["session_id"])
150+
assert.Equal(t, "l1", data["loader_id"])
151+
assert.Equal(t, "https://example.com", data["url"])
152+
})
153+
154+
t.Run("navigation_settled_carries_navData_and_navMeta", func(t *testing.T) {
155+
cs, ec := newTestComputed(t)
156+
cs.resetOnNavigation(0, ctx)
157+
158+
cs.onDOMContentLoaded()
159+
cs.onRequest()
160+
cs.onLoadingFinished()
161+
cs.onPageLoad()
162+
163+
ev := ec.waitFor(t, "navigation_settled", 3*time.Second)
164+
assert.Equal(t, events.CategoryPage, ev.Category)
165+
assert.Equal(t, "s1", ev.Source.Metadata[MetadataKeyCDPSessionID])
166+
assert.Equal(t, "t1", ev.Source.Metadata[MetadataKeyTargetID])
167+
var data map[string]any
168+
require.NoError(t, json.Unmarshal(ev.Data, &data))
169+
assert.Equal(t, "s1", data["session_id"])
170+
assert.Equal(t, "l1", data["loader_id"])
171+
})
172+
}
173+
174+
func TestStopSuppressesTimers(t *testing.T) {
175+
t.Run("stop_suppresses_network_idle", func(t *testing.T) {
176+
cs, ec := newTestComputed(t)
177+
cs.resetOnNavigation(0, navContext{})
178+
cs.onRequest()
179+
cs.onLoadingFinished() // arms 500ms network_idle timer
180+
cs.stop()
181+
ec.assertNone(t, "network_idle", 1200*time.Millisecond)
182+
})
183+
184+
t.Run("stop_suppresses_layout_settled", func(t *testing.T) {
185+
cs, ec := newTestComputed(t)
186+
cs.resetOnNavigation(0, navContext{})
187+
cs.onPageLoad() // arms 1s layout_settled timer
188+
cs.stop()
189+
ec.assertNone(t, "layout_settled", 1500*time.Millisecond)
190+
})
191+
}

0 commit comments

Comments
 (0)