1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//! Demonstrates the network monitor API (`tab.monitor()`).
//!
//! Launches a browser, navigates to example.com, and runs a network monitor
//! that prints every HTTP exchange (method / URL / status) and WebSocket
//! frame observed while the page loads.
//!
//! The monitor is a [`futures::Stream`] over
//! [`zendriver::NetworkEvent`](zendriver::monitor::NetworkEvent) — it runs in
//! the background and delivers events as the browser fires them. Dropping the
//! monitor (or calling `.stop()`) cancels its background task.
//!
//! Requires the `monitor` cargo feature:
//! `cargo run --example network_monitor --features monitor`.
use futures::StreamExt;
use zendriver::Browser;
use zendriver::monitor::{FrameDirection, NetworkDeliveryBoundary, NetworkEvent};
#[tokio::main]
#[allow(clippy::result_large_err)] // example boundary; users wrap in their own Error
async fn main() -> zendriver::Result<()> {
tracing_subscriber::fmt::init();
let browser = Browser::builder().headless(true).launch().await?;
let tab = browser.main_tab();
// Start the monitor BEFORE navigating so no events are missed.
// An optional URL pattern restricts events to matching URLs (substring
// match). Omit `.url_pattern(...)` to observe all network activity.
let mut monitor = tab.monitor().url_pattern("example.com").start().await?;
tab.goto("https://example.com").await?;
tab.wait_for_load().await?;
// Drain events until the channel is empty (monitor still running but
// nothing new is in flight). In a real application you would drive the
// stream until a specific event arrives or a timeout fires.
while let Ok(Some(event)) =
tokio::time::timeout(std::time::Duration::from_millis(500), monitor.next()).await
{
match event {
NetworkEvent::Http(exchange) => {
let status = exchange.response.as_ref().map_or_else(
|| exchange.error.clone().unwrap_or_default(),
|r| r.status.to_string(),
);
println!(
"[HTTP] {} {} -> {}",
exchange.request.method, exchange.request.url, status
);
}
// Only emitted when the monitor is started with
// `.stream_bodies(true)` (not enabled in this example) — shown
// here for completeness since the match must be exhaustive.
NetworkEvent::HttpData { request_id, chunk } => {
println!("[DATA] id={request_id} {} bytes", chunk.len());
}
NetworkEvent::WebSocketOpen { url, request_id } => {
println!("[WS ] open id={request_id} url={url}");
}
NetworkEvent::WebSocketFrame {
request_id,
direction,
opcode,
payload,
} => {
let dir = match direction {
FrameDirection::Sent => "sent",
FrameDirection::Received => "recv",
};
println!("[WS ] frame {dir} id={request_id} opcode={opcode} payload={payload:?}");
}
NetworkEvent::WebSocketClose { request_id } => {
println!("[WS ] close id={request_id}");
}
NetworkEvent::EventSourceMessage {
request_id,
event_name,
data,
..
} => {
println!("[SSE ] id={request_id} event={event_name:?} data={data:?}");
}
// A delivery-loss boundary — a lagged/reconnected/disconnected
// transport, a correlation-map eviction, or an undecodable
// payload. Ignoring this variant is fine (every fully-observed
// exchange above still arrives); printing it here just shows
// where a real consumer would decide whether to resync, alert,
// or restart the monitor. `Disconnected` in particular means
// this monitor's correlator task has already ended — see
// `NetworkDeliveryBoundary::Disconnected`.
NetworkEvent::DeliveryBoundary(boundary) => {
println!("[GAP ] {boundary:?}");
if matches!(boundary, NetworkDeliveryBoundary::Disconnected { .. }) {
break;
}
}
}
}
browser.close().await?;
Ok(())
}