Skip to content

Commit 28287dc

Browse files
authored
feat(stripe): migrate Stagehand payment example to v3 (#61)
- V3Options in stagehand.config; add stripe dependency - observe/act on Stagehand; ObserveResult and actWithCache helpers Made-with: Cursor
1 parent 94fe063 commit 28287dc

5 files changed

Lines changed: 73 additions & 124 deletions

File tree

examples/integrations/stripe/stagehand/4-make-payment.ts

Lines changed: 20 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
* To edit config, see `stagehand.config.ts`
1111
*
1212
*/
13-
import { Page, BrowserContext, Stagehand } from "@browserbasehq/stagehand";
13+
import { Stagehand } from "@browserbasehq/stagehand";
1414
import { z } from "zod";
1515
import chalk from "chalk";
1616
import dotenv from "dotenv";
@@ -21,55 +21,42 @@ dotenv.config();
2121
const cardId = "ic_1Qu1XJGhqv5yXZ43z9OdjbXs"; // replace with your card id from the previous step
2222

2323
export async function main({
24-
page,
25-
context,
2624
stagehand,
2725
}: {
28-
page: Page; // Playwright Page with act, extract, and observe methods
29-
context: BrowserContext; // Playwright BrowserContext
30-
stagehand: Stagehand; // Stagehand instance
26+
stagehand: Stagehand;
3127
}) {
32-
// Learn more about Stagehand: https://www.stagehand.dev/
33-
28+
const page = stagehand.context.pages()[0];
3429
const paymentInfo = await getCard(cardId);
3530

3631
// Navigate to Red Cross donation page
3732
await page.goto('https://www.redcross.org/donate/donation.html/')
38-
const donationAmount = await page.observe({
39-
instruction: "Find the donation amounts",
40-
returnAction: true,
41-
onlyVisible: false,
42-
});
33+
const donationAmount = await stagehand.observe(
34+
"Find the donation amounts"
35+
);
4336
// Click the first donation amount
44-
await page.act(donationAmount[0])
37+
await stagehand.act(donationAmount[0])
4538

4639
// Find the continue button and click it
47-
const continueButton = await page.observe({
48-
instruction: "Find the continue button and click it",
49-
returnAction: true,
50-
onlyVisible: false,
51-
});
52-
await page.act(continueButton[0])
40+
const continueButton = await stagehand.observe(
41+
"Find the continue button and click it"
42+
);
43+
await stagehand.act(continueButton[0])
5344

5445
// Find the credit card button and click it
55-
const creditCardButton = await page.observe({
56-
instruction: "Find the credit card button and click it",
57-
returnAction: true,
58-
onlyVisible: false,
59-
});
60-
await page.act(creditCardButton[0])
46+
const creditCardButton = await stagehand.observe(
47+
"Find the credit card button and click it"
48+
);
49+
await stagehand.act(creditCardButton[0])
6150

62-
await page.act({action: "click the continue button"})
51+
await stagehand.act("click the continue button")
6352

64-
const formValues = await page.observe({
65-
instruction: `Fill in the form with the following values: ${JSON.stringify(paymentInfo)}`,
66-
returnAction: true,
67-
onlyVisible: false,
68-
});
53+
const formValues = await stagehand.observe(
54+
`Fill in the form with the following values: ${JSON.stringify(paymentInfo)}`
55+
);
6956
console.log("formValues", formValues);
7057

7158
// Fill in the form with the values
7259
for (const value of formValues) {
73-
await page.act(value);
60+
await stagehand.act(value);
7461
}
7562
}

examples/integrations/stripe/stagehand/index.ts

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,7 @@ async function run() {
3636
);
3737
}
3838

39-
const page = stagehand.page;
40-
const context = stagehand.context;
4139
await main({
42-
page,
43-
context,
4440
stagehand,
4541
});
4642
await stagehand.close();

examples/integrations/stripe/stagehand/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
},
99
"dependencies": {
1010
"@browserbasehq/sdk": "^2.6.0",
11-
"@browserbasehq/stagehand": "^2.4.4",
11+
"@browserbasehq/stagehand": "^3.2.0",
12+
"stripe": "^17.7.0",
1213
"@playwright/test": "^1.49.1",
1314
"boxen": "^8.0.1",
1415
"chalk": "^5.3.0",
Lines changed: 9 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,24 @@
1-
import type { ConstructorParams, LogLine } from "@browserbasehq/stagehand";
1+
import type { LogLine, V3Options } from "@browserbasehq/stagehand";
22
import dotenv from "dotenv";
3-
false
4-
false
53

64
dotenv.config();
75

8-
const StagehandConfig: ConstructorParams = {
6+
const StagehandConfig: V3Options = {
97
env: "BROWSERBASE",
10-
apiKey: process.env.BROWSERBASE_API_KEY /* API key for authentication */,
11-
projectId: process.env.BROWSERBASE_PROJECT_ID /* Project identifier */,
12-
debugDom: undefined /* Enable DOM debugging features */,
13-
headless: false /* Run browser in headless mode */,
14-
logger: (message: LogLine) =>
15-
console.log(logLineToString(message)) /* Custom logging function */,
16-
domSettleTimeoutMs: 30_000 /* Timeout for DOM to settle in milliseconds */,
8+
apiKey: process.env.BROWSERBASE_API_KEY,
9+
projectId: process.env.BROWSERBASE_PROJECT_ID,
10+
domSettleTimeout: 30_000,
11+
logger: (message: LogLine) => console.log(logLineToString(message)),
12+
model: "openai/gpt-4o",
1713
browserbaseSessionCreateParams: {
1814
projectId: process.env.BROWSERBASE_PROJECT_ID!,
1915
},
20-
enableCaching: undefined /* Enable caching functionality */,
21-
browserbaseSessionID:
22-
undefined /* Session ID for resuming Browserbase sessions */,
23-
modelName: "gpt-4o" /* Name of the model to use */,
24-
modelClientOptions: {
25-
apiKey: process.env.OPENAI_API_KEY,
26-
} /* Configuration options for the model client */,
27-
28-
16+
browserbaseSessionID: undefined,
2917
};
3018

3119
export default StagehandConfig;
3220

33-
/**
34-
* Custom logging function that you can use to filter logs.
35-
*
36-
* General pattern here is that `message` will always be unique with no params
37-
* Any param you would put in a log is in `auxiliary`.
38-
*
39-
* For example, an error log looks like this:
40-
*
41-
* ```
42-
* {
43-
* category: "error",
44-
* message: "Some specific error occurred",
45-
* auxiliary: {
46-
* message: { value: "Error message", type: "string" },
47-
* trace: { value: "Error trace", type: "string" }
48-
* }
49-
* }
50-
* ```
51-
*
52-
* You can then use `logLineToString` to filter for a specific log pattern like
53-
*
54-
* ```
55-
* if (logLine.message === "Some specific error occurred") {
56-
* console.log(logLineToString(logLine));
57-
* }
58-
* ```
59-
*/
6021
export function logLineToString(logLine: LogLine): string {
61-
// If you want more detail, set this to false. However, this will make the logs
62-
// more verbose and harder to read.
6322
const HIDE_AUXILIARY = true;
6423

6524
try {
@@ -68,7 +27,6 @@ export function logLineToString(logLine: LogLine): string {
6827
return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message}\n ${logLine.auxiliary.error.value}\n ${logLine.auxiliary.trace.value}`;
6928
}
7029

71-
// If we want to hide auxiliary information, we don't add it to the log
7230
return `${timestamp}::[stagehand:${logLine.category}] ${logLine.message} ${
7331
logLine.auxiliary && !HIDE_AUXILIARY
7432
? JSON.stringify(logLine.auxiliary)
@@ -78,4 +36,4 @@ export function logLineToString(logLine: LogLine): string {
7836
console.error(`Error logging line:`, error);
7937
return "error logging line";
8038
}
81-
}
39+
}
Lines changed: 42 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { ObserveResult, Page } from "@browserbasehq/stagehand";
1+
import type {
2+
Action,
3+
ObserveResult,
4+
Page,
5+
Stagehand,
6+
} from "@browserbasehq/stagehand";
27
import boxen from "boxen";
38
import chalk from "chalk";
49
import fs from "fs/promises";
@@ -10,15 +15,10 @@ export function announce(message: string, title?: string) {
1015
padding: 1,
1116
margin: 3,
1217
title: title || "Stagehand",
13-
})
18+
}),
1419
);
1520
}
1621

17-
/**
18-
* Get an environment variable and throw an error if it's not found
19-
* @param name - The name of the environment variable
20-
* @returns The value of the environment variable
21-
*/
2222
export function getEnvVar(name: string, required = true): string | undefined {
2323
const value = process.env[name];
2424
if (!value && required) {
@@ -27,12 +27,6 @@ export function getEnvVar(name: string, required = true): string | undefined {
2727
return value;
2828
}
2929

30-
/**
31-
* Validate a Zod schema against some data
32-
* @param schema - The Zod schema to validate against
33-
* @param data - The data to validate
34-
* @returns Whether the data is valid against the schema
35-
*/
3630
export function validateZodSchema(schema: z.ZodTypeAny, data: unknown) {
3731
try {
3832
schema.parse(data);
@@ -42,11 +36,9 @@ export function validateZodSchema(schema: z.ZodTypeAny, data: unknown) {
4236
}
4337
}
4438

45-
export async function drawObserveOverlay(page: Page, results: ObserveResult[]) {
46-
// Convert single xpath to array for consistent handling
47-
const xpathList = results.map((result) => result.selector);
39+
export async function drawObserveOverlay(page: Page, results: ObserveResult) {
40+
const xpathList = results.map((result: Action) => result.selector);
4841

49-
// Filter out empty xpaths
5042
const validXpaths = xpathList.filter((xpath) => xpath !== "xpath=");
5143

5244
await page.evaluate((selectors) => {
@@ -59,7 +51,7 @@ export async function drawObserveOverlay(page: Page, results: ObserveResult[]) {
5951
document,
6052
null,
6153
XPathResult.FIRST_ORDERED_NODE_TYPE,
62-
null
54+
null,
6355
).singleNodeValue;
6456
} else {
6557
element = document.querySelector(selector);
@@ -84,7 +76,6 @@ export async function drawObserveOverlay(page: Page, results: ObserveResult[]) {
8476
}
8577

8678
export async function clearOverlays(page: Page) {
87-
// remove existing stagehandObserve attributes
8879
await page.evaluate(() => {
8980
const elements = document.querySelectorAll('[stagehandObserve="true"]');
9081
elements.forEach((el) => {
@@ -97,39 +88,55 @@ export async function clearOverlays(page: Page) {
9788
});
9889
}
9990

100-
export async function simpleCache(
101-
instruction: string,
102-
actionToCache: ObserveResult
103-
) {
104-
// Save action to cache.json
91+
export async function simpleCache(instruction: string, actionToCache: Action) {
10592
try {
106-
// Read existing cache if it exists
107-
let cache: Record<string, ObserveResult> = {};
93+
let cache: Record<string, Action> = {};
10894
try {
10995
const existingCache = await fs.readFile("cache.json", "utf-8");
11096
cache = JSON.parse(existingCache);
111-
} catch (error) {
112-
// File doesn't exist yet, use empty cache
97+
} catch {
98+
// no file yet
11399
}
114100

115-
// Add new action to cache
116101
cache[instruction] = actionToCache;
117102

118-
// Write updated cache to file
119103
await fs.writeFile("cache.json", JSON.stringify(cache, null, 2));
120104
} catch (error) {
121105
console.error(chalk.red("Failed to save to cache:"), error);
122106
}
123107
}
124108

125-
export async function readCache(
126-
instruction: string
127-
): Promise<ObserveResult | null> {
109+
export async function readCache(instruction: string): Promise<Action | null> {
128110
try {
129111
const existingCache = await fs.readFile("cache.json", "utf-8");
130-
const cache: Record<string, ObserveResult> = JSON.parse(existingCache);
112+
const cache: Record<string, Action> = JSON.parse(existingCache);
131113
return cache[instruction] || null;
132-
} catch (error) {
114+
} catch {
133115
return null;
134116
}
135117
}
118+
119+
export async function actWithCache(
120+
stagehand: Stagehand,
121+
page: Page,
122+
instruction: string,
123+
): Promise<void> {
124+
const cachedAction = await readCache(instruction);
125+
if (cachedAction) {
126+
console.log(chalk.blue("Using cached action for:"), instruction);
127+
await stagehand.act(cachedAction);
128+
return;
129+
}
130+
131+
const results = await stagehand.observe(instruction);
132+
console.log(chalk.blue("Got results:"), results);
133+
134+
const actionToCache = results[0];
135+
console.log(chalk.blue("Taking cacheable action:"), actionToCache);
136+
await simpleCache(instruction, actionToCache);
137+
await drawObserveOverlay(page, results);
138+
await page.waitForTimeout(1000);
139+
await clearOverlays(page);
140+
141+
await stagehand.act(actionToCache);
142+
}

0 commit comments

Comments
 (0)