=== 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.8
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.8 =
* Added: the plugin now checks phpray.dev for new releases and offers updates
  through the normal WordPress screens, including the auto-update toggle. Until
  now there was no update path at all: the plugin is not in the wordpress.org
  directory, so WordPress never learned that a release existed. Seven versions
  shipped between 0.1.1 and 0.1.7 and none of them reached anyone who had
  already installed it — every fix stayed broken in the field. The check
  downloads one static JSON file and sends nothing about your site.

= 0.1.7 =
* Fixed: WP-CLI and cron runs were recorded as HTTP requests with an empty URI
  and status 0 — 7 of the first 24 records on a fresh install. They now show as
  `<wp-cli>` or `<wp-cron>` with method `CLI`, so a slow cron is findable
  instead of being a blank row that drags the averages.
* Fixed: `PHPRAY_VERSION` said 0.1.5 while the plugin header said 0.1.6, so the
  version in every trace was wrong. The test that should have caught it had the
  expected version hard-coded to 0.1.1 and had been failing since 0.1.2; it now
  checks that the header, the constant and Stable tag agree with each other.

= 0.1.6 =
* Added: the Server key field now links to the console, where the key is
  created, and to the plugin documentation. The description already said the
  key is "created in the console" but gave no way to get there, so cloud mode
  ended in a dead end for anyone who did not already know the console exists.

= 0.1.5 =
* Fixed: a rejected server key was only visible in a table cell on the plugin
  page. A rejected key means nothing is being sent at all and sending stays
  paused until the settings are saved, so the plugin page now says so at the
  top, the same way it does when the log directory is not writable.

= 0.1.4 =
* Fixed: a silent failure. When the log directory was not writable by the web
  server user, nothing was recorded and the plugin said nothing about it. This
  happens when the directory was created by WP-CLI running as root while PHP
  runs as www-data. The plugin page now says plainly that nothing is being
  recorded and names the directory and its owner, and `wp phpray status`
  reports writability together with the user it is checking as.

= 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.
