Plugin Conflicts Guardian: log blocked activations, blocked updates, and post-update rollbacks to logstash#48565
Conversation
Emit an `Activation blocked` event from `pcg_guard_evaluate_plugins()` whenever the activation guard returns a non-empty $blocked map, so we can measure how often the pre-flight check catches a real fatal in the wild — alongside the existing transport-error event in PCG_Load_Tester. Best-effort: silently no-ops off WordPress.com (no `log2logstash`) and never escalates a logging failure into a fatal on the activation request path. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
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! |
Extends the new logstash coverage to the update path. `update-guard.php` emits an `Update blocked` event when the parse-error gate refuses an install or update; `update-healthcheck.php` emits an `Update rolled back` event for each plugin the post-update probe reverted, including the rollback-status / restored_to fields so we can separately track clean restores from `rollback_failed` / `rollback_unavailable`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Code Coverage SummaryCoverage changed in 5 files.
1 file is newly checked for coverage.
Coverage check overridden by
Coverage tests to be added later
|
Drop the explanatory prose to one-line docblocks and keep only the non-obvious inline comment ("Basename only — absolute paths leak install layout"). Behavior unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Move the on-demand `log2logstash` require + dispatch into a single helper in `pcg-log.php`. Callers in `activation-guard.php`, `update-guard.php`, `update-healthcheck.php`, and `class-pcg-load-tester.php::log_probe_error()` now pass just the message slug + properties array. Behavior unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The test file requires update-guard.php directly, so it must also load the new shared logger; otherwise calls to pcg_log_event() from inside the guard fatal at test time. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
| * @param array $extra Event-specific properties; JSON-encoded into the `extra` field. | ||
| * @return void | ||
| */ | ||
| function pcg_log_event( $message, array $extra ) { |
There was a problem hiding this comment.
Since this is best-effort , should we wrap it in a try/catch block? That way we prevent exceptions here blocking the rest of the flows.
There was a problem hiding this comment.
Done in 8ceac93 — wrapped the whole dispatcher in try/catch with a fail-open empty handler. The require + json_encode + log2logstash chain now can't escalate into a fatal on the activation / install / update request paths.
| 'checked' => $checked, | ||
| 'blocked' => array_keys( $blocked ), | ||
| 'status' => (string) ( $result['status'] ?? '' ), | ||
| // Basename only — absolute paths leak install layout. |
There was a problem hiding this comment.
reason might also leak absolute paths if it's just raw PHP error text 😅
Since this is log, it should be fine?
There was a problem hiding this comment.
Yeah, fine for log purposes — reason is the raw probe verdict (PHP fatal/parse-error message). It can carry an absolute path, but everything stays in our internal logstash bucket; no PII, no exposure to the site owner. We accept that as the cost of having an actually useful reason field for triage. The file field stays sanitized to basename only because that one's structured/queried, so worth keeping consistent.
There was a problem hiding this comment.
Actually went ahead and sanitized in ef3e3ca — added pcg_log_redact_paths() that walks the extra payload before JSON-encoding and rewrites any ABSPATH / WP_CONTENT_DIR prefix to .../. Keeps the relative tail (plugins/foo/bar.php) which is the useful part for triage, drops the install-layout leak. Applied centrally in pcg_log_event() so all events (including the pre-existing Probe transport error) get it for free.
| 'Update blocked', | ||
| array( | ||
| 'action' => (string) $action, | ||
| 'slug' => (string) ( $hook_extra['plugin'] ?? ( $hook_extra['theme'] ?? '' ) ), |
There was a problem hiding this comment.
Plugin_Upgrader::install() doesn't populate hook_extra['plugin']. We can pass $source and fall back to basename( untrailingslashit( $source ) ).
There was a problem hiding this comment.
Good catch — fixed in 8ceac93. Threaded $source through to pcg_update_guard_log_blocked() and fall back to basename( untrailingslashit( $source ) ) when hook_extra['plugin'] is empty (install path).
…ll slug Two review nits from #48565: * Wrap `pcg_log_event()` in try/catch — the helper is documented as best-effort, but `wp_json_encode()` / `log2logstash()` could still throw, and these run on activation / install / update request paths where any escalation defeats the point. * `Plugin_Upgrader::install()` doesn't populate `hook_extra['plugin']`, so the install case logged an empty slug. Thread `$source` through to `pcg_update_guard_log_blocked()` and fall back to `basename( untrailingslashit( $source ) )`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Follow-up to taipeicoder's note that `reason` (raw probe verdict / parse- error message) can carry absolute paths. Walk the extra payload before JSON-encoding and rewrite any `ABSPATH` / `WP_CONTENT_DIR` prefix to `.../`. Keeps the relative tail (`plugins/foo/bar.php`) — the useful part for triage — and drops the install-layout leak. Applied centrally in pcg_log_event() so all three events plus the existing `Probe transport error` event get it for free. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Proposed changes
Follow-up from #48553 (review comment): emit logstash events whenever Plugin Conflicts Guardian refuses a bad change or recovers from one, so we can measure how often the various PCG gates catch a real fatal in the wild.
Three new events, all under the existing
plugin-conflicts-guardianfeature bucket, all mirroring the JSON-encodedextrashape used by the existingProbe transport errorevent inPCG_Load_Tester:Activation blocked—pcg_guard_evaluate_plugins()(inactivation-guard.php) calls a newpcg_guard_log_blocked_activation()helper right before returning a non-empty$blockedmap.extra:checked(probe batch),blocked(basenames being blocked; empty-string entry indicates a batch-level fallback),status(fatal/throwable),file(basename only),line,reason(raw probe-verdict message).Update blocked—pcg_update_guard_check()(inupdate-guard.php) calls a newpcg_update_guard_log_blocked()helper just before returning theWP_Errorthat aborts the install/update.extra:action(install/update),slug,file(basename),line,reason,error_count(so a single broken file is distinguishable from a wholesale broken package).Update rolled back—pcg_healthcheck_after_update()(inupdate-healthcheck.php) calls a newpcg_healthcheck_log_rollback()helper inside the per-candidate rollback loop.extra:plugin,new_version,previous_version, the probe verdict that triggered rollback (probe_status/probe_file/probe_line/probe_reason), and the rollback outcome (rollback_status— distinguishesreactivated/restored/rollback_failed/rollback_unavailable— andrestored_to).All three are no-ops outside WordPress.com (no
log2logstash) and best-effort: a logging failure must never escalate into a fatal, since these run on the activation / install / update request paths.Related product discussion/links
Does this pull request change what data or activity we track or use?
Yes — adds three new logstash event slugs (
Activation blocked,Update blocked,Update rolled back) under the existingplugin-conflicts-guardianfeature bucket. Properties shipped per event are listed above; no PII, no new outbound HTTP calls beyond the existing logstash transport.Testing instructions
jetpack-mu-wpcomto a test site withpcg_guard_activationenabled (the new default after Plugin Conflicts Guardian: default pcg_guard_activation to true #48553) andpcg_guard_updatesenabled.Activation blockedrow in theplugin-conflicts-guardianlogstash bucket.Update blockedrow carryingaction: install, the offending file basename + line, anderror_count.Update rolled backrow withrollback_status: reactivated(orrestored/rollback_faileddepending on the snapshot path).log2logstash) → all three guards still work, no log emitted, no fatal.🤖 Generated with Claude Code