Skip to content

Puppeteer screenshot feature#4634

Open
tintinthong wants to merge 7 commits intomainfrom
screenshot-feature
Open

Puppeteer screenshot feature#4634
tintinthong wants to merge 7 commits intomainfrom
screenshot-feature

Conversation

@tintinthong
Copy link
Copy Markdown
Contributor

@tintinthong tintinthong commented May 4, 2026

Card Screenshot Infrastructure

End-to-end screenshot capture for any Boxel card. Open the new
Screenshot Card Demo in the experiments realm, link a card, pick
isolated or embedded, click Take Screenshot — a fully-painted
PNG is rendered through the existing Puppeteer prerender pool, written
into the card's own realm under Screenshots/, and shown inline.

What's in the PR

  • Phase 1 — Prerender API. POST /_screenshot-card on the
    realm-server. Queue-backed, mirroring /_run-command. Returns base64
    PNG + dimensions.
  • Phase 2 — Host command. ScreenshotCardCommand callable
    in-process or via /_run-command. Writes the PNG to the card's realm
    via WriteBinaryFileCommand. Hard-fails if the caller can't write
    there.
  • Phase 3 — Demo card. ScreenshotCardDemo in
    packages/experiments-realm — the harness that drives the full flow
    with one button.

Phase 4 (wiring into listing-create) lives in a separate repo / PR.

How to test

  1. Bring up the standard local-dev matrix (realm-server,
    prerender-server, prerender-manager, worker, host).
  2. In the experiments realm, create a Screenshot Card Demo
    instance
    .
  3. Link a card (e.g. Author/alice-enwunder or
    ImageDefPlayground/mango-demo).
  4. Edit the Format field — pick isolated or embedded from the
    dropdown.
  5. Click Take Screenshot.

Expected: the result section appears with a realm URL and an inline
PNG preview that fully shows the card content — no spinners, no empty
image placeholders. The file lands at
packages/experiments-realm/Screenshots/<slug>-<uuid8>.png.

If you want to hit the API directly:

curl -X POST http://localhost:4201/_screenshot-card \
  -H "Authorization: Bearer $REALM_SERVER_JWT" \
  -H 'Content-Type: application/vnd.api+json' \
  -d '{"data":{"type":"screenshot-card","attributes":{
        "realmURL":"http://localhost:4201/experiments/",
        "cardId":"http://localhost:4201/experiments/Author/alice-enwunder",
        "format":"isolated"}}}' \
  | jq -r '.data.attributes.base64' | base64 -d > /tmp/card.png && open /tmp/card.png

format must be "isolated" or "embedded". runAs is derived from
the JWT.

Automated tests

cd packages/realm-server
TEST_MODULES="screenshot-card-test.ts,prerender-proxy-test.ts" \
  npx qunit --require ts-node/register/transpile-only tests/index.ts

TEST_MODULES="server-endpoints/run-command-endpoint-test.ts,server-endpoints/screenshot-card-endpoint-test.ts" \
  pnpm run test

10/10 each. The server-endpoints test must run alongside another
server-endpoints test (shares cached realm setup).

Notes for reviewers

  • Settle + image-paint waitscaptureScreenshot in
    packages/realm-server/prerender/utils.ts calls
    __waitForRenderLoadStability() (existing) and a new
    waitForImagePaint() that waits for <img> elements, CSS
    background-image URLs, and document.fonts.ready before capture.
    Without the second wait, avatars and thumbnails would be missing in
    the PNG.
  • Three forwarding layers — realm-server (/_screenshot-card),
    prerender-manager (/prerender-screenshot proxy), prerender-server
    (/prerender-screenshot route). Easy to forget the manager.
  • Output is imageDefUrl: string, not linksTo(ImageDef) — we
    tried the cleaner relationship form, but the framework's
    link-resolution lifecycle on a freshly-written PNG took a path the
    auth service worker didn't cover (manifested as "Missing
    Authorization header"). Plain <img src=URL> goes through the SW
    reliably (same path that makes ImageDefPlayground/mango.png
    render). Cleaner relationship is a follow-up.
  • Permission policyScreenshotCardCommand writes the PNG into
    the card's own realm. If the caller lacks write access there it
    throws; no silent fallback.
  • Concurrency group on the queue is screenshot:<realmURL>,
    mirroring command:<realmURL> for run-command.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9812a8af5

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +454 to +459
export type ScreenshotPrerenderArgs = {
realm: string;
url: string;
auth: string;
format: 'isolated' | 'embedded';
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Thread screenshot priority through prerender call chain

/_screenshot-card enqueues with userInitiatedPriority, but the screenshot prerender API drops that signal because ScreenshotPrerenderArgs has no priority field (and the task call therefore cannot forward jobInfo.priority). In busy environments (for example while indexer traffic is active), screenshot renders will compete at default priority and can be delayed behind background work, unlike runCommand which explicitly forwards priority to prerender queues.

Useful? React with 👍 / 👎.

@tintinthong tintinthong requested a review from Copilot May 4, 2026 12:19
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds the Phase 1 backend plumbing for a queue-backed “screenshot a card render” flow, wiring realm-server → queue/worker → prerender-server → Puppeteer page.screenshot() and returning a JSON:API envelope containing base64 PNG output.

Changes:

  • Introduces a new screenshot-card queue job + worker task that checks permissions and calls prerenderer.prerenderScreenshot(...).
  • Adds a new realm-server endpoint POST /_screenshot-card plus tests for request validation and handler/job wiring.
  • Extends the prerender-server with a /prerender-screenshot route and Puppeteer capture implementation.

Reviewed changes

Copilot reviewed 15 out of 15 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
packages/runtime-common/worker.ts Registers the new screenshot-card job type with the worker queue.
packages/runtime-common/tasks/screenshot-card.ts Implements the worker task: permission checks + calls prerenderer.prerenderScreenshot.
packages/runtime-common/tasks/index.ts Exports the new screenshot task.
packages/runtime-common/jobs/screenshot-card.ts Adds the enqueue helper + timeout/concurrency group for screenshot jobs.
packages/runtime-common/index.ts Introduces screenshot prerender request/response types and extends the Prerenderer interface.
packages/realm-server/routes.ts Wires POST /_screenshot-card into the realm-server router behind JWT middleware.
packages/realm-server/handlers/handle-screenshot-card.ts Implements the endpoint handler: validates JSON:API body, enqueues job, returns result.
packages/realm-server/prerender/utils.ts Adds Puppeteer capture helper captureScreenshot() (settle + screenshot + error detection).
packages/realm-server/prerender/render-runner.ts Adds captureScreenshotAttempt to drive the screenshot render flow on a pooled page.
packages/realm-server/prerender/prerenderer.ts Exposes prerenderScreenshot() on the prerender-server’s local Prerenderer implementation.
packages/realm-server/prerender/prerender-app.ts Adds /prerender-screenshot route parsing + dispatch to prerenderer.prerenderScreenshot.
packages/realm-server/prerender/remote-prerenderer.ts Adds remote client support for calling /prerender-screenshot.
packages/realm-server/tests/screenshot-card-test.ts Adds handler-level tests that assert the job enqueue shape and response forwarding.
packages/realm-server/tests/server-endpoints/screenshot-card-endpoint-test.ts Adds endpoint validation tests (auth required, required attrs, invalid JSON/format).
packages/realm-server/tests/index.ts Registers the new tests in the realm-server test suite.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +79 to +84
let result = await prerenderer.prerenderScreenshot({
realm: normalizedRealmURL,
url: cardId,
auth,
format,
});
Comment thread packages/realm-server/prerender/remote-prerenderer.ts Outdated
Comment on lines +286 to +316
let rawAffinityValue = attrs.affinityValue;
let rawFormat = attrs.format;
let renderOptions = parseRenderOptions(attrs);
let formatIsValid = rawFormat === 'isolated' || rawFormat === 'embedded';
let missing = missingAttrs([
{ value: rawUrl, name: 'url' },
{ value: rawRealm, name: 'realm' },
{ value: rawAuth, name: 'auth' },
{
value: rawAffinityType === 'realm' ? rawAffinityType : undefined,
name: 'affinityType',
},
{ value: rawAffinityValue, name: 'affinityValue' },
{
value: formatIsValid ? rawFormat : undefined,
name: 'format',
},
]);
return {
args:
missing.length > 0
? undefined
: {
affinityType: 'realm',
affinityValue: rawAffinityValue as string,
realm: rawRealm as string,
url: rawUrl as string,
auth: rawAuth as string,
format: rawFormat as 'isolated' | 'embedded',
renderOptions,
},
Comment on lines +418 to +420
const { page, reused, launchMs, waits, pageId, release } =
await this.#getPageForAffinity(affinityKey, auth, 'file', signal);
const poolInfo: PoolInfo = {
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.

this is a new capability. in order to make sure that your prerender requests are prioritized above background reindexing, you should thread thru the job priority. this should already be happening for commands, modules, and indexing. so use those examples.

Comment thread packages/runtime-common/index.ts Outdated
Comment thread packages/realm-server/prerender/utils.ts Outdated
@tintinthong tintinthong changed the title infrastructure for screenshot feature Puppeteer screenshot feature May 4, 2026
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

Preview deployments

Host Test Results

    1 files  ±0      1 suites  ±0   1h 59m 15s ⏱️ - 1m 49s
2 564 tests +1  2 538 ✅  - 10  15 💤 ±0   0 ❌ ± 0  11 🔥 +11 
2 583 runs  +1  2 546 ✅  - 21  15 💤 ±0  11 ❌ +11  11 🔥 +11 

Results for commit 42f73fc. ± Comparison against earlier commit f11f64b.

For more details on these errors, see this check.

Realm Server Test Results

    1 files  ± 0      1 suites  ±0   18m 3s ⏱️ -9s
1 238 tests +22  1 238 ✅ +22  0 💤 ±0  0 ❌ ±0 
1 316 runs  +22  1 316 ✅ +22  0 💤 ±0  0 ❌ ±0 

Results for commit 42f73fc. ± Comparison against earlier commit f11f64b.

@habdelra
Copy link
Copy Markdown
Contributor

habdelra commented May 4, 2026

so one thing that concerns me is that lives live totally outside of our prerendering load handling mechanism. which means that this has the ability to starve chrome tabs from the rest of the system and unwind all the perf work that I'm doing right now. We need to incorporate this into our our prerender queuing and admission system. you can use this ticket for reference https://linear.app/cardstack/issue/CS-10976/create-prerendering-prioritization

UPDATE: you are actually getting the page correctly that respects the queuing and perf considerations

@habdelra
Copy link
Copy Markdown
Contributor

habdelra commented May 4, 2026

Also, I know that this is triggered from a command, but it seems like this could really benefit from realm affinity such that you don't land on a cold loader or a cold store when you are trying to take a snapshot. i don't think (at least today) that the user auth for the realm affinity would be different than the user auth for the user affinity when you are doing a screenshot, since for private realms the only user that can access the realm is the user that created it. so this is probably ok. for in the future when we have shared realms this is probably not quite right...

);

const { page, reused, launchMs, waits, pageId, release } =
await this.#getPageForAffinity(
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.

ah, nvm about my comment about not using the queue. this is the correct way to get a page that is mindful of the prerender queue 👍

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.

3 participants