Wpcomsh fatal-error: emit a transportable plugin/version/core/php signature#48369
Conversation
…nature Add `wpcom_build_fatal_error_signature()` / `wpcom_decode_fatal_error_signature()` to jetpack-mu-wpcom as a shared helper, then wire wpcomsh's fatal-error screen to compute the signature once per fatal, fire `do_action( 'wpcomsh_fatal_signature', ... )`, and ship it to logstash via `WPCOMSH_Log::unsafe_direct_log()` — the in-repo precedent already used by safeguard, woa, marketplace, and the Atomic storage provider. The signature is PII-free: only the kind, lowercased extension slug, extension version, WordPress version, and PHP version (normalized to MAJOR.MINOR.PATCH so distro suffixes don't fragment the grouping). It travels as a single base64url-encoded JSON token, fully decodable by consumers so they can group on parts. Screen and recovery email rendering are unchanged — the signature is purely server-side telemetry.
|
Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.
Interested in more tips and information?
|
|
Thank you for your PR! When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:
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:
If you have questions about anything, reach out in #jetpack-developers for guidance! Wpcomsh plugin:
If you have any questions about the release process, please ask in the #jetpack-releases channel on Slack. |
Code Coverage SummaryCoverage changed in 1 file.
1 file is newly checked for coverage.
Coverage check overridden by
I don't care about code coverage for this PR
|
Collapse the action + default listener pair into a direct call from the screen filter. The action existed for hypothetical third-party listeners; none exist. Other mu-plugins that want the same signature shape can call `wpcom_build_fatal_error_signature()` directly from their own fatal-detection paths (e.g. #48261's activation probe) — the shared helper in jetpack-mu-wpcom is the correct extension point, not a parallel wpcomsh-specific hook. Also log the decoded parts (kind, slug, extension_version, wp_version, php_version) alongside the encoded signature so Kibana queries can term-aggregate without an ingest-time base64+JSON decode step. Use `class_exists( 'WPCOMSH_Log', false )` to skip autoloader I/O during fatal handling. Net: -1 file, ~150 fewer lines, no public API surface for nonexistent consumers. Helper in jetpack-mu-wpcom is unchanged and remains available for cross-mu-plugin reuse. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A persistent fatal on a high-traffic site would otherwise emit one logstash row + one outbound wp_remote_post() per visitor, since the filter fires for every rendered fatal and unsafe_direct_log() always schedules a shutdown send. Gate on wp_cache_add() with a 5-min TTL keyed by sha256(signature), placed before the WPCOMSH_Log require so dedup hits skip the file load. Throwable-catch around the cache call fails open so a broken cache never silences telemetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s hits Identification (glob + get_plugin_data) was running for every visitor of a persistent fatal even though the anonymous render path doesn't display plugin info — its only use on that path is to feed the signature logger. Hoist user_id / is_admin resolution into the screen filter, identify unconditionally for admins (the rendered notice needs $plugin), and gate identify+log behind a coarse $error['file'] cache key for anonymous viewers. Thread the resolved user_id and is_admin into wpcomsh_fatal_build_render_context so they're not recomputed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Drop verbose framing and references outside wpcomsh / jetpack (log2logstash, RFC 4648, libsodium-seal aside, fix #31284, memcached/Atomic detail, Error_Handler precedent). No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The relative require for the signature helper used vendor/, but the released wpcomsh has the package at jetpack_vendor/. Switch both fallback requires (signature helper and WPCOMSH_Log) to the WPCOMSH__PLUGIN_DIR_PATH + jetpack_vendor convention used elsewhere in wpcomsh (lib/tonesque.php, lib/class.color.php), and gate on defined() since wpcom-fatal-error/load.php is required before constants.php. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c_extension_conflict Follow-up to #48369. Adds a sibling `WPCOMSH_Log::unsafe_direct_log_logstash( $feature, $message, $options = [] )` that POSTs to /rest/v1.1/logstash with caller-supplied `properties` (indexed under `properties.*` in Kibana for filter/sort/aggregate), `severity`, and `extra` (unstructured context). Switches the fatal-error signature emitter to it under feature `atomic_extension_conflict` (severity `critical`), so these records get their own Kibana bucket — and the decoded parts (`signature`, `kind`, `slug`, `extension_version`, `wp_version`, `php_version`) land under `properties.*` rather than the noisier nested `extra.*` path used by the parent PR's /automated-transfers/log envelope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* wpcomsh recovery-mode sync: include per-extension error info 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> * 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>
…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>
Proposed changes
wpcom_build_fatal_error_signature()/wpcom_decode_fatal_error_signature()to jetpack-mu-wpcom (src/common/fatal-error-signature.php) — a shared helper that produces a base64url-encoded JSON token over{kind, slug, version, wp, php}, fully reversible so consumers can group on the decoded parts. Sized for reuse by other mu-plugins (e.g. Plugin Conflicts Guardian: Pre-flight activation gate #48261's activation-probe failure path) so they can correlate on the same encoded token.WPCOMSH_Log::unsafe_direct_log(). Telemetry runs independent of viewer (admin or anonymous) so dashboards aren't biased toward admin-only sites.wp_cache_add( 'wpcomsh_fatal_file:' . sha256($error['file']), …, 5 min ). Identification (glob+get_plugin_data) only runs on the first anonymous request per file/5-min, then no-ops the rest.wp_cache_add( 'wpcomsh_fatal_sig:' . sha256($signature), …, 5 min ). Belt-and-suspenders for the case where the file-path gate doesn't catch (e.g. same plugin loading from multiple paths).kind,slug,extension_version,wp_version,php_version) as separate fields alongside the encodedsignature, so dashboards can term-aggregate without a base64+JSON decode step.class_exists( 'WPCOMSH_Log', false )+ on-demandrequire_oncefromWPCOMSH__PLUGIN_DIR_PATH . '/jetpack_vendor/automattic/jetpack-mu-wpcom/...'(the wpcomsh convention used bylib/tonesque.phpandlib/class.color.php), gated ondefined( 'WPCOMSH__PLUGIN_DIR_PATH' )for the bootstrap window beforeconstants.phpis loaded.Related product discussion/links
wpcom_build_fatal_error_signature()directly from its activation-probe failure path. The encodedsignaturefield stays in the wpcomsh logstash payload so cross-system consumers can join on the same opaque token.Does this pull request change what data or activity we track or use?
Yes. On the first fatal per (site, signature, 5-min) where wpcomsh can identify the offending extension, a new logstash record is queued via the existing
WPCOMSH_Logpipeline.Signature contents (the new fields this PR adds to the logstash record):
extra.signature— base64url-encoded JSON token. Decoded JSON contains:kind—plugin,muplugin, orthemeslug— extension slug, lowercased and trimmedversion— extension version stringwp—$wp_versionphp—PHP_MAJOR_VERSION.PHP_MINOR_VERSION.PHP_RELEASE_VERSION(normalized so distro suffixes don't fragment grouping)extra.kind,extra.slug,extra.extension_version,extra.wp_version,extra.php_version— the same parts emitted as separate fields for term-aggregation.Logstash record envelope (added by
WPCOMSH_Log::send_to_api(), applies to every wpcomsh telemetry record):siteurl— the result ofget_site_url(). Consistent with the existing safeguard / woa / marketplace / atomic-storage telemetry baseline; no new exposure beyond that baseline.What we deliberately do not include: error message, stack trace, file paths, user id, request URI.
Coverage caveat:
wpcomsh_fatal_identify_plugin()only resolves directory-based plugins/themes (e.g.wp-content/plugins/akismet/akismet.php). Single-file extensions (e.g.wp-content/mu-plugins/foo.php) aren't identified and therefore produce no signature record. Pre-existing limitation of the identification helper, not introduced by this PR.Testing instructions
wp-content/mu-plugins/boom/boom.phpcontainingtrigger_error( 'boom', E_USER_ERROR )oninit).log2logstashindex, filtertags:atomic_wpcomsh_errors AND extra.messages.message:"wpcomsh_fatal_signature", or just free-text"wpcomsh_fatal_signature"), confirm a record arrives shortly after the request. The wpcom receiver wraps it: top-levelmessageis[remote_error] [blog: …] [transfer: …], our payload sits atextra.messages[0].messagewith the decoded parts underextra.messages[0].extra.*(signature,kind,slug,extension_version,wp_version,php_version).extra.*fields.wpcomsh_fatal_signaturerecord is emitted.