Skip to content

Commit 7ac123a

Browse files
committed
Various improvements and changes
- drop thresholds - drop console.log queries - add clickable duplicate queries panel - move out html/css into a template file
1 parent d39eced commit 7ac123a

File tree

5 files changed

+123
-108
lines changed

5 files changed

+123
-108
lines changed

README.md

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -37,25 +37,14 @@ DEVBAR_SHOW_BAR = True
3737

3838
# Add DevBar-* response headers (default: False)
3939
DEVBAR_SHOW_HEADERS = True
40-
41-
# Enable console logging for duplicate queries (default: True)
42-
DEVBAR_ENABLE_CONSOLE = True
43-
44-
# Performance thresholds for warning/critical levels (defaults shown)
45-
DEVBAR_THRESHOLDS = {
46-
"time_warning": 500, # ms
47-
"time_critical": 1500, # ms
48-
"count_warning": 20, # queries
49-
"count_critical": 50, # queries
50-
}
5140
```
5241

5342
## Response Headers
5443

5544
When `DEVBAR_SHOW_HEADERS = True`, performance metrics are added as HTTP response headers. This is useful for:
5645

5746
- **API endpoints** where the HTML overlay can't be displayed
58-
- **Automated testing** to assert performance thresholds (e.g., fail CI if query count exceeds a limit)
47+
- **Automated testing** to assert performance metrics (e.g., fail CI if query count exceeds a limit)
5948
- **Monitoring tools** that can capture and aggregate header values
6049

6150
Headers included:

src/django_devbar/conf.py

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,6 @@
77
"top-left": "top:0;left:0",
88
}
99

10-
DEFAULT_THRESHOLDS = {
11-
"time_warning": 500,
12-
"time_critical": 1500,
13-
"count_warning": 20,
14-
"count_critical": 50,
15-
}
16-
1710

1811
def get_position():
1912
key = getattr(settings, "DEVBAR_POSITION", "bottom-right")
@@ -26,14 +19,3 @@ def get_show_bar():
2619

2720
def get_show_headers():
2821
return getattr(settings, "DEVBAR_SHOW_HEADERS", False)
29-
30-
31-
def get_enable_console():
32-
return getattr(settings, "DEVBAR_ENABLE_CONSOLE", True)
33-
34-
35-
def get_thresholds():
36-
user_thresholds = getattr(settings, "DEVBAR_THRESHOLDS", {})
37-
final = DEFAULT_THRESHOLDS.copy()
38-
final.update(user_thresholds)
39-
return final

src/django_devbar/middleware.py

Lines changed: 29 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,61 +1,25 @@
1-
import json
21
import re
32
from contextlib import ExitStack
3+
from pathlib import Path
44
from time import perf_counter
55

66
from django.db import connections
7+
from django.template import Context, Engine
78

89
from . import tracker
910
from .conf import (
1011
get_position,
1112
get_show_bar,
1213
get_show_headers,
13-
get_thresholds,
14-
get_enable_console,
1514
)
1615

17-
STYLE_BLOCK = """<style>
18-
#django-devbar {
19-
position: fixed; %s; z-index: 999999999;
20-
display: flex; align-items: center; gap: 5px;
21-
font-family: -apple-system, system-ui, sans-serif;
22-
font-size: 11px; font-weight: 500;
23-
padding: 4px 8px; margin: 8px; border-radius: 4px;
24-
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
25-
box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
26-
transition: all 0.2s ease;
27-
cursor: default;
28-
line-height: 1.3;
29-
background: rgba(20, 20, 20, 0.92);
30-
color: #f5f5f5;
31-
}
32-
#django-devbar.level-warn { border-left: 3px solid #f59e0b; }
33-
#django-devbar.level-crit { border-left: 3px solid #dc2626; }
34-
#django-devbar span { opacity: 0.7; }
35-
#django-devbar strong { opacity: 1; font-weight: 600; }
36-
#django-devbar .duplicate-badge { color: #f59e0b; font-weight: 600; }
37-
@media (max-width: 640px) { #django-devbar { display: none; } }
38-
</style>"""
39-
40-
BAR_TEMPLATE = """<div id="django-devbar" class="level-%s">
41-
<span>db</span> <strong>%.0fms</strong> <span>·</span>
42-
<span>app</span> <strong>%.0fms</strong> <span>·</span>
43-
<span>queries</span> <strong>%d</strong>%s
44-
</div>"""
45-
46-
SCRIPT_TEMPLATE = """<script>
47-
(function() {
48-
const stats = %s;
49-
if (stats.duplicates && stats.duplicates.length > 0) {
50-
console.groupCollapsed(`⚠️ Django DevBar: Duplicate Queries Detected (${stats.duplicates.length})`);
51-
console.table(stats.duplicates.map(d => ({SQL: d.sql, Parameters: d.params, Duration_ms: d.duration})));
52-
console.groupEnd();
53-
}
54-
})();
55-
</script>"""
56-
5716
BODY_CLOSE_RE = re.compile(rb"</body\s*>", re.IGNORECASE)
5817

18+
_template_engine = Engine(
19+
dirs=[Path(__file__).parent / "templates"],
20+
autoescape=True,
21+
)
22+
5923

6024
class DevBarMiddleware:
6125
def __init__(self, get_response):
@@ -81,8 +45,7 @@ def __call__(self, request):
8145
stats["python_time"] = python_time
8246
stats["total_time"] = total_time
8347

84-
thresholds = get_thresholds()
85-
level = self._determine_level(stats, total_time, thresholds)
48+
level = "warn" if stats["has_duplicates"] else "ok"
8649

8750
if get_show_headers():
8851
self._add_headers(response, stats)
@@ -92,20 +55,6 @@ def __call__(self, request):
9255

9356
return response
9457

95-
def _determine_level(self, stats, total_time, thresholds):
96-
if (
97-
total_time > thresholds["time_critical"]
98-
or stats["count"] > thresholds["count_critical"]
99-
):
100-
return "crit"
101-
if (
102-
stats["has_duplicates"]
103-
or total_time > thresholds["time_warning"]
104-
or stats["count"] > thresholds["count_warning"]
105-
):
106-
return "warn"
107-
return "ok"
108-
10958
def _add_headers(self, response, stats):
11059
response["DevBar-Query-Count"] = str(stats["count"])
11160
response["DevBar-DB-Time"] = f"{stats['duration']:.0f}ms"
@@ -129,28 +78,30 @@ def _inject_devbar(self, response, stats, level):
12978
if not matches:
13079
return
13180

132-
dup_marker = (
133-
' <strong class="duplicate-badge">(d)</strong>'
134-
if stats["has_duplicates"]
135-
else ""
81+
duplicates_html = self._build_duplicates_html(stats.get("duplicate_queries", []))
82+
83+
template = _template_engine.get_template("django_devbar/devbar.html")
84+
html = template.render(
85+
Context(
86+
{
87+
"position": get_position(),
88+
"level": level,
89+
"db_time": stats["duration"],
90+
"app_time": stats["python_time"],
91+
"query_count": stats["count"],
92+
"duplicates_html": duplicates_html,
93+
}
94+
)
13695
)
13796

138-
css = STYLE_BLOCK % get_position()
139-
html = BAR_TEMPLATE % (
140-
level,
141-
stats["duration"],
142-
stats["python_time"],
143-
stats["count"],
144-
dup_marker,
145-
)
146-
147-
script = ""
148-
if get_enable_console() and stats.get("duplicate_queries"):
149-
console_data = {"duplicates": stats["duplicate_queries"]}
150-
script = SCRIPT_TEMPLATE % json.dumps(console_data)
151-
152-
payload = (css + html + script).encode(response.charset or "utf-8")
97+
payload = html.encode(response.charset or "utf-8")
15398

15499
idx = matches[-1].start()
155100
response.content = content[:idx] + payload + content[idx:]
156101
response["Content-Length"] = str(len(response.content))
102+
103+
def _build_duplicates_html(self, duplicates):
104+
if not duplicates:
105+
return ""
106+
template = _template_engine.get_template("django_devbar/duplicates.html")
107+
return template.render(Context({"duplicates": duplicates}))
Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
<style>
2+
#django-devbar {
3+
position: fixed; {{ position }}; z-index: 999999999;
4+
display: flex; align-items: center; gap: 5px;
5+
font-family: -apple-system, system-ui, sans-serif;
6+
font-size: 11px; font-weight: 500;
7+
padding: 4px 8px; margin: 8px; border-radius: 4px;
8+
backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);
9+
box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
10+
transition: all 0.2s ease;
11+
cursor: default;
12+
line-height: 1.3;
13+
background: rgba(20, 20, 20, 0.92);
14+
color: #f5f5f5;
15+
}
16+
#django-devbar.level-warn { border-left: 3px solid #f59e0b; }
17+
#django-devbar span { opacity: 0.7; }
18+
#django-devbar strong { opacity: 1; font-weight: 600; }
19+
#django-devbar details { display: inline; position: relative; }
20+
#django-devbar summary {
21+
all: revert;
22+
color: #f59e0b; font-weight: 600; cursor: pointer; list-style: none;
23+
}
24+
#django-devbar summary::-webkit-details-marker { display: none; }
25+
#django-devbar details[open] > .dup-list {
26+
position: absolute; bottom: 100%; right: -8px; margin-bottom: 8px;
27+
background: rgba(20, 20, 20, 0.92); border-radius: 4px; padding: 6px;
28+
width: min(500px, 90vw); max-height: 200px; overflow-y: auto; overflow-x: hidden;
29+
box-sizing: border-box;
30+
box-shadow: 0 2px 8px rgba(0,0,0,0.15), 0 1px 2px rgba(0,0,0,0.2);
31+
}
32+
#django-devbar .dup-list details {
33+
display: block;
34+
margin: 0 0 6px 0;
35+
font-size: 11px;
36+
color: #f5f5f5;
37+
}
38+
#django-devbar .dup-list details:last-child { margin-bottom: 0; }
39+
#django-devbar .dup-list summary {
40+
all: revert;
41+
cursor: pointer;
42+
list-style: none;
43+
padding: 6px 8px;
44+
background: rgba(251, 191, 36, 0.1);
45+
border-radius: 3px;
46+
border-left: 2px solid #fbbf24;
47+
transition: background 0.15s ease;
48+
}
49+
#django-devbar .dup-list summary:hover {
50+
background: rgba(251, 191, 36, 0.2);
51+
}
52+
#django-devbar .dup-list summary code {
53+
white-space: nowrap;
54+
overflow: hidden;
55+
text-overflow: ellipsis;
56+
display: inline-block;
57+
max-width: 420px;
58+
vertical-align: bottom;
59+
}
60+
#django-devbar .dup-list summary::before {
61+
content: '▶ ';
62+
display: inline-block;
63+
transition: transform 0.15s ease;
64+
color: #fbbf24;
65+
font-size: 8px;
66+
margin-right: 6px;
67+
}
68+
#django-devbar .dup-list details[open] > summary::before {
69+
transform: rotate(90deg);
70+
}
71+
#django-devbar .dup-list code {
72+
all: revert;
73+
color: #fbbf24; font-family: 'SF Mono', Consolas, 'Liberation Mono', Menlo, monospace;
74+
overflow-wrap: break-word;
75+
}
76+
#django-devbar .dup-list details[open] > code {
77+
display: block;
78+
padding: 8px;
79+
background: rgba(0, 0, 0, 0.3);
80+
border-radius: 3px;
81+
margin-top: 6px;
82+
white-space: pre-wrap;
83+
line-height: 1.5;
84+
font-size: 10px;
85+
}
86+
@media (max-width: 640px) { #django-devbar { display: none; } }
87+
</style>
88+
<div id="django-devbar" class="level-{{ level }}">
89+
<span>db</span> <strong>{{ db_time|floatformat:0 }}ms</strong> <span>·</span>
90+
<span>app</span> <strong>{{ app_time|floatformat:0 }}ms</strong> <span>·</span>
91+
<span>queries</span> <strong>{{ query_count }}</strong>{{ duplicates_html|safe }}
92+
</div>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{% if duplicates %}<details><summary>({{ duplicates|length }}d)</summary><div class="dup-list">{% for dup in duplicates %}<details><summary><code>{{ dup.sql }}</code></summary><code>{{ dup.sql }}</code></details>{% endfor %}</div></details>{% endif %}

0 commit comments

Comments
 (0)