Skip to content

Wpcomsh recovery-mode sync: include per-extension error info#48440

Merged
arthur791004 merged 6 commits into
trunkfrom
add/recovery-mode-sync-error-info
May 4, 2026
Merged

Wpcomsh recovery-mode sync: include per-extension error info#48440
arthur791004 merged 6 commits into
trunkfrom
add/recovery-mode-sync-error-info

Conversation

@arthur791004
Copy link
Copy Markdown
Contributor

@arthur791004 arthur791004 commented May 1, 2026

Proposed changes

Follow-up to #48213. The recovery-mode state snapshot now also carries a normalized list of per-extension errors so wpcom-side consumers (Calypso) can surface what fataled instead of just that something fataled.

Each record carries:

  • kindplugin or theme
  • slug — extension slug as recorded by WP recovery mode
  • version — resolved via get_plugins() / wp_get_theme()
  • errno, message, line — straight from WP's captured error array
  • file — reduced to the basename so server paths don't leak

Where the errors come from

Two sources, picked in order:

  1. *_paused_extensions option — once the admin clicks the recovery email link, WP creates a session and writes paused extensions there. We read it live on every snapshot via wp_recovery_mode()->get_session_id().
  2. error_get_last() fallback — on the fatal request itself, before the admin has clicked the link, no paused-extensions option exists yet. We're called inside WP's fatal-handler shutdown stack (via update_option_recovery_mode_email_last_sent), so error_get_last() still holds the original fatal. We resolve the file path back to a plugin/theme slug and emit one record from that, so the very first POST already carries the error info.

The fallback only fires when WP actually sends a recovery email (rate-limited to ~once/day, skipped if a session is already active). Subsequent fatals are picked up via the paused-extensions path once the admin enters recovery.

Design choice

Errors live in the snapshot rather than a wpcomsh-side option — symmetric with how entered_at / exited_at are read. No new option, no thread-through arg.

Consequences:

  • Every POST (email / session-start / session-end) emits a complete state.
  • A persistent recovery session (~daily email re-send) keeps reporting current errors, not just the first day's snapshot.
  • On session_end, the paused-extensions option has already been deleted by WP, so the snapshot returns [] — wpcom naturally sees the state cleared.

Privacy

message and line are admin-visible on WP's recovery email and recovery screen, so the new exposure is the same content already shown to the site admin. file is reduced to its basename to avoid leaking server-internal paths. No user identifiers, request URIs, stack traces, or PII.

Does this pull request change what data or activity we track or use?

Yes. This PR extends the existing recovery-mode-status POST (added in #48213) with a new recovery_session_errors array containing per-extension fatal info: kind (plugin/theme), slug, version, errno, message, file (basename only), and line. The data is sourced from WP's own recovery-mode state (*_paused_extensions option, or error_get_last() during the fatal-handler shutdown) and mirrors what's already shown to the site admin in WP's recovery email and recovery screen. No user identifiers, request URIs, stack traces, or PII are added.

Testing instructions

  1. Deploy this branch to an Atomic test site (jetpack rsync plugins/wpcomsh <site>).
  2. Install + activate a plugin that fatals on load (e.g. one that calls trigger_error( 'boom', E_USER_ERROR );).
  3. Visit the front-end to trigger WP's fatal-error handler.
  4. Confirm the outbound POST to /sites/<blog_id>/recovery-mode-status already includes recovery_session_errors with one entry — {kind: \"plugin\", slug: <slug>, version: <ver>, errno: <int>, message: \"boom\", file: \"<basename>.php\", line: <int>}. (This is the error_get_last() fallback path.)
  5. Click the recovery link in the email. Confirm the session-start POST also carries the same recovery_session_errors array, now sourced from *_paused_extensions.
  6. Exit recovery mode via the admin bar. Confirm the session-end POST carries recovery_session_errors: [].
  7. Add a second fatal-on-load plugin while in recovery; confirm the next POST that fires picks up both entries.

Related

  • wpcomsh: Report recovery-mode state to wpcom #48213 — the original recovery-mode-sync PR.
  • A wpcom-side change is needed to allowlist + persist the new recovery_session_errors field on jetpack_recovery_mode_status; this PR alone won't make it visible in the GET sites response.

🤖 Generated with Claude Code

Follow-up to #48213. The state snapshot now also carries an extracted
view of the live *_paused_extensions option, so wpcom-side consumers
(Calypso) can surface what fataled instead of just that something
fataled.

Each record carries kind/slug/version + errno/message/file/line plus
the transportable signature token from #48369, so a fatal seen via
the recovery email and via the wpcomsh fatal-error screen can be
joined on the same opaque token. file is reduced to its basename so
server paths don't leak.

Reading from the live option on every snapshot (instead of stashing
errors in our own option, or threading them through one capture path)
means every POST — email / session-start / session-end — emits a
complete state, and session-end naturally shows errors=[] without any
explicit clear step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (WordPress.com Site Helper), and enable the add/recovery-mode-sync-error-info branch.

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 1, 2026

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!


Wpcomsh plugin:

  • Next scheduled release: Atomic deploys happen twice daily on weekdays (p9o2xV-2EN-p2)

If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack.

@github-actions github-actions Bot added the [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. label May 1, 2026
@arthur791004 arthur791004 marked this pull request as ready for review May 1, 2026 06:22
@arthur791004 arthur791004 self-assigned this May 1, 2026
@jp-launch-control
Copy link
Copy Markdown

jp-launch-control Bot commented May 1, 2026

Code Coverage Summary

Coverage changed in 1 file.

File Coverage Δ% Δ Uncovered
projects/plugins/wpcomsh/feature-plugins/class-wpcomsh-recovery-mode-sync.php 5/196 (2.55%) -2.45% 96 💔

Full summary · PHP report

Coverage check overridden by Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR .

@arthur791004 arthur791004 added the Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR label May 1, 2026
arthur791004 and others added 2 commits May 1, 2026 14:32
The new recovery_session_errors field is an array of records, so the
existing array<string,int> phpdoc no longer fits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The flat fields (kind/slug/version/errno/message/file/line) cover the
Calypso display use case. The signature was for cross-surface
analytics joining (recovery email vs. fatal-error screen logstash),
which has no consumer yet. We can re-add when one materializes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@arthur791004 arthur791004 added the [Status] Needs Review This PR is ready for review. label May 1, 2026
@github-actions github-actions Bot removed the [Status] Needs Review This PR is ready for review. label May 1, 2026
arthur791004 and others added 2 commits May 1, 2026 16:19
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
So the fatal-request POST already carries the error info, instead of
waiting for the admin to click the recovery email link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
}
$normalized = wp_normalize_path( $file );

if ( defined( 'WP_PLUGIN_DIR' ) ) {
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.

It would be better to follow the implementation from core's get_extension_for_error to capture exactly what is stored in *_paused_extensions

if ( defined( 'WP_PLUGIN_DIR' ) ) {
    $plugin_dir = wp_normalize_path( WP_PLUGIN_DIR ) . '/';
    if ( str_starts_with( $normalized, $plugin_dir ) ) {
        $rel   = substr( $normalized, strlen( $plugin_dir ) );
        $parts = explode( '/', $rel );
        return array( 'plugin', $parts[0] );
    }
}

https://github.com/WordPress/wordpress-develop/blob/b03a05e74ee1d740015f0ee85723b080aef0ad04/src/wp-includes/class-wp-recovery-mode.php#L365-L374

Copy link
Copy Markdown
Contributor Author

@arthur791004 arthur791004 May 4, 2026

Choose a reason for hiding this comment

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

Good catch — switched to core's pattern ($parts[0] under WP_PLUGIN_DIR) so the slug we capture pre-session matches the key WP itself stores in *_paused_extensions once a session opens. Dropped the get_plugins() walk since we no longer need the main-file path here. Pushed in bacb8612be.

Use the first path segment under WP_PLUGIN_DIR as the plugin slug — the
same value WP_Recovery_Mode::get_extension_for_error() produces and the
key WP itself uses inside *_paused_extensions. Previously we returned
the main-file path (e.g. akismet/akismet.php), which would not match the
slug stored once a session is created for the same fatal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@taipeicoder taipeicoder left a comment

Choose a reason for hiding this comment

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

🚢

@arthur791004 arthur791004 merged commit 688444b into trunk May 4, 2026
69 checks passed
@arthur791004 arthur791004 deleted the add/recovery-mode-sync-error-info branch May 4, 2026 06:00
@github-actions github-actions Bot removed [Status] Needs Author Reply We need more details from you. This label will be auto-added until the PR meets all requirements. [Status] Needs Review This PR is ready for review. [Status] In Progress labels May 4, 2026
Copilot AI pushed a commit to dognose24/jetpack that referenced this pull request May 4, 2026
…tic#48440)

* wpcomsh recovery-mode sync: include per-extension error info

Follow-up to Automattic#48213. The state snapshot now also carries an extracted
view of the live *_paused_extensions option, so wpcom-side consumers
(Calypso) can surface what fataled instead of just that something
fataled.

Each record carries kind/slug/version + errno/message/file/line plus
the transportable signature token from Automattic#48369, so a fatal seen via
the recovery email and via the wpcomsh fatal-error screen can be
joined on the same opaque token. file is reduced to its basename so
server paths don't leak.

Reading from the live option on every snapshot (instead of stashing
errors in our own option, or threading them through one capture path)
means every POST — email / session-start / session-end — emits a
complete state, and session-end naturally shows errors=[] without any
explicit clear step.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Phan: widen \$payload type to array<string,mixed>

The new recovery_session_errors field is an array of records, so the
existing array<string,int> phpdoc no longer fits.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop signature from recovery-mode-sync error records

The flat fields (kind/slug/version/errno/message/file/line) cover the
Calypso display use case. The signature was for cross-surface
analytics joining (recovery email vs. fatal-error screen logstash),
which has no consumer yet. We can re-add when one materializes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Drop signature mention from changelog entry

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Capture error_get_last() at email-send time

So the fatal-request POST already carries the error info, instead of
waiting for the admin to click the recovery email link.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Recovery sync: match core's slug shape in resolve_extension_for_file

Use the first path segment under WP_PLUGIN_DIR as the plugin slug — the
same value WP_Recovery_Mode::get_extension_for_error() produces and the
key WP itself uses inside *_paused_extensions. Previously we returned
the main-file path (e.g. akismet/akismet.php), which would not match the
slug stored once a session is created for the same fatal.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: dognose24 <6869813+dognose24@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Coverage tests to be added later Use to ignore the Code coverage requirement check when tests will be added in a follow-up PR [Plugin] Wpcomsh

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants