WP Manifestindependent plugin directory
manifest / developer / wp-trace

WP Trace

Runtime tracing and causal execution analysis for WordPress — why did WordPress do this?

by Rafi Ahmed · github.com/rafiahmedd/wp-trace

0stars
0forks

Install

The author publishes release zips, so WP-CLI can install straight from GitHub:

wp plugin install https://github.com/rafiahmedd/wp-trace/releases/download/v0.1.0/wp-trace.zip

Runtime tracing and causal execution analysis for WordPress — "Chrome DevTools for the WordPress backend."

Most debugging tools show disconnected panels: a query log here, hooks there, errors somewhere else. WP Trace reconstructs the causal execution chain of a request — request → hooks → callbacks → files → SQL → HTTP → errors — so you can answer the question that actually matters:

Why did WordPress do this?

  • Local-first: no account, no cloud, no telemetry — traces stay in your database
  • Plugin-agnostic: works with any plugin/theme stack, no framework assumptions
  • Coexists with Query Monitor (including its db.php drop-in), WooCommerce, REST-heavy plugins
  • Zero runtime dependencies: no Composer, no npm on the server — ships as a self-contained build

Requirements

WordPress 6.4+ (developed and tested against 7.1)
PHP 8.1+
Database MySQL 5.7+ / MariaDB 10.3+ (custom tables, dbDelta)
Capability manage_options for UI/REST/CLI (filterable)

Installation

From the ZIP — upload dist/wp-trace.zip through Plugins → Add New → Upload Plugin, then activate. That's it; the compiled UI ships inside the plugin.

From source:

git clone <repo> wp-content/plugins/wp-trace
cd wp-content/plugins/wp-trace/ui && npm install && npm run build

On activation, WP Trace creates its tables and schedules an hourly retention janitor. Tracing is disabled by default — enable it from the admin screen or CLI.


Quick start

  1. Open WP Trace in the admin menu.
  2. In Settings, pick a tracing mode (see below) and save.
  3. Browse your site, load the page you're debugging, hit a REST endpoint.
  4. Back in WP Trace, open the newest request from the list.
  5. Read the Overview (stats + findings), then jump into Timeline, Hooks, Database, HTTP or Errors. Export the trace as JSON when you need to share or archive it.

Tracing modes

Mode Captures Overhead Best for
Disabled nothing (reads one option at boot) none production-ish default
Basic request envelope (type/URI/timing/memory/status/user), SQL summary with callers, outgoing HTTP summary, PHP errors, hook fire tallies ≈ free — uses WordPress' own counters and SAVEQUERIES; no monkey-patching always-on visibility
Detailed everything in Basic plus the full causal tree: every hook callback with duration, priority, source file and owning plugin/theme; queries and HTTP calls tree-linked to the callback that caused them ~15–25 % on very large stacks (50+ plugins, 100k+ hook fires); lower on typical sites debugging sessions

Guidance: run basic when you want passive visibility; flip to detailed when hunting a specific problem. wp trace enable --mode=detailed / disable makes this a one-liner.


The admin UI

  • Requests — every traced request with type, method, URI, status, duration, query/HTTP/error counts, memory and truncation flag. Filter by type, slow (>500 ms), with-errors; optional 5-second live refresh. Row click opens the trace.
  • Request detail
    • Overview — stat cards (duration, queries, HTTP, callbacks, hook fires, errors, memory, user, WP/PHP version), detection findings with owning components, hottest hooks.
    • Timeline — every event as an offset/duration bar across the whole request; indent shows nesting; click an event for its SQL, file:line, ownership, hook and priority. Raise the minimum-duration filter on huge traces.
    • Hooks — per-hook aggregation: calls, total/average time, slowest callback and its owner.
    • Database — every query with duration, owning plugin and caller.
    • HTTP — outgoing requests with status, duration and response size.
    • Errors — warnings/notices/deprecations/exceptions with file, line and owner; suppressed (@) errors flagged.
  • Settings — mode, retention, slow-query/slow-callback thresholds.

Reading ownership

Every event carries a resolved component: core, plugin <slug>, mu-plugin, theme, child-theme or unknown. A query executed inside plugins/foo/vendor/... is attributed to plugin foo — vendor paths resolve to their parent plugin. This is what turns "some query is slow" into "licenzo-pro runs a slow query from Validator.php:92".

Detection findings

Deterministic heuristics run at finalization and appear in the Overview (they're observations, never diagnoses):

Finding Trigger
Potential duplicate query same normalized query shape ≥ 2 times (literals/whitespace-insensitive)
Potential slow query query above the slow-query threshold (default 100 ms)
Potential duplicate HTTP request same method + host + path ≥ 2 times
Potential slow callback hook callback above threshold (default 250 ms)
Potential recursive hook chain a hook re-entering itself through its callback ancestry (e.g. save_post → wp_update_post → save_post)
Repeated option writes same option updated ≥ 3 times in one request

Each finding links to the offending event sequences and names the owning components.


WP-CLI

wp trace status                                   # mode, storage, schema, wpdb class, xdebug state
wp trace enable [--mode=basic|detailed]           # default basic
wp trace disable
wp trace requests [--type=rest] [--limit=20] [--format=table|csv|json|ids]
wp trace inspect <id> [--section=summary|tree|events|hooks|queries|http|errors] [--format=table|json]
wp trace export <id> [--file=trace.json]          # stable JSON format v1
wp trace clear [--force]
wp trace delete <id> [<id>...]

wp trace invocations are excluded from tracing themselves (no feedback loops).

REST API

All routes live under /wp-json/wp-trace/v1, use standard cookie+nonce auth and require manage_options (both capabilities are filterable). Requests to this namespace are not traced.

Method Route Purpose
GET /requests list — filters: type, method, status, slow (µs), errors, search, orderby, order, pagination
GET /requests/{id} request row + summary + findings
DELETE /requests/{id} delete one trace (manage capability)
DELETE /requests delete all (manage capability)
GET /requests/{id}/events flat events — type, per_page
GET /requests/{id}/tree nested causal tree — min_duration_us pruning
GET /requests/{id}/export trace JSON v1 download (manage capability)
GET / POST /settings read / update settings

Export format

wp trace export and the UI's Export JSON emit a stable, self-contained trace (version 1):

{
  "version": 1,
  "generator": "wp-trace",
  "request": { "type": "rest", "method": "POST", "uri": "/wp-json/learner-lms/v1/courses", "duration_us": 842000, "…": "…" },
  "summary": { "counters": { "callback": 326 }, "findings": [ "…" ] },
  "events": [
    { "seq": 1, "parent_seq": 0, "type": "callback", "name": "CourseController::index",
      "component": { "type": "plugin", "id": "learner-lms" }, "offset_us": 312500, "duration_us": 240000 }
  ]
}

The format is designed for bug reports, diffing, CI artifacts and future tooling.


Settings reference

Setting Default Meaning
mode disabled tracing mode
retention_count 100 newest N requests kept (hourly janitor + batched trims)
slow_query_ms 100 slow-query finding threshold
slow_callback_ms 250 slow-callback finding threshold
max_events 20000 global stored-event cap (query/HTTP/error events survive truncation via their own caps)
max_queries / max_http / max_hook_spans 2000 / 500 / 3000 per-type caps
min_callback_us 100 callbacks faster than this are counted but not stored — the main performance dial
memory_guard_pct 80 at 80 % of memory_limit the collector degrades to counting-only

Kill switches: WP_TRACE_DISABLE (constant, removes all hooks), WP_TRACE_MODE (constant, overrides the option), WP_TRACE=0 (environment, for CLI runs).

Privacy & security

  • Traces contain sensitive runtime data by nature — they are gated behind manage_options everywhere (UI, REST permission_callback, admin forms with nonces).
  • Never captured: request/response headers, cookies, $_POST/$_FILES bodies, environment secrets.
  • Redacted before persistence: URI query parameters and any array key matching sensitive patterns (password, token, api_key, auth, cookie, nonce, payment terms — configurable list). There is no "raw" copy to leak: storage only ever sees redacted data.
  • Storage is retention-bounded (default 100 requests) with one-click/CLI purge; uninstall.php drops both tables and all options.
  • Strings and nesting depth in stored metadata are size-capped as a memory defense.

Performance notes

Finalization (sanitize → detect → batch insert) is bounded and self-profiled — every trace's summary includes finalize phase timings and the stored-row count, so you can see the cost on your own site. Measured on a worst-case lab (50 plugins, ~100k hook fires per request):

  • disabled: no overhead beyond one autoloaded option read
  • basic: within measurement noise
  • detailed: finalizer ~170 ms; total page impact ~15–25 % / ~0.5 s on that stack, dominated by per-callback instrumentation cost

Tuning detailed mode for slower machines: raise min_callback_us (e.g. 250), lower max_events (e.g. 8000). A warning banner appears in traces recorded while Xdebug was active, since it inflates timings.


Compatibility & limitations

  • Coverage starts at the plugin's own load: everything from plugins_loaded onward is traced; core bootstrap, mu-plugin file loading and the pre-plugin option queries are not.
  • Works with Query Monitor's db.php drop-in (traces layer on top of QM_DB). Other custom database drop-ins (hyperdb-style) fall back to flat SAVEQUERIES capture without query tree-linkage — the trace summary flags this compat_mode.
  • HTTP calls bypassing the WordPress HTTP API (raw cURL, Guzzle) are invisible; pre_http_request short-circuits are recorded with approximate duration.
  • Deprecation notices only fire when WP_DEBUG is enabled (WordPress behavior).
  • Multisite: tables are per-site (prefixed); tested on single site.

Uninstall

Deactivating keeps data (only the retention cron is cleared). Deleting the plugin through WordPress drops both tables and deletes all options — traces are destroyed with it.


Development

php bin/generate-classmap.php          # regenerate includes/classmap.php after adding classes
php tests/run.php                      # WordPress-free unit tests
php tests/Integration/smoke.php        # full pipeline against a real install (WP_TRACE_WP_ABSPATH=/path)
cd ui && npm install && npm run build  # build the admin app → ui/build (shipped in the ZIP)
php bin/make-pot.php                   # regenerate languages/wp-trace.pot
./bin/build-zip.sh                     # build dist/wp-trace.zip
  • PHP: namespaced OOP (WPTrace\), one class per file under src/, PSR-4 layout with a generated classmap autoloader — no Composer at runtime.
  • UI: Vite + React (no runtime dependencies beyond React), hash routing, plain CSS. Enqueued only on the plugin screen; loads as an ES module.
  • Architecture: full design with WordPress-source citations lives in docs/ARCHITECTURE.md; the product plan in docs/PLAN.md.
  • Safety invariant: the tracer must never break the traced site — every integration point has a fallback path (wpdb swap verification + compat mode, capped counting-only collector, finalizer wrapped so failures are swallowed).

License

GPL-2.0-or-later, like WordPress itself.

Releases

1 release. Each count is every asset in that release; expand a row for the breakdown.

Tag
Published
Assets
Downloads
v0.1.0 latest
Sep 19, 2026 23h ago
wp-trace.zip
0