From 2030581bdb7ff2f6f749f1f87ce9326215f990a1 Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Fri, 18 Apr 2025 17:07:32 -0700 Subject: [PATCH 1/5] proxy page.evaluate() --- lib/StagehandPage.ts | 51 +++++++++++++++++++++++++++++++++++++++++++- lib/dom/global.d.ts | 1 + lib/index.ts | 8 ++++++- 3 files changed, 58 insertions(+), 2 deletions(-) diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 6d2aad423..3ec163427 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -31,9 +31,11 @@ import { StagehandDefaultError, } from "../types/stagehandErrors"; import { StagehandAPIError } from "@/types/stagehandApiErrors"; +import { scriptContent } from "@/lib/dom/build/scriptContent"; export class StagehandPage { private stagehand: Stagehand; + private rawPage: PlaywrightPage; private intPage: Page; private intContext: StagehandContext; private actHandler: StagehandActHandler; @@ -60,6 +62,7 @@ export class StagehandPage { api?: StagehandAPI, waitForCaptchaSolves?: boolean, ) { + this.rawPage = page; // Create a proxy to intercept all method calls and property access this.intPage = new Proxy(page, { get: (target: PlaywrightPage, prop: keyof PlaywrightPage) => { @@ -117,6 +120,34 @@ export class StagehandPage { } } + private async ensureStagehandScript(): Promise { + try { + const injected = await this.rawPage.evaluate( + () => !!window.__stagehandInjected, + ); + + if (injected) return; + + const guardedScript = `if (!window.__stagehandInjected) { \ +window.__stagehandInjected = true; \ +${scriptContent} \ +}`; + + await this.rawPage.addInitScript({ content: guardedScript }); + await this.rawPage.evaluate(guardedScript); + } catch (err) { + this.stagehand.log({ + category: "dom", + message: "Failed to inject Stagehand helper script", + level: 1, + auxiliary: { + error: { value: (err as Error).message, type: "string" }, + trace: { value: (err as Error).stack, type: "string" }, + }, + }); + } + } + private async _refreshPageFromAPI() { if (!this.api) return; @@ -217,7 +248,7 @@ export class StagehandPage { async init(): Promise { try { - const page = this.intPage; + const page = this.rawPage; const stagehand = this.stagehand; // Create a proxy that updates active page on method calls @@ -225,6 +256,24 @@ export class StagehandPage { get: (target: PlaywrightPage, prop: string | symbol) => { const value = target[prop as keyof PlaywrightPage]; + // Inject-on-demand for evaluate + if ( + prop === "evaluate" || + prop === "evaluateHandle" || + prop === "$eval" || + prop === "$$eval" + ) { + return async (...args: unknown[]) => { + this.intContext.setActivePage(this); + // Make sure helpers exist before the user’s evaluation + await this.ensureStagehandScript(); + return (value as (...a: unknown[]) => unknown).apply( + target, + args, + ); + }; + } + // Handle enhanced methods if (prop === "act" || prop === "extract" || prop === "observe") { if (!this.llmClient) { diff --git a/lib/dom/global.d.ts b/lib/dom/global.d.ts index 742c15433..aff82fa29 100644 --- a/lib/dom/global.d.ts +++ b/lib/dom/global.d.ts @@ -3,6 +3,7 @@ import { StagehandContainer } from "./StagehandContainer"; export {}; declare global { interface Window { + __stagehandInjected?: boolean; chunkNumber: number; showChunks?: boolean; processDom: (chunksSeen: Array) => Promise<{ diff --git a/lib/index.ts b/lib/index.ts index a12b63a72..735647fb3 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -715,8 +715,14 @@ export class Stagehand { await this.page.setViewportSize({ width: 1280, height: 720 }); } + const guardedScript = ` + if (!window.__stagehandInjected) { + window.__stagehandInjected = true; + ${scriptContent} + } +`; await this.context.addInitScript({ - content: scriptContent, + content: guardedScript, }); this.browserbaseSessionID = sessionId; From 3163126ed78d4a624ed314774076d1cbe5bc99ba Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Mon, 21 Apr 2025 11:20:53 -0700 Subject: [PATCH 2/5] add test --- .../tests/page/addInitScript.test.ts | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/evals/deterministic/tests/page/addInitScript.test.ts b/evals/deterministic/tests/page/addInitScript.test.ts index 56be8ef41..835b12be6 100644 --- a/evals/deterministic/tests/page/addInitScript.test.ts +++ b/evals/deterministic/tests/page/addInitScript.test.ts @@ -37,4 +37,32 @@ test.describe("StagehandPage - addInitScript", () => { await stagehand.close(); }); + + test("checks if init scripts are re-added and available even if they've been deleted", async () => { + const stagehand = new Stagehand(StagehandConfig); + await stagehand.init(); + + const page = stagehand.page; + await page.goto( + "https://browserbase.github.io/stagehand-eval-sites/sites/aigrant/", + ); + + // delete the __stagehandInjected flag, and delete the + // getScrollableElementXpaths function + await page.evaluate(() => { + delete window.getScrollableElementXpaths; + delete window.__stagehandInjected; + }); + + // attempt to call the getScrollableElementXpaths function + // which we previously deleted. page.evaluate should realize + // its been deleted and re-inject it + const xpaths = await page.evaluate(() => { + return window.getScrollableElementXpaths(); + }); + + await stagehand.close(); + // this is the only scrollable element on the page + expect(xpaths).toContain("/html"); + }); }); From 3567cadcc7a4717acadbbe8248d978ff49b4ab9e Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Mon, 21 Apr 2025 11:48:19 -0700 Subject: [PATCH 3/5] changeset --- .changeset/empty-bugs-occur.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/empty-bugs-occur.md diff --git a/.changeset/empty-bugs-occur.md b/.changeset/empty-bugs-occur.md new file mode 100644 index 000000000..5eecb5480 --- /dev/null +++ b/.changeset/empty-bugs-occur.md @@ -0,0 +1,5 @@ +--- +"@browserbasehq/stagehand": patch +--- + +wrap page.evaluate to make sure we have injected browser side scripts before calling them From 5972aefd70ba343bb79240f36c286cbf6e2962e6 Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Mon, 21 Apr 2025 11:53:33 -0700 Subject: [PATCH 4/5] re throw error after logging (thanks greptile) --- lib/StagehandPage.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 3ec163427..92836e3e8 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -145,6 +145,7 @@ ${scriptContent} \ trace: { value: (err as Error).stack, type: "string" }, }, }); + throw err; } } From 7cb8562b7d637d5a049e2f87e164439e9650d0a4 Mon Sep 17 00:00:00 2001 From: Sean McGuire Date: Mon, 21 Apr 2025 11:55:47 -0700 Subject: [PATCH 5/5] rm comment --- lib/StagehandPage.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/StagehandPage.ts b/lib/StagehandPage.ts index 92836e3e8..557c95cfb 100644 --- a/lib/StagehandPage.ts +++ b/lib/StagehandPage.ts @@ -266,7 +266,7 @@ ${scriptContent} \ ) { return async (...args: unknown[]) => { this.intContext.setActivePage(this); - // Make sure helpers exist before the user’s evaluation + // Make sure helpers exist await this.ensureStagehandScript(); return (value as (...a: unknown[]) => unknown).apply( target,