The Exp Debug bar
Read this page if you use the debug report, if you restrict debug output by IP address, or if your scripts or stylesheets work with the classic report. From Exponential 6.0.15 the debug report at the bottom of a page is the Exp Debug bar: a pinned bar with a live summary of the request and a panel with tabs over the report's sections. Its Settings tab shows every debug setting, the value in effect and the file it comes from, and changes it in the scope you choose (global override, a siteaccess or an extension). Every change is written through the exp:ini engine (console-exp-ini.md), logged, and can be undone from the bar.
In short
| What changed | The HTML debug report is the Exp Debug bar (lib/ezutils/classes/expdebugbarreport.php). New registry settings/debugbar.ini, server functions expdebugbar::*, write log var/<site>/log/debugbar.log. DebugIPList[] entries may carry a label and an expiry, and CIDR ranges of both address families match. |
| Who is affected | Sites that list a plain IPv6 address in DebugIPList[]: before, any IPv6 client got debug output as soon as the list had one; now that entry means that one address. Users who change debug settings from the bar: they need setup/setup (cache controls: setup/managecache). Custom CSS or tests that match the old heading text (it now reads "Exp Debug"). |
| How to check | grep -n "Settings\[\]" settings/debugbar.ini | head and php vendor/bin/phpunit tests/tests/kernel/classes/debugbar/ |
| How to fix | Add every IPv6 address or network that needs debug output to DebugIPList[] explicitly. Give setup/setup to roles that debug. Section ids are unchanged, so scripts written for the classic report keep working. |
Text reports (Debug=inline on the command line) and the popup window are unchanged. An engine archive without
expdebugbarreport.php prints the classic report.
This page describes the server side first (the registry, values and scopes, writes, the log and undo, the IP list, the summary, the server functions, access and the RAD survey), then the bar itself.
| Part | Where |
|---|---|
| Registry | settings/debugbar.ini, and extension/<ext>/settings/debugbar.ini.append.php |
| Settings registry | expDebugBarRegistry (kernel/classes/debugbar/expdebugbarregistry.php) |
| Values, writes, undo, presets | expDebugBarSettings (kernel/classes/debugbar/expdebugbarsettings.php) |
| IP list engine | expDebugBarIPList (kernel/classes/debugbar/expdebugbariplist.php), used by eZDebug::isAllowedByCurrentIP() |
| Write log | expDebugBarLog (kernel/classes/debugbar/expdebugbarlog.php), var/<site>/log/debugbar.log |
| Summary | expDebugBarSummary (kernel/classes/debugbar/expdebugbarsummary.php) |
| Server functions | expDebugBarServerFunctions, [ezjscServer_expdebugbar] in extension/ezjscore/settings/ezjscore.ini |
| Tests | tests/tests/kernel/classes/debugbar/ |
The settings
settings/debugbar.ini lists the settings in [DebugBarSettings] Settings[], and describes each in a block of
its own:
[Setting_debug_output]
File=site.ini
Block=DebugSettings
Variable=DebugOutput
Type=bool
Label=Debug output
Group=output
Help=Shows the debug report (this bar) at the bottom of every page for those who get debug.| Key | |
|---|---|
File, Block, Variable | the setting |
Type | bool, enum (with Values[]), list (optionally Values[]: what may be added), text, iplist, userlist |
Label, Group, Help | what the bar shows; Group is one of Groups[] (GroupNames[<id>] names it) |
On, Off | bool only: the words written. Without them, the words of the value in effect are used (enabled/disabled, true/false, keeping Enabled or TRUE) |
The kernel registers 35 settings in nine groups: debug output (DebugOutput, Debug inline/popup, DebugRedirection, DisplayDebugWarnings, DebugLogOnly, DisplayIncludedFiles, AlwaysLog[], ScriptDebugOutput, DebugToolbar, REST Debug), who gets debug (DebugByIP, DebugIPList[], DebugByUser, DebugUserIDList[]), templates (Debug, ShowXHTMLCode, ShowUsedTemplates, ShowTemplatePathComments, ShowMethodDebug, DevelopmentMode, TemplateCompile, TemplateCache, CompileAccumulators, CompileTimingPoints, CompileComments), database (SQLOutput, QueryAnalysisOutput, SlowQueriesOutput, DebugTransactions), translations (RegionalSettings Debug and DevelopmentMode), mail (DebugSending, DebugReceiverEmail), debug conditions (debug.ini ConditionDebug), caching (ViewCaching).
Two kinds of settings are found rather than registered:
DiscoverBlocks[]=<file>;<block>;<group>: every bool variable of the block, as it is in effect. The kernel listsdebug.ini;GeneralCondition;conditions, so every debug condition (kernel-content-view,kernel-urltranslator, ... and the ones extensions add) is a switch, with the idcond_<variable>.- Extension debug switches (
ExtensionDiscovery=enabled): in every active extension'ssettings/*.ini,*.ini.append.phpand*.ini.append, a variable whose name matchesDiscoveryPattern(/debug/i) and whose value is a bool word. Its id isext_<ext>_<file>_<block>_<variable>, its groupextensions.DiscoveryExclude[]=<ext>or<ext>;<file>leaves some out. A setting the registry has already is not found twice.
On a reference installation the bar has 82 settings: 35 registered, 43 debug conditions, 4 extension switches (cjw_newsletter, owsimpleoperator, bccie).
An extension's settings
An extension registers its own in extension/<ext>/settings/debugbar.ini.append.php; eZINI merges it into the
kernel file, and the setting is marked as the extension's:
[DebugBarSettings]
Settings[]=myext_trace
[Setting_myext_trace]
File=myext.ini
Block=TraceSettings
Variable=Trace
Type=bool
Label=Trace calls
Group=extensions
Help=Writes every call of the myext API to var/log/myext.log.
*/ ?>Presets
Presets[] names [Preset_<id>] blocks: Name, Description, Values[<setting id>]=<value> (for a bool on
or off). The kernel has Template work, SQL tuning, Everything and Off. A user saves presets of
their own from the bar (stored in their preferences, expdebugbar_presets), either with values given or as a
snapshot of the bool and enum settings in effect.
Applying a preset writes each value that differs from the one in effect as a write of its own, all in one group, so the group can be undone at once. If one of the writes needs a confirmation (below), nothing is written.
Values in effect, origin and scopes
For a siteaccess (the current one by default), expDebugBarSettings::describe() gives:
- effective: eZINI's own value, from an uncached eZINI with that siteaccess's override directories
(
expIniLocator::iniFor()), so a write is seen at once, before the ini cache is cleared; - files: every file of the load order that sets the variable, with its scope;
- origin: the last of them, the file whose value wins;
- default: the value of
settings/<file>.ini; - scopes: the value each scope's file gives (
null: it does not set it).
The scopes the bar offers are global (settings/override), every siteaccess, every active extension, and the
extension siteaccess directories of the siteaccess. The default scope is the siteaccess. The shipped defaults
(settings/*.ini) are never written. Describing all 82 settings reads each INI file once and takes about 140 ms.
Writes
expDebugBarSettings::write( $id, $op, $value, $scope, $options ):
| op | for | does |
|---|---|---|
set | bool, enum, text | bool takes on/off/1/0/true/false/enabled/disabled/yes/no and writes the setting's own words |
toggle | bool | flips the value in effect |
unset | all | removes the variable from that scope's file (the next file in the load order then wins) |
add, remove | lists | one value; for an IP list remove takes the address and finds the line with its label |
replace | lists | Var[] (the reset line) and the values |
The editor (expIniEditor) changes only the lines of the setting, keeps the file's owner, group and mode, writes
atomically and copies the old file to var/backup/ini/<stamp>/<path> first. After the write the ini cache tag is
cleared. PHP-FPM sees the change on its next request; Velocity's workers keep the settings of their warm-up and see
it after exp:velocity restart, which the answer says.
dry_run returns the diff and writes nothing. A value that the editor refuses (a line break, ##, */ in a PHP
wrapped file) is refused with its reason.
Never locking yourself out
A write to DebugByIP, DebugIPList[], DebugByUser, DebugUserIDList[] or DebugOutput is checked against
the values in effect after it, computed by replaying the load order with the new file. It is refused with
needs_confirm when:
- lockout: DebugByIP would be enabled and the list would not include the current request's address (an expired entry does not count), while it does get debug output now; or DebugByUser would be enabled and the user list would not include the current user;
- open: the list would get an active
/0entry with DebugByIP on, or DebugByIP would be switched off while DebugOutput is on and DebugByUser off, so every visitor would get debug output.
Sent again with confirm=1, the write goes ahead.
The IP list
DebugIPList[] entries keep their plain format and may carry a label and an expiry:
DebugIPList[]=203.0.113.7
DebugIPList[]=10.0.0.0/8
DebugIPList[]=2001:db8::/32 ; VPN
DebugIPList[]=203.0.113.7/32 ; Laptop ; expires=2026-10-02T15:00
DebugIPList[]=commandline- Addresses and networks of both families, CIDR prefixes at any bit (
198.51.100.64/26,2001:db8:abcd:12::/63). A client that a dual-stack server reports as::ffff:203.0.113.9matches IPv4 entries. - A label is any text without
;,##,*/or a line break (label=before it is optional). expires=takesYYYY-MM-DD,YYYY-MM-DDTHH:MM[:SS], withZor an offset; without one, the server's time zone. The bar writes+1h,+30m,+2d,today(23:59) andtomorrowas that date. An expired entry no longer matches, and stays in the list until someone removes it. An expiry that cannot be read never matches.
eZDebug::isAllowedByCurrentIP() asks expDebugBarIPList::match() whenever the class is loadable, so the bar's
test and the real check are the same code, and keeps the entry that matched in $GLOBALS['eZDebugIPMatch'] for
the bar. A process that cannot load the class (a Velocity worker started before the class existed) takes the
address part of each line and leaves expired entries out, so labels and expiries never break the old check.
One behaviour changed with the engine: the old check let every IPv6 client in as soon as the list had any plain IPv6 address (it compared the entry with itself). A plain IPv6 entry now means that one address.
expdebugbar::iptest shows, for an address (default: the request's), whether the list lets it in, which entry
matched, every entry parsed, the address as the server sees it (REMOTE_ADDR) and as the kernel takes it
(eZSys::clientIP(), which reads ClientIpByCustomHTTPHeader such as X-Forwarded-For only when that setting is
set), the entries to suggest (/32 and /24, /128 and /64), and warnings: invalid, expired, open (a
/0 entry), empty (no active entry) and lockout (the list does not include the address).
The write log and undo
Every write is one JSON line in var/<site>/log/debugbar.log (LogFile in debugbar.ini): id, time, user id,
login, address, op, setting, file, block, variable, scope, path, the value written, the value in that file
before and after, the value in effect before and after, the backup, what the write created (file, block,
variable), and the sha1 of the file before and after. Cache clearing from the bar is logged too (op cache).
A log the bar creates gets the owner and group of the log directory and mode 0640.
undo( <entry id> ) applies the inverse through the editor: the old value set again, the variable removed again,
a list given back in its old order, an added value removed, and a block the write created removed when it is
empty. The answer says whether the file is now byte for byte its backup (byte_identical), which it is whenever
the writes after it were undone first. An undo is refused when the setting in that file is no longer what the
write left (another write changed it; force=1 reverts it anyway), when the entry was undone already, or when it
changed a secret (the log has no secret values). The undo is logged with undoes naming the entry.
undoGroup( <group> ) undoes a preset, newest write first.
The summary
expDebugBarSummary::collect() gives what eZDebug measured: the page time, the SQL statements and their time (the
*_query accumulators of the database drivers, or eZDB's counter), the peak memory and the limit, the templates
used, messages per level, included files, the accumulators and the timing points, who gets debug and the IP entry
that matched, the engine (Velocity or the SAPI), the siteaccess, and the SQL query cache of the request
(querycache: eZDBQueryCache::debugSnapshot() with the stale statements replaced by their number, stale_count;
null while the cache is off). Every figure has a level, ok, warn
or high, against Thresholds[] in debugbar.ini (time_ms, sql_count, sql_ms, memory_mb, templates, each
<warn>;<high>; opcache_memory and apcu_memory are the memory in use in percent, opcache and apcu in the
summary, null while they are off or for a user who may not read them (ReadAccess); querycache_hit_rate is a hit rate in percent
and turns the level up below its values, see
expDebugBarReport::queryCacheLevel(), the one place of these rules: expDebugBarSummary::queryCache() and the bar's own fallback both build the figures with expDebugBarReport::queryCacheFigures()). expDebugBarSummary::json() is the same as JSON that is safe inside a <script> element.
Server functions
/ezjscore/call/expdebugbar::<function> (with ?ContentType=json). ezjscore answers
{"error_text": "", "content": ...}; a refusal is in error_text.
| Function | Request | Needs |
|---|---|---|
settings[::<siteaccess>] | GET | to read: setup/setup, or the debug output under ReadAccess debug; can_write tells whether the answer may be changed |
set | POST setting, op, value, scope [siteaccess, confirm, dry_run] | setup/setup |
undo | POST entry or group [force] | setup/setup |
preset | POST preset + scope (apply), save [+ values JSON or snapshot=1], or delete | setup/setup |
cache | GET action=list; POST action=clear, by=all|tag|id|node|velocity|opcache|querycache_counters, names, node_id | list: setup/managecache, or the debug output under ReadAccess debug; clear: setup/managecache |
iptest | address, list (JSON array) | setup/setup, or the debug output under ReadAccess debug |
log[::<limit>] | GET | setup/setup, or the debug output under ReadAccess debug |
summary | GET | debug output for the request (the report itself; a visitor gets no IP list entry in it) |
policies[::<role>[::<offset>[::<search>]]] | GET; everything in the path: a role id or -, the offset, the search as base64url of its UTF-8 text or - (expdebugbar::policies::-::0::Y29udGVudC9yZWFk searches content/read) | setup/setup, or the debug output under ReadAccess debug: the policies of the current user's roles, a page of 200 rows (see User) |
session | GET | who sees the User tab (setup/setup; whoever gets the debug output under ReadAccess=debug, under privileged an anonymous visitor with debug output): the request's own PHP session, keys as a tree, grouped by extension, secrets masked, and its frame (see User) |
panelheight | POST height (pixels, 0 for the default) | a logged-in user (never an anonymous visitor) with debug output for the request: stores the user preference expdebugbar_panel_height (see The panel's height) |
Every POST must carry the form token (ezxform_token, or the header X-CSRF-Token); the settings answer has
it. Someone who only sees the debug report gets the settings without the IP and user lists, the log or the
cache list: who gets debug output and who changed what are for those who may change them. Secret values are
masked as in exp:ini.
cache with by=node clears the view cache of one node, the page the bar is on
(eZContentCacheManager::clearNodeViewCacheArray()); velocity clears Velocity's response cache; opcache
resets the OPcache of the process that answers; querycache_counters zeroes this server's query cache counters (the
cached results themselves are the cache id querycache). The list gives every cache with how it is cleared
(expCacheManager), the tags, when each was last cleared from the bar, Velocity's cache status, the OPcache state
and the SQL query cache (querycache: eZDBQueryCache::inventory(), settings, this server's counters, the APCu
memory, the entries grouped by the tables they read with rows, size and hits, and the largest entries; an entry
carries no statement text, only the start of its key). The inventory reads the entries of the cache in APCu with their rows (at
most 5,000 and for at most one second; it says when it stopped early), so it runs only when the Cache tab is opened.
The summary (expdebugbar::summary, the page's JSON) carries only the number of writes that made everything stale
(stale_count); the statements are in the report of the SQL tab. inventory() leaves out the state file's path itself. After a clear or a reset of the counters the section is read again. The query cache is no longer listed a second time under "By ID".
Example (replace the host names and YOUR_USER:YOUR_PASSWORD with your own; the second call is made without signing in):
curl -s -u YOUR_USER:YOUR_PASSWORD 'https://admin.example.com/ezjscore/call/expdebugbar::iptest?ContentType=json'
{"error_text":"","content":{"address":"...","valid":true,"family":4,"remote_addr":"...","client_ip":"...",
"trusts_proxy":false,"proxy_header":null,"suggest":{"self":".../32","network":".../24","family":4},
"debug_by_ip":false,"allowed":false,"matched":null,...}}
curl -s 'https://www.example.com/ezjscore/call/expdebugbar::log?ContentType=json' # from an address without debug output
{"error_text":"Sign in with setup access to change debug settings (policy setup\/setup)","content":""}RAD
The extension point survey (setup/radsurvey) reads two registries out of debugbar.ini: Debug bar settings
(every [Setting_<id>], checked for a valid Type) and Debug bar presets (every [Preset_<id>], checked
for a name), kernel and extension files alike. Their entries name no class, so they are added to the total once.
The survey also carries the bar's own figures, which are not added again: debugbar_settings,
debugbar_registered, debugbar_extension_registered, debugbar_discovered, debugbar_presets,
debugbar_problems (expRADSurvey::debugBar(), which is expDebugBarRegistry::survey()).
Tests
php vendor/bin/phpunit tests/tests/kernel/classes/debugbar/| Test | Covers |
|---|---|
expDebugBarReportPanelHeightTest (tests/tests/lib/ezutils/) | the panel height: the preference name and limits, panelHeight() on whole numbers, text, fractions, negatives and values out of range, savedPanelHeight() with a reader passed in and never asking it for an anonymous visitor, the data attributes on the rendered panel (no style attribute), the handle's styles and keys, the server function's checks. No database |
expDebugBarIPListTest | addresses and CIDR of both families, IPv4-mapped clients, entries with labels and expiries, writing entries, the match, the warnings, the suggestions. Needs no kernel |
expDebugBarSettingsTest | the registry and discovery, the values in effect against expIniLocator::where(), writes of every type and op with the log and undo, dry runs, refusals, a siteaccess scope, presets as a group, the lock-out and open checks, eZDebug's IP check and its fallback, the summary, the server functions (registration, answers, POST, token and policy, what a visitor without policy sees) |
RadSurveyDebugBarTest | the two registries, the total, the bar's figures, the new problem kinds |
expDebugBarPoliciesTest | the policies of the User tab (UP-01 to UP-04, UP-06 to UP-08): rows and limitation values as one sorted list, the selection by role and by search (module/function, role name, class name, limitation, stored value), a search for a module and function that also finds the wildcards granting it, a row with readable values, the section only for those allowed, with the address for the role links, the script's words, the queries in chunks of 500 ids, 600 policies of 300 roles. No database |
expDebugBarUserTest | the User tab: the user's parts of the view cache key and each ignore tweak, the preferences key against the code it replaced, the preferences of every view mode (one name with two defaults), the fingerprint, the profile from a view's key (parts a listener left out or changed, pr_user), anonymous visitors, the chip, the tab escaped, the copy text. No database |
expDebugBarUserPoliciesTest | who sees the User tab, and how its policies are asked for (UP-05 under both values of ReadAccess, UP-09, UP-10, UP-11 ReadAccess and the cache list without its form for a reader, UP-12 a reader by the debug output gets nobody else's data): the arguments of expdebugbar::policies in the path and what is refused, the script's request without a query string. No database |
expDebugBarSessionTest | the section Session of the User tab (US-01 to US-06): keys grouped by the extension that prefixes them, the kernel first; secrets masked with everything below them, the masks of debugbar.ini or the defaults, an invalid mask taken as text; a node's type, size, cut strings, binary as hex, objects with every property; the limits of depth, children and entries; the shortened session id; the section offered to whoever sees the tab, the script's words and request. No session |
expDebugBarSettingsTest starts the kernel once on the admin siteaccess with the installation's own database, as
the subitems column tests do; no test database is created. Its writes go to a probe block
([ExpDebugBarTestProbe] of site.ini) in settings/override and settings/siteaccess/admin, with the ini cache
left alone, and every test undoes its writes and checks the files byte for byte against their state before the
test (and puts them back if they differ). Its log is a file of its own under var/tmp/debugbar-test/.
The bar
| Part | Where |
|---|---|
| Report markup, summary, data block | expDebugBarReport (lib/ezutils/classes/expdebugbarreport.php), called by eZDebug::printReportInternal() for HTML reports |
| Open and close, "Keep open on reload", saved panel height, minimised bar (runs at once, before the first paint) | design/standard/javascript/expdebugbar-head.js |
| Tabs, filter, settings, IP list, presets, cache, change log | design/standard/javascript/expdebugbar.js |
| Styles (light and dark) | design/standard/stylesheets/expdebugbar.css |
Toolbar theme (Theme=toolbar) | design/standard/stylesheets/expdebugbar-toolbar.css, linked after it |
| Words | context design/standard/debugbar in share/translations/*/translation.ts (eng-US, ger-DE) |
The report links the script and the stylesheet itself, so every design gets the bar: admin, admin2, admin3, admin4
and the public designs, with or without debug.css. The links are plain paths, with no ?v= or other version
query (owner rule); after a change to one of the files a browser that still shows the old one is reloaded by hand.
Text reports (Debug=inline on the command line) and the popup window are unchanged. Without
expdebugbarreport.php (an older engine archive) eZDebug prints the classic report.
No inline CSS
Neither report prints a <style> element or a style="" attribute (AGENTS.md "Admin design and template styling
rules"), and no inline script: the part that must run before the first paint is expdebugbar-head.js, linked
without defer, so a Content-Security-Policy with script-src 'self' holds. The bar's markup is complete at the first
paint (the minimise symbol, the link, the chips, "Keep open on reload" and the hidden tab that restores the bar are
all printed by expDebugBarReport::barHtml()), the bar keeps at least the height of one line, and its sides are
logical (margin-inline-*, inset-inline-*), so it does not grow or move after load and its link stays at the end
of the line in a right-to-left interface (#571).
- The bar: every rule, the pinned bar and the panel above it included, is in
expdebugbar.cssand takes its colours from the bar's--xd-*tokens, each with a value forprefers-color-scheme: darkand for admin4'shtml[data-a4-theme="dark"]. The report links the stylesheet before the bar, so the browser has the rules before it draws the bar; no pre-paint rule is needed. The two heights the script measures or restores at run time are custom properties on the root element (--exp-debug-bar-h,--exp-debug-panel-h), not style attributes in the markup. - The classic report: its rules are in
debug.css, which the report links itself (or the design loads, witheZDebug::setUseExternalCSS( true )), scoped to#debug:not([data-exp-debug-bar])with a small copy of the tokens. Its bar follows the colour scheme; its panel keeps the light rows of the classic table rules in both. - The tables of both: the figures of the timing points and the time accumulators are
td.num(right aligned in equal digits), the time of a message headingtd.debugtime, the headingtd.debuglabel, an accumulator group's nametd.groupname, and the count rows of the included files and the template usagetr.summary; no<b>or<i>. eZDebug::$UseDebugBar(default true) set to false prints the classic HTML report; the test below uses it.
expDebugReportNoInlineCssTest (tests/tests/lib/ezutils/, no database) renders the bar and the classic report
with messages, timing points and accumulators, and fails on any style="" attribute, any <style> element and any
asset URL with a ?v= query; it also checks the query cache report's source, the plain-text report, and that every
--xd-* token has its dark values.
The pinned bar
The bar stays at the bottom of the window wherever the page is scrolled; a click on "Exp Debug" opens the panel
above it, which scrolls on its own. No page ever has to be scrolled to use it. "Keep open on reload" remembers the
open panel in the browser (localStorage keys exp-debug-keep, exp-debug-open), and the last tab
(exp-debug-tab); in the default theme it reads "Keep open", the whole words as its tooltip. Escape closes the panel.
Themes
debugbar.ini [DebugBarSettings] Theme sets how the pinned bar looks; the panel above it is the same in both. It is
also a setting of the bar (Settings tab, Debug output, "Bar theme").
| Value | The bar |
|---|---|
default | The summary as light chips (dark in a dark colour scheme), the details of a chip in its tooltip (title). It starts with the request: the HTTP status and module/view, filled green, amber for a redirect (3xx but 304), red from status 400; it opens the Rendering tab, or the Server tab for a user who does not see it. Also any value that is not toolbar. |
toolbar | A dark, compact toolbar in both colour schemes, in the manner of the Symfony web debug toolbar: each block with an icon, the first block the request (HTTP status and module/view, green, amber for a redirect (3xx but 304), red from status 400; it opens the Rendering tab, or the Server tab for a user who does not see it), amber and red blocks filled with their colour, and the details of a block in a popup above it while the pointer or the keyboard focus is on it. "Keep open on reload" shows its box alone, the words as its tooltip. "Exp Debug", the switch of the panel, is a lighter block of its own at the right end, filled orange while the panel is open; its arrow points up, and down while it is open. |
In the toolbar theme a chip carries its text in .exp-debug-chip-text and its popup in .exp-debug-chip-info
(aria-hidden, the chip's aria-label says the same) and has no title. The popup lists the rows a chip gives
('rows', label => value: page and query time, queries and their share of the page time, peak memory and the
memory limit, templates, the counts of errors, warnings, notices and deprecations, the request's status, module,
view, siteaccess and node), or the chip's title alone. A chip of an extension's panel gets a generic icon; the
icons are Bootstrap Icons (share/icons/bootstrap-icons, MIT) inlined in the stylesheet as masks, so no further file
is loaded. The status is http_response_code() when the report is rendered; where PHP does not know it (the command
line, a persistent worker) the block shows module/view only.
The panel in the toolbar theme follows the Symfony profiler, from a window width of 761 px on: the tabs are a dark menu
down the left side (an icon each, the count at the right, the open tab marked, the filter box above them; the up and
down arrows move between them), the open tab has a large title, and the tabs whose chip has rows (Messages, Timing,
SQL, Templates, Memory, Server) start with those figures as metrics in large type (expDebugBarReport::metricsHtml(),
a <dl class="exp-debug-metrics">). Tables have open rows with a line between them; a message has a bar in the colour
of its level. The panel's colours stay those of expdebugbar.css, light and dark. Narrower windows keep the row of
tabs above the panel.
The panel's height
The open panel is full width, so only its height changes. The strip on its top edge (the edge facing the page, the
panel opens upward from the bar) is a handle: drag it with the mouse, a pen or a finger, or focus it (it is a
role="separator" with aria-valuenow, aria-valuemin and aria-valuemax) and use the keys:
| Key | Does |
|---|---|
| Arrow up / down | 16 px higher / lower |
| Shift + arrow up / down | 64 px higher / lower |
| Home / End | the lowest / the highest the window allows |
| Double-click on the handle | back to the default height (60 % of the window) |
The height stays between 120 px and the window's height minus the pinned bar, also when the window changes size (a
height kept from a taller window comes back when the window is tall enough again). The stylesheet holds the same
limit (height: min(var(--exp-debug-panel-h, 60vh), calc(100vh - var(--exp-debug-bar-h)))), so a saved height never
pushes the panel's top out of the window.
The height is saved once at the end of a drag, or 0.6 s after the last key press:
- Logged in: in the user preference
expdebugbar_panel_height(eZPreferences, throughexpdebugbar::panelheight), so it follows the user to any browser.0means the default height. The report reads it back (expDebugBarReport::savedPanelHeight()) and prints it on the panel asdata-exp-debug-panel-height; the report's first script (expdebugbar-head.js) sets--exp-debug-panel-hfrom it before the panel opens, so it opens at that height without a jump. - Anonymous (the bar shown by IP): no preference is read or saved and no request is sent, since eZPreferences
keep an anonymous user's values in the session and the debug bar must never start a session
(
savedPanelHeight()returns 0 without asking, andexpdebugbar::panelheightrefuses an anonymous visitor before the token check). The height is kept in the browser instead:localStoragekeyexp-debug-panel-height, every access intry/catch(blocked storage means the default height), read back and held between 120 and 4000 px byexpdebugbar-head.jsbefore the panel opens; anything but a whole number of pixels is ignored. It stays in that browser only; another browser or a private window starts at the default. On a siteaccess that requires a sign-in (the admin),ezjscore/callneeds a signed-in user since 6.0.15, so on an anonymous admin page (the sign-in page) the panels that ask the server (cache, log, summary) show "Could not load"; the panel height works as before, since it sends no request.
A logged-in user's preference is authoritative: localStorage is not looked at for a logged-in user. The panel says
which applies in data-exp-debug-panel-store (preference or browser).
A page that carries the debug report is never kept by the page cache (X-Exp-Cache: BYPASS (debug report), see
What is never cached): the report and the saved height are rendered for each
request. Before 2026-10-14 a signed-in user's front page came from that cache with the report of an earlier request in
it, so a reload showed the height from before the last resize.
A stored value is a whole number of pixels: expDebugBarReport::panelHeight() turns anything else (text, a
fraction, a negative number, a unit) into "not a height" (the server function refuses it, the report uses the
default) and holds a number between 120 and 4000 (PANEL_HEIGHT_MIN, PANEL_HEIGHT_MAX).
The summary in the bar shows the request at a glance; each value opens its tab:
| Value | Tab | Amber / red from |
|---|---|---|
| page time | Timing | [DebugBarSettings] Thresholds[time_ms] (default 1 s / 3 s without the server side) |
| SQL count / time | SQL | Thresholds[sql_count], Thresholds[sql_ms] |
| peak memory | Memory | Thresholds[memory_mb] (else 50 % / 80 % of memory_limit) |
templates used (only with ShowUsedTemplates) | Templates | Thresholds[templates] |
SQL query cache (QC 83 % · -186 ms (186 %): hit rate, database time saved, and that time in percent of the page's time; only while the cache is on) | SQL | Thresholds[querycache_hit_rate] (amber below 50 %, red below 10 %; amber when a write made everything stale, red in shared without APCu) |
| warnings, errors | Messages (filtered to that level) | any warning is amber, any error red |
deprecations (3 depr) | Messages | any deprecation is amber |
OPcache memory in use (OPc 66 %, the title with MB, hit rate, scripts and restarts; only while OPcache is on for the process) | Server | Thresholds[opcache_memory] (amber from 80 %, red from 95 %) |
APCu memory in use (APCu 25 %, the title with MB, entries, hit rate and expunges; only while APCu is on for the process) | Server | Thresholds[apcu_memory] (amber from 75 %, red from 90 %) |
versions (PHP 8.4.19 | Exp 6.0.15, Exponential shortened to Exp; the last chip, at the right end) | Server | never coloured |
The OPcache, APCu and versions chips show to whoever gets the debug output under ReadAccess debug, the default;
under privileged only to a user with setup/setup or setup/managecache, who also sees the privileged rows of the
Server tab (process, host, the cache list). APCu is one memory for every site of the PHP pool; at 75 % in use the
SQL query cache has already stopped adding results (querycache.ini ApcuReserve=25). The Server tab lists APCu
below OPcache. A process without them (the command line, where apc.enable_cli is usually off) shows no chip for them. The three are the last chips, at the right end of the summary.
PHP deprecations are only counted in the bar: with site.ini [DebugSettings] DeprecatedLog=enabled (the default)
each distinct message is written once per request to var/log/deprecated.log, and none is kept in memory or listed
in the Messages tab. They are still part of the warning count of the page (the "Some general warnings occurred" box
of DisplayDebugWarnings). The Settings tab switches DeprecatedLog ("Deprecations to deprecated.log"); off,
deprecations are warnings again, listed one by one. See
Deprecations and log sizes.
The levels come from expDebugBarSummary when it is loaded, so the thresholds in debugbar.ini apply; otherwise
the report's own defaults.
The tabs
| Tab | Shows |
|---|---|
| Messages | the notices, warnings, errors, debug and timing messages, with buttons per level; the SQL statements are moved to the SQL tab |
| Settings | the settings UI below, and under "Classic controls" the classic toolbar (design:setup/debug_toolbar.tpl, or a site's override of it, rendered as before) |
| Cache | the cache controls below; without the server functions a form that clears this page, content, template, INI or all caches through setup/cachetoolbar |
| Timing | the timing points and the time accumulators, as before (#timingpoints, #timeaccumulators) |
| SQL | number of queries, time, share of the page, average, the database's accumulators, and the statements (SQLOutput) with "Slowest first" |
| Templates | the templates used (ShowUsedTemplates) and the CSS/JS packer report, or how to turn them on |
| Included files | the included PHP files (DisplayIncludedFiles), or their number and how to list them |
| Memory | the classic "Main resources" table, memory now, peak, limit and share, and the five steps that took the most |
| Rendering | how the page was rendered: the controller, the content view cache and its key, the compiled templates (see below); for whoever gets the debug output under ReadAccess debug, and for setup/setup |
| User | the user, their cache profile (the parts of the content view cache key that depend on them), their preferences, role assignments, on demand their own PHP session (secrets masked) and, on demand and for whoever may read the settings, the policies of those roles (see below); for whoever gets the debug output under ReadAccess debug, and for setup/setup |
| (extensions) | a tab per panel an extension registers, after Memory (see Panels of extensions) |
| Server | where the request entered and which process rendered the report (Exponential Velocity, PHP-FPM, mod_php, the command line ...), in one sentence (see below), the facts behind it, OPcache status, and Velocity's response cache; called Velocity until 16 October 2026, its section id is still exp-debug-panel-velocity |
| Other | reports appended by extensions with eZDebug::appendBottomReport() that belong to no tab (only when there are any) |
Every section keeps its id, so scripts and stylesheets written for the classic report still find it.
The filter box above the tabs filters the rows of every tab at once (messages, timing rows, accumulators, statements, files, settings, caches); a tab with no match is dimmed and says so. Escape in the box clears it.
Settings
Drawn from expdebugbar::settings when the tab is first opened. Per group (Debug output, Who gets debug,
Templates, Database, ... and the extension switches), each setting shows its name, file, block and variable, its
help, a control for its type (switch, list of values, text, one entry per line), the value in effect and where it
comes from (default, global override, siteaccess, extension; the file is in the tooltip), and a "Write to" picker
with every writable scope and the value that scope's file gives. Apply writes the value to the chosen scope,
Reset removes the variable from that scope. A write that would lock you out or open debug to everyone asks first.
"siteaccess" at the top changes whose values in effect are shown. Presets (Template work, SQL tuning, Everything, Off, and the user's own) are applied to the scope picked next to them and can be reverted at once; "Save current as preset" stores the values in effect under a name.
The change log lists the last writes (who, which setting, old and new value, scope) with Undo; an undo that finds the file changed since asks before forcing it. A user who may read the settings but not change them (ReadAccess) sees every value, the lists and the log with the controls disabled and the note "Sign in with a user who may change settings (setup/setup) to change them here."; the Cache tab lists the caches without the buttons that clear them. A user who may not read them sees only the note "Sign in with setup access to change debug settings", or in the Cache tab "Sign in with cache access to manage caches".
Who reads what: ReadAccess
debugbar.ini [DebugBarSettings] ReadAccess decides who reads what the bar shows; changing settings and clearing
caches always need setup/setup and setup/managecache, with the form token (expDebugBarRegistry::readAccess()).
| ReadAccess | Who reads |
|---|---|
debug (the default) | whoever gets the debug output (DebugByIP, DebugByUser), logged in or not, reads everything but other people's data: the settings with their values (the IP and the user list masked), the change log without who made each change, the IP test for their own address only, the cache list, the policies of their roles, the session's save path and handler class, the User and the Rendering tab, the versions of PHP and Exponential, OPcache and APCu, the process and the host |
privileged | the stricter rule: the settings, the log, the IP test and the policies for setup/setup, the cache list for setup/managecache, the versions, OPcache and APCu for either; the User and the Rendering tab for setup/setup and for an anonymous visitor who gets the debug output |
Who gets the debug output is decided by the IP and the user list (below): under debug they are the list of who reads
the bar. A site that gives the debug output to visitors it does not trust with the configuration sets
ReadAccess=privileged.
Warning: debug output on a public address shows each visitor their own session. With DebugOutput=enabled and
DebugByIP disabled (or an IP list that matches public networks) every visitor gets the debug output, and under
ReadAccess=debug reads the User tab with their own PHP session (secrets masked by name and by value), their roles,
policies and preferences, and the site's debug settings with their file paths. Nobody ever reads another visitor's
session or user data: the session answer is the request's own session only, no-store, and for a signed-in user
a POST with the form token; a reader without setup/setup gets the change log without who made each change (user
id, login and address), the IP and the user list masked and the IP test only for their own address
(expDebugBarServerFunctions::readsWithoutPolicy(), forReader(), listsMasked(), ownIpAnswer()). Still,
set ReadAccess=privileged in production, and keep the debug output to an IP or user list.
Who gets debug: the IP list and the user list
The IP list (DebugIPList[]) shows the address the server sees for this request (and REMOTE_ADDR when the site
trusts a proxy header), the entry this request matched, and each entry with its label, expiry and whether it
matched. "Add my IPv4 address" / "Add my IPv6 address" and "Add my /24" / "Add my /64" add the request's own
address or network; the CIDR field takes IPv4 and IPv6 addresses and ranges and says at once what is wrong
(prefix over 32 or 128, malformed address, already listed); each entry gets a label and an expiry (1 hour, today,
until removed). "Test an address" asks the server whether an address would match. Warnings say when the list
would lock you out, is empty while "Debug by IP" is on, or opens debug to every address.
The user list (DebugUserIDList[]) takes one user ID per line, "Add me" adds the signed-in user.
Cache
Drawn from expdebugbar::cache: "This page only" (the view cache of the node the page shows), Velocity's
response cache, OPcache of the serving process, every cache tag and every cache ID with its description and when
it was last cleared, and all caches. Each clear says what it did.
Rendering: controller, content view cache and compiled templates
The tab answers "why is this page slow or not cached" without reading the code. Whoever gets the debug output sees it
and its chips under ReadAccess debug, as the User tab, logged in or not, and a user with setup/setup: the cache key carries the user's own
role and limitation ids, and an anonymous visitor's are those of every anonymous visitor. It opens with an overview in one line: the module view that ran last, the state of
the content view cache in a word (hit, generated, off; the reason as its tooltip), the cache blocks read from
the cache of all, the templates rendered and those compiled in this request. Below it the five parts as sub-tabs,
like the User tab: the one shown last is kept (exp-debug-rendering-subtab), the arrow keys, Home and End move
between them, and the explanation each part opens with is folded away until "?" shows it (exp-debug-rendering-help).
Without the script the parts are shown one below the other:
- Controller: every module view that ran, in order (an error page or a forward adds one), with the view class
that ran it (
Exponential\View\Kernel\..., see cli_cronjob_view_abstractions.md) and the script; the siteaccess, the navigation part and, folded, the design order (the first base that has a template wins). - Template tree: every template in the order it started
(
eZTemplate::fetch()for the outermost,processURI()for each{include},node_view_guiand the like), nested under the template that included it with a guide line per level, and the time it took including what it included (not the parsing or compiling of the template itself). Where the overrides chose another file than the template's own place in a design (node/view/full.tpl→.../override/templates/full/folder.tpl) the file follows the name in the accent colour; otherwise the file is the tooltip. A template run without the compiler is marked not compiled. Templates of a view read from the view cache are not rendered and so not listed. At most 2000 entries, every template counted;debugbar.ini [DebugBarSettings] RenderTree=disabledleaves the tree out. - Content view cache, when a content view ran: one row laid out like the cache blocks below, with the node and
view mode, the state and its reason, the time to look the file up and to generate the view, the size, the file's
age, and folded the key part by part as it was hashed, with the hash and the cache file. A part a
content/view/cachekeyslistener added or changed is marked listener.
| State | Chip | Meaning |
|---|---|---|
| Read from the cache | View: hit | the file was used; with its age and size (no age or size for Valkey) |
| Generated and stored in the cache | View: new | no file, or the old one was not used; the reason says why (expired by cache_ttl, by a template in development mode, by a cache clear, no_cache) |
| Generated without storing it in the cache | View: new | the cache file said no_cache (a view that sets no_cache leaves only that note in its file), or the result is not kept |
| Not used | View: off | ViewCaching disabled, the caller turned it off ($Params['ViewCache']), the view mode is not in CachedViewModes, or ViewCacheTweaks[<node>] contains disabled |
- Cache blocks: every
{cache-block}in the order it was looked up, with the template and line, the state (hit, generated, not found), the time to look it up and to generate it (an outer block's time holds the blocks inside it), the size, the expiry (expiry,subtree_expiry) and, folded, the keys and the cache file. At most 200 rows; the line above the table counts every block. The chip CB hits/count is raised while a block was not read from the cache. A template compiled before this version names no template and no generation time: clear the template cache once (php bin/php/ezcache.php --clear-tag=template). Code that callseZTemplateCacheBlock::retrieve()itself (content/treemenu) shows its keys without a template. - Templates:
ViewCaching,TemplateCompile,CompileExecution,TemplateCacheandDevelopmentModein effect, how many templates were rendered, how often a compiled template ran and how many were compiled in this request, with their files. The chip Compiled N is raised while N is not 0: right after a template cache clear every template is compiled once.
Values longer than 200 characters are cut; the whole value is the tooltip. The collector is expRenderStats
(kernel/classes/debugbar/exprenderstats.php). The kernel notes into it where it decides (eZModule::run(),
ModuleView::main(), the content/view view, eZNodeviewfunctions, eZTemplateCompiler, eZTemplateCacheBlock, eZTemplate);
every note returns at once without debug output, and ezpKernelWeb starts each request empty, also in a persistent
worker. The compiled code of a cache-block passes its placement to eZTemplateCacheBlock::handle() (new optional
parameters $placement and $keys) and stores through eZTemplateCacheBlock::store(), which notes the generation
time; custom code that calls handle() or storeCache() as before keeps working.
Tests: expRenderStatsTest, expRenderStatsCacheBlockTest and expRenderStatsTreeTest (no database).
User: roles and cache profile
Whether two users get the same content view cache files, and if not, why. The chip admin · VC 3f9ac21e in the
status bar names the user (the login, after a person icon) and their cache profile: a fingerprint (8 hex digits) of the parts of the view cache key that depend on
the user. Two users with the same profile share the cache files of the views under the same ViewCacheTweaks; compare
the chips of two browsers, or copy both profiles as text. A second fingerprint after it (VC 3f9ac21e · 7b0d12aa)
covers the keys a content/view/cachekeys listener added for the view of this page; · pr_user says every
logged-in user has files of their own.
The parts, as eZNodeviewfunctions::generateViewCacheFile() puts them in the key, from the same functions:
| Part | From |
|---|---|
userroles | the role ids of the user and their groups (eZNodeviewfunctions::userCacheKeys()) |
userlimitedlist | the values of limited assignments (subtrees, sections) |
discountlist | the user's discount rules |
uiswitch | an interface the signed-in user chose (expUISwitch) |
userpreferences | the preferences of site.ini [ContentSettings] CachedViewPreferences[], of every view mode: per view mode the part its key takes (eZNodeviewfunctions::userPreferencesKey()), so the same profile means the same part in every view mode |
pr_user | the user id, with the tweak pr_user (it is in the file name) |
A ViewCacheTweaks entry (ignore_userroles, ignore_userlimitedlist, ignore_discountlist,
ignore_userpreferences) leaves its part out: the tweaks of the node of this page, the global ones on a page without
a content view. With a content view on the page its key decides: a part a listener left out is shown as such and not
counted, a part it changed counts with the key's value, marked listener.
The tab opens with an overview in one or two lines: the user (id and login), logged in, the siteaccess, the
session (how many keys and how large; only these figures are in the page), the cache profile with same as anonymous
visitors or different, and the number of role assignments. Below it four sub-tabs; the one shown last is kept
(exp-debug-user-subtab), the arrow keys, Home and End move between them, and with the filter box on each names its
matching rows:
- Cache profile: the profile with every part and a button that copies it as text (
name=valueper line). - Preferences: the preferences per view mode (
CachedViewPreferences) with the user's value and the default. - Roles and policies: the role assignments (role id and name, the assignment's limitation, and whether it is assigned to the user or to a group, by name and object id), and the policies (below; ReadAccess).
- Session: the user's own PHP session (below).
The explanations of the parts are folded away; "?" at the right of the sub-tabs shows them, and the choice is kept
(exp-debug-user-help). Without the script the parts are shown one below the other. A logged-in user also sees the profile of anonymous visitors, same as yours or
different: the anonymous user's parts under the same tweaks, with the default preferences (an anonymous visitor's
are in their session), without the keys of listeners, which may read the session and cannot be asked for them.
For whoever gets the debug output (DebugByIP, DebugByUser) under ReadAccess debug, the default, and for
setup/setup. The tab holds the user's own
data: a logged-in user sees their own roles, groups and limitation values, an anonymous visitor those of the anonymous
user, which are every anonymous visitor's, and on a page with a content view the values of the keys of listeners. What
a reader of the settings may see (the policies and the session's save path and handler class) is asked for
separately. Under ReadAccess=privileged, as before, a logged-in user without setup/setup sees neither this tab
nor the Rendering tab, only a chip with their login.
The Rendering tab follows the same rule. The data comes from expDebugBarUser
(kernel/classes/debugbar/expdebugbaruser.php); it reads the role assignments with one query and the preferences
with eZPreferences::values(), only when the bar is rendered for such a user.
Policies. For whoever may read the settings (setup/setup, or the debug output under ReadAccess=debug) the sub-tab Roles and policies ends with a folded section Policies: the policies of the user's roles,
loaded only when it is opened (expdebugbar::policies), so a user with hundreds of roles costs the page nothing. The
first answer lists the roles with their number of policies (the assignments as the tooltip of each) and the first
page; a role, a search and Load more ask again, 200 rows at a time. A row is the module and function (* marked),
the limitations with readable values (classes, sections, siteaccesses and nodes by name, with the id) and the role.
The search looks in module/function (content/read), role names, limitation types, values as stored and the names of
classes, sections and siteaccesses; other limitations (Owner, Group, StateGroup_* ...) show their stored values.
A search that names a module and a function also finds the policies that grant it with a wildcard: content/read
lists content/* and */* as well, content/* every function of content. The role ids here and in the Roles table
link to role/view/<id> in the current siteaccess (in an administration siteaccess the role's page; elsewhere only
where the module role is reachable). The role, the page and the search travel in the path
(expdebugbar::policies::<role>::<offset>::<search>, the search as base64url, since it may hold a /), never in the
query string; anything else is refused. The published policies, their limitations and values of all roles are read
through the persistent objects of ezpolicy, ezpolicy_limitation and ezpolicy_limitation_value, three queries
per 500 ids, so hundreds of roles work on every database (MySQL, PostgreSQL, SQLite, Oracle, MongoDB); the names of
classes, sections and siteaccesses are read once, the names of the nodes of a page 500 at a time
(expDebugBarPolicies, kernel/classes/debugbar/expdebugbarpolicies.php); only the latest answer fills the table
when the search changes while one is on its way.
Tests: expDebugBarUserTest, expDebugBarPoliciesTest and expDebugBarUserPoliciesTest (no database).
Session. The sub-tab Session shows the user's own PHP session, loaded only when it is first seen: the bar
open, the User tab and the sub-tab shown (expdebugbar::session, answered with Cache-Control: private, no-store),
so it is never part of the page. It answers what a request of this very session has stored:
the keys of $_SESSION as a table per group with the columns key, type, size as stored (serialized) and value; a
nested array or object opens with ▸ into indented rows below it, a short one of up to six plain values reads on one
line ([85, 93], {region: "x"}), strings are quoted, long ones cut (the whole length in the tooltip), binary values
show as hex. A search box finds keys and values and shows the rows above a match; "Open all" and "Close all" fold the
tables. The keys are grouped by the active extension whose name starts them (ezfind_…, myext_…; the longest name
wins), the kernel's and every other key first. Values of keys that look like secrets are masked: only the type and
the length are shown, and everything below such a key is masked too. The masks are parts of key names, regular
expressions without delimiters, case-insensitive, in debugbar.ini [DebugBarSettings] SessionMask[] (token, csrf,
nonce, secret, passw, hash, key$, credential, auth, jwt, bearer, otp, saml, assertion, ticket, sid$; an extension adds its own). Below the keys the frame: whether a
session was started, the cookie's name, the session id shortened to four characters (a screenshot does not hand
over the session), the handler (session.save_handler; the handler class and the save path for whoever may read the settings),
session.gc_maxlifetime, site.ini [Session] SessionTimeout, ActivityTimeout, CookieTimeout and RememberMeTimeout,
and the cookie's lifetime, path, domain, secure, httponly and samesite. At most 8 levels, 200 children per level
and 3000 entries are listed, the rest is counted (keys after the last entry as not listed); a session over 64 KB is
marked large. Keys and texts that are not UTF-8 show as hex (0x…). The masks hide secrets, not personal data: a
name or an address the application keeps in the session is shown as stored, to whoever may see the section. The section is offered to
whoever sees the User tab (see above, ReadAccess); it only ever shows the
session of the request that asks, there is no way to read another one, and nothing in it can change the session
(expDebugBarSession, kernel/classes/debugbar/expdebugbarsession.php).
Masked values, the text and the warnings. Values are also masked by what they look like, whatever their key is
called: a JSON Web Token anywhere in a text, the credentials of an Authorization header (Bearer …, Basic …), a PEM
private key, well-known API key prefixes (sk_live_…, ghp_…, xoxb-…, AKIA…), 32 or more hex digits, and a random
text of 32 or more base64 characters (digits and both cases, varied enough not to be a word or a slug). A key that looks
like a token itself (a token used as an array index) is shortened to four characters and masked with everything below
it. The badge masked says in its tooltip why: the key's name, the value, or a masked key above it. More patterns go in
debugbar.ini [DebugBarSettings] SessionValueMask[] (regular expressions without delimiters, case-sensitive; (?i) at
the start for case-insensitive), matched against the first 4096 bytes of every text and every key; the built-in ones
always apply. Copy as text copies the session as one path = value per line, grouped like the table and built on
the server from the same masked entries (masked values as ***, the session id shortened), to set two sessions side by
side or to attach to an issue. A session larger than SessionWarnBytes (65536 bytes) is marked large in the overview
and in the sub-tab, with the reason: every request of the user reads the whole session, and writes it when it changed.
When PHP keeps the session in files (session.save_handler=files), a hint says that the file is locked for the whole
request, so the requests of one user (several tabs, AJAX calls) wait for each other. The session is asked for with
POST, with the form token when signed in; a request that the browser marks as sent by another site is refused
(Sec-Fetch-Site), and the answer, a refusal included, carries Cache-Control: private, no-store, max-age=0, so
neither the browser nor a proxy nor Velocity's response cache keeps it. Tests: expDebugBarSessionTest (no database).
Panels of extensions
An extension can add its own tab to the bar, with a chip in the summary and data in the page's JSON - for example
the layout a page was built from. It names an id and a class in its settings/debugbar.ini.append.php:
[DebugBarSettings]
Panels[]=mylayouts
[Panel_mylayouts]
Class=myLayoutsDebugBarPanelThe class implements expDebugBarPanelInterface (kernel/classes/debugbar/expdebugbarpanelinterface.php):
class myLayoutsDebugBarPanel implements expDebugBarPanelInterface
{
public function visible()
{
// the bar also shows to anonymous visitors when DebugByIP matches
$access = eZUser::currentUser()->hasAccessTo( 'mylayouts', 'edit' );
return $access['accessWord'] !== 'no';
}
public function title() { return ezpI18n::tr( 'extension/mylayouts', 'Layouts' ); }
public function chip() { return array( 'text' => 'Layout: Home', 'title' => 'Rule home', 'level' => 'ok' ); }
public function html() { return '<p>' . htmlspecialchars( 'Home', ENT_QUOTES ) . '</p>'; }
public function data() { return array( 'layout' => 'home' ); }
}- The tab's id is
x-<id>in lower case (data-tab="x-mylayouts", sectionexp-debug-panel-x-mylayouts), so it never meets a built-in tab. An id is letters, digits and_; two ids that differ only in case are one id. visible()comes first; a panel that says no draws nothing.html()is printed as it is: the panel escapes what it puts in. The chip's text and title are escaped by the bar, a level other thanok,warnorhighisok, and a click on the chip opens the tab.data()goes topanels.<id>of the JSON (#exp-debug-data).- An entry without a usable class (no
Class, an unknown class, one without the interface) and a panel that throws or gives no title are left out with a warning in the debug output; the rest of the bar is drawn. - The class is created without arguments for every report, before
visible()and also for anonymous visitors: keep the constructor trivial, without data access, and the rest cheap, since the bar renders on every page that shows it.html()returns its markup and prints nothing itself.
Server: where the request entered and what rendered it
The tab opens with one sentence that says where the request entered and which process rendered the report, then
the facts behind it (expDebugBarReport::engineFacts(), from the server variables and engineProbe()):
| Rendered by | Sentence (example) |
|---|---|
| a Velocity worker | Received by Velocity on port 8080 and rendered by a Velocity worker. |
| the Velocity parent (no pool) | Received by Velocity on port 8080 and rendered by the Velocity parent process (no worker). |
php-cgi, started by Velocity for a script under Q.webserver.cgi.patterns | Received by Velocity on port 8080 and rendered by php-cgi, which Velocity starts for this script (Q.webserver.cgi.patterns). |
| PHP-FPM behind Apache (or nginx) | Served by Apache / PHP-FPM (port 443), not by Velocity. |
| mod_php, FrankenPHP, another SAPI | Served by Apache with mod_php (port 80), not by Velocity. |
The rows: Entered (scheme, port, and the forwarded headers seen, such as X-Real-IP and X-Forwarded-For from
a proxy in front), Received by (Exponential Velocity and its version, or the server named by
SERVER_SOFTWARE), Rendered by, PHP and its SAPI, Process and Host (only for a user with setup or cache
access), Rendered at, and Cached: a page with the report is rendered for each request and kept by no cache.
Only signals a client cannot set decide: QBIX_SERVER_VERSION (defined only in a Velocity process), the SAPI, and
SERVER_SOFTWARE and SERVER_PORT as the server set them. A worker is told from the parent by its parent process,
which is the Velocity parent or its zygote (qbixserver). Forwarded headers are listed as seen and decide nothing,
since a client can send them too. Velocity answers every request itself; it never hands one on to PHP-FPM or Apache,
so a report rendered by PHP-FPM always belongs to a request that went to Apache (for example nginx in front of Apache on 443).
A report that came out of a cache. The page carries the report of the request that rendered it. Pages with the
report are sent Cache-Control: private, no-store (eZDebugReportNoStore() in eZDisplayDebug()), so neither
Velocity's response cache nor the role-aware page cache (X-Exp-Cache: BYPASS (debug report)) keeps them. Should one
still arrive from somewhere (a page stored before 2026-10-14, a browser or service worker replaying it), the script
compares the report's port (data-entry-port) with the address in the browser and its time (data-rendered-at)
with the time the page was loaded, and says so above the facts: "This report was rendered at 04:08:02 by PHP-FPM
244909 for a request on port 443, 9 min before this page was loaded on port 8080: the page came from a cache, not
from this request." (data-exp-debug-replayed; 120 s of room for a clock that differs).
Why the tab changed (2026-10-14): on :8080 the tab said "Not running on Velocity", engine Apache (fpm-fcgi). The page
was the signed-in front page from the role-aware page cache, stored at 04:08:02 by PHP-FPM (process 244909) for a
request on :443 and answered on :8080 by Velocity's parent straight from that shared store (Q.web.appCache), with
PHP-FPM's report in it. Velocity's own response cache did the same for anonymous pages: one rendered for a listed
address was kept with its report and answered to every visitor of :8080 (X-Cache: HIT).
The OPcache box says why it has nothing to show: opcache_get_status() switched off for that PHP (a pool's
disable_functions) or OPcache not loaded.
Test: expDebugBarReportEngineTest (no database; server variables and the probe passed in).
Tests
The bar is tested in a browser at 960 px wide and device scale factor 2, on the admin, admin2 and admin4 designs
and the public site, light and dark: the pinned bar and panel, keep open on reload, the tabs by mouse and keyboard,
the summary against the report, the filter, the IP validation, no page errors. On the admin page also a setting
written to the global override and undone, a preset applied and reverted (each settings file compared byte for
byte; on a failure the test writes the files back), the IP list controls, and "This page only". Screenshots go to
var/tmp/debug-bar-<engine>-<page>-<tab>.png. A check of the words makes sure every
string of the bar is translated.
Pages from the cache
A visitor the bar is shown to by address is answered from the role-aware HTTP cache, with the bar of the
render that stored the page: its first chip says Cached or Rendered, Render now asks for a full render
with the view parameter (exp_render)/full. See The role-aware HTTP cache, "Visitors the debug bar
is shown to".