Skip to content

Affected capture

View as Markdown

Affected capture is StoryShelf’s answer to Chromatic’s TurboSnap: instead of rendering every story on every build, the server renders only the stories downstream of your change and inherits the rest unchanged from their baselines. It is on by default and fails open — anything it cannot prove unchanged gets rendered.

flowchart LR
    A[Create build] --> B[Server returns baselineSha]
    B --> C[CLI: git diff baseline..HEAD]
    C --> D[Trace via preview-stats.json]
    D --> E[POST affected set]
    E --> F[Render affected only]
    F --> G[Inherit the rest unchanged]
  1. Ancestor. POST /api/v1/projects/:slug/builds returns baselineSha: the latest prior revision on the same branch with a build (null for first builds).
  2. Changed files. The CLI diffs baselineSha..HEAD (plus staged and untracked files) to get the repo-relative change list.
  3. Trace. The change list is traced through the bundler dependency graph (preview-stats.json in the built Storybook) down to story files. New stories — files with no baseline — always render regardless of the trace.
  4. Record. The CLI posts { baselineSha, changedFiles, affectedImportPaths } to POST …/builds/:id/affected before uploading the bundle, so the computation is auditable on the build row.
  5. Partition. The orchestrator splits discovered stories into render vs inherit sets, renders only the affected ones, and creates unchanged snapshots for the rest that reuse the baseline screenshot (inherited: true). Counts include inherited snapshots; changedCount excludes them. The build stores how many it inherited as inheritedCount.
MethodEffect
storyshelf upload --fullRender every story for this upload
STORYSHELF_FULL=1Same, via env (useful for release pipelines)
"affectedOnly": false in .storybook/storyshelf.jsonDisable for the project (see Configuration)

Build identity: sha and branch without git

Section titled “Build identity: sha and branch without git”

upload resolves each field independently — flags > env > local git > synthesized (local-<random> sha on the local branch). A project with no git at all uploads with zero identity flags; those builds always render everything (not-a-git-repo below). The full story — synthetic identity, environments as branches, checkout-less pipelines — lives in Uploading without git.

Affected capture never fails an upload. When it cannot prove a story unchanged, it renders everything and prints the reason:

ReasonMeaning
shallow-cloneNo git history (actions/checkout needs fetch-depth: 0)
not-a-git-repoNo git repository — upload used a synthetic local identity
dependency-graph-unavailableNo usable preview-stats.json in the built Storybook
story-index-unreadableNo index.json/stories.json to map stories to files
global-file-changedA change touches preview.*, manager.*, .storybook/main.*, or a lockfile — these can affect every story
too-many-changed-filesChange list exceeds 10,000 files (large merges, vendored drops)
First buildNo baseline revision exists yet (baselineSha: null)

Typical CLI output:

Terminal window
Affected capture: rendering 3 stories (baseline abc1234)
# or
Affected capture: no stories affected (baseline abc1234)
# or
Full capture (shallow-clone): rendering all stories

Untraced files. Files that appear in the graph but cannot meaningfully change rendering (generated clients, mock data, docs) can be excluded so touching them doesn’t re-render their dependents:

Terminal window
storyshelf upload --untraced "**/*.generated.ts" --untraced "**/mocks/**"

For project-wide exclusions that apply to every upload, commit them once in .storybook/storyshelf.json instead of repeating flags (see Configuration):

{ "slug": "my-design-system", "untraced": ["**/*.generated.ts"] } }

Config and flag globs are combined (union, deduplicated). Untraced files are dropped before tracing — stories reachable only through them are inherited. Stories reachable through another (traced) changed file still render.

Custom stats file. The graph is read from <buildDir>/preview-stats.json by default (emitted by the Vite builder). Override per run:

Terminal window
storyshelf upload --stats-file ./stats/preview-stats.json

Diagnose. storyshelf doctor reports affected-capture readiness: shallow clones and missing stats produce warnings (not failures).

  • The build page shows an affected capture badge (or full capture when opted out) plus N rendered, M inherited counts, and an Inherited stat next to the other counts. The numbers are the ones stored on the build (snapshotCount, inheritedCount), also served by the API.
  • The project’s Builds page shows Inherited from baseline: the average, over the project’s last 20 builds that hold snapshots, of each build’s inheritedCount / snapshotCount. It is computed from the stored counts only, so it reads no snapshots. A high number means affected capture is skipping most of your Storybook; a number near zero means most changes reach most stories (a shared import, for example), and the trace is worth a look.
  • Inherited snapshots carry an inherited badge and unchanged status — they need no review.
  • Capture attempt logs include an affected capture partitioned line with { rendered, inherited, total }.

Same shape (ancestor → diff → graph trace → selective render), different economics and scope:

  • No billed snapshots — skipping saves your server’s CPU, not money. There is no 0.2× copy charge because there is no billing at all.
  • Vite stats only — the trace reads preview-stats.json (plus tolerant Webpack-reasons shapes). Custom builders without stats fall back to full renders.
  • Server verifies — the orchestrator re-partitions from the stored payload and only inherits stories with a resolvable baseline, so a stale or forged list can only over-render, never under-test.