Skip to content

Debugging and Observability

This guide shows a practical instrumentation stack for diagnosing flaky browser automation.

  • Console monitor plugin: stream console.log, warn, and error output.
  • Error plugin: capture uncaught runtime exceptions and rejected promises.
  • Debugger plugin: pause execution and inspect script behavior.
  • Network/events hooks: correlate browser activity with automation steps.

Start console monitoring before navigation so startup logs are not missed.

import { startConsoleMonitor } from "webotron/plugins/console-monitor";
const monitor = await startConsoleMonitor(view, {
onEntry(entry) {
console.log(`[console:${entry.level}] ${entry.message}`);
},
});
await view.navigate("https://example.com");
await view.waitForDocumentReady({ state: "complete", timeoutMs: 15000 });
import { createErrorPlugin } from "webotron/plugins/error";
const errors = createErrorPlugin({
target: view,
onException(exception) {
console.error("Runtime exception", exception);
},
});
await errors.enable();

Use this to detect failures that do not surface through your action methods.

import { createDebuggerPlugin } from "webotron/plugins/debugger";
const debuggerPlugin = createDebuggerPlugin({
target: view,
onPause(info) {
console.log("Paused because:", info.reason);
},
});
await debuggerPlugin.enable();
await debuggerPlugin.setPauseOnExceptions("uncaught");
await debuggerPlugin.addBreakpoint({
url: "https://example.com/app.js",
lineNumber: 120,
});

During debugging sessions, pair this with narrow test cases to isolate one failing interaction.

4) Inspect call frames and local variables

Section titled “4) Inspect call frames and local variables”

When execution pauses, the debugger gives you ordered call frames. Frame 0 is usually the top-most paused frame.

const paused = await debuggerPlugin.waitForPause(5000);
const frameIndex = 0;
const frame = paused.callFrames[frameIndex];
console.log(frame.functionName, frame.url, frame.lineNumber, frame.columnNumber);
const locals = await debuggerPlugin.getLocals(frameIndex);
for (const entry of locals) {
console.log(entry.scopeIndex, entry.variable.name, entry.variable.value);
}

Use getLocal() when you want one variable without scanning the whole scope list.

const currentUser = await debuggerPlugin.getLocal(0, 0, "currentUser");
console.log(currentUser?.variable.value);

If a scope is writable, you can change a live variable before resuming.

const mutation = await debuggerPlugin.setVariableValue(
frame.callFrameId,
0,
"shouldThrow",
false,
);
if (!mutation.ok) {
throw new Error(mutation.error ?? "Unable to mutate paused variable");
}
await debuggerPlugin.resume();

When you need a computed expression instead of a variable assignment, use evaluateOnCallFrame():

const result = await debuggerPlugin.evaluateOnCallFrame(
frame.callFrameId,
"({ title: document.title, href: location.href })",
);
console.log(result);
await debuggerPlugin.stepOver();
await debuggerPlugin.stepInto();
await debuggerPlugin.stepOut();

These are useful when you want to see how a variable changes one statement at a time.

Add explicit log checkpoints around high-risk operations:

console.log("checkpoint:start-checkout");
await view.click("[data-testid='checkout']");
console.log("checkpoint:wait-confirmation");
await view.waitForElement(".confirmation", { strategy: "observer", timeoutMs: 8000 });
console.log("checkpoint:done");

This gives you a timeline when combined with plugin events.

await monitor.stop();
await errors.disable();
await view.close();

Stop plugins explicitly so subscriptions do not leak between test runs.

Symptom: Click succeeds but UI never updates

Section titled “Symptom: Click succeeds but UI never updates”
  • Check console warnings for client-side validation or permission issues.
  • Confirm request/response path with your network/event guide setup.
  • Verify post-click waits target the right selector.
  • Increase timeout ceilings for first-page hydration.
  • Log document readiness and element readiness checkpoints.
  • Prefer deterministic waits over fixed sleeps.

Symptom: Page script throws before your first action

Section titled “Symptom: Page script throws before your first action”
  • Start console and error plugins before navigate().
  • Record URL/title transitions to verify you are on the expected route.

Symptom: You hit a breakpoint but cannot see the variable you expect

Section titled “Symptom: You hit a breakpoint but cannot see the variable you expect”
  • Check the scope chain and inspect the correct scopeIndex.
  • Use getLocal() for a precise lookup.
  • Remember that some values live in closure scopes rather than the local scope.

Symptom: Variable mutation does not seem to take effect

Section titled “Symptom: Variable mutation does not seem to take effect”
  • Confirm the scope is writable.
  • Use setVariableValue() on the exact paused call frame and scope index.
  • If the mutation is not assignable, fall back to evaluateOnCallFrame() for a targeted expression.
  1. Start console monitor.
  2. Start error plugin.
  3. Navigate and wait for document ready.
  4. Perform actions with selector-based waits.
  5. Stop plugins and close view.