Skip to content

feat: add network traffic capture for test runs#75

Open
droid-ash wants to merge 8 commits intomainfrom
log-network-capture
Open

feat: add network traffic capture for test runs#75
droid-ash wants to merge 8 commits intomainfrom
log-network-capture

Conversation

@droid-ash
Copy link
Copy Markdown
Contributor

@droid-ash droid-ash commented Apr 9, 2026

Summary

  • Add finalrun log-network standalone command to capture HTTP/HTTPS traffic from Android/iOS devices, streaming requests live and writing HAR files on exit
  • Integrate network capture into the test pipeline — when enabled, traffic is recorded per-session and surfaced in HTML reports with a dedicated Network tab
  • Include mitmproxy CA setup, ADB/iOS proxy configuration, and docs covering setup, configuration, and troubleshooting

Test plan

  • Run finalrun log-network --platform android and verify live traffic output + HAR file generation
  • Run a test suite with network capture enabled and confirm network logs appear in the HTML report
  • Verify iOS proxy setup and CA trust flow
  • Confirm no binary encoding issues in captured logs after body capture removal

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added network traffic capture during test runs—enable via network.capture: true configuration to automatically record HTTP/HTTPS traffic for each test with automatic HAR export
    • Added finalrun log-network command for standalone interactive network capture on Android and iOS devices with live terminal output
    • Added Network tab to test reports displaying captured requests with search/filtering, request details (headers, timing), and HAR file download
    • Network capture includes TLS error detection for certificate pinning and trust issues
  • New Configuration

    • Added network.capture workspace configuration option to enable/disable network capture globally
  • Documentation

    • Added comprehensive guides for network capture setup, usage, troubleshooting, and CLI reference

droid-ash and others added 8 commits April 9, 2026 04:39
Adds a new CLI command that captures network traffic from Android
emulators/devices and iOS simulators via an HTTPS-intercepting proxy.
Streams requests live to the terminal and exports HAR 1.2 files.

Key design decisions:
- Android: system proxy via `adb shell settings put global http_proxy`
- iOS: PAC file served over HTTP with DIRECT fallback (crash-safe —
  Mac internet survives process death, unlike direct networksetup proxy)
- CA cert generated once at ~/.finalrun/ca/, reused across runs
- Pre-capture connectivity test prevents broken internet if CA not trusted
- Crash recovery via ~/.finalrun/proxy-state.json restores proxy on next run
- TLS failures labeled by cause: pinning vs missing user CA trust

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Wire NetworkCaptureManager into the test execution lifecycle following
the device logs pattern. Key additions:

- NetworkCaptureManager: session-scoped mockttp proxy with per-test HAR
  slicing, full request/response body capture, TLS error tracking
- NetworkCaptureSetup: Android (adb proxy) + iOS (PAC server) config
- Test lifecycle: network capture start/stop per test in sessionRunner,
  opt-in via `network.capture: true` in .finalrun/config.yaml
- HAR artifact writing with header redaction (Authorization, Cookie,
  tokens) and ${secrets.*} value redaction
- Report UI: Network tab with request table, status filters, search,
  slide-out detail panel (Headers/Request/Response tabs), video sync
- Types: networkLogFile/StartedAt/CompletedAt on TestResult

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Standalone capture.ts now captures request/response bodies (text up
  to 512KB, binary skipped) matching NetworkCaptureManager behavior
- Added comment noting relationship between standalone and integrated
- Verified end-to-end on both Android (Wikipedia + Google Services)
  and iOS (Wikipedia + Apple Services)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The per-test network capture was wired but the session-level setup was
missing: no proxy started, no device proxy configured, no CA
verification. This meant "Network capture started" printed successfully
even when nothing was actually capturing.

Now when network.capture is enabled:
- Starts mockttp proxy session before the test loop
- Configures device proxy (Android: adb settings, iOS: PAC server)
- Verifies CA is trusted via test HTTPS request
- If CA not trusted: prints clear warning with fix instructions
  ("run finalrun log-network to set up"), continues without capture
- Per-test capture only runs if session setup succeeded
- Proxy restored in finally block on both success and failure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Suppress mockttp "Failed to handle request" error during CA
  verification by temporarily raising log level to ERROR
- Demote internal proxy setup logs (port, proxy target) from INFO
  to DEBUG — user shouldn't see these
- Replace noisy per-test "Network capture started" with debug-only
  marker log
- Session-level success message now shows one clean line:
  "Network capture active (proxy on 127.0.0.1:8000, CA verified)"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three fixes for network capture during test runs:

1. Remove CA cert push from Android configureProxy() — pushing during
   a test causes CertPathValidatorException. Cert setup belongs in the
   standalone `finalrun log-network` command only.

2. Replace host-side HTTPS verification (which caused "Failed to handle
   request: unable to get local issuer certificate" errors) with
   traffic-based detection: wait 3s for device background traffic, then
   check if proxy saw successful entries vs TLS errors. This tests the
   DEVICE's trust, not the host's.

3. Downgrade per-test "Network capture started" to debug-only — the
   session-level message is the meaningful confirmation.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- New docs/network-logging.md: user-facing guide covering setup, what
  you get, what gets captured/redacted, pinning, troubleshooting
- docs/configuration.md: add network.capture field to config table
- docs/cli-reference.md: add log-network command section
- README.md: add Network Logging link to docs section
- Downgrade all network capture Logger.i to Logger.d (no more noisy
  "proxy started on port" / "proxy set to" / "proceeding optimistically"
  messages in normal output)
- Remove 3-second traffic-based verification wait — proxy setup is
  now silent, warnings come from post-test HAR analysis

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…sues

Binary response bodies (compressed, protobuf, images) were being stored
as UTF-8 text in the HAR, producing garbled output. Rather than adding
binary detection heuristics, remove body capture entirely for now.

Changes:
- NetworkCaptureManager: stop reading req.body/response.body
- capture.ts (standalone): same
- reportWriter: remove body text redaction (header redaction stays)
- reportServer: remove body fields from view model
- reportTemplate: remove Request Body / Response Body tabs from detail
  panel, keep Headers tab only (General + Response Headers + Request
  Headers)
- docs/network-logging.md: mark request/response body as "Not yet"

What stays: method, URL, status, all headers, timing, size, query
params, TLS failure labeling, video sync, search, status filters.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 9, 2026

📝 Walkthrough

Walkthrough

This PR implements comprehensive HTTP/HTTPS network capture for test runs and via a standalone CLI command. It adds CA certificate generation/management, platform-specific MITM proxy configuration (Android/iOS), HAR file export, test report integration with a Network tab, and crash-safe proxy state persistence.

Changes

Cohort / File(s) Summary
Documentation
README.md, docs/cli-reference.md, docs/configuration.md, docs/network-capture.md, docs/network-logging.md
Added documentation for network capture feature: new network.capture config field, finalrun log-network CLI command reference, user guides for setup/usage, platform-specific instructions, HAR format details, and troubleshooting.
CLI Entry Point
packages/cli/bin/finalrun.ts
Added log-network subcommand accepting --platform, --device, and --out flags; wired networkCapture option from workspace config into test runner invocation.
Log-Network Command Implementation
packages/cli/src/commands/logNetwork/index.ts
Orchestrates platform-specific (Android/iOS) end-to-end network capture: validates platform, performs proxy state recovery, manages CA loading/installation, starts MITM proxy, configures device/macOS routing, verifies HTTPS via test request, streams requests until Ctrl+C, writes HAR and TLS error summary.
Android Device Integration
packages/cli/src/commands/logNetwork/adb.ts
ADB wrapper providing device listing, shell execution, file push, reverse tunneling, proxy management, and property queries for Android device control during capture setup.
iOS Simulator Integration
packages/cli/src/commands/logNetwork/ios.ts
iOS simulator management: lists booted simulators, installs CA certificates, launches/terminates apps, runs PAC HTTP server for proxy auto-config, manages macOS autoproxy settings.
CA Certificate Management
packages/cli/src/commands/logNetwork/ca.ts
Generates and persists local CA certificates in ~/.finalrun/ca/ with PEM and DER formats; reuses existing certs across runs for consistency.
Network Capture Core
packages/cli/src/commands/logNetwork/capture.ts
Wraps mockttp HTTPS MITM proxy to record request/response lifecycle, compute timings, capture TLS client errors, and export captured data to HAR format.
Utility Modules
packages/cli/src/commands/logNetwork/colors.ts, packages/cli/src/commands/logNetwork/livePrinter.ts, packages/cli/src/commands/logNetwork/proxyState.ts
Terminal color formatting, formatted live printing of network entries/TLS errors with status codes and timings, and JSON-based persistence/recovery of proxy configuration state in ~/.finalrun/proxy-state.json.
Test/Session Execution
packages/cli/src/testRunner.ts, packages/cli/src/sessionRunner.ts
Extended test runner to initialize network capture session (CA loading, proxy startup, platform-specific setup); updated session runner to start/stop per-test capture, build network artifacts, and handle cleanup.
Workspace Configuration
packages/cli/src/workspace.ts
Added network.capture boolean config field to enable/disable network capture during test runs.
Report Generation
packages/cli/src/reportServer.ts, packages/cli/src/reportWriter.ts
Extended report server to serve .har files; updated report writer to copy HAR artifacts, parse and redact sensitive headers/query params, record test metadata (networkLogFile, timestamps).
Report UI/Templates
packages/cli/src/reportTemplate.ts
Added Network tab to test report with request list (searchable, status-filterable), detail pane with headers, network-to-video synchronization (seek video on row click, highlight nearest request during playback), and TLS error row rendering.
Device-Node Package
packages/device-node/src/device/Device.ts, packages/device-node/src/device/NetworkCaptureManager.ts, packages/device-node/src/device/NetworkCaptureSetup.ts, packages/device-node/src/index.ts
Integrated network capture manager into Device class; NetworkCaptureManager handles session-scoped MITM proxy lifecycle and per-test HAR generation; NetworkCaptureSetup provides platform-specific proxy routing (Android adb reverse/global proxy, iOS PAC + macOS autoproxy); barrel exports new types.
Common Types
packages/common/src/interfaces/DeviceAgent.ts, packages/common/src/models/NetworkLog.ts, packages/common/src/models/TestResult.ts, packages/common/src/index.ts
Added NetworkLogCaptureResult type; extended DeviceAgent interface with network capture lifecycle methods; extended TestResult with optional networkLogFile and timestamps.
Goal Executor
packages/goal-executor/src/TestExecutor.ts
Updated TestExecutionResult interface to include optional networkLog field.
Dependencies
packages/cli/package.json
Added mockttp (^4.3.0) dependency for MITM proxy functionality.

Sequence Diagram(s)

sequenceDiagram
    participant User as User (CLI)
    participant Cmd as log-network<br/>Command
    participant CA as CA<br/>Manager
    participant Device as Device<br/>(adb/xcrun)
    participant Proxy as Capture<br/>Proxy (mockttp)
    participant Stream as Output<br/>(HAR/Terminal)

    User->>Cmd: finalrun log-network --platform=android --device=<serial>
    Cmd->>Cmd: Validate platform, recover stale proxy state
    Cmd->>CA: loadOrGenerateCA()
    CA-->>Cmd: {cert, key, files}
    Cmd->>Device: Detect/select target device
    Device-->>Cmd: Device serial
    Cmd->>Device: Push CA cert to device
    Device-->>Cmd: Success
    Cmd->>Device: Guide trust setup (API-level dependent)
    Cmd->>Proxy: start(cert, key, callbacks)
    Proxy-->>Cmd: Running on port P
    Cmd->>Device: Configure proxy (adb reverse or emulator loopback)
    Device-->>Cmd: Proxy configured
    Cmd->>Cmd: Persist proxy state for crash recovery
    Cmd->>Proxy: Verify HTTPS via test request
    Proxy->>Proxy: Intercept request, check TLS
    Proxy-->>Cmd: Verify result (trusted/untrusted/unknown)
    alt Verification Failed
        Cmd->>Device: Clean up proxy
        Cmd-->>User: Error: Device does not trust CA
    else Verification Succeeded
        Cmd-->>User: Capture started, streaming requests...
        loop Until Ctrl+C
            Proxy->>Proxy: Intercept request/response
            Proxy->>Stream: printEntry(entry) live to terminal
            Proxy->>Proxy: Record in entries array
        end
        User->>Cmd: Ctrl+C (SIGTERM)
        Cmd->>Proxy: stop()
        Proxy-->>Cmd: Stopped
        Cmd->>Device: Clear proxy config
        Device-->>Cmd: Cleared
        Cmd->>Cmd: Clear persisted proxy state
        Proxy->>Stream: toHar() → write to file
        Cmd->>Stream: Write TLS error summary
        Cmd-->>User: Complete, HAR saved
    end
Loading
sequenceDiagram
    participant TestRunner as Test Runner
    participant SessionMgr as Session<br/>Manager
    participant Device as Device<br/>(Network Capture)
    participant Proxy as Capture<br/>Proxy
    participant Writer as Report<br/>Writer

    TestRunner->>SessionMgr: Initialize with networkCapture=true
    SessionMgr->>Device: startNetworkSession(cert, key)
    Device->>Proxy: Initialize proxy
    Proxy-->>Device: proxyPort
    Device-->>SessionMgr: Success
    SessionMgr->>Device: setupNetworkProxy(platform, proxyPort)
    Device-->>SessionMgr: Proxy configured
    loop For Each Test
        SessionMgr->>Device: startNetworkCapture(runId, testId)
        Device->>Proxy: Record start index
        Proxy-->>Device: Started
        Device-->>SessionMgr: Capture began
        SessionMgr->>SessionMgr: Execute test
        SessionMgr->>Device: stopNetworkCapture(runId, testId)
        Device->>Proxy: Extract captured entries/TLS errors
        Proxy-->>Device: Entries slice
        Device->>Device: Build HAR file
        Device-->>SessionMgr: {filePath, startedAt, completedAt}
        SessionMgr->>SessionMgr: Attach networkLog to test result
    end
    SessionMgr->>Device: stopNetworkSession()
    Device->>Proxy: Stop proxy
    Proxy-->>Device: Stopped
    Device-->>SessionMgr: Cleaned up
    SessionMgr->>Writer: Write test artifacts
    Writer->>Writer: Copy HAR, redact sensitive data
    Writer->>Writer: Record networkLogFile path
    Writer-->>TestRunner: Report ready
Loading
sequenceDiagram
    participant User as User (Browser)
    participant UI as Report UI
    participant Video as Video<br/>Player
    participant Network as Network<br/>Tab

    User->>UI: Open test report
    UI->>UI: Load networkLogEntries from manifest
    UI->>UI: Render Network tab with entries
    User->>Network: Click request row
    Network->>Network: Extract startedDateTime
    Network->>Video: Seek to timestamp
    Video-->>Network: Playing at time
    User->>Video: Play test video
    loop During playback
        Video->>Video: timeupdate event
        Video->>Network: Get current time
        Network->>Network: Find nearest entry by timestamp
        Network->>UI: Highlight row
    end
    User->>Network: Type in search filter
    Network->>Network: Filter entries by URL/status
    User->>Network: Select entry → view Headers
    Network->>Network: Display request/response headers
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested reviewers

  • arnoldlaishram

🐰 A network tale, captured so fine,
HTTPS traffic in HAR does align,
Android and iOS both see the light,
TLS certs trusted, proxy set right,
From terminal streams to reports displayed bright! ✨🔍

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: add network traffic capture for test runs' is clear, concise, and directly describes the main feature added throughout the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch log-network-capture

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 13

🧹 Nitpick comments (13)
packages/cli/package.json (1)

42-60: Keep bundleDependencies aligned with new runtime dependency.

mockttp was added to dependencies but not to bundleDependencies, which can create packaging drift in bundled/offline install flows.

📦 Proposed update
   "bundleDependencies": [
@@
     "dotenv",
+    "mockttp",
     "ora",

Also applies to: 87-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/package.json` around lines 42 - 60, bundleDependencies in
package.json is missing the newly added runtime dependency "mockttp", causing
packaging drift; update the bundleDependencies array (the "bundleDependencies"
entry in package.json) to include "mockttp" alongside the other entries to keep
bundled/offline installs consistent with the dependencies list.
packages/cli/src/commands/logNetwork/colors.ts (1)

6-16: Honor NO_COLOR/non-TTY output to avoid escape codes in piped logs.

Right now color codes are always emitted. Add a runtime gate so redirected output stays clean.

♻️ Proposed refactor
 const ESC = '\x1b[';
 const RESET = `${ESC}0m`;
+const ENABLE_COLORS = Boolean(process.stdout.isTTY) && !process.env.NO_COLOR;
+const wrap = (code: number, s: string) => (ENABLE_COLORS ? `${ESC}${code}m${s}${RESET}` : s);

 export const colors = {
-  green: (s: string) => `${ESC}32m${s}${RESET}`,
-  red: (s: string) => `${ESC}31m${s}${RESET}`,
-  yellow: (s: string) => `${ESC}33m${s}${RESET}`,
-  cyan: (s: string) => `${ESC}36m${s}${RESET}`,
-  magenta: (s: string) => `${ESC}35m${s}${RESET}`,
-  blue: (s: string) => `${ESC}34m${s}${RESET}`,
-  dim: (s: string) => `${ESC}2m${s}${RESET}`,
-  bold: (s: string) => `${ESC}1m${s}${RESET}`,
+  green: (s: string) => wrap(32, s),
+  red: (s: string) => wrap(31, s),
+  yellow: (s: string) => wrap(33, s),
+  cyan: (s: string) => wrap(36, s),
+  magenta: (s: string) => wrap(35, s),
+  blue: (s: string) => wrap(34, s),
+  dim: (s: string) => wrap(2, s),
+  bold: (s: string) => wrap(1, s),
   white: (s: string) => s,
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/logNetwork/colors.ts` around lines 6 - 16, The
colors object currently always emits ANSI escapes; change it to respect NO_COLOR
and non-TTY output by computing a single boolean (e.g., allowColors =
!process.env.NO_COLOR && process.stdout?.isTTY && process.env.TERM !== 'dumb')
at module init and, if false, replace all functions on the exported colors
object (green, red, yellow, cyan, magenta, blue, dim, bold) to be identity
functions that return the input string (and keep white as-is); keep ESC and
RESET defined but only used when allowColors is true so redirected/piped logs
contain no escape codes.
packages/cli/src/reportServer.ts (1)

306-327: Consider extracting HAR entry mapping for readability.

The nested type casts and property access chains are verbose. While the defensive approach is appropriate for parsing external JSON, extracting the entry mapping could improve readability.

♻️ Optional: Extract helper for cleaner entry mapping
function mapHarEntry(e: Record<string, unknown>) {
  const request = e['request'] as Record<string, unknown> | undefined;
  const response = e['response'] as Record<string, unknown> | undefined;
  const content = response?.['content'] as Record<string, unknown> | undefined;
  
  return {
    startedDateTime: e['startedDateTime'],
    method: request?.['method'],
    url: request?.['url'],
    status: response?.['status'],
    statusText: response?.['statusText'],
    time: e['time'],
    responseSize: content?.['size'] ?? response?.['bodySize'] ?? 0,
    requestHeaders: request?.['headers'],
    responseHeaders: response?.['headers'],
  };
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/reportServer.ts` around lines 306 - 327, The readNetworkHar
function has verbose nested casts and property chains; extract the HAR entry
mapping into a small helper (e.g., mapHarEntry) to improve readability and
reduce repetition: inside readNetworkHar call entries = (har?.log?.entries ??
[]).map(mapHarEntry) and implement mapHarEntry to safely pull
request/response/content as Record<string, unknown> | undefined and return the
object fields (startedDateTime, method, url, status, statusText, time,
responseSize using content?.size ?? response?.bodySize ?? 0, requestHeaders,
responseHeaders). Keep the existing defensive checks and error handling behavior
unchanged.
packages/cli/src/workspace.ts (1)

431-437: Consider throwing on malformed network config for consistency.

The function silently returns undefined when network is present but malformed (e.g., an array or primitive). Other config readers in this file like readOptionalTrimmedString throw errors for invalid types, which helps users identify misconfigurations.

If a user writes network: "true" instead of network: { capture: true }, they won't receive any feedback that their config is being ignored.

♻️ Optional: Add validation for malformed network config
 function readNetworkConfig(raw: unknown): { capture?: boolean } | undefined {
   if (raw === undefined || raw === null) return undefined;
-  if (typeof raw !== 'object' || Array.isArray(raw)) return undefined;
+  if (typeof raw !== 'object' || Array.isArray(raw)) {
+    throw new Error('network config must be a YAML mapping.');
+  }
   const obj = raw as Record<string, unknown>;
   return {
     capture: typeof obj['capture'] === 'boolean' ? obj['capture'] : undefined,
   };
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/workspace.ts` around lines 431 - 437, readNetworkConfig
currently silently returns undefined for malformed but present `network` values
(e.g., strings/arrays); change it to validate and throw when `raw` is not
undefined/null but also not a plain object (or is an array), and also throw if
`obj['capture']` is present but not a boolean. Update the function
readNetworkConfig to keep the existing behavior for undefined/null, but if raw
is present and (typeof raw !== 'object' || Array.isArray(raw')) throw a
TypeError with a clear message like "Invalid 'network' config: expected an
object with optional boolean 'capture'"; likewise throw if capture exists and
typeof capture !== 'boolean'. This preserves the return shape { capture?:
boolean } on success and provides consistent error feedback to users.
packages/cli/src/testRunner.ts (1)

258-266: Complex one-liner and potential partial setup state.

Two observations:

  1. Line 259 is a dense one-liner with dynamic import, instantiation, and method chaining. Consider extracting for readability:
const { CliFilePathUtil } = await import('./filePathUtil.js');
const filePathUtil = new CliFilePathUtil(undefined, undefined, { downloadAssets: false });
const adbPath = (await filePathUtil.getADBPath()) ?? 'adb';
  1. If configureProxy (Line 265) throws, the catch block on Line 270 logs a warning but networkCaptureAvailable remains false. However, the network session was already started successfully (Line 252). The cleanup block (Lines 432-438) only stops the session if networkCaptureAvailable is true, potentially leaving the session running.
♻️ Ensure session cleanup even on proxy config failure
         if (sessionResponse.success) {
           const proxyPort = goalSession.device.getNetworkProxyPort();
           const deviceInfo = goalSession.device.getDeviceInfo();
           const deviceId = deviceInfo.id ?? deviceInfo.deviceUUID;
           const platform = goalSession.platform;

           if (platform === 'android') {
-            const adbPath = (await new (await import('./filePathUtil.js')).CliFilePathUtil(undefined, undefined, { downloadAssets: false }).getADBPath()) ?? 'adb';
+            const { CliFilePathUtil } = await import('./filePathUtil.js');
+            const filePathUtil = new CliFilePathUtil(undefined, undefined, { downloadAssets: false });
+            const adbPath = (await filePathUtil.getADBPath()) ?? 'adb';
             networkProxySetup = new AndroidNetworkProxySetup(adbPath, deviceId);
           } else {
             networkProxySetup = new IOSNetworkProxySetup(deviceId);
           }

-          await networkProxySetup.configureProxy(proxyPort);
-          networkCaptureAvailable = true;
+          try {
+            await networkProxySetup.configureProxy(proxyPort);
+            networkCaptureAvailable = true;
+          } catch (proxyError) {
+            Logger.w('Network proxy configuration failed:', proxyError);
+            // Stop the session since proxy setup failed
+            await goalSession.device.stopNetworkSession().catch(() => {});
+            networkProxySetup = undefined;
+          }
         } else {
           Logger.w(`Failed to start network capture session: ${sessionResponse.message}`);
         }
       } catch (error) {
         Logger.w('Network capture setup failed:', error);
       }
packages/device-node/src/device/NetworkCaptureSetup.ts (2)

90-96: Shell command constructed with string interpolation.

The shell commands use string interpolation with value parameter. While the values in this context come from controlled sources (proxy port numbers or previously read device settings), consider validating the proxy format to prevent unexpected behavior if the input is malformed.

Optional: Add basic format validation
 private async _setGlobalProxy(value: string): Promise<void> {
+  // Basic validation: proxy should be host:port format
+  if (!/^[\w.-]+:\d+$/.test(value) && value !== ':0') {
+    throw new Error(`Invalid proxy format: ${value}`);
+  }
   await this._shell(`settings put global http_proxy ${value}`);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/device-node/src/device/NetworkCaptureSetup.ts` around lines 90 - 96,
The shell commands in _setGlobalProxy and _clearGlobalProxy are built via string
interpolation with the unvalidated value; add a validation step in
_setGlobalProxy to ensure value matches the expected proxy format (e.g.,
host:port or host:port;scheme) and explicitly allow the special clear token ":0"
used by _clearGlobalProxy, rejecting or throwing for malformed inputs; then call
this._shell only after validation (or sanitize/escape the value) and keep
_clearGlobalProxy using the constant ':0' to avoid interpolation issues.

225-244: Fallback returns first service even without active IP.

If no service has an active IP address, the function falls back to services[0] (line 243). This may configure proxy on an inactive interface. Consider returning null instead to let the caller handle the no-active-service case explicitly.

Optional: Return null when no active service found
   for (const svc of services) {
     try {
       const info = await run('networksetup', ['-getinfo', svc]);
       if (info.includes('IP address') && !info.includes('IP address: none')) {
         return svc;
       }
     } catch {
       continue;
     }
   }
-  return services[0] ?? null;
+  return null;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/device-node/src/device/NetworkCaptureSetup.ts` around lines 225 -
244, The function findActiveNetworkService currently falls back to returning
services[0] when no service has an active IP, which can cause proxy
configuration on an inactive interface; change the fallback to return null
instead and update any callers of findActiveNetworkService to handle a null
return (e.g., skip configuration, surface an explicit error, or choose a safe
default). Locate the logic in findActiveNetworkService (the final return
statement) and replace the services[0] fallback with null, and audit usages of
findActiveNetworkService to add null-checks and appropriate error handling or
user-facing messages.
packages/cli/src/reportTemplate.ts (1)

1595-1598: Consider escaping single quotes for consistency.

The escapeHtmlJS function escapes &, <, >, and ", but not single quotes ('). While this is generally safe for innerHTML contexts, the server-side escapeHtml function (line 2617-2624) does escape single quotes. Consider aligning these for consistency.

Optional: align escapeHtmlJS with escapeHtml
 function escapeHtmlJS(s) {
   if (!s) return '';
-  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
+  return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;');
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/reportTemplate.ts` around lines 1595 - 1598, The client-side
escapeHtmlJS function currently escapes &, <, > and " but omits single quotes,
causing inconsistency with the server-side escapeHtml; update escapeHtmlJS to
also replace single quotes (') with an HTML entity (e.g., &#39;) so its output
matches escapeHtml and avoids single-quote injection issues, keeping the
replacement sequence consistent with the existing chain in escapeHtmlJS.
packages/device-node/src/device/NetworkCaptureManager.ts (2)

294-342: HTTP version is hardcoded to HTTP/1.1.

The HAR builder hardcodes httpVersion: 'HTTP/1.1' (lines 305, 314) regardless of the actual protocol used. While mockttp may not expose this information easily, this could be misleading for HTTP/2 traffic.

Consider adding a comment noting this limitation, or storing the actual HTTP version if mockttp provides it in the request/response objects.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/device-node/src/device/NetworkCaptureManager.ts` around lines 294 -
342, The HAR builder in function buildHar hardcodes httpVersion: 'HTTP/1.1' for
both request and response entries which can misrepresent HTTP/2 traffic; update
the request and response httpVersion fields to use any real protocol/version
properties available on the NetworkCapturedEntry (e.g., e.httpVersion,
e.protocol, or e.requestProtocol if such fields exist), falling back to
'HTTP/1.1' when absent, and add a short code comment explaining the fallback and
limitation if the capture layer (mockttp) doesn't expose the protocol.

238-240: Confusing fallback logic for startedAt calculation.

The ternary chain for startedAt is hard to follow. The second condition this._testStartIndexMap.get(mapKey) !== undefined is always true at this point since startIndex was already retrieved from it.

Simplify the startedAt calculation
-    const startedAt = testEntries.length > 0
-      ? testEntries[0]!.startedAt
-      : (this._testStartIndexMap.get(mapKey) !== undefined ? new Date() : completedAt);
+    const startedAt = testEntries.length > 0 ? testEntries[0]!.startedAt : new Date();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/device-node/src/device/NetworkCaptureManager.ts` around lines 238 -
240, The startedAt ternary is confusing and redundantly calls
this._testStartIndexMap.get(mapKey) even though startIndex was already
retrieved; replace the chain to use the local startIndex variable and
testEntries only: set startedAt = testEntries.length > 0 ?
testEntries[0]!.startedAt : (startIndex !== undefined ? new Date() :
completedAt), ensuring you reference testEntries, startIndex and completedAt
(and remove the extra _testStartIndexMap.get call) so the logic is clear and
correct.
packages/cli/src/commands/logNetwork/ios.ts (1)

65-96: Code duplication with NetworkCaptureSetup.ts.

This startPacServer function and the autoproxy management functions (lines 105-147) are nearly identical to those in packages/device-node/src/device/NetworkCaptureSetup.ts. Consider extracting these shared utilities into a common module to reduce maintenance burden.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/logNetwork/ios.ts` around lines 65 - 96, The
startPacServer function and the autoproxy management logic duplicated from
NetworkCaptureSetup.ts should be extracted into a shared utility module and
imported where needed; specifically, move startPacServer and the corresponding
autoproxy functions into a new common module (e.g., a shared/network utility)
with the same exported function names (startPacServer and any autoproxy
enable/disable helpers) and appropriate return types, update
packages/cli/src/commands/logNetwork/ios.ts to import those functions instead of
defining them, and update packages/device-node/src/device/NetworkCaptureSetup.ts
to import the same shared exports; ensure the new module exports identical
signatures and that any internal types or dependencies are adjusted so both
callers compile without duplicated logic.
packages/cli/src/commands/logNetwork/adb.ts (2)

40-48: Unnecessary try-catch block.

The try-catch on lines 41-47 doesn't serve a purpose since return 'adb' cannot throw. The catch block is unreachable.

Simplify by removing try-catch
-  // Fallback: hope it's on PATH.
-  try {
-    // We rely on execFile('adb', ...) working if PATH resolves it.
-    // A richer resolve path can be added later.
-    return 'adb';
-  } catch {
-    return null;
-  }
+  // Fallback: hope it's on PATH. We rely on execFile('adb', ...) working
+  // if PATH resolves it. A richer resolve path can be added later.
+  return 'adb';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/logNetwork/adb.ts` around lines 40 - 48, The
try-catch around the fallback return is unnecessary because "return 'adb'"
cannot throw; remove the try { return 'adb'; } catch { return null; } block and
replace it with a direct return 'adb' (leaving the surrounding function—the
fallback branch that currently returns 'adb'—intact), ensuring any earlier code
paths that might return null remain unchanged.

141-156: Shell commands use string interpolation with input values.

The setGlobalProxy (line 146) and getProp (line 155) functions pass parameters directly into shell commands via string interpolation. While the current callers use controlled inputs, this pattern is fragile if the functions are reused with user-provided values.

Optional: Consider input validation for shell commands

For setGlobalProxy, validate the host:port format. For getProp, consider using a whitelist of allowed property names or escaping the input.

 export async function getProp(adbPath: string, serial: string, key: string): Promise<string> {
+  // Validate key contains only safe characters
+  if (!/^[\w.]+$/.test(key)) {
+    throw new AdbError(`Invalid property key: ${key}`);
+  }
   return await shell(adbPath, serial, `getprop ${key}`);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/logNetwork/adb.ts` around lines 141 - 156, The
shell commands in setGlobalProxy and getProp interpolate untrusted values;
validate and/or escape inputs before calling shell. For setGlobalProxy (function
setGlobalProxy) validate hostPort with a strict regex (e.g. host or IP + ":" +
port range) and reject or throw on invalid formats, or escape it by wrapping in
single quotes and replacing any embedded single quotes; for getProp (function
getProp) either restrict key to an allowlist or validate against a tight pattern
like /^[A-Za-z0-9._-]+$/ and reject invalid characters, or escape the key
similarly; alternatively refactor shell invocation to accept argv-style args to
avoid building a single interpolated command. Ensure clear error handling when
validation fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@docs/network-logging.md`:
- Around line 77-85: The Markdown contains unlabeled fenced code blocks; add
language identifiers to each unlabeled triple-backtick fence to satisfy MD040:
use "text" for plain directory listing and HTTP log samples (e.g., the block
starting with "artifacts/{runId}/tests/{testId}/" and the block containing "GET
https://en.wikipedia.org/..."), and use "bash" for command examples (e.g., the
block with "finalrun log-network --platform=<android|ios> [options]"). Update
the three affected ranges (around the directory tree, the HTTP/har sample, and
the CLI invocation) to include these labels so markdownlint passes and rendering
stays consistent.
- Line 3: The intro sentence currently claiming "request and response bodies"
are captured contradicts the later note that bodies are not yet captured; update
the intro (the paragraph that mentions "method, URL, status code, headers,
request and response bodies, and timing" and the HAR 1.2 sentence) to accurately
state that request and response bodies are not captured yet (e.g., list captured
fields as method, URL, status code, headers, and timing, and note bodies are
planned/future), and make the phrasing consistent with the later line that says
bodies are not captured yet so both places (the intro and the existing note
about bodies) match.

In `@packages/cli/bin/finalrun.ts`:
- Around line 242-250: The CLI shows inconsistent platform support for the
log-network command: update the help text for program.command('log-network') and
its .requiredOption('--platform <platform>') to match actual supported platforms
used by runLogNetworkCommand; if iOS is supported, change the option help from
"(android)" to "(android|ios)" and adjust the command description to mention
both platforms, otherwise keep the description generic or explicitly state
"android only" and update docs/cli-reference.md to match; ensure any mention of
supported platforms in runLogNetworkCommand or related validation logic is
consistent with the help text.

In `@packages/cli/src/commands/logNetwork/capture.ts`:
- Around line 49-53: The start method retains previous in-memory capture state
(entries/TLS errors/pending requests) when reused, causing cross-session
contamination; modify start(cert, key, callbacks) to reset/clear the internal
state before creating or starting the mockttp server — e.g., reinitialize
this._entries, this._tlsErrors, this._pendingRequests (and any other per-run
buffers) to empty arrays/objects and remove any leftover listeners on
this._server, or recreate this._server anew so prior data is not reused; apply
the same reset logic where the server can be restarted (the corresponding
stop/restart flow referenced around the other start/stop call sites) so each run
begins with a clean in-memory state.

In `@packages/cli/src/commands/logNetwork/index.ts`:
- Around line 453-454: Add an actual xcrun availability check before marking the
iOS host-tools step as passed: import execFileAsync and call
execFileAsync('xcrun', ['--version']) (or equivalent) where printStep('Checking
host tools (xcrun)') is currently invoked; if the call succeeds, call
printStep(..., 'pass'), otherwise call printStep(..., 'fail') and surface an
error (exit or throw) so listBootedSimulators doesn't get a cryptic failure
later. Reference symbols: printStep, listBootedSimulators, and execFileAsync
(ensure execFileAsync is imported at top of the file).
- Around line 191-236: recoverStaleProxy currently only checks
isProxyResponding(state.proxyPort) and may allow multiple processes to
concurrently restore proxy settings; modify recoverStaleProxy to first validate
the saved PID/state by using isProcessAlive(state.pid) or
isAnyProcessAlive(state) after loadProxyState() (and before acting) and only
proceed with restoration if the original process is confirmed dead, otherwise
abort; additionally implement a simple lock/atomic marker around recovery (e.g.,
create a recovery lock file or use an existing lock helper) so that only one
process can perform restoration, and ensure clearProxyState() is only called by
the process that acquired the lock.
- Around line 148-185: testProxyConnectivity is currently creating an HTTPS
request to example.com directly and passing non-standard host/port on
https.Agent (which are ignored), so the proxy is bypassed; change the
implementation to actually target the proxy or use a proper proxy agent: either
(A) make the request connect to 127.0.0.1:proxyPort (set request hostname/port
to the proxy and set the request path to the full destination URL or use a
CONNECT method) and remove the invalid agent.host/port usage, or (B) install and
use a proxy agent (e.g., http-proxy-agent / https-proxy-agent) and pass that
agent to https.request so the request is tunneled through the mockttp proxy;
update testProxyConnectivity, the https.request options and agent construction
accordingly so the request is routed through the proxy and captured by
NetworkCapture.

In `@packages/cli/src/commands/logNetwork/livePrinter.ts`:
- Around line 20-23: The code currently reads terminal width from process.stdout
(cols) causing incorrect URL truncation when an alternate output stream is
passed to livePrinter; change the width source to use the passed-in stream by
replacing references to (process.stdout as { columns?: number }).columns with
(stream as { columns?: number }).columns (or similar) so cols is derived from
the function's stream parameter, then recompute fixedWidth/urlWidth and call
truncate(entry.url, urlWidth) as before; update any variable names if needed
inside livePrinter to ensure the stream argument is used consistently for column
sizing.

In `@packages/cli/src/commands/logNetwork/proxyState.ts`:
- Around line 29-32: saveProxyState currently writes STATE_FILE directly and can
leave a partially written proxy-state.json on crashes; change saveProxyState to
write atomically by: ensure STATE_DIR exists, create a temp file in STATE_DIR
(e.g. STATE_FILE + '.tmp' or use a unique suffix), open the temp file with
fsp.open, write JSON into it, call filehandle.datasync()/fsync to flush to disk,
close the handle, then rename (fs.rename) the temp file to STATE_FILE to perform
an atomic replace; reference saveProxyState, STATE_DIR and STATE_FILE when
locating where to implement this change.

In `@packages/cli/src/reportWriter.ts`:
- Around line 781-785: The HAR redaction loop currently only calls
redactHarHeaders and redactHarQueryParams, leaving request.url and non-keyword
secret values unredacted and not applying provided bindings; update the loop
over har.log.entries to also call the HAR value redaction routine on
entry.request.url and entry.response?.url (e.g., call
redactHarValues(entry.request?.url, bindings) and
redactHarValues(entry.response?.url, bindings)), and ensure redactHarHeaders and
redactHarQueryParams accept and use the bindings parameter so they redact
non-keyword secret values (and update any internal helper that performs value
redaction to apply bindings-based replacements); apply the same changes to the
other HAR-processing block (the code around lines 1010-1036) so both places
sanitize request.url and use bindings consistently.
- Around line 789-791: The current behavior leaves an unredacted HAR file on
disk when redaction fails (the call that logs via Logger.w and uses
this._formatError); update this failure path to securely remove the unredacted
HAR instead of keeping it: after catching the redaction error, attempt to delete
the on-disk HAR within a safe try/catch (log any deletion error via Logger.e),
and only retain any redacted output if redaction actually succeeded; ensure you
reference the same symbols (Logger.w, this._formatError) and perform the
filesystem unlink/remove call in the same error-handling block so the raw HAR is
not persisted on disk on redaction failure.

In `@packages/cli/src/sessionRunner.ts`:
- Around line 535-540: The code clears activeNetworkCapture when
stopNetworkCapture returns success: false, preventing the finally cleanup from
calling abort; update the failure branch in the stop logic (the block that logs
with Logger.w and handles stopNetResponse) to call activeNetworkCapture.abort()
(or activeNetworkCapture?.abort()) before setting activeNetworkCapture =
undefined, guarding with a null check and swallowing/logging any abort errors so
cleanup proceeds safely; reference the stopNetworkCapture call, the
activeNetworkCapture variable, and the Logger.w log path when making this
change.

In `@packages/device-node/src/device/NetworkCaptureManager.ts`:
- Around line 113-144: Pending request entries in _pendingRequests can be left
orphaned if no response arrives; add a cleanup mechanism to prevent memory leaks
by: when starting the capture (where _server.on('request') and
_server.on('response') are registered) also start a periodic timer that scans
_pendingRequests and removes entries older than a threshold (e.g., 60s) and
optionally converts them into completed NetworkCapturedEntry records in _entries
with an appropriate status/duration; also subscribe to request-level
abort/error/TLS events from _server to delete the specific pending entry
immediately; ensure the timer is cleared and any remaining pending entries are
flushed/cleared in stopSession so no timer or map entries survive session end.

---

Nitpick comments:
In `@packages/cli/package.json`:
- Around line 42-60: bundleDependencies in package.json is missing the newly
added runtime dependency "mockttp", causing packaging drift; update the
bundleDependencies array (the "bundleDependencies" entry in package.json) to
include "mockttp" alongside the other entries to keep bundled/offline installs
consistent with the dependencies list.

In `@packages/cli/src/commands/logNetwork/adb.ts`:
- Around line 40-48: The try-catch around the fallback return is unnecessary
because "return 'adb'" cannot throw; remove the try { return 'adb'; } catch {
return null; } block and replace it with a direct return 'adb' (leaving the
surrounding function—the fallback branch that currently returns 'adb'—intact),
ensuring any earlier code paths that might return null remain unchanged.
- Around line 141-156: The shell commands in setGlobalProxy and getProp
interpolate untrusted values; validate and/or escape inputs before calling
shell. For setGlobalProxy (function setGlobalProxy) validate hostPort with a
strict regex (e.g. host or IP + ":" + port range) and reject or throw on invalid
formats, or escape it by wrapping in single quotes and replacing any embedded
single quotes; for getProp (function getProp) either restrict key to an
allowlist or validate against a tight pattern like /^[A-Za-z0-9._-]+$/ and
reject invalid characters, or escape the key similarly; alternatively refactor
shell invocation to accept argv-style args to avoid building a single
interpolated command. Ensure clear error handling when validation fails.

In `@packages/cli/src/commands/logNetwork/colors.ts`:
- Around line 6-16: The colors object currently always emits ANSI escapes;
change it to respect NO_COLOR and non-TTY output by computing a single boolean
(e.g., allowColors = !process.env.NO_COLOR && process.stdout?.isTTY &&
process.env.TERM !== 'dumb') at module init and, if false, replace all functions
on the exported colors object (green, red, yellow, cyan, magenta, blue, dim,
bold) to be identity functions that return the input string (and keep white
as-is); keep ESC and RESET defined but only used when allowColors is true so
redirected/piped logs contain no escape codes.

In `@packages/cli/src/commands/logNetwork/ios.ts`:
- Around line 65-96: The startPacServer function and the autoproxy management
logic duplicated from NetworkCaptureSetup.ts should be extracted into a shared
utility module and imported where needed; specifically, move startPacServer and
the corresponding autoproxy functions into a new common module (e.g., a
shared/network utility) with the same exported function names (startPacServer
and any autoproxy enable/disable helpers) and appropriate return types, update
packages/cli/src/commands/logNetwork/ios.ts to import those functions instead of
defining them, and update packages/device-node/src/device/NetworkCaptureSetup.ts
to import the same shared exports; ensure the new module exports identical
signatures and that any internal types or dependencies are adjusted so both
callers compile without duplicated logic.

In `@packages/cli/src/reportServer.ts`:
- Around line 306-327: The readNetworkHar function has verbose nested casts and
property chains; extract the HAR entry mapping into a small helper (e.g.,
mapHarEntry) to improve readability and reduce repetition: inside readNetworkHar
call entries = (har?.log?.entries ?? []).map(mapHarEntry) and implement
mapHarEntry to safely pull request/response/content as Record<string, unknown> |
undefined and return the object fields (startedDateTime, method, url, status,
statusText, time, responseSize using content?.size ?? response?.bodySize ?? 0,
requestHeaders, responseHeaders). Keep the existing defensive checks and error
handling behavior unchanged.

In `@packages/cli/src/reportTemplate.ts`:
- Around line 1595-1598: The client-side escapeHtmlJS function currently escapes
&, <, > and " but omits single quotes, causing inconsistency with the
server-side escapeHtml; update escapeHtmlJS to also replace single quotes (')
with an HTML entity (e.g., &#39;) so its output matches escapeHtml and avoids
single-quote injection issues, keeping the replacement sequence consistent with
the existing chain in escapeHtmlJS.

In `@packages/cli/src/workspace.ts`:
- Around line 431-437: readNetworkConfig currently silently returns undefined
for malformed but present `network` values (e.g., strings/arrays); change it to
validate and throw when `raw` is not undefined/null but also not a plain object
(or is an array), and also throw if `obj['capture']` is present but not a
boolean. Update the function readNetworkConfig to keep the existing behavior for
undefined/null, but if raw is present and (typeof raw !== 'object' ||
Array.isArray(raw')) throw a TypeError with a clear message like "Invalid
'network' config: expected an object with optional boolean 'capture'"; likewise
throw if capture exists and typeof capture !== 'boolean'. This preserves the
return shape { capture?: boolean } on success and provides consistent error
feedback to users.

In `@packages/device-node/src/device/NetworkCaptureManager.ts`:
- Around line 294-342: The HAR builder in function buildHar hardcodes
httpVersion: 'HTTP/1.1' for both request and response entries which can
misrepresent HTTP/2 traffic; update the request and response httpVersion fields
to use any real protocol/version properties available on the
NetworkCapturedEntry (e.g., e.httpVersion, e.protocol, or e.requestProtocol if
such fields exist), falling back to 'HTTP/1.1' when absent, and add a short code
comment explaining the fallback and limitation if the capture layer (mockttp)
doesn't expose the protocol.
- Around line 238-240: The startedAt ternary is confusing and redundantly calls
this._testStartIndexMap.get(mapKey) even though startIndex was already
retrieved; replace the chain to use the local startIndex variable and
testEntries only: set startedAt = testEntries.length > 0 ?
testEntries[0]!.startedAt : (startIndex !== undefined ? new Date() :
completedAt), ensuring you reference testEntries, startIndex and completedAt
(and remove the extra _testStartIndexMap.get call) so the logic is clear and
correct.

In `@packages/device-node/src/device/NetworkCaptureSetup.ts`:
- Around line 90-96: The shell commands in _setGlobalProxy and _clearGlobalProxy
are built via string interpolation with the unvalidated value; add a validation
step in _setGlobalProxy to ensure value matches the expected proxy format (e.g.,
host:port or host:port;scheme) and explicitly allow the special clear token ":0"
used by _clearGlobalProxy, rejecting or throwing for malformed inputs; then call
this._shell only after validation (or sanitize/escape the value) and keep
_clearGlobalProxy using the constant ':0' to avoid interpolation issues.
- Around line 225-244: The function findActiveNetworkService currently falls
back to returning services[0] when no service has an active IP, which can cause
proxy configuration on an inactive interface; change the fallback to return null
instead and update any callers of findActiveNetworkService to handle a null
return (e.g., skip configuration, surface an explicit error, or choose a safe
default). Locate the logic in findActiveNetworkService (the final return
statement) and replace the services[0] fallback with null, and audit usages of
findActiveNetworkService to add null-checks and appropriate error handling or
user-facing messages.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 25ce6a74-69c0-427b-80f1-119e65d3a4db

📥 Commits

Reviewing files that changed from the base of the PR and between 9bdba1e and f2fe25c.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (30)
  • README.md
  • docs/cli-reference.md
  • docs/configuration.md
  • docs/network-capture.md
  • docs/network-logging.md
  • packages/cli/bin/finalrun.ts
  • packages/cli/package.json
  • packages/cli/src/commands/logNetwork/adb.ts
  • packages/cli/src/commands/logNetwork/ca.ts
  • packages/cli/src/commands/logNetwork/capture.ts
  • packages/cli/src/commands/logNetwork/colors.ts
  • packages/cli/src/commands/logNetwork/index.ts
  • packages/cli/src/commands/logNetwork/ios.ts
  • packages/cli/src/commands/logNetwork/livePrinter.ts
  • packages/cli/src/commands/logNetwork/proxyState.ts
  • packages/cli/src/reportServer.ts
  • packages/cli/src/reportTemplate.ts
  • packages/cli/src/reportWriter.ts
  • packages/cli/src/sessionRunner.ts
  • packages/cli/src/testRunner.ts
  • packages/cli/src/workspace.ts
  • packages/common/src/index.ts
  • packages/common/src/interfaces/DeviceAgent.ts
  • packages/common/src/models/NetworkLog.ts
  • packages/common/src/models/TestResult.ts
  • packages/device-node/src/device/Device.ts
  • packages/device-node/src/device/NetworkCaptureManager.ts
  • packages/device-node/src/device/NetworkCaptureSetup.ts
  • packages/device-node/src/index.ts
  • packages/goal-executor/src/TestExecutor.ts

@@ -0,0 +1,222 @@
# Network Logging

FinalRun can capture every HTTP/HTTPS request your app makes during a test run — method, URL, status code, headers, request and response bodies, and timing. Traffic is recorded as [HAR 1.2](https://w3c.github.io/web-performance/specs/HAR/Overview.html) (the same format Chrome DevTools uses) and displayed in your test report with a searchable Network tab.
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Intro text contradicts current body-capture behavior.

Line [3] says request/response bodies are captured, but Lines [139]-[140] say they are not captured yet. Please align the intro with current behavior.

Also applies to: 139-140

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/network-logging.md` at line 3, The intro sentence currently claiming
"request and response bodies" are captured contradicts the later note that
bodies are not yet captured; update the intro (the paragraph that mentions
"method, URL, status code, headers, request and response bodies, and timing" and
the HAR 1.2 sentence) to accurately state that request and response bodies are
not captured yet (e.g., list captured fields as method, URL, status code,
headers, and timing, and note bodies are planned/future), and make the phrasing
consistent with the later line that says bodies are not captured yet so both
places (the intro and the existing note about bodies) match.

Comment on lines +77 to +85
```
artifacts/{runId}/tests/{testId}/
├── result.json
├── recording.mp4
├── device.log
├── network.har ← new
├── actions/
└── screenshots/
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add language identifiers to unlabeled fenced blocks (MD040).

Use text/bash on these fences to satisfy markdownlint and keep rendering consistent.

🔧 Proposed fix
-```
+```text
 artifacts/{runId}/tests/{testId}/
 ├── result.json
 ├── recording.mp4
@@
-```
+```text
   GET     https://en.wikipedia.org/api/rest_v1/feed/featured/2026/04/07   200   111ms  31.8 KB
   POST    https://intake-analytics.wikimedia.org/v1/events                201   194ms      0 B
@@
-```
+```bash
 finalrun log-network --platform=<android|ios> [options]

Also applies to: 121-126, 211-218

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/network-logging.md` around lines 77 - 85, The Markdown contains
unlabeled fenced code blocks; add language identifiers to each unlabeled
triple-backtick fence to satisfy MD040: use "text" for plain directory listing
and HTTP log samples (e.g., the block starting with
"artifacts/{runId}/tests/{testId}/" and the block containing "GET
https://en.wikipedia.org/..."), and use "bash" for command examples (e.g., the
block with "finalrun log-network --platform=<android|ios> [options]"). Update
the three affected ranges (around the directory tree, the HTTP/har sample, and
the CLI invocation) to include these labels so markdownlint passes and rendering
stays consistent.

Comment on lines +242 to +250
program
.command('log-network')
.description('Capture and display HTTP(S) network traffic from a connected device')
.requiredOption('--platform <platform>', 'Target platform (android)')
.option('--device <serial>', 'Device serial (auto-detected if only one)')
.option('--out <path>', 'Output HAR file path (default: auto-generated)')
.action(async (options: { platform: string; device?: string; out?: string }) => {
await runLogNetworkCommand(options);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Inconsistent platform description between command and help text.

The command description on Line 244 says "HTTP(S) network traffic from a connected device" (implying both platforms), while the --platform option help text on Line 245 says only (android). However, the CLI reference documentation (docs/cli-reference.md) shows --platform <android|ios>.

If iOS is supported, update the help text:

-  .requiredOption('--platform <platform>', 'Target platform (android)')
+  .requiredOption('--platform <platform>', 'Target platform (android or ios)')

If only Android is currently supported, update the documentation to match.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
program
.command('log-network')
.description('Capture and display HTTP(S) network traffic from a connected device')
.requiredOption('--platform <platform>', 'Target platform (android)')
.option('--device <serial>', 'Device serial (auto-detected if only one)')
.option('--out <path>', 'Output HAR file path (default: auto-generated)')
.action(async (options: { platform: string; device?: string; out?: string }) => {
await runLogNetworkCommand(options);
});
program
.command('log-network')
.description('Capture and display HTTP(S) network traffic from a connected device')
.requiredOption('--platform <platform>', 'Target platform (android or ios)')
.option('--device <serial>', 'Device serial (auto-detected if only one)')
.option('--out <path>', 'Output HAR file path (default: auto-generated)')
.action(async (options: { platform: string; device?: string; out?: string }) => {
await runLogNetworkCommand(options);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/bin/finalrun.ts` around lines 242 - 250, The CLI shows
inconsistent platform support for the log-network command: update the help text
for program.command('log-network') and its .requiredOption('--platform
<platform>') to match actual supported platforms used by runLogNetworkCommand;
if iOS is supported, change the option help from "(android)" to "(android|ios)"
and adjust the command description to mention both platforms, otherwise keep the
description generic or explicitly state "android only" and update
docs/cli-reference.md to match; ensure any mention of supported platforms in
runLogNetworkCommand or related validation logic is consistent with the help
text.

Comment on lines +49 to +53
async start(cert: string, key: string, callbacks: NetworkCaptureCallbacks): Promise<number> {
this._server = mockttp.getLocal({ https: { cert, key } });
await this._server.forAnyRequest().thenPassThrough();
await this._server.start();
this._port = this._server.port;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset in-memory capture state between runs to prevent cross-session contamination.

Current lifecycle keeps prior entries/TLS errors/pending requests if the same instance is started again, producing incorrect HAR data.

🧹 Proposed fix
 async start(cert: string, key: string, callbacks: NetworkCaptureCallbacks): Promise<number> {
+  if (this._server) throw new Error('NetworkCapture is already running');
+  this._entries = [];
+  this._tlsErrors = [];
+  this._pendingRequests.clear();
+  this._port = 0;
+
   this._server = mockttp.getLocal({ https: { cert, key } });
   await this._server.forAnyRequest().thenPassThrough();
   await this._server.start();
   this._port = this._server.port;
@@
 async stop(): Promise<void> {
   if (this._server) {
     await this._server.stop();
     this._server = null;
   }
+  this._pendingRequests.clear();
+  this._port = 0;
 }

Also applies to: 108-113

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/logNetwork/capture.ts` around lines 49 - 53, The
start method retains previous in-memory capture state (entries/TLS
errors/pending requests) when reused, causing cross-session contamination;
modify start(cert, key, callbacks) to reset/clear the internal state before
creating or starting the mockttp server — e.g., reinitialize this._entries,
this._tlsErrors, this._pendingRequests (and any other per-run buffers) to empty
arrays/objects and remove any leftover listeners on this._server, or recreate
this._server anew so prior data is not reused; apply the same reset logic where
the server can be restarted (the corresponding stop/restart flow referenced
around the other start/stop call sites) so each run begins with a clean
in-memory state.

Comment on lines +148 to +185
async function testProxyConnectivity(
proxyPort: number,
ca: { cert: string },
capture: NetworkCapture,
): Promise<boolean> {
const countBefore = capture.entries.length;
const result = await new Promise<boolean>((resolve) => {
const req = https.request(
{
hostname: 'example.com',
port: 443,
path: '/',
method: 'HEAD',
agent: new https.Agent({
host: '127.0.0.1',
port: proxyPort,
ca: ca.cert,
} as https.AgentOptions),
timeout: 5000,
},
(res) => {
res.resume();
resolve(res.statusCode !== undefined);
},
);
req.on('error', () => resolve(false));
req.on('timeout', () => {
req.destroy();
resolve(false);
});
req.end();
});
// Remove the test request from captured entries so it doesn't pollute the HAR.
if (capture.entries.length > countBefore) {
(capture.entries as CapturedEntry[]).splice(countBefore);
}
return result;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for how the capture proxy handles HTTPS - does it handle CONNECT tunneling?
rg -n "CONNECT|tunnel" --type=ts packages/cli/src/commands/logNetwork/

Repository: final-run/finalrun-agent

Length of output: 187


🏁 Script executed:

# Check the full file context around the test function
wc -l packages/cli/src/commands/logNetwork/index.ts

Repository: final-run/finalrun-agent

Length of output: 117


🏁 Script executed:

# Read the full file to understand the context
head -200 packages/cli/src/commands/logNetwork/index.ts

Repository: final-run/finalrun-agent

Length of output: 6262


🏁 Script executed:

# Search for NetworkCapture and proxy implementations
rg -n "class NetworkCapture|interface NetworkCapture|function.*Proxy" --type=ts packages/cli/src/commands/logNetwork/

Repository: final-run/finalrun-agent

Length of output: 1379


🏁 Script executed:

# Check if there's a custom proxy server implementation
find packages/cli/src -name "*proxy*" -type f

Repository: final-run/finalrun-agent

Length of output: 118


🏁 Script executed:

# Check imports and dependencies in this file
head -50 packages/cli/src/commands/logNetwork/index.ts

Repository: final-run/finalrun-agent

Length of output: 1274


🏁 Script executed:

# Search for where the proxy server is created (createServer, listen, etc.)
rg -n "createServer|listen.*proxy|new.*Proxy" --type=ts packages/cli/src/commands/logNetwork/

Repository: final-run/finalrun-agent

Length of output: 535


🏁 Script executed:

# Check the capture.js file where NetworkCapture is defined
cat packages/cli/src/commands/logNetwork/capture.ts | head -100

Repository: final-run/finalrun-agent

Length of output: 3464


🏁 Script executed:

# Search for how the proxy is started/created in the main file
grep -n "startProxy\|createProxy\|new.*Server\|listen" packages/cli/src/commands/logNetwork/index.ts

Repository: final-run/finalrun-agent

Length of output: 247


🏁 Script executed:

# Look for CONNECT method handling or tunnel logic
rg -n "CONNECT|\.connect\(" --type=ts packages/cli/src/commands/logNetwork/

Repository: final-run/finalrun-agent

Length of output: 50


🏁 Script executed:

# Check package.json for http-proxy-agent or https-proxy-agent
rg -n "proxy-agent|mockttp" packages/cli/package.json

Repository: final-run/finalrun-agent

Length of output: 95


🏁 Script executed:

# Check if there are any other uses of https.Agent in the codebase
rg -n "https.Agent|http.Agent|new Agent" --type=ts packages/cli/src/

Repository: final-run/finalrun-agent

Length of output: 234


🏁 Script executed:

# Look at how mockttp is configured and used
grep -A 20 "mockttp.getLocal" packages/cli/src/commands/logNetwork/capture.ts

Repository: final-run/finalrun-agent

Length of output: 813


🏁 Script executed:

# Check if maybe the test proxy uses environment variables or global proxy settings
rg -n "http_proxy|https_proxy|HTTP_PROXY|HTTPS_PROXY" --type=ts packages/cli/src/commands/logNetwork/

Repository: final-run/finalrun-agent

Length of output: 436


🏁 Script executed:

# Search for how the test/proxy is actually being used elsewhere in the code
rg -n "testProxyConnectivity|isProxyResponding" --type=ts packages/cli/src/commands/logNetwork/ -B 2 -A 2

Repository: final-run/finalrun-agent

Length of output: 2382


🏁 Script executed:

# Check if mockttp provides any special client methods
rg -n "mockttp\." --type=ts packages/cli/src/commands/logNetwork/capture.ts | head -20

Repository: final-run/finalrun-agent

Length of output: 186


🏁 Script executed:

# Check the exact context around where testProxyConnectivity is called
sed -n '405,420p' packages/cli/src/commands/logNetwork/index.ts

Repository: final-run/finalrun-agent

Length of output: 742


🏁 Script executed:

# Look for any documentation or comments about how this proxy test works
sed -n '140,180p' packages/cli/src/commands/logNetwork/index.ts

Repository: final-run/finalrun-agent

Length of output: 1127


🏁 Script executed:

# Check if Node.js has documentation on how agent options work
web_search
Node.js https.Agent host port properties proxy

Repository: final-run/finalrun-agent

Length of output: 164


🏁 Script executed:

# Search for proper proxy agent usage patterns in Node.js ecosystem
rg -n "https-proxy-agent|http-proxy-agent|HttpProxyAgent|HttpsProxyAgent" --type=ts packages/

Repository: final-run/finalrun-agent

Length of output: 50


🏁 Script executed:

# Check Node.js documentation or comments about agent configuration
rg -n "Agent.*proxy|proxy.*Agent" --type=ts packages/cli/src/

Repository: final-run/finalrun-agent

Length of output: 50


🌐 Web query:

Node.js https.Agent host port properties socket connection

💡 Result:

In Node.js, an https.Agent (like http.Agent) pools/reuses sockets by a “name” derived from the request’s connection options, not from some agent.host / agent.port property.

How the Agent decides which socket to use (host/port)

  • The agent computes a key via agent.getName(options).
  • For http.Agent, that name is essentially: host:port:localAddress (and sometimes :family).
  • For https.Agent, the name includes host:port:localAddress plus TLS options (CA/cert/ciphers/etc.), because those affect whether a socket can be reused. [1]

So, host/port are per request options, and the agent’s pools (e.g., agent.sockets, agent.freeSockets) are organized by that computed key.

Where to read the actual connected host/port (on the socket)

Once a request has a socket, the socket exposes the real endpoints:

  • socket.remoteAddress, socket.remotePort = the peer you connected to
  • socket.localAddress, socket.localPort = your side of the TCP connection [2]

You can access it from the request:

const req = https.request({ hostname: 'example.com', port: 443, agent }, (res) => {});
req.on('socket', (socket) => {
  console.log(socket.remoteAddress, socket.remotePort);
  console.log(socket.localAddress, socket.localPort);
});
req.end();

Which options control host/port

  • http.request / https.request take host / hostname and port in the options; host is “domain/IP of the server to issue the request to” (and family affects resolution). [3]
  • For https.request, the default port is 443 if you don’t specify it. [4]

References:
[1] Node.js HTTP docs (agent.getName(options) and HTTPS agent name includes TLS options) (nodejs.org)
[2] Node.js net.Socket docs (remotePort, localAddress, etc.) (nodejs.org)
[3] Node.js HTTP request option host / hostname (and related options) (nodejs.org)
[4] Node.js HTTPS docs (port default 443) (nodejs.org)

Citations:


The HTTPS agent configuration does not route requests through the proxy.

The https.request() call uses hostname: 'example.com' and port: 443 to determine the connection destination. The non-standard host and port properties on the https.Agent are ignored at runtime—Node.js only uses the request's own hostname/port options to determine where to connect. This means the request connects directly to example.com:443, bypassing the mockttp proxy at 127.0.0.1:proxyPort entirely.

For proper HTTPS proxy testing, the request must use either the CONNECT tunneling method (which requires an http-proxy-agent or https-proxy-agent library) or make the request to the proxy itself (not to the final destination). As written, this test only validates that the CA certificate is accepted by example.com, not that the proxy is working or intercepting traffic as intended.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/logNetwork/index.ts` around lines 148 - 185,
testProxyConnectivity is currently creating an HTTPS request to example.com
directly and passing non-standard host/port on https.Agent (which are ignored),
so the proxy is bypassed; change the implementation to actually target the proxy
or use a proper proxy agent: either (A) make the request connect to
127.0.0.1:proxyPort (set request hostname/port to the proxy and set the request
path to the full destination URL or use a CONNECT method) and remove the invalid
agent.host/port usage, or (B) install and use a proxy agent (e.g.,
http-proxy-agent / https-proxy-agent) and pass that agent to https.request so
the request is tunneled through the mockttp proxy; update testProxyConnectivity,
the https.request options and agent construction accordingly so the request is
routed through the proxy and captured by NetworkCapture.

Comment on lines +29 to +32
export async function saveProxyState(state: SavedProxyState): Promise<void> {
await fsp.mkdir(STATE_DIR, { recursive: true });
await fsp.writeFile(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Make state-file writes atomic to preserve crash recovery guarantees.

Line [31] can leave a partially written proxy-state.json on abrupt termination, which breaks stale-session restoration on next run.

🛠️ Proposed fix
 export async function saveProxyState(state: SavedProxyState): Promise<void> {
   await fsp.mkdir(STATE_DIR, { recursive: true });
-  await fsp.writeFile(STATE_FILE, JSON.stringify(state, null, 2), 'utf8');
+  const tmpFile = `${STATE_FILE}.tmp-${process.pid}`;
+  await fsp.writeFile(tmpFile, JSON.stringify(state, null, 2), { encoding: 'utf8', mode: 0o600 });
+  await fsp.rename(tmpFile, STATE_FILE);
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/commands/logNetwork/proxyState.ts` around lines 29 - 32,
saveProxyState currently writes STATE_FILE directly and can leave a partially
written proxy-state.json on crashes; change saveProxyState to write atomically
by: ensure STATE_DIR exists, create a temp file in STATE_DIR (e.g. STATE_FILE +
'.tmp' or use a unique suffix), open the temp file with fsp.open, write JSON
into it, call filehandle.datasync()/fsync to flush to disk, close the handle,
then rename (fs.rename) the temp file to STATE_FILE to perform an atomic
replace; reference saveProxyState, STATE_DIR and STATE_FILE when locating where
to implement this change.

Comment on lines +781 to +785
for (const entry of har.log.entries) {
redactHarHeaders(entry.request?.headers);
redactHarHeaders(entry.response?.headers);
redactHarQueryParams(entry.request?.queryString);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redaction is incomplete: sensitive data can still leak via request.url and non-keyword secret values.

Current logic redacts header/query arrays, but request.url (used in report rendering) can still retain raw query secrets. Also, bindings is passed but not applied during HAR value redaction.

🔧 Proposed fix
-          redactHarHeaders(entry.request?.headers);
-          redactHarHeaders(entry.response?.headers);
-          redactHarQueryParams(entry.request?.queryString);
+          redactHarHeaders(entry.request?.headers, bindings);
+          redactHarHeaders(entry.response?.headers, bindings);
+          redactHarQueryParams(entry.request?.queryString, bindings);
+          redactHarRequestUrl(entry.request, bindings);
-function redactHarHeaders(headers: Array<{ name: string; value: string }> | undefined): void {
+function redactHarHeaders(
+  headers: Array<{ name: string; value: string }> | undefined,
+  bindings: RuntimeBindings,
+): void {
   if (!headers) return;
   for (const header of headers) {
-    if (shouldRedactHeader(header.name)) {
-      header.value = '[REDACTED]';
-    }
+    const value = redactResolvedValue(header.value, bindings) ?? header.value;
+    header.value = shouldRedactHeader(header.name) ? '[REDACTED]' : value;
   }
 }
-function redactHarQueryParams(queryString: Array<{ name: string; value: string }> | undefined): void {
+function redactHarQueryParams(
+  queryString: Array<{ name: string; value: string }> | undefined,
+  bindings: RuntimeBindings,
+): void {
   if (!queryString) return;
   for (const param of queryString) {
-    if (REDACTED_QUERY_PARAM_NAMES.has(param.name.toLowerCase())) {
-      param.value = '[REDACTED]';
-    }
+    const value = redactResolvedValue(param.value, bindings) ?? param.value;
+    param.value = REDACTED_QUERY_PARAM_NAMES.has(param.name.toLowerCase()) ? '[REDACTED]' : value;
   }
 }
+
+function redactHarRequestUrl(
+  request: { url?: string } | undefined,
+  bindings: RuntimeBindings,
+): void {
+  if (!request?.url) return;
+  try {
+    const u = new URL(request.url);
+    for (const [key, value] of u.searchParams.entries()) {
+      if (REDACTED_QUERY_PARAM_NAMES.has(key.toLowerCase())) {
+        u.searchParams.set(key, '[REDACTED]');
+      } else {
+        u.searchParams.set(key, redactResolvedValue(value, bindings) ?? value);
+      }
+    }
+    request.url = u.toString();
+  } catch {
+    request.url = redactResolvedValue(request.url, bindings) ?? request.url;
+  }
+}

Also applies to: 1010-1036

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/reportWriter.ts` around lines 781 - 785, The HAR redaction
loop currently only calls redactHarHeaders and redactHarQueryParams, leaving
request.url and non-keyword secret values unredacted and not applying provided
bindings; update the loop over har.log.entries to also call the HAR value
redaction routine on entry.request.url and entry.response?.url (e.g., call
redactHarValues(entry.request?.url, bindings) and
redactHarValues(entry.response?.url, bindings)), and ensure redactHarHeaders and
redactHarQueryParams accept and use the bindings parameter so they redact
non-keyword secret values (and update any internal helper that performs value
redaction to apply bindings-based replacements); apply the same changes to the
other HAR-processing block (the code around lines 1010-1036) so both places
sanitize request.url and use bindings consistently.

Comment on lines +789 to +791
Logger.w(`Failed to redact network HAR file: ${this._formatError(error)}`);
// Keep the unredacted copy rather than deleting — better to have data.
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Redaction failure currently leaves an unredacted HAR on disk.

Keeping raw HAR data after parse/redaction errors is a fail-open behavior and can expose secrets/PII in artifacts.

🔧 Proposed fix
     } catch (error) {
       Logger.w(`Failed to redact network HAR file: ${this._formatError(error)}`);
-      // Keep the unredacted copy rather than deleting — better to have data.
+      await fsp.unlink(targetPath).catch(() => {});
+      return undefined;
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Logger.w(`Failed to redact network HAR file: ${this._formatError(error)}`);
// Keep the unredacted copy rather than deleting — better to have data.
}
Logger.w(`Failed to redact network HAR file: ${this._formatError(error)}`);
await fsp.unlink(targetPath).catch(() => {});
return undefined;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/reportWriter.ts` around lines 789 - 791, The current
behavior leaves an unredacted HAR file on disk when redaction fails (the call
that logs via Logger.w and uses this._formatError); update this failure path to
securely remove the unredacted HAR instead of keeping it: after catching the
redaction error, attempt to delete the on-disk HAR within a safe try/catch (log
any deletion error via Logger.e), and only retain any redacted output if
redaction actually succeeded; ensure you reference the same symbols (Logger.w,
this._formatError) and perform the filesystem unlink/remove call in the same
error-handling block so the raw HAR is not persisted on disk on redaction
failure.

Comment on lines +535 to +540
Logger.w(
`Unable to stop network capture for test ${activeNetworkCapture.testId}: ` +
`${stopNetResponse.message ?? 'unknown error'}`,
);
activeNetworkCapture = undefined;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Call abort on failed stopNetworkCapture before clearing state.

When stopNetworkCapture returns success: false, the code clears activeNetworkCapture immediately, so the finally cleanup won’t run abort for that run.

🔧 Proposed fix
         } else {
           Logger.w(
             `Unable to stop network capture for test ${activeNetworkCapture.testId}: ` +
             `${stopNetResponse.message ?? 'unknown error'}`,
           );
+          try {
+            await session.device.abortNetworkCapture(activeNetworkCapture.runId);
+          } catch (error) {
+            Logger.w('Failed to finalize network capture after stop failure:', error);
+          }
           activeNetworkCapture = undefined;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
Logger.w(
`Unable to stop network capture for test ${activeNetworkCapture.testId}: ` +
`${stopNetResponse.message ?? 'unknown error'}`,
);
activeNetworkCapture = undefined;
}
Logger.w(
`Unable to stop network capture for test ${activeNetworkCapture.testId}: ` +
`${stopNetResponse.message ?? 'unknown error'}`,
);
try {
await session.device.abortNetworkCapture(activeNetworkCapture.runId);
} catch (error) {
Logger.w('Failed to finalize network capture after stop failure:', error);
}
activeNetworkCapture = undefined;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli/src/sessionRunner.ts` around lines 535 - 540, The code clears
activeNetworkCapture when stopNetworkCapture returns success: false, preventing
the finally cleanup from calling abort; update the failure branch in the stop
logic (the block that logs with Logger.w and handles stopNetResponse) to call
activeNetworkCapture.abort() (or activeNetworkCapture?.abort()) before setting
activeNetworkCapture = undefined, guarding with a null check and
swallowing/logging any abort errors so cleanup proceeds safely; reference the
stopNetworkCapture call, the activeNetworkCapture variable, and the Logger.w log
path when making this change.

Comment on lines +113 to +144
// Subscribe to request events — store request metadata (no body).
await this._server.on('request', (req) => {
this._pendingRequests.set(req.id, {
startedAt: new Date(),
method: req.method,
url: req.url,
headers: flattenHeaders(req.headers),
bodySize: req.body?.buffer?.byteLength ?? 0,
});
});

// Subscribe to response events — correlate with request, store entry.
await this._server.on('response', (response) => {
const completedAt = new Date();
const pending = this._pendingRequests.get(response.id);
this._pendingRequests.delete(response.id);

const entry: NetworkCapturedEntry = {
startedAt: pending?.startedAt ?? completedAt,
completedAt,
method: pending?.method ?? '???',
url: pending?.url ?? '',
statusCode: response.statusCode,
statusMessage: response.statusMessage ?? '',
requestHeaders: pending?.headers ?? {},
responseHeaders: flattenHeaders(response.headers),
requestBodySize: pending?.bodySize ?? 0,
responseBodySize: response.body?.buffer?.byteLength ?? 0,
durationMs: completedAt.getTime() - (pending?.startedAt ?? completedAt).getTime(),
};
this._entries.push(entry);
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Potential memory leak from orphaned pending requests.

If a request is initiated but never receives a response (e.g., due to timeout, connection reset, or TLS error), its entry remains in _pendingRequests indefinitely. Over a long session with many failed requests, this could accumulate significant memory.

Consider adding a cleanup mechanism, such as:

  • Periodic pruning of old entries (e.g., older than 60 seconds)
  • Clearing on abort or TLS error events for the specific request
Suggested improvement: Add orphan cleanup on TLS errors
       // Subscribe to TLS errors.
       await this._server.on('tls-client-error', (failure) => {
         const f = failure as unknown as {
           failureCause?: string;
           tlsMetadata?: { sniHostname?: string };
+          remoteIpAddress?: string;
         };
         this._tlsErrors.push({
           hostname: f.tlsMetadata?.sniHostname ?? 'unknown',
           failureCause: f.failureCause ?? 'unknown',
           timestamp: new Date(),
         });
+        // Note: mockttp doesn't provide request ID for TLS errors,
+        // so we can't directly clean up _pendingRequests here.
+        // Consider adding periodic cleanup in stopSession or a timer.
       });

A more complete solution would add a periodic cleanup timer or clear old pending requests when stopping the session (which already happens in stopSession).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/device-node/src/device/NetworkCaptureManager.ts` around lines 113 -
144, Pending request entries in _pendingRequests can be left orphaned if no
response arrives; add a cleanup mechanism to prevent memory leaks by: when
starting the capture (where _server.on('request') and _server.on('response') are
registered) also start a periodic timer that scans _pendingRequests and removes
entries older than a threshold (e.g., 60s) and optionally converts them into
completed NetworkCapturedEntry records in _entries with an appropriate
status/duration; also subscribe to request-level abort/error/TLS events from
_server to delete the specific pending entry immediately; ensure the timer is
cleared and any remaining pending entries are flushed/cleared in stopSession so
no timer or map entries survive session end.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant