Skip to content

feat: Auto-show triggered network requests after DOM actions #111

@szymdzum

Description

@szymdzum

Summary

DOM action commands automatically show network requests they triggered, using event-driven network idle detection instead of arbitrary timeouts. Zero flags required - it just works.

Problem

After performing a DOM action, agents need to verify it triggered the expected API calls:

bdg dom click "button.submit"
# Did it work? What API calls happened?
bdg peek --network --last 5
# Manual correlation by timestamp... error-prone

Solution: Auto-Show Triggered Requests

DOM actions automatically display triggered network requests:

bdg dom click "button.submit"

Output:

Clicked: button.submit

Triggered:
  POST /api/submit → 200 OK (1.2s)
  GET /api/refresh → 200 OK (89ms)

Network idle after 1.4s

No flags. No trace commands. No workflow to remember.

Event-Driven Network Idle Detection

Instead of hardcoded wait times (unreliable, arbitrary), use CDP events to detect when network activity has settled:

async function waitForNetworkIdle(
  options: { idleThreshold?: number; maxWait?: number } = {}
): Promise<NetworkRequest[]> {
  const { idleThreshold = 100, maxWait = 10000 } = options;
  
  let lastActivity = Date.now();
  let pendingRequests = 0;
  const triggeredRequests: NetworkRequest[] = [];
  const actionTimestamp = Date.now();

  const onRequestStart = (params: RequestWillBeSentParams) => {
    pendingRequests++;
    lastActivity = Date.now();
    if (params.timestamp >= actionTimestamp) {
      // Track requests that started after our action
      triggeredRequests.push(createRequestFromParams(params));
    }
  };

  const onRequestEnd = (params: LoadingFinishedParams) => {
    pendingRequests = Math.max(0, pendingRequests - 1);
    lastActivity = Date.now();
    // Update request with completion status
    updateRequestStatus(triggeredRequests, params);
  };

  // Subscribe to CDP events
  cdp.on('Network.requestWillBeSent', onRequestStart);
  cdp.on('Network.loadingFinished', onRequestEnd);
  cdp.on('Network.loadingFailed', onRequestEnd);

  try {
    const start = Date.now();
    while (Date.now() - start < maxWait) {
      const timeSinceActivity = Date.now() - lastActivity;
      
      // Network is idle when: no pending requests AND quiet for threshold
      if (pendingRequests === 0 && timeSinceActivity >= idleThreshold) {
        break;
      }
      await sleep(50);
    }
  } finally {
    // Cleanup listeners
    cdp.off('Network.requestWillBeSent', onRequestStart);
    cdp.off('Network.loadingFinished', onRequestEnd);
    cdp.off('Network.loadingFailed', onRequestEnd);
  }

  return triggeredRequests;
}

Behavior by Scenario

Scenario Behavior Total Time
Fast API (50ms) Waits for completion + 100ms idle ~150ms
Slow API (2s) Waits for completion + 100ms idle ~2.1s
Multiple requests Waits for ALL to complete Longest + 100ms
No requests triggered Returns after 100ms idle ~100ms
Hung/slow request Max timeout safety net 10s max
WebSocket messages Included in triggered output N/A

Integration with DOM Commands

File: src/commands/dom/index.ts

// click handler
.action(async (selector, options) => {
  await runCommand(async () => {
    const result = await performClick(selector, options);
    
    // Auto-capture triggered requests (unless --no-wait)
    if (!options.noWait) {
      const triggered = await waitForNetworkIdle();
      return { 
        success: true, 
        data: { ...result, triggered } 
      };
    }
    
    return { success: true, data: result };
  }, options, formatClickResult);
});

Output Formats

Human-Readable (default)

Clicked: button.submit

Triggered:
  POST /api/submit → 200 OK (1.2s)
  GET /api/refresh → 200 OK (89ms)
  WS wss://realtime.example.com ← 2 messages

Network idle after 1.4s

JSON (--json)

{
  "action": "click",
  "selector": "button.submit",
  "success": true,
  "triggered": {
    "requests": [
      {
        "id": "req_123",
        "method": "POST",
        "url": "/api/submit",
        "status": 200,
        "duration": 1200
      }
    ],
    "websocket": [
      {
        "url": "wss://realtime.example.com",
        "received": 2,
        "sent": 0
      }
    ],
    "idleAfter": 1400
  }
}

Quiet Mode (-q)

Clicked: button.submit

Configuration

Environment variables for edge cases:

Variable Default Description
BDG_IDLE_THRESHOLD 100 ms of quiet to consider "idle"
BDG_IDLE_TIMEOUT 10000 Max wait in ms (safety net)

CLI override:

bdg dom click "button" --no-wait    # Skip network idle detection

Affected Commands

All DOM action commands gain automatic triggered request display:

  • bdg dom click
  • bdg dom fill
  • bdg dom submit
  • bdg dom pressKey

Implementation Files

File Change
src/telemetry/networkIdle.ts New: Network idle detection logic
src/commands/dom/index.ts Integrate idle detection after actions
src/commands/dom/helpers.ts Add waitForNetworkIdle helper
src/ui/formatters/dom.ts Format triggered requests output
src/commands/shared/optionTypes.ts Add noWait option type

Error Handling

Timeout Reached

Clicked: button.submit

Triggered (timeout after 10s):
  POST /api/submit → pending
  
Warning: Network did not idle within 10s. Some requests may still be in flight.

No Requests

Clicked: button.submit

No network requests triggered (idle after 100ms)

Testing Strategy

Unit Tests

  • waitForNetworkIdle with mock CDP events
  • Idle threshold timing accuracy
  • Max timeout enforcement
  • Request correlation by timestamp

Integration Tests

# Fast endpoint
bdg https://httpbin.org/forms/post
bdg dom fill "input[name=custname]" "Test"
bdg dom click "button"
# Verify: POST /post appears in triggered

# Slow endpoint
bdg https://httpbin.org/delay/2
bdg dom eval "fetch('/delay/2')"
# Verify: Waits ~2.1s, shows request

# No network
bdg dom click "button.local-only"
# Verify: Shows "No network requests triggered"

Benefits

  • Zero learning curve - No flags to remember, no workflow
  • Accurate - Event-driven, not arbitrary timeouts
  • Fast - Only waits as long as needed
  • Comprehensive - Captures all requests including WebSocket
  • Agent-friendly - JSON output includes all correlation data

Acceptance Criteria

  • DOM actions auto-display triggered network requests
  • Uses CDP events for network idle detection (no hardcoded waits)
  • Respects BDG_IDLE_THRESHOLD and BDG_IDLE_TIMEOUT env vars
  • --no-wait flag skips idle detection
  • JSON output includes triggered object
  • WebSocket messages included
  • Timeout warning when max wait exceeded
  • Works with click, fill, submit, pressKey

Non-Goals

  • Explicit trace start/stop commands (not needed with auto-detection)
  • Historical trace replay
  • Trace persistence across sessions

Related

  • Network telemetry: src/telemetry/network.ts
  • DOM commands: src/commands/dom/index.ts
  • TelemetryStore: src/daemon/worker/TelemetryStore.ts
  • Roadmap: docs/roadmap/ROADMAP.md (Section 2.1)

Metadata

Metadata

Assignees

No one assigned

    Labels

    agent-friendlyImproves agent workflowsenhancementNew feature or requesthigh-impactHigh value for agents/users

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions