Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tender-zoos-think.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@browserbasehq/stagehand": patch
---

include error cause in UnderstudyCommandException
46 changes: 25 additions & 21 deletions packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,10 @@ import { resolveLocatorWithHops } from "../../understudy/deepLocator.js";
import type { Page } from "../../understudy/page.js";
import { v3Logger } from "../../logger.js";
import { SessionFileLogger } from "../../flowLogger.js";
import { StagehandClickError } from "../../types/public/sdkErrors.js";

export class UnderstudyCommandException extends Error {
constructor(message: string) {
super(message);
this.name = "UnderstudyCommandException";
}
}
import {
StagehandClickError,
UnderstudyCommandException,
} from "../../types/public/sdkErrors.js";

export interface UnderstudyMethodHandlerContext {
method: string;
Expand Down Expand Up @@ -127,7 +123,10 @@ export async function performUnderstudyMethod(
args: { value: JSON.stringify(args), type: "object" },
},
});
throw new UnderstudyCommandException(msg);
if (e instanceof UnderstudyCommandException) {
throw e;
}
throw new UnderstudyCommandException(msg, e);
} finally {
SessionFileLogger.logUnderstudyActionCompleted();
}
Expand Down Expand Up @@ -163,17 +162,19 @@ export async function selectOption(ctx: UnderstudyMethodHandlerContext) {
const text = args[0]?.toString() || "";
await locator.selectOption(text);
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
const stack = e instanceof Error ? e.stack : undefined;
v3Logger({
category: "action",
message: "error selecting option",
level: 0,
auxiliary: {
error: { value: e.message, type: "string" },
trace: { value: e.stack, type: "string" },
error: { value: msg, type: "string" },
trace: { value: stack ?? "", type: "string" },
xpath: { value: xpath, type: "string" },
},
});
throw new UnderstudyCommandException(e.message);
throw new UnderstudyCommandException(msg, e);
}
}

Expand Down Expand Up @@ -225,7 +226,8 @@ async function scrollByPixelOffset(
const { x, y } = await locator.centroid();
await page.scroll(x, y, dx, dy);
} catch (e) {
throw new UnderstudyCommandException(e.message);
const msg = e instanceof Error ? e.message : String(e);
throw new UnderstudyCommandException(msg, e);
}
}

Expand Down Expand Up @@ -263,7 +265,7 @@ async function fillOrType(ctx: UnderstudyMethodHandlerContext): Promise<void> {
xpath: { value: xpath, type: "string" },
},
});
throw new UnderstudyCommandException(msg);
throw new UnderstudyCommandException(msg, e);
}
}

Expand All @@ -282,7 +284,7 @@ async function typeText(ctx: UnderstudyMethodHandlerContext): Promise<void> {
xpath: { value: xpath, type: "string" },
},
});
throw new UnderstudyCommandException(msg);
throw new UnderstudyCommandException(msg, e);
}
}

Expand Down Expand Up @@ -312,7 +314,7 @@ async function pressKey(ctx: UnderstudyMethodHandlerContext): Promise<void> {
xpath: { value: xpath, type: "string" },
},
});
throw new UnderstudyCommandException(msg);
throw new UnderstudyCommandException(msg, e);
}
}

Expand Down Expand Up @@ -352,7 +354,7 @@ async function doubleClick(ctx: UnderstudyMethodHandlerContext): Promise<void> {
xpath: { value: xpath, type: "string" },
},
});
throw new UnderstudyCommandException(msg);
throw new UnderstudyCommandException(msg, e);
}
}

Expand Down Expand Up @@ -429,7 +431,7 @@ async function dragAndDrop(ctx: UnderstudyMethodHandlerContext): Promise<void> {
to: { value: toXPath, type: "string" },
},
});
throw new UnderstudyCommandException(msg);
throw new UnderstudyCommandException(msg, e);
}
}

Expand Down Expand Up @@ -508,17 +510,19 @@ export async function hover(ctx: UnderstudyMethodHandlerContext) {
try {
await locator.hover();
} catch (e) {
const msg = e instanceof Error ? e.message : String(e);
const stack = e instanceof Error ? e.stack : undefined;
v3Logger({
category: "action",
message: "error attempting to hover",
level: 0,
auxiliary: {
error: { value: e.message, type: "string" },
trace: { value: e.stack, type: "string" },
error: { value: msg, type: "string" },
trace: { value: stack ?? "", type: "string" },
xpath: { value: xpath, type: "string" },
},
});
throw new UnderstudyCommandException(e.message);
throw new UnderstudyCommandException(msg, e);
}
}

Expand Down
7 changes: 7 additions & 0 deletions packages/core/lib/v3/types/public/sdkErrors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -399,3 +399,10 @@ export class StagehandSnapshotError extends StagehandError {
super(`error taking snapshot${suffix}`, cause);
}
}

export class UnderstudyCommandException extends StagehandError {
constructor(message: string, cause?: unknown) {
super(message, cause);
this.name = "UnderstudyCommandException";
}
}
1 change: 1 addition & 0 deletions packages/core/tests/public-api/public-error-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const publicErrorTypes = {
ActTimeoutError: Stagehand.ActTimeoutError,
ObserveTimeoutError: Stagehand.ObserveTimeoutError,
ExtractTimeoutError: Stagehand.ExtractTimeoutError,
UnderstudyCommandException: Stagehand.UnderstudyCommandException,
} as const;

const errorTypes = Object.keys(publicErrorTypes) as Array<
Expand Down
68 changes: 68 additions & 0 deletions packages/core/tests/understudy-command-exception.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { describe, expect, it } from "vitest";
import {
UnderstudyCommandException,
StagehandError,
} from "../lib/v3/types/public/sdkErrors.js";

describe("UnderstudyCommandException", () => {
it("extends StagehandError", () => {
const err = new UnderstudyCommandException("test");
expect(err).toBeInstanceOf(StagehandError);
expect(err).toBeInstanceOf(Error);
});

it("has the correct name", () => {
const err = new UnderstudyCommandException("test");
expect(err.name).toBe("UnderstudyCommandException");
});

it("preserves the message", () => {
const err = new UnderstudyCommandException("something broke");
expect(err.message).toBe("something broke");
});

it("stores the original error as cause when provided", () => {
const original = new Error("root cause");
const err = new UnderstudyCommandException("wrapper message", original);

expect(err.cause).toBe(original);
expect((err.cause as Error).message).toBe("root cause");
expect((err.cause as Error).stack).toBeDefined();
});

it("stores non-Error cause values", () => {
const err = new UnderstudyCommandException("failed", "string cause");
expect(err.cause).toBe("string cause");
});

it("has undefined cause when none is provided", () => {
const err = new UnderstudyCommandException("no cause");
expect(err.cause).toBeUndefined();
});

it("generates its own stack trace", () => {
const err = new UnderstudyCommandException("test");
expect(err.stack).toBeDefined();
expect(err.stack).toContain("UnderstudyCommandException");
});

it("preserves the original stack via cause for debugging", () => {
function deepFunction() {
throw new Error("deep error");
}

let original: Error;
try {
deepFunction();
} catch (e) {
original = e as Error;
}

const wrapped = new UnderstudyCommandException(original!.message, original);

// The wrapper has its own stack
expect(wrapped.stack).toBeDefined();
// The original stack is accessible via cause
expect((wrapped.cause as Error).stack).toContain("deepFunction");
});
});
Loading