Skip to the content
Documentation
Help Start
Reading in:
  • German: 9,059 of 9,059 pages
About the help in other languages
All help pages

The Exp Debug bar: upgrade notes

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…

Download as PDF

Address: https://doc.exponential.earth/exphelp/viewer/bc/6.0/debug-bar.md

On this page

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 changedThe 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 affectedSites 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 checkgrep -n "Settings\[\]" settings/debugbar.ini | head and php vendor/bin/phpunit tests/tests/kernel/classes/debugbar/
How to fixAdd 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.

PartWhere
Registrysettings/debugbar.ini, and extension/<ext>/settings/debugbar.ini.append.php
Settings registryexpDebugBarRegistry (kernel/classes/debugbar/expdebugbarregistry.php)
Values, writes, undo, presetsexpDebugBarSettings (kernel/classes/debugbar/expdebugbarsettings.php)
IP list engineexpDebugBarIPList (kernel/classes/debugbar/expdebugbariplist.php), used by eZDebug::isAllowedByCurrentIP()
Write logexpDebugBarLog (kernel/classes/debugbar/expdebugbarlog.php), var/<site>/log/debugbar.log
SummaryexpDebugBarSummary (kernel/classes/debugbar/expdebugbarsummary.php)
Server functionsexpDebugBarServerFunctions, [ezjscServer_expdebugbar] in extension/ezjscore/settings/ezjscore.ini
Teststests/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, Variablethe setting
Typebool, enum (with Values[]), list (optionally Values[]: what may be added), text, iplist, userlist
Label, Group, Helpwhat the bar shows; Group is one of Groups[] (GroupNames[<id>] names it)
On, Offbool 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 lists debug.ini;GeneralCondition;conditions, so every debug condition (kernel-content-view, kernel-urltranslator, ... and the ones extensions add) is a switch, with the id cond_<variable>.
  • Extension debug switches (ExtensionDiscovery=enabled): in every active extension's settings/*.ini, *.ini.append.php and *.ini.append, a variable whose name matches DiscoveryPattern (/debug/i) and whose value is a bool word. Its id is ext_<ext>_<file>_<block>_<variable>, its group extensions. 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:

<?php /* #?ini charset="utf-8"?
[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 ):

opfordoes
setbool, enum, textbool takes on/off/1/0/true/false/enabled/disabled/yes/no and writes the setting's own words
toggleboolflips the value in effect
unsetallremoves the variable from that scope's file (the next file in the load order then wins)
add, removelistsone value; for an IP list remove takes the address and finds the line with its label
replacelistsVar[] (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 /0 entry 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.9 matches IPv4 entries.
  • A label is any text without ;, ##, */ or a line break (label= before it is optional).
  • expires= takes YYYY-MM-DD, YYYY-MM-DDTHH:MM[:SS], with Z or an offset; without one, the server's time zone. The bar writes +1h, +30m, +2d, today (23:59) and tomorrow as 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.

FunctionRequestNeeds
settings[::<siteaccess>]GETto read: setup/setup, or the debug output under ReadAccess debug; can_write tells whether the answer may be changed
setPOST setting, op, value, scope [siteaccess, confirm, dry_run]setup/setup
undoPOST entry or group [force]setup/setup
presetPOST preset + scope (apply), save [+ values JSON or snapshot=1], or deletesetup/setup
cacheGET action=list; POST action=clear, by=all|tag|id|node|velocity|opcache|querycache_counters, names, node_idlist: setup/managecache, or the debug output under ReadAccess debug; clear: setup/managecache
iptestaddress, list (JSON array)setup/setup, or the debug output under ReadAccess debug
log[::<limit>]GETsetup/setup, or the debug output under ReadAccess debug
summaryGETdebug 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)
sessionGETwho 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)
panelheightPOST 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/
TestCovers
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
expDebugBarIPListTestaddresses and CIDR of both families, IPv4-mapped clients, entries with labels and expiries, writing entries, the match, the warnings, the suggestions. Needs no kernel
expDebugBarSettingsTestthe 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)
RadSurveyDebugBarTestthe two registries, the total, the bar's figures, the new problem kinds
expDebugBarPoliciesTestthe 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
expDebugBarUserTestthe 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
expDebugBarUserPoliciesTestwho 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
expDebugBarSessionTestthe 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

PartWhere
Report markup, summary, data blockexpDebugBarReport (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 logdesign/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
Wordscontext 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.css and takes its colours from the bar's --xd-* tokens, each with a value for prefers-color-scheme: dark and for admin4's html[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, with eZDebug::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 heading td.debugtime, the heading td.debuglabel, an accumulator group's name td.groupname, and the count rows of the included files and the template usage tr.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").

ValueThe bar
defaultThe 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.
toolbarA 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:

KeyDoes
Arrow up / down16 px higher / lower
Shift + arrow up / down64 px higher / lower
Home / Endthe lowest / the highest the window allows
Double-click on the handleback 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, through expdebugbar::panelheight), so it follows the user to any browser. 0 means the default height. The report reads it back (expDebugBarReport::savedPanelHeight()) and prints it on the panel as data-exp-debug-panel-height; the report's first script (expdebugbar-head.js) sets --exp-debug-panel-h from 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, and expdebugbar::panelheight refuses an anonymous visitor before the token check). The height is kept in the browser instead: localStorage key exp-debug-panel-height, every access in try/catch (blocked storage means the default height), read back and held between 120 and 4000 px by expdebugbar-head.js before 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/call needs 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:

ValueTabAmber / red from
page timeTiming[DebugBarSettings] Thresholds[time_ms] (default 1 s / 3 s without the server side)
SQL count / timeSQLThresholds[sql_count], Thresholds[sql_ms]
peak memoryMemoryThresholds[memory_mb] (else 50 % / 80 % of memory_limit)
templates used (only with ShowUsedTemplates)TemplatesThresholds[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)SQLThresholds[querycache_hit_rate] (amber below 50 %, red below 10 %; amber when a write made everything stale, red in shared without APCu)
warnings, errorsMessages (filtered to that level)any warning is amber, any error red
deprecations (3 depr)Messagesany 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)ServerThresholds[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)ServerThresholds[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)Servernever 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

TabShows
Messagesthe notices, warnings, errors, debug and timing messages, with buttons per level; the SQL statements are moved to the SQL tab
Settingsthe 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)
Cachethe cache controls below; without the server functions a form that clears this page, content, template, INI or all caches through setup/cachetoolbar
Timingthe timing points and the time accumulators, as before (#timingpoints, #timeaccumulators)
SQLnumber of queries, time, share of the page, average, the database's accumulators, and the statements (SQLOutput) with "Slowest first"
Templatesthe templates used (ShowUsedTemplates) and the CSS/JS packer report, or how to turn them on
Included filesthe included PHP files (DisplayIncludedFiles), or their number and how to list them
Memorythe classic "Main resources" table, memory now, peak, limit and share, and the five steps that took the most
Renderinghow 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
Userthe 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)
Serverwhere 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
Otherreports 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()).

ReadAccessWho 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
privilegedthe 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_gui and 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=disabled leaves 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/cachekeys listener added or changed is marked listener.
StateChipMeaning
Read from the cacheView: hitthe file was used; with its age and size (no age or size for Valkey)
Generated and stored in the cacheView: newno 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 cacheView: newthe 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 usedView: offViewCaching 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 calls eZTemplateCacheBlock::retrieve() itself (content/treemenu) shows its keys without a template.
  • Templates: ViewCaching, TemplateCompile, CompileExecution, TemplateCache and DevelopmentMode in 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:

PartFrom
userrolesthe role ids of the user and their groups (eZNodeviewfunctions::userCacheKeys())
userlimitedlistthe values of limited assignments (subtrees, sections)
discountlistthe user's discount rules
uiswitchan interface the signed-in user chose (expUISwitch)
userpreferencesthe 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_userthe 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=value per 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=myLayoutsDebugBarPanel

The 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", section exp-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 than ok, warn or high is ok, and a click on the chip opens the tab. data() goes to panels.<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 bySentence (example)
a Velocity workerReceived 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.patternsReceived 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 SAPIServed 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".

Something wrong on this page? Report an error

Documentation license

This documentation is free documentation, licensed under the GNU Free Documentation License 1.3 or later.

Exponential Copyright © 1998-2026 Exponential Foundation and 7x