Skip to content

Harden screenshot capturing#2945

Open
fredrikekelund wants to merge 4 commits intotrunkfrom
f26d/improve-screenshot-generation
Open

Harden screenshot capturing#2945
fredrikekelund wants to merge 4 commits intotrunkfrom
f26d/improve-screenshot-generation

Conversation

@fredrikekelund
Copy link
Copy Markdown
Contributor

Related issues

How AI was used in this PR

For small and directed tasks.

Proposed Changes

  1. Add an emitLoadingEvent argument to captureSiteThumbnail that suppresses thumbnail-loading events. This argument is true when the screenshot is updated in response to a SITE_EVENTS.UPDATED event. It's also inherited from the corresponding argument in loadThemeDetails.
  2. Make the waitForCapture callback from createScreenshotWindow throw if the page returns an HTTP status code >= 500. For sites without custom domains, we get similar behavior for free, since the most common error is that window.loadURL fails because no server is listening on the port. For custom domains, that never happens because the custom domain proxy always listens on ports 80/443.

Testing Instructions

  1. Have a site with a custom domain.
  2. Start the site.
  3. Cmd+Tab to quickly shift focus back and forth to Studio. Ensure that the Overview tab does not display a loading state when the window focus shifts, but only when the site starts.
  4. Stop the site.
  5. Repeat the Cmd+Tab behavior. Ensure that no Proxy error screenshot is captured.

Pre-merge Checklist

  • Have you checked for TypeScript, React or other console errors?

@fredrikekelund fredrikekelund requested review from a team and wojtekn March 30, 2026 13:58
@fredrikekelund fredrikekelund self-assigned this Mar 30, 2026
@wpmobilebot
Copy link
Copy Markdown
Collaborator

wpmobilebot commented Mar 30, 2026

📊 Performance Test Results

Comparing 4d4054e vs trunk

app-size

Metric trunk 4d4054e Diff Change
App Size (Mac) 1327.45 MB 1327.45 MB +0.00 MB ⚪ 0.0%

site-editor

Metric trunk 4d4054e Diff Change
load 1908 ms 1862 ms 46 ms ⚪ 0.0%

site-startup

Metric trunk 4d4054e Diff Change
siteCreation 8160 ms 8173 ms +13 ms ⚪ 0.0%
siteStartup 4859 ms 4849 ms 10 ms ⚪ 0.0%

Results are median values from multiple test runs.

Legend: 🟢 Improvement (faster) | 🔴 Regression (slower) | ⚪ No change (<50ms diff)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The changes in this file aren't related to the core purpose of this PR. I've just found the subscribeSiteEvents structure to be unnecessarily difficult to reason about (and I stumbled on it again while tracing the screenshot generation logic in this PR). My change simply moves the daemon bus event listeners to this file.

console.error( '[Proxy Error]', err.message );
if ( res && res instanceof http.ServerResponse ) {
res.writeHead( 500, { 'Content-Type': 'text/plain' } );
res.writeHead( 502, { 'Content-Type': 'text/plain' } );
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Not important to the core purpose of this PR, but 502 Bad Gateway better conveys the specific type of error.

};
},
{ concurrent: 3, max: 100, deduplicateKey: ( args: string[] ) => args.join( ' ' ) }
{ concurrent: 3, max: 100, deduplicateKey: ( args ) => args.join( ' ' ) }
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The sequential type definition was giving me grief. This is a leftover from when I was battling that for a while. It makes sense to remove the type since TS infers it from the type definitions of the arguments to the sequential callback.

throw new Error( 'Site not found.' );
}

void captureSiteThumbnail( id, emitLoadingEvent );
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I made it so captureSiteThumbnail is triggered earlier because server.getThemeDetails() calls WP-CLI. When WP-CLI returns a response, the CLI generates a SITE_EVENT.UPDATED event to Studio, which triggers another captureSiteThumbnail call.

In the end, I made it so neither of the captureSiteThumbnail calls triggers a thumbnail-loading event, but previously, the order in which things were called triggered two such events.

// captures across all sites (one at a time).
export const captureSiteThumbnail = sequential(
async ( id: string ): Promise< void > => {
export const captureSiteThumbnail = sequential< [ string, boolean? ], void >(
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This type definition is important because the type inference in sequential.ts cannot handle a callback with two arguments (where the second one is optional) and a deduplicateKey function that takes just a single argument.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Again, sites without custom URLs will encounter network errors here because no server is listening on the target port. For sites with custom URLs, there's always the custom domain proxy listening on port 80/443, which is why we need the HTTP status code check as an additional safety measure.

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.

2 participants