Skip to content

Storage state persistence broken in Stagehand v3 - userDataDir doesn't work, storageState() method missing #1250

@illia-fliplet

Description

@illia-fliplet

Storage state persistence broken in Stagehand v3 - userDataDir doesn't work, storageState() method missing

Before submitting an issue, please:

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:

  1. When userDataDir is specified, Chrome should create and use that directory to store user data
  2. When preserveUserDataDir: true is set, the directory should persist after stagehand.close()
  3. The directory should contain Chrome's standard profile structure (e.g., Default subdirectory)
  4. When a new Stagehand instance is created with the same userDataDir, it should automatically load the persisted authentication state

Actual Behavior:

  1. The userDataDir directory is never created - it doesn't exist after init(), during the session, or after close()
  2. Even when manually creating the directory before init(), Chrome does not write any data to it
  3. The storageState() method is not available on stagehand.context or page.context(), preventing manual extraction
  4. Authentication state cannot be persisted between sessions

Steps to Reproduce

  1. Create a Stagehand instance with userDataDir and preserveUserDataDir: true
  2. Perform login to establish authentication (cookies are set successfully)
  3. Close the Stagehand instance
  4. Check if the userDataDir directory 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 function

Screenshots / 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 context object is no longer a Playwright BrowserContext
  • 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
  • userDataDir actually 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 instances

Limitations 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.context is 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions