Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ jobs:
- name: "PHP CS Fixer (dry-run)"
run: composer cs-fixer

- name: "Setup Node.js"
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: '24.8.0'

- name: "JavaScript tests"
run: node --test tests/JavaScript/debugbar.test.js

Comment thread
niden marked this conversation as resolved.
tests:
name: "Tests (PHP ${{ matrix.php }}, Phalcon ${{ matrix.variant }})"
permissions:
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,26 @@ All notable changes to `phalcon/debugbar` are documented here. The format is bas

### Added

- A memory collector reports current and peak PHP request usage independently of
request history. When history is enabled, request time, current memory usage,
and the HTTP method/URI are also displayed as compact status indicators on the
right of the bar while the separate Time and Memory tabs remain available.
- Optional, extensible collector summaries rendered as headline metrics above a panel. The database collector uses them to report total queries, duplicate runs (executions after the first), and accumulated SQL time, and marks repeated statements with their execution count.
- Optional, session-isolated request history with filesystem retention, an
internal `GET/DELETE /_debugbar/open` controller, and a collapsible request
browser with refresh and clear controls that swaps the bar payload without
leaving the current page. A dynamic request control combining a search icon,
method, and URI replaces a dedicated History tab. History and collector panels
are mutually exclusive; selecting a stored request closes history and updates
the entire bar. Retention
cleanup covers abandoned session directories without delaying history reads and
distinguishes request-start and persistence timestamps. Metadata sidecars keep
request listings independent of collector payload size. Stored payloads carry a
format version and incompatible entries are rejected. An explicit absolute,
writable storage path is required when History passes the environment and
collector gates. The provider selects its injected history controller without
changing application routes and respects the URL service's base URI. Storage
consumers depend on a session-scoped history contract.

## [0.4.0](https://github.com/phalcon/debugbar/releases/tag/v0.4.0) (2026-07-14)

Expand Down
79 changes: 73 additions & 6 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ The remainder of this document covers the debug bar.

## Registering the Debug Bar

The bar is booted by `Phalcon\DebugBar\Provider`. It takes the MVC application and an optional configuration array. Its only coupling to the application is the application's events manager, so the application must have one set before the bar boots.
The bar is booted by `Phalcon\DebugBar\Provider`. It takes the MVC application and an optional configuration array. The application must have an events manager set before the bar boots. Request history additionally requires the application's `request` and shared `response` services. The provider does not add or modify application routes.

```php
<?php
Expand Down Expand Up @@ -67,12 +67,17 @@ The second argument to `Provider` is a nested array. Every key is optional.
| `access.allow_ips` | `list<string>` | `[]` (any client) | Client IP allowlist. Empty allows any address. |
| `access.callback` | `(Closure(): bool)\|null` | `null` | Extra gate. When present, it must also return `true`. |
| `assets.nonce` | `string\|null` | `null` | CSP nonce stamped on the injected `<style>` and `<script>` tags. |
| `collectors` | `array<string, bool>` | all enabled | Per-collector switch, keyed by collector name. |
| `collectors` | `array<string, bool>` | all applicable enabled | Per-collector switch, keyed by collector name. |
| `enabled` | `bool` | `true` | Master switch. When `false`, `boot()` returns after the gate. |
| `env.blocked` | `list<string>` | `['production', 'prod']` | Environment values that block the bar (case-insensitive). |
| `env.strict` | `bool` | `false` | When `true`, `boot()` throws in a blocked/undefined environment instead of returning silently. |
| `env.var` | `string` | `APP_ENV` | Environment variable inspected by the gate. |
| `headers` | `bool` | `true` | Emit the `X-Debug-Bar` diagnostic header. |
| `history.enabled` | `bool` | `false` | Store and browse recent requests for the active session. |
| `history.url` | `string` | `/_debugbar/open` | Internal GET/DELETE endpoint, relative to the application's base URI. |
| `history.path` | `string` | required when enabled | Absolute writable storage directory; keep it outside the document root. |
| `history.max_requests` | `int` | `100` | Maximum stored requests per session. |
| `history.ttl_seconds` | `int` | `86400` | Lifetime in seconds; active-session entries are checked immediately. |
| `redact.hidden` | `list<string>` | `[]` | Keys dropped from the output entirely. |
| `redact.mask` | `list<string>` | `[]` | Extra keys whose values are masked (added to the defaults). |

Expand All @@ -85,13 +90,62 @@ use Phalcon\DebugBar\Provider;
'env' => ['var' => 'APP_ENV', 'blocked' => ['production', 'staging']],
'access' => ['allow_ips' => ['127.0.0.1', '10.0.0.5']],
'collectors' => ['cache' => false, 'view' => false],
'history' => [
'enabled' => true,
'path' => dirname(__DIR__) . '/runtime/debugbar',
'max_requests' => 100,
'ttl_seconds' => 86400,
],
'redact' => ['mask' => ['api_key'], 'hidden' => ['secret_question']],
]))->boot();
```

When history is enabled, the provider registers its internal history controller
in the DI container and selects it on `application:beforeHandleRequest` for
`GET /_debugbar/open` and `DELETE /_debugbar/open`. The controller receives its
storage and access gate directly; no separate history services or routes are
registered. If the application's `url` service has a base URI of `/app1/`, the
endpoint is `/app1/_debugbar/open`. Without a `url` service, `history.url` is used
as configured. The endpoint uses the application's normal MVC lifecycle, so
module initialization and application event listeners still run. A GET
without an `id` returns the recent request metadata; `?id=<request-id>` returns
a stored payload. DELETE clears the active session's stored requests.
Only an actual HTTP `DELETE` is accepted for clearing; request method overrides
such as `_method=DELETE` are rejected.

The request indicator on the right (search icon, HTTP method, and URI) replaces a
dedicated History tab. It initially identifies the current request. Clicking it
closes any open collector panel and opens the history browser; clicking it again
closes the browser. Opening a collector panel also closes history, so only one
panel is visible at a time. The browser provides refresh and clear controls.
Selecting a stored request closes history, replaces the complete collector payload,
and updates the tabs plus the request time, current memory usage, method, and URI
without navigating away from the page. If request metadata is unavailable, the
history control uses `History` as its fallback label. An empty history displays
`No stored requests` and leaves the clear control disabled.

Request history is disabled by default. When enabled, it can still be switched
off through `collectors.history = false`; in that case no endpoint or storage is
registered. `history.path` must be an absolute path, configured explicitly, and
writable by the web-server user. Its validation happens only after the environment
and collector gates allow History to boot. Storage is isolated by a SHA-256 hash of the active PHP session id. Each stored
entry distinguishes the request start time (`requested_at`) from the time it was
persisted (`stored_at`). If the server does not expose `REQUEST_TIME_FLOAT`, the
persistence time is used for both values. Each payload has a small metadata sidecar,
so listing requests does not read the full collector payload. If a sidecar is
missing, the versioned payload is used as a fallback; incompatible stored payloads
are rejected. Reads clean expired entries only from the active session so opening the browser remains
fast on network filesystems. A rate-limited collection during request storage
removes expired entries, abandoned temporary files, and empty directories from all
sessions at most once per hour (or once per configured TTL when it is shorter). With
no active session, no request is written or exposed.

## Collectors

Each collector contributes one tab. A collector reads its data in one of four ways:
Collectors contribute data to the bar. Most renderable collectors appear as tabs.
When request history is enabled, request time and memory usage also appear in the
compact controls described below, while their collector tabs remain available.
A collector reads its data in one of four ways:

- **Snapshot** - reads state when the response is assembled.
- **Streamed** - subscribes to framework events and accumulates as they fire.
Expand All @@ -105,6 +159,7 @@ Each collector contributes one tab. A collector reads its data in one of four wa
| `database` | SQL statements, bindings, timings, and query summary | streamed |
| `exceptions` | Throwables, with stack traces | manual + streamed |
| `logger` | Log entries captured from a `Phalcon\Logger` adapter | adapter |
| `memory` | Current and peak memory used by the PHP request | snapshot |
| `messages` | Messages recorded through the facade | manual |
| `request` | Request method, URI, query, post, and headers (redacted) | snapshot |
| `route` | Matched module, controller, action, and parameters | streamed |
Expand Down Expand Up @@ -240,13 +295,25 @@ use Phalcon\DebugBar\Provider;

The bar's CSS and JavaScript are minified and injected inline; the bar has no external asset to host or serve. On CSP-restricted pages, set `assets.nonce` so the inline tags carry a nonce.

The bar sits at the bottom of the page. Each collector is a tab; a tab shows a badge when the collector reports a count or a summary value. Clicking a tab opens its panel:
The bar sits at the bottom of the page. Interactive collectors appear as tabs on
the left; a tab shows a badge when the collector reports a count or summary value.
When request history is enabled, request time and current memory usage remain
visible as compact indicators on the right alongside the Time and Memory tabs.
Each participating collector declares its own indicator and semantic value path;
the History collector does not depend on the Time or Memory payload shape.
The rightmost control combines
a search icon with the current HTTP method and URI; it opens or closes request
history instead of using a dedicated History tab. The history browser and collector
panels are mutually exclusive, so opening either closes the other. Selecting a
stored request closes history and updates the entire bar with that request's data.

Collectors may expose a summary above their panel. The database collector reports the total query count, duplicate runs, and accumulated SQL time. A duplicate run is each execution of a normalized statement after its first execution: running the same statement three times contributes two duplicate runs. These metrics remain visible with zero values when no queries are executed. Duplicate detection deliberately ignores binding values; repeated statements are highlighted and show their total execution count.
Interactive tabs use the following panel types:

- **grid** panels (version, request, config, session, route) render a key and value table.
- **list** panels (time, messages, database, view, cache) render labelled rows.
- **list** panels (messages, database, view, cache) render labelled rows.
- **exceptions** panels render one collapsible entry per throwable; the summary line stays visible and expands to the stack trace.
- **logs** panels (logger) render one entry per logged item; an entry with context is collapsible - the level and message stay visible and expand to the context.

Collectors may expose a summary above their panel. The database collector reports the total query count, duplicate runs, and accumulated SQL time. A duplicate run is each execution of a normalized statement after its first execution: running the same statement three times contributes two duplicate runs. These metrics remain visible with zero values when no queries are executed. Duplicate detection deliberately ignores binding values; repeated statements are highlighted and show their total execution count.

A handle at the right of the tab row collapses the whole bar to a corner button, so it never covers the host page's own controls. The collapsed state is remembered across page loads.
201 changes: 200 additions & 1 deletion resources/assets/debugbar.css
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
}

#phalcon-debugbar.is-collapsed .phalcon-debugbar-body,
#phalcon-debugbar.is-collapsed .phalcon-debugbar-tabs {
#phalcon-debugbar.is-collapsed .phalcon-debugbar-history-browser,
#phalcon-debugbar.is-collapsed .phalcon-debugbar-tabs,
#phalcon-debugbar.is-collapsed .phalcon-debugbar-indicators {
display: none;
}

Expand Down Expand Up @@ -93,6 +95,67 @@
background: rgba(0, 0, 0, 0.25);
}

#phalcon-debugbar .phalcon-debugbar-indicators {
display: flex;
flex: 0 1 auto;
min-width: 0;
align-items: stretch;
}

#phalcon-debugbar .phalcon-debugbar-indicator {
display: inline-flex;
min-width: 0;
align-items: center;
gap: 6px;
padding: 7px 10px;
border-left: 1px solid #2b2b40;
color: #b9b9d4;
white-space: nowrap;
}

#phalcon-debugbar .phalcon-debugbar-indicator-icon {
width: 15px;
height: 15px;
flex: 0 0 auto;
fill: none;
stroke: #a78bfa;
stroke-width: 1.5;
stroke-linecap: round;
stroke-linejoin: round;
}

#phalcon-debugbar .phalcon-debugbar-indicator-value,
#phalcon-debugbar .phalcon-debugbar-request-method {
color: #ffffff;
font-weight: 700;
}

#phalcon-debugbar .phalcon-debugbar-request-control {
max-width: 40vw;
}

#phalcon-debugbar button.phalcon-debugbar-request-control {
background: transparent;
border: 0;
border-left: 1px solid #2b2b40;
font: inherit;
cursor: pointer;
}

#phalcon-debugbar button.phalcon-debugbar-request-control:hover,
#phalcon-debugbar button.phalcon-debugbar-request-control.is-active {
background: #26263a;
}

#phalcon-debugbar .phalcon-debugbar-request-method {
color: #a78bfa;
}

#phalcon-debugbar .phalcon-debugbar-request-uri {
overflow: hidden;
text-overflow: ellipsis;
}

#phalcon-debugbar .phalcon-debugbar-toggle {
flex: 0 0 auto;
padding: 7px 12px;
Expand Down Expand Up @@ -145,6 +208,142 @@
font-size: 13px;
}

#phalcon-debugbar .phalcon-debugbar-history-browser {
display: none;
max-height: 210px;
overflow: auto;
background: #101018;
border-bottom: 1px solid #2b2b40;
}

#phalcon-debugbar .phalcon-debugbar-history-toolbar {
position: sticky;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 7px 14px;
background: #181824;
border-bottom: 1px solid #2b2b40;
}

#phalcon-debugbar .phalcon-debugbar-history-title {
color: #d5d5e8;
font-size: 12px;
}

#phalcon-debugbar .phalcon-debugbar-history-actions {
display: flex;
gap: 6px;
}

#phalcon-debugbar .phalcon-debugbar-history-action {
padding: 3px 9px;
border: 1px solid #454563;
border-radius: 3px;
background: #29293d;
color: #d5d5e8;
font: inherit;
cursor: pointer;
}

#phalcon-debugbar .phalcon-debugbar-history-action:hover {
background: #353550;
color: #ffffff;
}

#phalcon-debugbar .phalcon-debugbar-history-action.is-danger {
border-color: #7f3445;
color: #ff9aac;
}

#phalcon-debugbar .phalcon-debugbar-history-action.is-danger:hover {
background: #542532;
}

#phalcon-debugbar .phalcon-debugbar-history-action:disabled {
opacity: 0.5;
cursor: default;
}

#phalcon-debugbar .phalcon-debugbar-history-list {
display: flex;
flex-direction: column;
}

#phalcon-debugbar .phalcon-debugbar-history-request {
display: grid;
grid-template-columns: 58px minmax(180px, 1fr) 48px 210px;
gap: 10px;
align-items: center;
padding: 5px 14px;
background: transparent;
border: 0;
border-bottom: 1px solid #232334;
color: #b9b9d4;
font: inherit;
text-align: left;
cursor: pointer;
}

#phalcon-debugbar .phalcon-debugbar-history-request:hover,
#phalcon-debugbar .phalcon-debugbar-history-request.is-selected {
background: #26263a;
color: #ffffff;
}

#phalcon-debugbar .phalcon-debugbar-history-request.is-selected {
box-shadow: inset 3px 0 #7c3aed;
}

#phalcon-debugbar .phalcon-debugbar-history-method {
color: #a78bfa;
font-weight: 700;
}

#phalcon-debugbar .phalcon-debugbar-history-uri {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

#phalcon-debugbar .phalcon-debugbar-history-status,
#phalcon-debugbar .phalcon-debugbar-history-time {
color: #8f8faa;
}

#phalcon-debugbar .phalcon-debugbar-history-loading,
#phalcon-debugbar .phalcon-debugbar-history-empty,
#phalcon-debugbar .phalcon-debugbar-history-error {
padding: 8px 14px;
color: #8f8faa;
}

#phalcon-debugbar .phalcon-debugbar-history-error {
color: #ff6b81;
}

@media (max-width: 760px) {
#phalcon-debugbar .phalcon-debugbar-indicator {
padding-right: 7px;
padding-left: 7px;
}

#phalcon-debugbar .phalcon-debugbar-request-control {
max-width: 32vw;
}

#phalcon-debugbar .phalcon-debugbar-history-request {
grid-template-columns: 52px minmax(120px, 1fr) 42px;
}

#phalcon-debugbar .phalcon-debugbar-history-time {
display: none;
}
}

#phalcon-debugbar table {
width: 100%;
border-collapse: collapse;
Expand Down
Loading
Loading