Debugging and Observability
This guide shows a practical instrumentation stack for diagnosing flaky browser automation.
Suggested stack
Section titled “Suggested stack”- Console monitor plugin: stream
console.log,warn, anderroroutput. - 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.
1) Capture console output early
Section titled “1) Capture console output early”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 });2) Capture runtime exceptions
Section titled “2) Capture runtime exceptions”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.
3) Add breakpoints with debugger plugin
Section titled “3) Add breakpoints with debugger plugin”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);5) Change values while paused
Section titled “5) Change values while paused”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);6) Step through code
Section titled “6) Step through code”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.
7) Correlate with automation checkpoints
Section titled “7) Correlate with automation checkpoints”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.
8) Clean up instrumentation
Section titled “8) Clean up instrumentation”await monitor.stop();await errors.disable();await view.close();Stop plugins explicitly so subscriptions do not leak between test runs.
Common failure patterns
Section titled “Common failure patterns”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.
Symptom: Random timeout in CI only
Section titled “Symptom: Random timeout in CI only”- 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.
Recommended baseline for every new script
Section titled “Recommended baseline for every new script”- Start console monitor.
- Start error plugin.
- Navigate and wait for document ready.
- Perform actions with selector-based waits.
- Stop plugins and close view.