|
| 1 | +# Refactoring Analysis Report |
| 2 | + |
| 3 | +> Automated analysis of `agent-browser` codebase identifying key refactoring opportunities. |
| 4 | +
|
| 5 | +--- |
| 6 | + |
| 7 | +## Executive Summary |
| 8 | + |
| 9 | +The `agent-browser` CLI is a well-engineered Rust project (~37K LOC) with solid fundamentals. However, rapid feature growth has introduced technical debt in several areas. This analysis identifies **4 critical**, **8 high-priority**, and **12 medium-priority** refactoring points across the codebase. |
| 10 | + |
| 11 | +**Top 3 impact areas:** |
| 12 | +1. **God module decomposition** — `actions.rs` (7,389 LOC) and `commands.rs` (4,174 LOC) handle too many responsibilities |
| 13 | +2. **Error handling system** — Pervasive `Result<T, String>` prevents error categorization and context chaining |
| 14 | +3. **Code duplication** — 500+ instances of repeated patterns (browser checks, parameter extraction, selector parsing) |
| 15 | + |
| 16 | +--- |
| 17 | + |
| 18 | +## 1. Architecture-Level Issues |
| 19 | + |
| 20 | +### 1.1 God Object: `DaemonState` (CRITICAL) |
| 21 | + |
| 22 | +**File:** `cli/src/native/actions.rs:158-199` |
| 23 | + |
| 24 | +`DaemonState` contains 28 fields spanning every aspect of the daemon — browser management, event handling, network interception, recording/tracing, policy/auth, and stream server integration. Nearly every module receives `&mut DaemonState`, creating implicit coupling across all subsystems. |
| 25 | + |
| 26 | +**Impact:** Any modification risks cascading failures. No separation between transport layer (CDP/WebDriver), domain logic (navigation, interaction), and cross-cutting concerns (policy, tracing, recording). |
| 27 | + |
| 28 | +**Recommendation:** Decompose into focused manager structs: |
| 29 | +``` |
| 30 | +DaemonState |
| 31 | +├── BrowserContext (browser, pages, sessions) |
| 32 | +├── NetworkManager (routes, fetch handler, origin headers, domain filter) |
| 33 | +├── RecordingManager (recording state, tracing state, HAR entries) |
| 34 | +├── PolicyManager (policy, confirmations) |
| 35 | +└── StreamContext (stream client, stream server) |
| 36 | +``` |
| 37 | + |
| 38 | +### 1.2 No Error Type System (CRITICAL) |
| 39 | + |
| 40 | +**Affected:** All 35+ modules |
| 41 | + |
| 42 | +Every module uses `Result<T, String>` with no error categorization. Transient network errors look identical to config errors. Error context is lost during propagation. |
| 43 | + |
| 44 | +**Specific problems:** |
| 45 | +- `connection.rs:492-508` — Brittle string matching (`error.contains("os error 35")`) for error classification |
| 46 | +- No context chain — when errors propagate, intermediate information is lost |
| 47 | +- Inconsistent messages — some use "Failed to...", others expose raw technical details |
| 48 | +- `actions.rs:405` — Malformed CDP events silently discarded with `.ok()` |
| 49 | + |
| 50 | +**Recommendation:** Introduce a proper error enum: |
| 51 | +```rust |
| 52 | +enum AgentBrowserError { |
| 53 | + ConnectionError { source: io::Error, context: String }, |
| 54 | + CdpError { code: i64, message: String }, |
| 55 | + ValidationError { field: String, reason: String }, |
| 56 | + BrowserNotLaunched, |
| 57 | + TimeoutError { operation: String, duration: Duration }, |
| 58 | + // ... |
| 59 | +} |
| 60 | +``` |
| 61 | + |
| 62 | +### 1.3 Missing Trait Abstractions (HIGH) |
| 63 | + |
| 64 | +Only one trait exists in the codebase (`BrowserBackend` in `webdriver/backend.rs`), yet the architecture has several natural polymorphism boundaries: |
| 65 | + |
| 66 | +| Missing Trait | Benefit | Affected Modules | |
| 67 | +|--------------|---------|-----------------| |
| 68 | +| `DomAccessor` | Abstracts element queries from CDP specifics | element.rs, interaction.rs, snapshot.rs | |
| 69 | +| `EventSubscriber` | Unified event pub/sub lifecycle | recording.rs, tracing.rs, network.rs, stream.rs | |
| 70 | +| `LaunchStrategy` | Eliminates engine-specific branching | browser.rs (Chrome, Lightpanda, WebDriver) | |
| 71 | +| `CommandRouter` | Replaces 1,271-line match statement | actions.rs | |
| 72 | + |
| 73 | +### 1.4 Flag Propagation Ceremony (MEDIUM) |
| 74 | + |
| 75 | +Adding a new CLI flag requires changes in 5 files: |
| 76 | +1. `flags.rs` — Parsing logic |
| 77 | +2. `connection.rs::DaemonOptions` — Struct field |
| 78 | +3. `connection.rs::apply_daemon_env()` — Env var mapping |
| 79 | +4. `native::daemon.rs` — Environment reading |
| 80 | +5. `native::actions.rs` — Runtime behavior |
| 81 | + |
| 82 | +**Recommendation:** Single source of truth config struct with derive macros for CLI parsing and env var binding. |
| 83 | + |
| 84 | +--- |
| 85 | + |
| 86 | +## 2. File-Level Refactoring Points |
| 87 | + |
| 88 | +### 2.1 `actions.rs` — 7,389 LOC (CRITICAL) |
| 89 | + |
| 90 | +#### 2.1.1 Repeated Browser Manager Check (~120 occurrences) |
| 91 | + |
| 92 | +```rust |
| 93 | +let mgr = state.browser.as_ref().ok_or("Browser not launched")?; |
| 94 | +``` |
| 95 | + |
| 96 | +This exact pattern appears 120+ times throughout handler functions. |
| 97 | + |
| 98 | +**Fix:** Extract helper: |
| 99 | +```rust |
| 100 | +fn get_browser(state: &DaemonState) -> Result<&BrowserManager, String> { |
| 101 | + state.browser.as_ref().ok_or_else(|| "Browser not launched".to_string()) |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +#### 2.1.2 Repeated Parameter Extraction (~400+ occurrences) |
| 106 | + |
| 107 | +```rust |
| 108 | +let selector = cmd.get("selector").and_then(|v| v.as_str()).ok_or("Missing 'selector'")?; |
| 109 | +``` |
| 110 | + |
| 111 | +**Fix:** Extract typed getters: |
| 112 | +```rust |
| 113 | +fn get_str_param<'a>(cmd: &'a Value, key: &str) -> Result<&'a str, String> { |
| 114 | + cmd.get(key).and_then(|v| v.as_str()) |
| 115 | + .ok_or_else(|| format!("Missing '{}' parameter", key)) |
| 116 | +} |
| 117 | + |
| 118 | +fn get_bool_param(cmd: &Value, key: &str) -> bool { |
| 119 | + cmd.get(key).and_then(|v| v.as_bool()).unwrap_or(false) |
| 120 | +} |
| 121 | +``` |
| 122 | + |
| 123 | +#### 2.1.3 Massive Functions |
| 124 | + |
| 125 | +| Function | Lines | LOC | Issue | |
| 126 | +|----------|-------|-----|-------| |
| 127 | +| `parse_key_chord` | 2107-4380 | 2,273 | Should be split into key category handlers | |
| 128 | +| `browser_metadata_from_version` | 5508-6448 | 940 | Deep nesting, should be table-driven | |
| 129 | +| `build_role_selector` | 4381-5203 | 822 | Complex role mapping, extract to data structure | |
| 130 | +| `error_response` | 6735-7389 | 654 | Mixes response builders with test code | |
| 131 | +| `launch_options_from_env` | 1175-1621 | 446 | Repetitive env var parsing | |
| 132 | +| `open_url_in_browser` | 1647-2106 | 459 | Platform-specific branching | |
| 133 | + |
| 134 | +#### 2.1.4 Repeated Env Var Parsing |
| 135 | + |
| 136 | +Lines 1176-1212 show repetitive patterns: |
| 137 | +```rust |
| 138 | +.map(|v| v == "1" || v == "true").unwrap_or(false) // 10+ times |
| 139 | +v.split([',', '\n']).map(|s| s.trim().to_string()).filter(|s| !s.is_empty()).collect() // 5+ times |
| 140 | +``` |
| 141 | + |
| 142 | +**Fix:** Helper functions `parse_bool_env()` and `parse_csv_env()`. |
| 143 | + |
| 144 | +#### 2.1.5 Similar Handler Functions |
| 145 | + |
| 146 | +Mouse event handlers (`handle_mousemove`, `handle_mousedown`, `handle_mouseup`) and check/uncheck handlers share nearly identical boilerplate. Could use a macro or generic handler. |
| 147 | + |
| 148 | +### 2.2 `commands.rs` — 4,174 LOC (HIGH) |
| 149 | + |
| 150 | +#### 2.2.1 Monolithic `parse_command` Function (1,271 lines) |
| 151 | + |
| 152 | +Lines 74-1344 contain a single match statement handling 40+ commands. While helper functions exist (`parse_get`, `parse_set`, `parse_auth`), the main function remains enormous. |
| 153 | + |
| 154 | +**Recommendation:** Command registry pattern — each command implements a `Command` trait with `parse()` and `name()` methods. |
| 155 | + |
| 156 | +#### 2.2.2 Selector-Based Command Duplication |
| 157 | + |
| 158 | +Commands `dblclick`, `hover`, `focus`, `check`, `uncheck` (lines 139-202) all follow: |
| 159 | +```rust |
| 160 | +let sel = rest.first().ok_or_else(|| ParseError::MissingArguments { ... })?; |
| 161 | +Ok(json!({ "id": id, "action": "command", "selector": sel })) |
| 162 | +``` |
| 163 | + |
| 164 | +**Fix:** `simple_selector_action(rest, id, action, usage)` helper. |
| 165 | + |
| 166 | +#### 2.2.3 Flag-to-Value Extraction Pattern (10+ times) |
| 167 | + |
| 168 | +```rust |
| 169 | +let filter_idx = rest.iter().position(|&s| s == "--filter"); |
| 170 | +let filter = filter_idx.and_then(|i| rest.get(i + 1).copied()); |
| 171 | +``` |
| 172 | + |
| 173 | +**Fix:** `extract_flag_value(rest, "--filter")` helper. |
| 174 | + |
| 175 | +#### 2.2.4 Duplicate Diff Parsing (~250 lines) |
| 176 | + |
| 177 | +Lines 1349-1603: Snapshot diff and screenshot diff parse nearly identical flag combinations (`--baseline`, `--selector`, `--output`, `--depth`). Should be consolidated into shared diff option parser. |
| 178 | + |
| 179 | +#### 2.2.5 Identical Start/Stop Subcommand Patterns |
| 180 | + |
| 181 | +Trace, profiler, and record commands (lines 1018-1142) have identical structures: |
| 182 | +```rust |
| 183 | +const VALID: &[&str] = &["start", "stop"]; |
| 184 | +match sub { "start" => {...}, "stop" => {...}, _ => error } |
| 185 | +``` |
| 186 | + |
| 187 | +**Fix:** Generic `start_stop_command(name, rest, id)` helper. |
| 188 | + |
| 189 | +#### 2.2.6 Dialog Accept/Dismiss Duplication |
| 190 | + |
| 191 | +Lines 992-1004: Only difference is the `"response"` field value. Should be a single parameterized arm. |
| 192 | + |
| 193 | +### 2.3 `browser.rs` — 1,733 LOC (HIGH) |
| 194 | + |
| 195 | +#### 2.3.1 Target Create + Attach Duplication (3 locations) |
| 196 | + |
| 197 | +Identical CDP call sequence at lines 357-378, 675-696, and 746-767: |
| 198 | +```rust |
| 199 | +let result = self.client.send_command_typed("Target.createTarget", ...).await?; |
| 200 | +let attach = self.client.send_command_typed("Target.attachToTarget", ...).await?; |
| 201 | +``` |
| 202 | + |
| 203 | +**Fix:** Extract `create_and_attach_target(&self) -> Result<(String, String), String>`. |
| 204 | + |
| 205 | +#### 2.3.2 PageInfo Construction Duplication (4 locations) |
| 206 | + |
| 207 | +Lines 380-386, 403-409, 698-704, 772-778 all construct identical `PageInfo` structs. |
| 208 | + |
| 209 | +**Fix:** `PageInfo::new_blank(target_id, session_id)` constructor. |
| 210 | + |
| 211 | +#### 2.3.3 `launch()` Method — 113 Lines |
| 212 | + |
| 213 | +Lines 199-312 mix engine validation, browser launching, manager initialization, and feature configuration. Extract `configure_browser_features()` for HTTPS errors, user agent, color scheme, and downloads configuration. |
| 214 | + |
| 215 | +#### 2.3.4 Validation Logic Overlap |
| 216 | + |
| 217 | +`validate_launch_options()` (lines 20-58) and `validate_lightpanda_options()` (lines 61-88) share duplicate checks for extensions, profile, and storage_state incompatibilities. |
| 218 | + |
| 219 | +#### 2.3.5 Magic Values |
| 220 | + |
| 221 | +| Value | Lines | Meaning | |
| 222 | +|-------|-------|---------| |
| 223 | +| `25_000` | 259 | Default timeout (ms) | |
| 224 | +| `10_000` | 323 | Connection timeout (ms) | |
| 225 | +| `600` | 1143 | Network poll timeout (ms) | |
| 226 | +| `500` | 1190 | Idle detection threshold (ms) | |
| 227 | +| `"about:blank"` | 362, 680, 701, 744 | Default page URL | |
| 228 | +| `"page"` | 99, 144, 385, 408, 703 | Target type constant | |
| 229 | + |
| 230 | +All should be named constants. |
| 231 | + |
| 232 | +### 2.4 `output.rs` — Very Large (MEDIUM) |
| 233 | + |
| 234 | +Extremely large file handling all output formatting and help text. Should be split into: |
| 235 | +- `output/format.rs` — Response formatting logic |
| 236 | +- `output/help.rs` — Help text content (which is mostly static data) |
| 237 | + |
| 238 | +### 2.5 `connection.rs` (MEDIUM) |
| 239 | + |
| 240 | +#### 2.5.1 Brittle Error Detection |
| 241 | + |
| 242 | +Lines 492-508: Transient error detection via string matching: |
| 243 | +```rust |
| 244 | +fn is_transient_error(error: &str) -> bool { |
| 245 | + error.contains("os error 35") // EAGAIN macOS |
| 246 | + || error.contains("os error 11") // EAGAIN Linux |
| 247 | + // ... 11 more patterns |
| 248 | +} |
| 249 | +``` |
| 250 | + |
| 251 | +This breaks if error message formats change. Should use `io::ErrorKind` matching. |
| 252 | + |
| 253 | +--- |
| 254 | + |
| 255 | +## 3. Cross-Cutting Patterns |
| 256 | + |
| 257 | +### 3.1 JSON Path Repetition (~100+ occurrences) |
| 258 | + |
| 259 | +Deep JSON access chains scattered everywhere: |
| 260 | +```rust |
| 261 | +tree_result.get("frameTree").and_then(|t| t.get("frame")).and_then(|f| f.get("id"))... |
| 262 | +``` |
| 263 | + |
| 264 | +**Recommendation:** Consider a `json_path!()` macro or typed CDP response structs. |
| 265 | + |
| 266 | +### 3.2 Inconsistent Color Usage |
| 267 | + |
| 268 | +`color.rs` provides a clean API, but some modules use it while others use raw strings or skip coloring entirely. |
| 269 | + |
| 270 | +### 3.3 Scattered Validation |
| 271 | + |
| 272 | +Validation logic is spread across 5+ files (`validation.rs`, `browser.rs`, `policy.rs`, `flags.rs`, `commands.rs`) with inconsistent error types and return conventions. |
| 273 | + |
| 274 | +--- |
| 275 | + |
| 276 | +## 4. Priority Matrix |
| 277 | + |
| 278 | +### Critical (address first) |
| 279 | +| # | Issue | Files | Est. Reduction | |
| 280 | +|---|-------|-------|---------------| |
| 281 | +| 1 | Introduce error type enum | All modules | Better debugging, no LOC reduction | |
| 282 | +| 2 | Decompose `DaemonState` god object | actions.rs | Reduces coupling across 35+ modules | |
| 283 | +| 3 | Extract parameter/browser helpers in actions.rs | actions.rs | ~500+ duplicate lines eliminated | |
| 284 | +| 4 | Break down 2,273-line `parse_key_chord` | actions.rs | Better maintainability | |
| 285 | + |
| 286 | +### High Priority |
| 287 | +| # | Issue | Files | Est. Reduction | |
| 288 | +|---|-------|-------|---------------| |
| 289 | +| 5 | Consolidate selector commands in commands.rs | commands.rs | ~100 lines | |
| 290 | +| 6 | Unify diff option parsing | commands.rs | ~250 lines | |
| 291 | +| 7 | Extract target create+attach helper | browser.rs | ~60 lines | |
| 292 | +| 8 | Extract flag parsing helper in commands.rs | commands.rs | ~100 lines | |
| 293 | +| 9 | Generic start/stop command handler | commands.rs | ~80 lines | |
| 294 | +| 10 | Extract `configure_browser_features()` | browser.rs | Better separation | |
| 295 | +| 11 | Split output.rs into format + help | output.rs | Better organization | |
| 296 | +| 12 | Define trait abstractions (DomAccessor, EventSubscriber) | native/ | Decoupling | |
| 297 | + |
| 298 | +### Medium Priority |
| 299 | +| # | Issue | Files | Est. Reduction | |
| 300 | +|---|-------|-------|---------------| |
| 301 | +| 13 | Env var parsing helpers | actions.rs | ~50 lines | |
| 302 | +| 14 | Constants for magic values | browser.rs, actions.rs | Better readability | |
| 303 | +| 15 | Consolidate validation patterns | 5+ files | Consistency | |
| 304 | +| 16 | Typed CDP response structs | native/ | Type safety | |
| 305 | +| 17 | JSON path macro or helper | multiple | ~200 lines | |
| 306 | +| 18 | PageInfo constructor | browser.rs | ~40 lines | |
| 307 | +| 19 | Dialog accept/dismiss consolidation | commands.rs | ~15 lines | |
| 308 | +| 20 | Config struct with derive macros | flags.rs, connection.rs | Eliminates flag ceremony | |
| 309 | +| 21 | Color usage consistency | multiple | UX consistency | |
| 310 | +| 22 | Unified storage manager trait | state.rs, cookies.rs, storage.rs | Consistency | |
| 311 | +| 23 | Mouse event handler macro | actions.rs | ~60 lines | |
| 312 | +| 24 | Connection error kind matching | connection.rs | Robustness | |
| 313 | + |
| 314 | +--- |
| 315 | + |
| 316 | +## 5. Estimated Impact |
| 317 | + |
| 318 | +| Metric | Current | After Refactoring | |
| 319 | +|--------|---------|-------------------| |
| 320 | +| `actions.rs` LOC | 7,389 | ~5,500 (-25%) | |
| 321 | +| `commands.rs` LOC | 4,174 | ~3,300 (-21%) | |
| 322 | +| `browser.rs` LOC | 1,733 | ~1,500 (-13%) | |
| 323 | +| Duplicate patterns | 500+ instances | ~100 instances | |
| 324 | +| Error types | 1 (`String`) | 8-10 categorized variants | |
| 325 | +| Trait abstractions | 1 | 5-6 | |
| 326 | +| Max function length | 2,273 lines | <200 lines | |
| 327 | + |
| 328 | +--- |
| 329 | + |
| 330 | +*Analysis generated for `agent-browser` v0.22.0* |
0 commit comments