django-devbar 0.6.1__tar.gz → 0.7.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: django-devbar
3
- Version: 0.6.1
3
+ Version: 0.7.0
4
4
  Summary: Lightweight performance devbar for Django
5
5
  Keywords: django,debug,performance,monitoring,development
6
6
  Author: amureki
@@ -22,11 +22,12 @@ Classifier: Framework :: Django :: 5.2
22
22
  Classifier: Framework :: Django :: 6.0
23
23
  Classifier: Topic :: Software Development
24
24
  Classifier: Intended Audience :: Developers
25
+ Classifier: License :: OSI Approved :: MIT License
25
26
  Requires-Dist: django>=4.2
26
27
  Requires-Python: >=3.10
27
28
  Project-URL: Homepage, https://github.com/amureki/django-devbar
28
- Project-URL: Issues, https://github.com/amureki/django-devbar/issues
29
29
  Project-URL: Repository, https://github.com/amureki/django-devbar
30
+ Project-URL: Issues, https://github.com/amureki/django-devbar/issues
30
31
  Description-Content-Type: text/markdown
31
32
 
32
33
  # django-devbar
@@ -95,6 +96,8 @@ DEVBAR = {
95
96
  "POSITION": "bottom-right", # bottom-right (default), bottom-left, top-right, top-left
96
97
  "SHOW_BAR": None, # follows DEBUG; set True/False to override
97
98
  "ENABLE_DEVTOOLS_DATA": None, # follows DEBUG; set True/False to override
99
+ "DEVTOOLS_HEADER_MAX_BYTES": 6144, # max bytes for DevBar-Data header payload
100
+ "DEVTOOLS_MAX_QUERIES": None, # optional hard cap for q/dup entries sent to DevTools
98
101
  }
99
102
  ```
100
103
 
@@ -107,6 +110,9 @@ Django DevBar adds HTTP response headers with performance metrics:
107
110
  An additional header is included by default in DEBUG mode:
108
111
 
109
112
  - **DevBar-Data** - JSON header with comprehensive metrics including similar and duplicate query details
113
+ - Query-level flags are embedded in `q` entries (`dup` and `sim`)
114
+ - Payload is automatically truncated to fit `DEVTOOLS_HEADER_MAX_BYTES`
115
+ - When truncated, response includes metadata keys: `tr`, `q_total`, `q_sent`
110
116
 
111
117
  This is useful for:
112
118
 
@@ -64,6 +64,8 @@ DEVBAR = {
64
64
  "POSITION": "bottom-right", # bottom-right (default), bottom-left, top-right, top-left
65
65
  "SHOW_BAR": None, # follows DEBUG; set True/False to override
66
66
  "ENABLE_DEVTOOLS_DATA": None, # follows DEBUG; set True/False to override
67
+ "DEVTOOLS_HEADER_MAX_BYTES": 6144, # max bytes for DevBar-Data header payload
68
+ "DEVTOOLS_MAX_QUERIES": None, # optional hard cap for q/dup entries sent to DevTools
67
69
  }
68
70
  ```
69
71
 
@@ -76,6 +78,9 @@ Django DevBar adds HTTP response headers with performance metrics:
76
78
  An additional header is included by default in DEBUG mode:
77
79
 
78
80
  - **DevBar-Data** - JSON header with comprehensive metrics including similar and duplicate query details
81
+ - Query-level flags are embedded in `q` entries (`dup` and `sim`)
82
+ - Payload is automatically truncated to fit `DEVTOOLS_HEADER_MAX_BYTES`
83
+ - When truncated, response includes metadata keys: `tr`, `q_total`, `q_sent`
79
84
 
80
85
  This is useful for:
81
86
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-devbar"
3
- version = "0.6.1"
3
+ version = "0.7.0"
4
4
  description = "Lightweight performance devbar for Django"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -27,6 +27,7 @@ classifiers = [
27
27
  "Framework :: Django :: 6.0",
28
28
  "Topic :: Software Development",
29
29
  "Intended Audience :: Developers",
30
+ "License :: OSI Approved :: MIT License",
30
31
  ]
31
32
 
32
33
  [project.urls]
@@ -13,6 +13,8 @@ def get_config():
13
13
  "POSITION": "bottom-right",
14
14
  "SHOW_BAR": None, # None = use settings.DEBUG
15
15
  "ENABLE_DEVTOOLS_DATA": None, # None = use settings.DEBUG
16
+ "DEVTOOLS_HEADER_MAX_BYTES": 6144,
17
+ "DEVTOOLS_MAX_QUERIES": None,
16
18
  **getattr(settings, "DEVBAR", {}),
17
19
  }
18
20
 
@@ -33,3 +35,13 @@ def get_enable_devtools_data():
33
35
  config = get_config()
34
36
  enable = config["ENABLE_DEVTOOLS_DATA"]
35
37
  return settings.DEBUG if enable is None else enable
38
+
39
+
40
+ def get_devtools_header_max_bytes():
41
+ config = get_config()
42
+ return config["DEVTOOLS_HEADER_MAX_BYTES"]
43
+
44
+
45
+ def get_devtools_max_queries():
46
+ config = get_config()
47
+ return config["DEVTOOLS_MAX_QUERIES"]
@@ -9,6 +9,8 @@ from django.template import Context, Engine
9
9
 
10
10
  from . import tracker
11
11
  from .conf import (
12
+ get_devtools_header_max_bytes,
13
+ get_devtools_max_queries,
12
14
  get_enable_devtools_data,
13
15
  get_position,
14
16
  get_show_bar,
@@ -58,34 +60,104 @@ class DevBarMiddleware:
58
60
  return response
59
61
 
60
62
  def _add_devtools_data_header(self, response, stats):
61
- # Abbreviated keys used to minimize DevBar-Data header size
62
- extension_data = {
63
+ max_bytes = get_devtools_header_max_bytes()
64
+ if max_bytes <= 0:
65
+ return
66
+
67
+ summary_data = {
63
68
  "c": stats["count"],
64
69
  "db": round(stats["duration"], 2),
65
70
  "app": stats["python_time"],
66
71
  "full": stats["total_time"],
67
72
  }
73
+ if self._json_size(summary_data) > max_bytes:
74
+ return
68
75
 
69
- all_queries = stats.get("queries", [])
70
- duplicates = stats.get("duplicate_queries", [])
76
+ raw_queries = stats.get("queries", [])
77
+ total_query_count = len(raw_queries)
78
+ max_queries = get_devtools_max_queries()
79
+ all_queries = (
80
+ raw_queries[:max_queries] if max_queries is not None else raw_queries
81
+ )
82
+ processed_queries = [
83
+ {
84
+ "s": truncate_sql(q["sql"]),
85
+ "dur": q["duration"],
86
+ "dup": 1 if q["is_duplicate"] else 0,
87
+ "sim": 1 if q.get("is_similar") else 0,
88
+ }
89
+ for q in all_queries
90
+ ]
91
+ is_truncated = len(processed_queries) < total_query_count
71
92
 
72
- if all_queries:
73
- processed_queries = [
74
- {
75
- "s": truncate_sql(q["sql"]),
76
- "dur": q["duration"],
77
- "dup": 1 if q["is_duplicate"] else 0,
78
- "sim": 1 if q.get("is_similar") else 0,
79
- }
80
- for q in all_queries
81
- ]
82
- extension_data["q"] = processed_queries
93
+ full_payload = summary_data.copy()
94
+ if processed_queries:
95
+ full_payload["q"] = processed_queries
96
+ if self._json_size(full_payload) <= max_bytes and not is_truncated:
97
+ response["DevBar-Data"] = self._serialize_payload(full_payload)
98
+ return
83
99
 
84
- if duplicates:
85
- marked_duplicates = [{**d, "dup": 1} for d in duplicates]
86
- extension_data["dup"] = marked_duplicates
100
+ best_count = self._max_queries_that_fit(
101
+ summary_data,
102
+ processed_queries,
103
+ max_bytes,
104
+ total_query_count,
105
+ )
106
+ payload = self._build_truncated_payload(
107
+ summary_data,
108
+ processed_queries,
109
+ best_count,
110
+ total_query_count,
111
+ )
87
112
 
88
- response["DevBar-Data"] = json.dumps(extension_data)
113
+ if self._json_size(payload) <= max_bytes:
114
+ response["DevBar-Data"] = self._serialize_payload(payload)
115
+ else:
116
+ response["DevBar-Data"] = self._serialize_payload(summary_data)
117
+
118
+ def _json_size(self, data):
119
+ return len(self._serialize_payload(data).encode("utf-8"))
120
+
121
+ def _serialize_payload(self, payload):
122
+ return json.dumps(payload, separators=(",", ":"))
123
+
124
+ def _build_truncated_payload(
125
+ self,
126
+ summary_data,
127
+ processed_queries,
128
+ query_count,
129
+ queries_total,
130
+ ):
131
+ payload = summary_data.copy()
132
+ if query_count:
133
+ payload["q"] = processed_queries[:query_count]
134
+ payload["tr"] = 1
135
+ payload["q_total"] = queries_total
136
+ payload["q_sent"] = query_count
137
+ return payload
138
+
139
+ def _max_queries_that_fit(
140
+ self,
141
+ summary_data,
142
+ processed_queries,
143
+ max_bytes,
144
+ queries_total,
145
+ ):
146
+ low = 0
147
+ high = len(processed_queries)
148
+ while low < high:
149
+ mid = (low + high + 1) // 2
150
+ payload = self._build_truncated_payload(
151
+ summary_data,
152
+ processed_queries,
153
+ mid,
154
+ queries_total,
155
+ )
156
+ if self._json_size(payload) <= max_bytes:
157
+ low = mid
158
+ else:
159
+ high = mid - 1
160
+ return low
89
161
 
90
162
  def _add_server_timing_header(self, response, stats):
91
163
  parts = [