Skip to content

Manager API: Namespace localStorage keys by project ID#34352

Closed
cyphercodes wants to merge 1 commit into
storybookjs:nextfrom
cyphercodes:fix/issue-34194-storage-namespace
Closed

Manager API: Namespace localStorage keys by project ID#34352
cyphercodes wants to merge 1 commit into
storybookjs:nextfrom
cyphercodes:fix/issue-34194-storage-namespace

Conversation

@cyphercodes
Copy link
Copy Markdown
Contributor

@cyphercodes cyphercodes commented Mar 26, 2026

Problem

The Manager API store uses a constant STORAGE_KEY for all Storybook instances, causing localStorage/sessionStorage to be shared across different projects. This means that tag filters and other persisted state from one Storybook instance leak into another when users switch between projects.

Fixes #34194

Solution

Namespace storage keys using the existing anonymous project ID (derived from git remote URL + project path):

  • Added projectId parameter to the Store class
  • Storage keys are now formatted as @storybook/manager/store:${projectId} when a projectId is available
  • Falls back to the original key format for backward compatibility when projectId is undefined

Changes

  • code/core/src/manager-api/store.ts: Updated to accept and use projectId for namespacing storage keys
  • code/core/src/manager-api/root.tsx: Import getAnonymousProjectId and pass it to Store constructor

Testing

This fix ensures that:

  1. Different Storybook projects no longer share localStorage state
  2. Tag filters applied in one project don't affect another
  3. Backward compatibility is maintained for instances without a projectId

Summary by CodeRabbit

Release Notes

  • Chores
    • Implemented project-scoped data storage to enable isolated data management across different projects.

…t cross-instance state sharing

The Manager API store was using a constant STORAGE_KEY for all Storybook instances,
causing localStorage/sessionStorage to be shared across different projects. This meant
that tag filters and other persisted state from one Storybook would leak into another.

Changes:
- Add projectId parameter to Store class (derived from git remote URL + project path)
- Namespace storage keys as `@storybook/manager/store:${projectId}` when projectId is available
- Falls back to original key format for backward compatibility when projectId is undefined

Fixes storybookjs#34194
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

The changes introduce project-scoped storage namespacing to the store system. The ManagerProvider now initializes the store with an anonymous project ID, and the store uses this ID to scope all persistent storage keys with a project-specific namespace.

Changes

Cohort / File(s) Summary
Store initialization with project ID
code/core/src/manager-api/root.tsx
Adds getAnonymousProjectId import and passes projectId: getAnonymousProjectId() to the Store constructor.
Project-scoped storage keying
code/core/src/manager-api/store.ts
Introduces getStorageKey(projectId?) function for namespaced storage keys. Updates Upstream interface to accept optional projectId field, extends Store class with projectId property, and modifies get, set, and update helpers to use project-specific storage namespaces. Migration and state initialization now read/write persisted state using project-scoped keys.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@code/core/src/manager-api/root.tsx`:
- Line 48: The code calls getAnonymousProjectId() (which uses process.cwd() and
executeCommandSync()) in the browser context causing undefined projectId and
un-namespaced Store keys; stop resolving projectId in the client—update
ManagerProvider to accept an optional projectId prop (e.g., add projectId?:
string to ManagerProvider props and use that when constructing the Store) and
remove the direct getAnonymousProjectId() invocation from root.tsx;
alternatively, if SSR-safe resolution is intended, wrap any
getAnonymousProjectId() usage with a runtime guard (typeof process !==
'undefined') or move the call to server/build-time and pass the resulting
projectId into ManagerProvider as a prop.

In `@code/core/src/manager-api/store.ts`:
- Around line 160-162: Tests currently don't cover namespaced storage paths
because they instantiate Store without a projectId; add unit tests that
construct the Store with a non-empty projectId (e.g., using the same constructor
used in production) and assert that update(storage, delta, this.projectId)
writes to the namespaced key. Specifically, create tests that call Store methods
which trigger the update call (referencing the update function and the
Store/instance property this.projectId), verifying both session and local
persistence branches (store.session and store.local) produce keys/values that
include the provided projectId namespace.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 11166d01-8656-486a-a2d9-f450978d1185

📥 Commits

Reviewing files that changed from the base of the PR and between 810093c and 00a87d1.

📒 Files selected for processing (2)
  • code/core/src/manager-api/root.tsx
  • code/core/src/manager-api/store.ts


import { isEqual } from 'es-toolkit/predicate';

import { getAnonymousProjectId } from '../telemetry/anonymous-id';
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if getAnonymousProjectId has browser-compatible implementation or is build-time resolved

echo "=== Checking getAnonymousProjectId implementation ==="
cat code/core/src/telemetry/anonymous-id.ts 2>/dev/null || fd "anonymous-id" --exec cat {}

echo ""
echo "=== Check if value is injected via build/bundler config ==="
rg -n "anonymousProjectId|getAnonymousProjectId" --type ts --type js -g '!*.test.*' code/core/src/manager/ code/builders/ 2>/dev/null | head -30

echo ""
echo "=== Check for any globals or window injection ==="
rg -n "STORYBOOK.*PROJECT|projectId" code/core/src/manager-api/ --type ts -A 2 -B 2 | head -40

Repository: storybookjs/storybook

Length of output: 5171


🏁 Script executed:

# Check the exact usage in root.tsx
echo "=== Usage in root.tsx ===" 
cat -n code/core/src/manager-api/root.tsx | sed -n '40,160p'

echo ""
echo "=== Check if there's any webpack/build config handling this ===" 
fd -e config.js -e webpack -e vite | xargs grep -l "getAnonymousProjectId\|anonymous-id" 2>/dev/null || echo "No matching build config found"

echo ""
echo "=== Check if the import has any side-effects disabled ===" 
rg "getAnonymousProjectId" code/core/src/manager-api/root.tsx -B 5 -A 5

Repository: storybookjs/storybook

Length of output: 5025


🏁 Script executed:

# Check for comments explaining this pattern in telemetry or store files
echo "=== Comments in anonymous-id.ts ===" 
grep -n "// \|/\*" code/core/src/telemetry/anonymous-id.ts

echo ""
echo "=== Check if this is SSR or build-time executed ===" 
rg "import.*anonymous-id\|getAnonymousProjectId" code/core/src/manager-api/ -B 3 -A 3

echo ""
echo "=== Check if there's server-side setup that would inject projectId ===" 
rg "projectId" code/core/src/main/manager/manager-main.ts -A 2 -B 2 2>/dev/null | head -30

Repository: storybookjs/storybook

Length of output: 606


Remove Node.js-dependent code from browser context, or defer projectId resolution.

getAnonymousProjectId() calls process.cwd() and executeCommandSync() which are not available in the browser. The function's try-catch silently fails, returning undefined, causing the Store to use an un-namespaced storage key and defeating the project isolation purpose of this PR.

Solution options:

  1. Move this call to a build-time step or server-side initialization that passes projectId as a prop
  2. Accept projectId as an optional prop to ManagerProvider (from server-side setup)
  3. Wrap the Node.js code with proper typeof process !== 'undefined' checks if SSR-compatible behavior is intended
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/root.tsx` at line 48, The code calls
getAnonymousProjectId() (which uses process.cwd() and executeCommandSync()) in
the browser context causing undefined projectId and un-namespaced Store keys;
stop resolving projectId in the client—update ManagerProvider to accept an
optional projectId prop (e.g., add projectId?: string to ManagerProvider props
and use that when constructing the Store) and remove the direct
getAnonymousProjectId() invocation from root.tsx; alternatively, if SSR-safe
resolution is intended, wrap any getAnonymousProjectId() usage with a runtime
guard (typeof process !== 'undefined') or move the call to server/build-time and
pass the resulting projectId into ManagerProvider as a prop.

Comment on lines 160 to 162
const storage = persistence === 'session' ? store.session : store.local;
await update(storage, delta);
await update(storage, delta, this.projectId);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify test coverage for projectId-namespaced storage paths

echo "=== Checking Store instantiations in tests ==="
rg -n "new Store\(" --type js --type ts -g '*test*' -A 5

echo ""
echo "=== Checking if any tests pass projectId ==="
rg -n "projectId" --type js --type ts -g '*test*' code/core/src/manager-api/

Repository: storybookjs/storybook

Length of output: 5292


🏁 Script executed:

#!/bin/bash
# Check Store class definition and projectId usage
echo "=== Store class constructor signature ==="
head -50 code/core/src/manager-api/store.ts | cat -n

echo ""
echo "=== Lines around 160-162 in store.ts ==="
sed -n '150,170p' code/core/src/manager-api/store.ts | cat -n

echo ""
echo "=== Check if projectId is a constructor parameter ==="
rg -n "constructor\|projectId" code/core/src/manager-api/store.ts -A 2 -B 1

Repository: storybookjs/storybook

Length of output: 2681


🏁 Script executed:

#!/bin/bash
# Find the Store class constructor and projectId assignment
echo "=== Full Store class with constructor ==="
sed -n '50,130p' code/core/src/manager-api/store.ts | cat -n

echo ""
echo "=== Check root.tsx for Store instantiation ==="
rg -n "new Store" code/core/src/root.tsx -B 2 -A 5

Repository: storybookjs/storybook

Length of output: 3769


🏁 Script executed:

#!/bin/bash
# Find root.tsx
echo "=== Searching for root.tsx ==="
fd -name "root.tsx" code/

echo ""
echo "=== Searching for Store instantiation in production ==="
rg -n "new Store" code/core/src/ -t ts -t tsx --max-count 5

Repository: storybookjs/storybook

Length of output: 428


🏁 Script executed:

#!/bin/bash
# Search for root.tsx and Store instantiation with correct syntax
echo "=== Searching for root.tsx in entire repo ==="
fd root.tsx

echo ""
echo "=== Searching for Store instantiation in manager-api ==="
rg "new Store" code/core/src/manager-api/ -A 3

echo ""
echo "=== Check how Store is instantiated in production ==="
rg "new Store" code/ -A 3 --type ts --type js | head -50

Repository: storybookjs/storybook

Length of output: 6829


Add tests with projectId to cover namespaced storage paths.

The update call on line 162 correctly uses this.projectId to namespace the storage key. However, all existing tests instantiate the Store without a projectId, so they only test the fallback (non-namespaced) path. The production code in root.tsx always passes projectId: getAnonymousProjectId(), creating a coverage gap.

Consider adding test cases that pass a projectId to verify the namespaced storage behavior.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@code/core/src/manager-api/store.ts` around lines 160 - 162, Tests currently
don't cover namespaced storage paths because they instantiate Store without a
projectId; add unit tests that construct the Store with a non-empty projectId
(e.g., using the same constructor used in production) and assert that
update(storage, delta, this.projectId) writes to the namespaced key.
Specifically, create tests that call Store methods which trigger the update call
(referencing the update function and the Store/instance property
this.projectId), verifying both session and local persistence branches
(store.session and store.local) produce keys/values that include the provided
projectId namespace.

@valentinpalkovic
Copy link
Copy Markdown
Contributor

Superseded by #34195

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Manager API store localStorage persistence is shared across all SB instances

2 participants