Skip to content

Commit 4ae8ef5

Browse files
committed
Show all queries (truncated) in the extension)
1 parent f5f5e4c commit 4ae8ef5

File tree

5 files changed

+338
-43
lines changed

5 files changed

+338
-43
lines changed

chrome-extension/panel.html

Lines changed: 34 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,28 @@
2727
.metric-unit { color: #999; font-size: 10px; }
2828
.dup-warn { color: #d97706; font-weight: 500; }
2929

30-
.dups { background: #fffbeb; border-left: 2px solid #f59e0b; padding: 6px 8px; margin-bottom: 8px; border-radius: 0 4px 4px 0; }
31-
.dup { margin-bottom: 4px; font-size: 10px; }
32-
.dup:last-child { margin-bottom: 0; }
33-
.dup code { color: #333; word-break: break-all; }
34-
.dup-time { color: #d97706; font-weight: 500; margin-left: 4px; }
30+
.queries { background: #f0f9ff; border-left: 2px solid #0ea5e9; padding: 6px 8px; margin-bottom: 8px; border-radius: 0 4px 4px 0; max-height: 400px; overflow-y: auto; }
31+
.query { margin-bottom: 4px; font-size: 10px; }
32+
.query:last-child { margin-bottom: 0; }
33+
.query code { color: #333; word-break: break-all; }
34+
.sql-keyword { color: #0ea5e9; font-weight: 500; }
35+
.query-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; }
36+
.query-summary { flex: 1; overflow: hidden; }
37+
.query-summary code { display: block; white-space: pre-wrap; word-wrap: break-word; }
38+
.query-time { color: #0ea5e9; font-weight: 500; margin-left: 4px; flex-shrink: 0; }
39+
.query.duplicate { background: #fef3c7; }
40+
.waterfall-container { margin-top: 8px; padding: 8px 0; }
41+
.waterfall-row { display: flex; align-items: center; font-size: 10px; margin-top: 4px; padding: 4px 0; border-bottom: 1px solid #e5e7eb; }
42+
.waterfall-start { width: 55px; text-align: right; padding-right: 8px; color: #6b7280; font-size: 9px; flex-shrink: 0; }
43+
.waterfall-end { width: 55px; text-align: left; padding-left: 8px; color: #6b7280; font-size: 9px; flex-shrink: 0; }
44+
.waterfall-chart { flex: 1; height: 18px; position: relative; background: #f9fafb; border-radius: 2px; overflow: hidden; }
45+
.waterfall-grid-lines { position: absolute; top: 0; left: 0; right: 0; bottom: 0; pointer-events: none; z-index: 0; }
46+
.waterfall-grid-line { position: absolute; top: 0; bottom: 0; width: 1px; background: rgba(0,0,0,0.08); }
47+
.waterfall-grid-label { position: absolute; top: 0; font-size: 8px; color: #999; transform: translateX(-50%); white-space: nowrap; }
48+
.timing-bar { height: 12px; position: absolute; top: 3px; border-radius: 2px; min-width: 2px; z-index: 1; }
49+
.timing-bar.fast { background: #22c55e; }
50+
.timing-bar.medium { background: #eab308; }
51+
.timing-bar.slow { background: #ef4444; }
3552

3653
.history { margin-top: 8px; max-height: 300px; overflow-y: auto; }
3754
.history-title { font-weight: 500; margin-bottom: 6px; color: #666; font-size: 10px; text-transform: uppercase; position: sticky; top: 0; background: #f5f5f5; padding: 2px 0; }
@@ -59,8 +76,18 @@
5976
.request-method { background: #1a3a5c; color: #6db3f2; }
6077
.metric-label { color: #888; }
6178
.hist-stats { color: #999; }
62-
.dups { background: #2a2518; border-left-color: #f59e0b; }
63-
.dup code { color: #e0e0e0; }
79+
.queries { background: #0c1929; border-left-color: #0ea5e9; }
80+
.query code { color: #e0e0e0; }
81+
.sql-keyword { color: #38bdf8; font-weight: 500; }
82+
.query.duplicate { background: rgba(245, 158, 11, 0.2); }
83+
.waterfall-row { border-bottom-color: #374151; }
84+
.waterfall-start, .waterfall-end { color: #9ca3af; }
85+
.waterfall-chart { background: #1f2937; }
86+
.waterfall-grid-line { background: rgba(255,255,255,0.1); }
87+
.waterfall-grid-label { color: #666; }
88+
.timing-bar.fast { background: #16a34a; }
89+
.timing-bar.medium { background: #ca8a04; }
90+
.timing-bar.slow { background: #dc2626; }
6491
.type-page { background: #1e3a5f; color: #93c5fd; }
6592
.type-doc { background: #1e3a2f; color: #6ee7b7; }
6693
.type-xhr { background: #374151; color: #9ca3af; }

chrome-extension/panel.js

Lines changed: 67 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,12 @@ function escapeHtml(text) {
4848
return div.innerHTML;
4949
}
5050

51+
function formatSql(sql) {
52+
const escaped = escapeHtml(sql);
53+
const keywordRegex = /\b(SELECT|FROM|WHERE|JOIN|LEFT JOIN|RIGHT JOIN|INNER JOIN|OUTER JOIN|ON|AND|OR|ORDER BY|GROUP BY|HAVING|LIMIT|OFFSET|INSERT INTO|VALUES|UPDATE|SET|DELETE|CREATE TABLE|ALTER TABLE|DROP TABLE|DISTINCT|COUNT|SUM|AVG|MAX|MIN|UNION|EXISTS|IN|IS NULL|IS NOT NULL|LIKE|BETWEEN|CASE|WHEN|THEN|ELSE|END|ASC|DESC|AS|WITH|RECURSIVE)\b/gi;
54+
return escaped.replace(keywordRegex, (match) => `<span class="sql-keyword">${match}</span>`);
55+
}
56+
5157
function getPathFromUrl(url) {
5258
try {
5359
const parsed = new URL(url);
@@ -152,6 +158,55 @@ function getRequestType(req) {
152158
return { class: 'type-xhr', label: 'XHR' };
153159
}
154160

161+
function renderWaterfallChart(queries) {
162+
if (!Array.isArray(queries) || queries.length === 0) return '';
163+
164+
let cumulativeTime = 0;
165+
const queriesWithStartTime = queries.map(q => {
166+
const startTime = cumulativeTime;
167+
cumulativeTime += q.dur ?? 0;
168+
return { ...q, start_time: startTime };
169+
});
170+
171+
const maxEndTime = cumulativeTime;
172+
173+
const gridInterval = maxEndTime > 100 ? 20 : maxEndTime > 50 ? 10 : 5;
174+
const gridLines = [];
175+
for (let t = 0; t <= maxEndTime; t += gridInterval) {
176+
const position = (t / maxEndTime) * 100;
177+
gridLines.push(`
178+
<div class="waterfall-grid-line" style="left: ${position}%"></div>
179+
<div class="waterfall-grid-label" style="left: ${position}%">${t}ms</div>
180+
`);
181+
}
182+
183+
return `<div class="waterfall-grid-lines">${gridLines.join('')}</div>
184+
${queriesWithStartTime.map((query, idx) => {
185+
const duration = query.dur ?? 0;
186+
const startTime = query.start_time ?? 0;
187+
const durationClass = duration > 50 ? 'slow' : duration > 10 ? 'medium' : 'fast';
188+
const barWidth = maxEndTime > 0 ? (duration / maxEndTime) * 100 : 0;
189+
const barLeft = maxEndTime > 0 ? (startTime / maxEndTime) * 100 : 0;
190+
191+
return `
192+
<div class="query${query.dup ? ' duplicate' : ''}" data-idx="${idx}">
193+
<div class="query-header">
194+
<div class="query-summary">
195+
<code>${formatSql(query.s)}</code>
196+
</div>
197+
<span class="query-time">${duration.toFixed(1)}ms</span>
198+
</div>
199+
<div class="waterfall-row">
200+
<span class="waterfall-start">${startTime.toFixed(1)}ms</span>
201+
<div class="waterfall-chart">
202+
<div class="timing-bar ${durationClass}" style="width: ${barWidth}%; margin-left: ${barLeft}%"></div>
203+
</div>
204+
<span class="waterfall-end">${(startTime + duration).toFixed(1)}ms</span>
205+
</div>
206+
</div>`;
207+
}).join('')}`;
208+
}
209+
155210
function renderEmptyState() {
156211
const app = document.getElementById('app');
157212
const isLocalDomain = pageUrl && (
@@ -211,18 +266,18 @@ function renderUI() {
211266
<a href="${escapeHtml(url)}" target="_blank" class="url-link" title="Open in new tab" aria-label="Open request URL in new tab">↗</a>
212267
</div>
213268
<div class="metrics">
214-
${renderMetric('queries', data.count ?? 0)}
215-
${renderMetric('db', formatMs(data.db_time), 'ms')}
216-
${renderMetric('app', formatMs(data.app_time), 'ms')}
217-
${data.duplicates?.length ? `<span class="dup-warn">⚠ ${data.duplicates.length} dup</span>` : ''}
269+
${renderMetric('queries', data.c ?? 0)}
270+
${renderMetric('db', formatMs(data.db), 'ms')}
271+
${renderMetric('app', formatMs(data.app), 'ms')}
272+
${data.dup?.length ? `<span class="dup-warn">⚠ ${data.dup.length} dup</span>` : ''}
218273
<span class="metric-label">${formatTime(currentRequest.timestamp)}</span>
219274
</div>
220275
</div>`;
221276

222-
if (data.duplicates?.length > 0) {
223-
html += `<div class="dups">${data.duplicates.map(dup =>
224-
`<div class="dup"><code>${escapeHtml(dup.sql)}</code> <span class="dup-time">${(dup.duration ?? 0).toFixed(1)}ms</span></div>`
225-
).join('')}</div>`;
277+
if (Array.isArray(data.q) && data.q.length > 0) {
278+
html += `<div class="queries"><div class="waterfall-container">
279+
${renderWaterfallChart(data.q)}
280+
</div></div>`;
226281
}
227282

228283
const hasPageOrDoc = requestHistory.some(r => r.isMainPage || r.isDocument);
@@ -249,10 +304,10 @@ function renderUI() {
249304
<a href="${escapeHtml(req.url)}" target="_blank" class="url-link" title="Open in new tab" aria-label="Open request URL in new tab">↗</a>
250305
</div>
251306
<div class="hist-stats">
252-
${renderMetric('queries', req.data.count ?? 0)}
253-
${renderMetric('db', formatMs(req.data.db_time), 'ms')}
254-
${renderMetric('app', formatMs(req.data.app_time), 'ms')}
255-
${req.data.duplicates?.length ? `<span class="dup-warn">⚠</span>` : ''}
307+
${renderMetric('queries', req.data.c ?? 0)}
308+
${renderMetric('db', formatMs(req.data.db), 'ms')}
309+
${renderMetric('app', formatMs(req.data.app), 'ms')}
310+
${req.data.dup?.length ? `<span class="dup-warn">⚠</span>` : ''}
256311
<span class="metric-label">${formatTime(req.timestamp)}</span>
257312
</div>
258313
</div>`;

src/django_devbar/middleware.py

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
get_position,
1414
get_show_bar,
1515
)
16+
from .tracker import format_sql, truncate_sql
1617

1718
BODY_CLOSE_RE = re.compile(rb"</body\s*>", re.IGNORECASE)
1819

@@ -41,10 +42,10 @@ def __call__(self, request):
4142
stats = tracker.get_stats()
4243

4344
db_time = stats["duration"]
44-
python_time = max(0, total_time - db_time)
45+
python_time = round(max(0, total_time - db_time), 2)
4546

4647
stats["python_time"] = python_time
47-
stats["total_time"] = total_time
48+
stats["total_time"] = round(total_time, 2)
4849

4950
if get_enable_devtools_data():
5051
self._add_devtools_data_header(response, stats)
@@ -57,14 +58,31 @@ def __call__(self, request):
5758
return response
5859

5960
def _add_devtools_data_header(self, response, stats):
61+
# Abbreviated keys used to minimize DevBar-Data header size
6062
extension_data = {
61-
"count": stats["count"],
62-
"db_time": stats["duration"],
63-
"app_time": stats["python_time"],
64-
"total_time": stats["total_time"],
63+
"c": stats["count"],
64+
"db": round(stats["duration"], 2),
65+
"app": stats["python_time"],
66+
"full": stats["total_time"],
6567
}
66-
if stats.get("duplicate_queries"):
67-
extension_data["duplicates"] = stats["duplicate_queries"]
68+
69+
all_queries = stats.get("queries", [])
70+
duplicates = stats.get("duplicate_queries", [])
71+
72+
if all_queries:
73+
processed_queries = [
74+
{
75+
"s": q["sql"] if q["is_duplicate"] else truncate_sql(q["sql"]),
76+
"dur": q["duration"],
77+
"dup": 1 if q["is_duplicate"] else 0,
78+
}
79+
for q in all_queries
80+
]
81+
extension_data["q"] = processed_queries
82+
83+
if duplicates:
84+
marked_duplicates = [{**d, "dup": 1} for d in duplicates]
85+
extension_data["dup"] = marked_duplicates
6886

6987
response["DevBar-Data"] = json.dumps(extension_data)
7088

@@ -118,5 +136,8 @@ def _inject_devbar(self, response, stats):
118136
def _build_duplicates_html(self, duplicates):
119137
if not duplicates:
120138
return ""
139+
formatted_duplicates = [
140+
{**dup, "sql": format_sql(dup["sql"])} for dup in duplicates
141+
]
121142
template = _template_engine.get_template("django_devbar/duplicates.html")
122-
return template.render(Context({"duplicates": duplicates}))
143+
return template.render(Context({"duplicates": formatted_duplicates}))

0 commit comments

Comments
 (0)