=== PHPRay ===
Contributors: adambuhl
Tags: performance, profiling, debugging, monitoring, sql
Requires at least: 6.0
Tested up to: 7.1
Requires PHP: 7.4
Stable tag: 0.1.3
License: GPLv2 or later
License URI: https://www.gnu.org/licenses/old-licenses/gpl-2.0.html

Per-request performance collector that ships PHPRay-compatible JSON traces from hosts where the PHPRay PHP extension cannot be loaded.

== Description ==

PHPRay for WordPress is a userland collector for hosts where the PHPRay PHP extension cannot be installed (shared hosting, managed PHP, immutable builds). It wraps a normal WordPress request and produces the **exact same JSON trace format** the extension emits, so the same collector and console can consume both sources without changes.

For every request the plugin records:

* start/end timing (`duration_ms`), CPU user/sys time (`getrusage`), peak memory
* HTTP status and a fingerprinted URI (query strings reduced to `?key=*`)
* database activity: query count, total DB time, N+1 fingerprint count
* the slowest SQL queries with caller locations (sampled requests only)
* outbound `wp_remote_*` HTTP calls (url, method, status, duration, caller)
* PHP errors and fatal errors
* per-component (plugin/theme) time aggregation (sampled requests only)

Each record carries a severity `level` (`summary`, `normal`, `full`, `alert`) derived from configurable duration thresholds, errors and HTTP 5xx status.

**Output modes** (Tools → PHPRay):

* `file` — append JSON lines to `wp-content/uploads/phpray/phpray.log` (protected by `.htaccess`), rotated at 10 MB, keeping 3 files. A PHPRay collector on the same server reads this file.
* `collector` — POST ingest-v1 batches to a local PHPRay collector **0.15 or newer** (default `http://127.0.0.1:9191/api/v1/ingest`), optional bearer token. Older collectors have no HTTP ingest endpoint: use `file` mode with them.
* `cloud` — POST ingest-v1 batches to PHPRay Cloud (default `https://app.phpray.dev/v1/traces`) with your server key as `Authorization: Bearer prk_…`, plus per-minute aggregates to the sibling `/v1/aggregates` endpoint.

**Wire format (ingest protocol v1).** Every batch is an envelope `{"batch_id": "b_<uuid>", "sent_at": <unix ms>, "agent": "wp-plugin/<version>", "traces": [...]}` with at most 200 records; each record is the JSONL trace record above plus a `site_id` (`sha1(host + "\0" + ABSPATH)[:16]`, host taken from `home_url()`). In cloud mode only `normal`/`full`/`alert` records are shipped as traces; every request is counted in the per-minute aggregates (requests, errors, levels, duration avg/p50/p95/max, CPU, memory, DB, HTTP, components, top URIs), which is what the fleet view is built from.

**Delivery.** Records are buffered (up to 200) in a WordPress option and sent at most every 5 seconds (immediately for `alert`) with a blocking `wp_remote_post` issued after the response has been handed to the web server (`fastcgi_finish_request`). The console's answer drives the plugin: `202` removes the sent records and, if it reports `dropped_quota` with an `X-PHPRay-Trace-Sample` header, the plugin ships only that share of traces until the next full batch is accepted; `401` pauses sending and shows "rejected key" in Tools → PHPRay until the key is changed and saved; `413` halves the batch; `429` waits for `Retry-After`; other errors back off (5 s → 5 min) while the buffer is kept.

**Performance budget.** Non-sampled requests cost only a couple of `microtime()` calls, one `getrusage()`, one `memory_get_peak_usage()`, one JSON encode and one file append or buffer push. Heavy work (SQL caller capture, error handler, HTTP-call tracing, hook profiling, `SAVEQUERIES`) runs only on sampled requests (default 10 %, `mt_rand` decision at plugin load).

The admin page (Tools → PHPRay, `manage_options`) shows the current status (mode, last flush, buffer size, last error) and the last 50 collected records. A WP-CLI interface is included: `wp phpray status` and `wp phpray tail --lines=20`.

== Installation ==

1. Upload the `phpray` folder to `/wp-content/plugins/`.
2. Activate the plugin. On activation the `{$prefix}phpray_recent` table is created.
3. Open Tools → PHPRay and choose an output mode, sample rate and thresholds.
4. For `file` mode, point your PHPRay collector at `wp-content/uploads/phpray/phpray.log`.
5. For `collector` mode, run a PHPRay collector 0.15 or newer on the web server (default endpoint `http://127.0.0.1:9191/api/v1/ingest`).
6. For `cloud` mode, create a server in the PHPRay Cloud console and paste its server key (`prk_…`). The status box shows the site ID, the last accepted batch and any rejected-key or quota message.

== Frequently Asked Questions ==

= Does this replace the PHPRay extension? =

No. It is a fallback for hosts where the extension cannot be loaded. Both sources produce the same trace format, so you can run the plugin alongside or instead of the extension.

= What is "sampling"? =

A percentage of requests get the deep instrumentation (SQL capture with callers, error handler, HTTP tracing, hook profiling). The rest produce lightweight `summary` records only. A sampled request that finishes below the "normal" threshold still keeps its five slowest queries, so a fast site is not left with an empty report. The sample rate is configurable from 0 to 100 (default 10).

= Where are records stored in file mode? =

`wp-content/uploads/phpray/phpray.log` (JSON lines). The folder is protected by a deny-all `.htaccess` and an empty `index.php`. The file is rotated at 10 MB and 3 files are kept (`phpray.log`, `phpray.log.1`, `phpray.log.2`).

= Can I exclude some URIs or admin traffic? =

Yes. The settings page has an "ignore URIs" field (one prefix per line) and an "exclude logged-in admins" checkbox.

= Why does the Cloud trace list not show every request? =

By design: only `normal`, `full` and `alert` records are shipped as traces. Every request (including `summary`) is counted in the per-minute aggregates that feed the fleet view. When a site exceeds its plan quota the console asks the plugin to sample traces; the current share is shown in the status box.

== Screenshots ==

1. Status: whether collection is on, the output mode, the log file and its size, the PHP and WordPress versions.
2. Settings: output mode, endpoint, sample rate, duration thresholds and the URI prefixes to skip.
3. The most recent records: time, URI with query values masked, status, duration, query count and level.

== Changelog ==

= 0.1.3 =

* Failed requests always reach PHPRay Cloud, whatever the sample rate. A fast HTTP 500 — a checkout that dies in 12 ms — used to be dropped by the 10% sample or by the `summary` cut-off, which contradicted the product's promise that the one failing request is still there when you look. Responses with status 5xx and requests carrying a fatal PHP error (E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR) now bypass sampling. Warnings and notices stay sampled: an older WordPress can raise twenty per request and they would burn a plan's daily quota.

= 0.1.1 =

* Cloud mode works with PHPRay Cloud: ingest-v1 batch envelope (`batch_id`, `sent_at`, `agent`, `traces[]` with `site_id`), default endpoint `https://app.phpray.dev/v1/traces`, server key as bearer token, gzip above 8 KB.
* Per-minute aggregates for the fleet view, sent to `/v1/aggregates`.
* Response handling: `202` clears the sent records and applies the `X-PHPRay-Trace-Sample` hint when `dropped_quota` is reported; `401` pauses sending and is shown in Tools → PHPRay and `wp phpray status`; `413`/`429`/`5xx` keep the buffer and back off.
* Batches are sent at most every 5 s (immediately for `alert`) after the response is finished; one sender at a time.
* Collector mode uses the same envelope on `http://127.0.0.1:9191/api/v1/ingest` and is marked as requiring collector 0.15 or newer.
* `litespeed_finish_request()` support.

= 0.1.0 =

* Initial release.
