diff --git a/.changeset/tender-zoos-think.md b/.changeset/tender-zoos-think.md new file mode 100644 index 000000000..f6f7507c9 --- /dev/null +++ b/.changeset/tender-zoos-think.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +include error cause in UnderstudyCommandException diff --git a/packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts b/packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts index a0ad6124e..d99801f8c 100644 --- a/packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts +++ b/packages/core/lib/v3/handlers/handlerUtils/actHandlerUtils.ts @@ -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; @@ -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(); } @@ -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); } } @@ -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); } } @@ -263,7 +265,7 @@ async function fillOrType(ctx: UnderstudyMethodHandlerContext): Promise { xpath: { value: xpath, type: "string" }, }, }); - throw new UnderstudyCommandException(msg); + throw new UnderstudyCommandException(msg, e); } } @@ -282,7 +284,7 @@ async function typeText(ctx: UnderstudyMethodHandlerContext): Promise { xpath: { value: xpath, type: "string" }, }, }); - throw new UnderstudyCommandException(msg); + throw new UnderstudyCommandException(msg, e); } } @@ -312,7 +314,7 @@ async function pressKey(ctx: UnderstudyMethodHandlerContext): Promise { xpath: { value: xpath, type: "string" }, }, }); - throw new UnderstudyCommandException(msg); + throw new UnderstudyCommandException(msg, e); } } @@ -352,7 +354,7 @@ async function doubleClick(ctx: UnderstudyMethodHandlerContext): Promise { xpath: { value: xpath, type: "string" }, }, }); - throw new UnderstudyCommandException(msg); + throw new UnderstudyCommandException(msg, e); } } @@ -429,7 +431,7 @@ async function dragAndDrop(ctx: UnderstudyMethodHandlerContext): Promise { to: { value: toXPath, type: "string" }, }, }); - throw new UnderstudyCommandException(msg); + throw new UnderstudyCommandException(msg, e); } } @@ -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); } } diff --git a/packages/core/lib/v3/types/public/sdkErrors.ts b/packages/core/lib/v3/types/public/sdkErrors.ts index 88389afc7..0f6beb2be 100644 --- a/packages/core/lib/v3/types/public/sdkErrors.ts +++ b/packages/core/lib/v3/types/public/sdkErrors.ts @@ -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"; + } +} diff --git a/packages/core/tests/public-api/public-error-types.test.ts b/packages/core/tests/public-api/public-error-types.test.ts index 8f2dc170e..354fe5265 100644 --- a/packages/core/tests/public-api/public-error-types.test.ts +++ b/packages/core/tests/public-api/public-error-types.test.ts @@ -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< diff --git a/packages/core/tests/understudy-command-exception.test.ts b/packages/core/tests/understudy-command-exception.test.ts new file mode 100644 index 000000000..1304f84be --- /dev/null +++ b/packages/core/tests/understudy-command-exception.test.ts @@ -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"); + }); +});