-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Description
Storage state persistence broken in Stagehand v3 - userDataDir doesn't work, storageState() method missing
Before submitting an issue, please:
- Check the documentation for relevant information
- Search existing issues to avoid duplicates
Environment Information
Please provide the following information to help us reproduce and resolve your issue:
Stagehand:
- Language/SDK: TypeScript
- Stagehand version: 3.0.1
AI Provider:
- Provider: Google
- Model: gemini-2.5-flash-preview-04-17
Issue Description
Storage state persistence is broken in Stagehand v3. The userDataDir and preserveUserDataDir options in localBrowserLaunchOptions do not persist browser data (cookies, localStorage) as expected. Additionally, the storageState() method is no longer available on the context/page objects, preventing manual extraction of authentication state.
Expected Behavior:
- When
userDataDiris specified, Chrome should create and use that directory to store user data - When
preserveUserDataDir: trueis set, the directory should persist afterstagehand.close() - The directory should contain Chrome's standard profile structure (e.g.,
Defaultsubdirectory) - When a new Stagehand instance is created with the same
userDataDir, it should automatically load the persisted authentication state
Actual Behavior:
- The
userDataDirdirectory is never created - it doesn't exist afterinit(), during the session, or afterclose() - Even when manually creating the directory before
init(), Chrome does not write any data to it - The
storageState()method is not available onstagehand.contextorpage.context(), preventing manual extraction - Authentication state cannot be persisted between sessions
Steps to Reproduce
- Create a Stagehand instance with
userDataDirandpreserveUserDataDir: true - Perform login to establish authentication (cookies are set successfully)
- Close the Stagehand instance
- Check if the
userDataDirdirectory exists and contains data
Minimal Reproduction Code
import { Stagehand } from '@browserbasehq/stagehand';
import { join } from 'path';
import { existsSync, readdirSync } from 'fs';
const adminUserDataDir = join(process.cwd(), 'chrome-user-data', 'admin');
const stagehand = new Stagehand({
env: 'LOCAL',
verbose: 1,
localBrowserLaunchOptions: {
headless: false,
userDataDir: adminUserDataDir,
preserveUserDataDir: true
}
});
await stagehand.init();
const page = stagehand.context.pages()[0];
// Perform login (cookies are set successfully)
await page.goto('https://example.com/login');
// ... login steps ...
// Login succeeds, cookies exist (verified via document.cookie)
console.log(`Directory exists after init: ${existsSync(adminUserDataDir)}`); // false
await stagehand.close();
console.log(`Directory exists after close: ${existsSync(adminUserDataDir)}`); // false
// Even if directory is manually created before init(), it remains empty
if (existsSync(adminUserDataDir)) {
const files = readdirSync(adminUserDataDir);
console.log(`Directory contents: ${files.length} items`); // 0 - empty
}Error Messages / Log trace
No errors are thrown, but the directory is never created:
Directory exists after init: false
Directory exists after close: false
When attempting to use storageState() method:
// Attempt 1: stagehand.context.storageState()
const storageState = await stagehand.context.storageState();
// Error: stagehand.context.storageState is not a function
// Attempt 2: page.context().storageState()
const page = stagehand.context.pages()[0];
const storageState = await page.context().storageState();
// Error: page.context is not a functionScreenshots / Videos
N/A - Issue is about missing functionality rather than visual bugs.
Root Cause Analysis
Issue 1: userDataDir Not Passed to Chrome
The userDataDir option in localBrowserLaunchOptions appears to not be passed correctly to the underlying Chrome browser instance. This could be due to:
- Stagehand v3's new architecture using Chrome DevTools Protocol instead of Playwright
- The option being filtered out or ignored during browser launch
- A bug in how Stagehand v3 handles
localBrowserLaunchOptions
Issue 2: storageState() Method Removed
In Stagehand v2, storageState() was available via Playwright's BrowserContext. In Stagehand v3:
- Stagehand removed its internal Playwright dependency
- The
contextobject is no longer a PlaywrightBrowserContext - The
storageState()method was not re-implemented in Stagehand v3's new architecture
Issue 3: API Changes Not Documented
The migration from v2 to v3 removed critical functionality without:
- Clear documentation of breaking changes
- Migration guide for storage state persistence
- Alternative methods for persisting authentication
Previous Approach (Playwright Browser Context)
Before Stagehand v3, we could use Playwright's native browser context methods:
Method 1: Using storageState() Method
import { chromium } from '@playwright/test';
const browser = await chromium.launch();
const context = await browser.newContext();
// Perform login
const page = await context.newPage();
await page.goto('https://example.com/login');
// ... login steps ...
// Save storage state (cookies + localStorage)
await context.storageState({ path: 'storage-state/admin.json' });
// Later, load storage state
const context2 = await browser.newContext({
storageState: 'storage-state/admin.json'
});Method 2: Using launchPersistentContext with userDataDir
import { chromium } from '@playwright/test';
// Launch browser with persistent user data directory
const context = await chromium.launchPersistentContext('./chrome-user-data/admin', {
headless: false
});
// Login and close - data automatically persisted
await context.close();
// Later, launch with same userDataDir - authentication persists
const context2 = await chromium.launchPersistentContext('./chrome-user-data/admin', {
headless: false
});Method 3: Using Stagehand v2 (with Playwright under the hood)
import { Stagehand } from '@browserbasehq/stagehand';
const stagehand = new Stagehand({ env: 'LOCAL' });
await stagehand.init();
await loginAsAdmin(stagehand);
// Access Playwright context and save storage state
const playwrightContext = stagehand.page.context();
await playwrightContext.storageState({ path: 'storage-state/admin.json' });Key Differences from Stagehand v3:
- ✅
context.storageState()method was available - ✅
page.context()returned Playwright BrowserContext - ✅
userDataDiractually worked and persisted data - ✅ No manual cookie/localStorage extraction needed
Current Workaround
We've implemented a manual workaround that extracts cookies and localStorage manually:
// Extract cookies and localStorage
const cookies = await page.evaluate(() => {
return document.cookie.split(';').map(cookie => {
const [name, ...valueParts] = cookie.trim().split('=');
return { name, value: valueParts.join('=') };
});
});
const localStorage = await page.evaluate(() => {
const items = [];
for (let i = 0; i < window.localStorage.length; i++) {
const key = window.localStorage.key(i);
if (key) {
items.push({ name: key, value: window.localStorage.getItem(key) || '' });
}
}
return items;
});
// Save to JSON file in Playwright storage state format
// Load and apply when creating new browser instancesLimitations of workaround:
- Requires manual extraction code
- May miss HttpOnly cookies (not accessible via
document.cookie) - localStorage must be applied after navigating to the correct origin
- More error-prone than native browser persistence
Impact
- High: Authentication state cannot be persisted between test runs
- High: Tests must re-authenticate on every run, increasing execution time
- Medium: Workarounds require manual cookie/localStorage extraction and application
- Medium: No official way to share authentication state across multiple test files
Proposed Solutions
Option 1: Fix userDataDir Support
Ensure userDataDir and preserveUserDataDir are correctly passed to Chrome and that Chrome actually uses the specified directory.
Option 2: Re-implement storageState() Method
Add a storageState() method to Stagehand v3's context object that extracts and returns cookies/localStorage in Playwright format:
const storageState = await stagehand.context.storageState();
await stagehand.context.storageState({ path: 'storage-state/admin.json' });Option 3: Provide Official Storage State API
Create a new Stagehand v3 API for persisting and loading authentication state:
// Save storage state
await stagehand.saveStorageState('path/to/storage-state.json');
// Load storage state
const stagehand = new Stagehand({
env: 'LOCAL',
storageState: 'path/to/storage-state.json'
});Related Issues
Are there any related issues or PRs?
- Related to: Stagehand v3 migration (removed Playwright dependency)
- Related to:
page.context()is no longer available - Related to:
stagehand.contextis not a Playwright BrowserContext
Additional Context
- This issue affects all users migrating from Stagehand v2 to v3
- The workaround is functional but not ideal for production use
- We recommend prioritizing a fix as this is a critical feature for test automation
- Documentation on storage state persistence in v3 is missing