1+ import json
12import re
23from contextlib import ExitStack
34from time import perf_counter
45
56from django .db import connections
67
78from . import tracker
8- from .conf import get_position , get_show_bar , get_show_headers
9-
10- DEVBAR_HTML = (
11- '<div id="django-devbar" style="position:fixed;{position};'
12- "background:rgba(0,0,0,0.7);color:rgba(255,255,255,0.85);"
13- "padding:4px 8px;margin:8px;"
14- "font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;"
15- "font-size:10px;line-height:1.3;font-weight:500;letter-spacing:0.02em;"
16- "z-index:99999;border-radius:3px;"
17- 'backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)">'
18- '<span style="opacity:0.5">queries</span> {count}{duplicates} <span style="opacity:0.5">·</span> '
19- '<span style="opacity:0.5">db</span> {duration:.0f}ms <span style="opacity:0.5">·</span> '
20- '<span style="opacity:0.5">total</span> {response_time:.0f}ms</div>'
9+ from .conf import (
10+ get_position ,
11+ get_show_bar ,
12+ get_show_headers ,
13+ get_thresholds ,
14+ get_enable_console ,
2115)
2216
23- DUPLICATE_MARKER = (
24- ' <span style="color:#f59e0b" title="Duplicate queries detected">(d)</span>'
25- )
17+ STYLE_BLOCK = """<style>
18+ #django-devbar {
19+ position: fixed; %s; z-index: 99999;
20+ font-family: -apple-system, system-ui, sans-serif;
21+ font-size: 11px; font-weight: 500;
22+ padding: 4px 8px; border-radius: 4px;
23+ backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
24+ box-shadow: 0 2px 5px rgba(0,0,0,0.1);
25+ transition: all 0.2s ease;
26+ cursor: default;
27+ line-height: 1.3;
28+ background: rgba(20, 20, 20, 0.85);
29+ color: #e5e5e5;
30+ }
31+ #django-devbar.level-warn { border-left: 3px solid #f59e0b; }
32+ #django-devbar.level-crit { border-left: 3px solid #dc2626; }
33+ #django-devbar span { opacity: 0.7; margin-right: 2px; }
34+ #django-devbar strong { opacity: 1; font-weight: 600; margin-right: 6px; }
35+ </style>"""
36+
37+ BAR_TEMPLATE = """<div id="django-devbar" class="level-%s">
38+ <span>db</span> <strong>%.0fms</strong>
39+ <span>py</span> <strong>%.0fms</strong>
40+ <span>count</span> <strong>%d</strong>%s
41+ </div>"""
42+
43+ SCRIPT_TEMPLATE = """<script>
44+ (function() {
45+ const stats = %s;
46+ if (stats.duplicates && stats.duplicates.length > 0) {
47+ console.groupCollapsed(`⚠️ Django DevBar: Duplicate Queries Detected (${stats.duplicates.length})`);
48+ console.table(stats.duplicates.map(d => ({SQL: d.sql, Parameters: d.params, Duration_ms: d.duration})));
49+ console.groupEnd();
50+ }
51+ })();
52+ </script>"""
2653
2754BODY_CLOSE_RE = re .compile (rb"</body\s*>" , re .IGNORECASE )
2855
@@ -32,8 +59,8 @@ def __init__(self, get_response):
3259 self .get_response = get_response
3360
3461 def __call__ (self , request ):
35- request_start = perf_counter ()
3662 tracker .reset ()
63+ request_start = perf_counter ()
3764
3865 with ExitStack () as stack :
3966 for alias in connections :
@@ -42,48 +69,85 @@ def __call__(self, request):
4269 )
4370 response = self .get_response (request )
4471
45- response_time = (perf_counter () - request_start ) * 1000
72+ total_time = (perf_counter () - request_start ) * 1000
4673 stats = tracker .get_stats ()
4774
75+ db_time = stats ["duration" ]
76+ python_time = max (0 , total_time - db_time )
77+
78+ stats ["python_time" ] = python_time
79+ stats ["total_time" ] = total_time
80+
81+ thresholds = get_thresholds ()
82+ level = self ._determine_level (stats , total_time , thresholds )
83+
4884 if get_show_headers ():
49- self ._add_headers (response , stats , response_time )
85+ self ._add_headers (response , stats )
5086
5187 if get_show_bar () and self ._can_inject (response ):
52- self ._inject_devbar (response , stats , response_time )
88+ self ._inject_devbar (response , stats , level )
5389
5490 return response
5591
56- def _add_headers (self , response , stats , response_time ):
57- response ["X-DevBar-Query-Count" ] = str (stats ["count" ])
58- response ["X-DevBar-Query-Duration" ] = f"{ stats ['duration' ]:.1f} "
59- response ["X-DevBar-Response-Time" ] = f"{ response_time :.1f} "
92+ def _determine_level (self , stats , total_time , thresholds ):
93+ if (
94+ total_time > thresholds ["time_critical" ]
95+ or stats ["count" ] > thresholds ["count_critical" ]
96+ ):
97+ return "crit"
98+ if (
99+ stats ["has_duplicates" ]
100+ or total_time > thresholds ["time_warning" ]
101+ or stats ["count" ] > thresholds ["count_warning" ]
102+ ):
103+ return "warn"
104+ return "ok"
105+
106+ def _add_headers (self , response , stats ):
107+ response ["DevBar-Query-Count" ] = str (stats ["count" ])
108+ response ["DevBar-DB-Time" ] = f"{ stats ['duration' ]:.0f} ms"
109+ response ["DevBar-App-Time" ] = f"{ stats ['python_time' ]:.0f} ms"
60110 if stats ["has_duplicates" ]:
61- response ["X- DevBar-Has- Duplicates" ] = "1"
111+ response ["DevBar-Duplicates" ] = str ( len ( stats [ "duplicate_queries" ]))
62112
63113 def _can_inject (self , response ):
64114 if getattr (response , "streaming" , False ):
65115 return False
66116 content_type = response .get ("Content-Type" , "" ).lower ()
67- if "text/ html" not in content_type :
117+ if "html" not in content_type :
68118 return False
69119 if response .get ("Content-Encoding" ):
70120 return False
71121 return hasattr (response , "content" )
72122
73- def _inject_devbar (self , response , stats , response_time ):
123+ def _inject_devbar (self , response , stats , level ):
74124 content = response .content
75125 matches = list (BODY_CLOSE_RE .finditer (content ))
76126 if not matches :
77127 return
78128
79- devbar_html = DEVBAR_HTML .format (
80- position = get_position (),
81- count = stats ["count" ],
82- duplicates = DUPLICATE_MARKER if stats ["has_duplicates" ] else "" ,
83- duration = stats ["duration" ],
84- response_time = response_time ,
85- ).encode (response .charset or "utf-8" )
129+ dup_marker = (
130+ ' <strong style="color:#f59e0b">(D)</strong>'
131+ if stats ["has_duplicates" ]
132+ else ""
133+ )
134+
135+ css = STYLE_BLOCK % get_position ()
136+ html = BAR_TEMPLATE % (
137+ level ,
138+ stats ["duration" ],
139+ stats ["python_time" ],
140+ stats ["count" ],
141+ dup_marker ,
142+ )
143+
144+ script = ""
145+ if get_enable_console () and stats .get ("duplicate_queries" ):
146+ console_data = {"duplicates" : stats ["duplicate_queries" ]}
147+ script = SCRIPT_TEMPLATE % json .dumps (console_data )
148+
149+ payload = (css + html + script ).encode (response .charset or "utf-8" )
86150
87151 idx = matches [- 1 ].start ()
88- response .content = content [:idx ] + devbar_html + content [idx :]
152+ response .content = content [:idx ] + payload + content [idx :]
89153 response ["Content-Length" ] = str (len (response .content ))
0 commit comments