Skip to content

Commit 5dad639

Browse files
[feat]: add page.snapshot() (#1518)
# why - to expose a public API for the snapshotting functionality used by stagehand internally # what changed - added a new `page.snapshot()` function, which returns a `SnapshotResult` object containing: - a stringified, hierarchical tree representation of the DOM (the same one that is given to the LLM internally) - an xpath map, which contains a mapping of xpaths for elements on the page, keyed by IDs - a URL map, which contains a mapping of all URLs inlcuded in the page, keyed by IDs # test plan - we already have comprehensive testing for the internals of this function (`captureHybridSnapshot()`), and this new function does not have any additional branching logic. existing unit tests & evals should suffice. <!-- This is an auto-generated description by cubic. --> --- ## Summary by cubic Exposes a public page.snapshot() API to capture a formatted DOM snapshot with xpath and URL maps. Addresses STG-1082 by making the internal snapshotter available to users. - New Features - Added page.snapshot(): returns SnapshotResult { formattedTree, xpathMap, urlMap } using captureHybridSnapshot (pierces shadow DOM). - Introduced StagehandSnapshotError for snapshot failures (keeps original cause). - Exported SnapshotResult type. - Refactors - StagehandError now accepts and stores an optional cause for better error chaining. <sup>Written for commit e89840f. Summary will update on new commits.</sup> <!-- End of auto-generated description by cubic. -->
1 parent e56c6eb commit 5dad639

File tree

5 files changed

+54
-4
lines changed

5 files changed

+54
-4
lines changed

.changeset/tiny-pens-serve.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@browserbasehq/stagehand": patch
3+
---
4+
5+
add page.snapshot() for capturing a stringified DOM snapshot of the page, including an xpath map & url map

packages/core/lib/v3/types/public/page.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,9 @@ export type { ConsoleListener } from "../../understudy/consoleMessage";
1111

1212
export type LoadState = "load" | "domcontentloaded" | "networkidle";
1313
export { Response } from "../../understudy/response";
14+
15+
export type SnapshotResult = {
16+
formattedTree: string;
17+
xpathMap: Record<string, string>;
18+
urlMap: Record<string, string>;
19+
};

packages/core/lib/v3/types/public/sdkErrors.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@ import { ZodError } from "zod";
33
import { STAGEHAND_VERSION } from "../../../version";
44

55
export class StagehandError extends Error {
6-
constructor(message: string) {
6+
public readonly cause?: unknown;
7+
8+
constructor(message: string, cause?: unknown) {
79
super(message);
810
this.name = this.constructor.name;
11+
if (cause !== undefined) {
12+
this.cause = cause;
13+
}
914
}
1015
}
1116

@@ -370,3 +375,15 @@ export class StagehandClosedError extends StagehandError {
370375
super("Stagehand session was closed");
371376
}
372377
}
378+
379+
export class StagehandSnapshotError extends StagehandError {
380+
constructor(cause?: unknown) {
381+
const suffix =
382+
cause instanceof Error
383+
? `: ${cause.message}`
384+
: cause
385+
? `: ${String(cause)}`
386+
: "";
387+
super(`error taking snapshot${suffix}`, cause);
388+
}
389+
}

packages/core/lib/v3/understudy/page.ts

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,23 @@ import { CdpConnection } from "./cdp";
77
import { Frame } from "./frame";
88
import { FrameLocator } from "./frameLocator";
99
import { deepLocatorFromPage, resolveLocatorTarget } from "./deepLocator";
10-
import { resolveXpathForLocation } from "./a11y/snapshot";
10+
import {
11+
captureHybridSnapshot,
12+
resolveXpathForLocation,
13+
} from "./a11y/snapshot";
1114
import { FrameRegistry } from "./frameRegistry";
1215
import { executionContexts } from "./executionContextRegistry";
13-
import { LoadState } from "../types/public/page";
16+
import { LoadState, SnapshotResult } from "../types/public/page";
1417
import { NetworkManager } from "./networkManager";
1518
import { LifecycleWatcher } from "./lifecycleWatcher";
1619
import { NavigationResponseTracker } from "./navigationResponseTracker";
1720
import { Response, isSerializableResponse } from "./response";
1821
import { ConsoleMessage, ConsoleListener } from "./consoleMessage";
1922
import type { StagehandAPIClient } from "../api";
20-
import type { LocalBrowserLaunchOptions } from "../types/public";
23+
import {
24+
LocalBrowserLaunchOptions,
25+
StagehandSnapshotError,
26+
} from "../types/public";
2127
import type { Locator } from "./locator";
2228
import {
2329
StagehandInvalidArgumentError,
@@ -1789,6 +1795,21 @@ export class Page {
17891795
}
17901796
}
17911797

1798+
@logAction("Page.snapshot")
1799+
async snapshot(): Promise<SnapshotResult> {
1800+
try {
1801+
const { combinedTree, combinedXpathMap, combinedUrlMap } =
1802+
await captureHybridSnapshot(this, { pierceShadow: true });
1803+
return {
1804+
formattedTree: combinedTree,
1805+
xpathMap: combinedXpathMap,
1806+
urlMap: combinedUrlMap,
1807+
};
1808+
} catch (err) {
1809+
throw new StagehandSnapshotError(err);
1810+
}
1811+
}
1812+
17921813
// Track pressed modifier keys
17931814
private _pressedModifiers = new Set<string>();
17941815

packages/core/tests/public-api/public-error-types.test.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ export const publicErrorTypes = {
4848
Stagehand.StagehandShadowSegmentNotFoundError,
4949
StreamingCallbacksInNonStreamingModeError:
5050
Stagehand.StreamingCallbacksInNonStreamingModeError,
51+
StagehandSnapshotError: Stagehand.StagehandSnapshotError,
5152
TimeoutError: Stagehand.TimeoutError,
5253
UnsupportedAISDKModelProviderError:
5354
Stagehand.UnsupportedAISDKModelProviderError,

0 commit comments

Comments
 (0)