dolibarr/htdocs/debugbar/class/DataCollector/DolQueryCollector.php
Alexandre Janniaux 163c51ae31
NEW debugbar: Add backtrace capture for database query failures (#36612)
* debugbar: Add backtrace capture for database query failures

Implements backtrace forwarding to capture and display backtraces when
database queries fail, making it easier for developers to identify the
source of database errors.

Backtrace are captured in TraceableDB::endTracing() when queries fail
using debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS) and stored to be
fowarded to the DolQueryCollector that will expose it to the debugbar
integration.

Because of that, the backtrace is now captured at the correct location,
ie. when the query fails, rather than when the error is retrieved,
ensuring it shows the actual application code that triggered the
problematic query.

This is particularly useful for debugging MySQL to PostgreSQL
compatibility issues, module development and maintenance, and quickly
identifying query origins during development. In particular, paired with
the list and time profiling of each request, it will allow finding the
location where queries are not properly optimized.

Before this patch, it was possible to try and find snippets from the
queries but because queries are generated at runtime, it was tedious and
could lead to the wrong location.

Related to #34050

* debugbar: Add SQL query backtrace

Extends the debugbar SQL widget with backtrace display capabilities.

To keep vendor files pristine, a custom widget (TracingSQLQueriesWidget)
extends the vendor SQLQueriesWidget using DOM manipulation to inject
Dolibarr-specific features after parent render completes.

The widget intercepts the parent's data binding callback via
wrapDataBinding() to enhance rendered queries with backtrace buttons.
This patch pattern depends on php-debugbar internals but might break if
the vendor API changes, without breaking the rendering done by
php-debugbar.

A tracing toggle icon in the status bar allows switching between
tracing failed queries only (default, minimal overhead) and tracing
all queries. The setting persists via cookie (debugbar_full_tracing)
which the PHP backend reads to decide whether to capture backtraces.

Custom styles in widgets.css is for the tracing state (eye/eye-slash
icons) and so as to format the backtrace display within a frame and
using monospace font.

Closes #34050
2026-05-03 16:26:33 +02:00

140 lines
3.4 KiB
PHP

<?php
/* Copyright (C) 2023 Laurent Destailleur <eldy@users.sourceforge.net>
* Copyright (C) 2024-2025 MDW <mdeweerd@users.noreply.github.com>
* Copyright (C) 2024 Frédéric France <frederic.france@free.fr>
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/**
* \file htdocs/debugbar/class/DataCollector/DolQueryCollector.php
* \brief Class for debugbar collection
* \ingroup debugbar
*/
use DebugBar\DataCollector\AssetProvider;
use DebugBar\DataCollector\DataCollector;
use DebugBar\DataCollector\Renderable;
dol_include_once('/debugbar/class/TraceableDB.php');
/**
* DolQueryCollector class
*/
class DolQueryCollector extends DataCollector implements Renderable, AssetProvider
{
/**
* @var object Database handler
*/
protected $db;
/**
* Constructor
*/
public function __construct()
{
global $db;
// Replace $db handler with new handler override by TraceableDB
$db = new TraceableDB($db);
$this->db = $db;
}
/**
* Return collected data
*
* @return array<string,mixed> Array of collected data
*/
public function collect()
{
$queries = array();
$totalExecTime = 0;
$totalMemoryUsage = 0;
$totalFailed = 0;
foreach ($this->db->queries as $query) {
$queries[] = array(
'sql' => $query['sql'],
'duration' => $query['duration'],
'duration_str' => round((float) $query['duration'] * 1000, 2),
'memory' => $query['memory_usage'],
'is_success' => $query['is_success'],
'error_code' => $query['error_code'],
'error_message' => $query['error_message'],
'backtrace' => isset($query['backtrace']) ? $query['backtrace'] : null
);
$totalExecTime += $query['duration'];
$totalMemoryUsage += $query['memory_usage'];
if (!$query['is_success']) {
$totalFailed += 1;
}
}
return array(
'nb_statements' => count($queries),
'nb_failed_statements' => $totalFailed,
'accumulated_duration' => $totalExecTime,
'memory_usage' => $totalMemoryUsage,
'statements' => $queries
);
}
/**
* Return collector name
*
* @return string Name
*/
public function getName()
{
return 'query';
}
/**
* Return widget settings
*
* @return array<string,array{icon?:string,widget?:string,tooltip?:string,map:string,default:int|string}> Array
*/
public function getWidgets()
{
global $langs;
$title = $langs->transnoentities('Database');
return array(
"$title" => array(
"icon" => "arrow-right",
"widget" => "PhpDebugBar.Widgets.SQLQueriesWidget",
"map" => "query",
"default" => "[]"
),
"$title:badge" => array(
"map" => "query.nb_statements",
"default" => 0
)
);
}
/**
* Return assets
*
* @return array<string,string> Array
*/
public function getAssets()
{
return array(
'css' => 'widgets/sqlqueries/widget.css',
'js' => 'widgets/sqlqueries/widget.js'
);
}
}