django-devbar 0.6.0__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.0
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
@@ -71,16 +72,21 @@ uv add --dev django-devbar
71
72
  pip install django-devbar
72
73
  ```
73
74
 
74
- Add to your middleware as early as possible, but after any middleware that encodes the response (e.g., `GZipMiddleware`):
75
+ Add to your middleware early. For example:
75
76
 
76
77
  ```python
77
78
  MIDDLEWARE = [
78
79
  "django.middleware.security.SecurityMiddleware",
79
- "django_devbar.DevBarMiddleware",
80
80
  # ...
81
81
  ]
82
+
83
+ if DEBUG:
84
+ idx = MIDDLEWARE.index("django.middleware.security.SecurityMiddleware")
85
+ MIDDLEWARE.insert(idx + 1, "django_devbar.DevBarMiddleware")
82
86
  ```
83
87
 
88
+ This keeps the middleware active only in development and avoids import errors if the package isn't installed in production.
89
+
84
90
  ## Configuration
85
91
 
86
92
  All settings are optional. Configure via a `DEVBAR` dict in your Django settings:
@@ -90,6 +96,8 @@ DEVBAR = {
90
96
  "POSITION": "bottom-right", # bottom-right (default), bottom-left, top-right, top-left
91
97
  "SHOW_BAR": None, # follows DEBUG; set True/False to override
92
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
93
101
  }
94
102
  ```
95
103
 
@@ -102,6 +110,9 @@ Django DevBar adds HTTP response headers with performance metrics:
102
110
  An additional header is included by default in DEBUG mode:
103
111
 
104
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`
105
116
 
106
117
  This is useful for:
107
118
 
@@ -40,16 +40,21 @@ uv add --dev django-devbar
40
40
  pip install django-devbar
41
41
  ```
42
42
 
43
- Add to your middleware as early as possible, but after any middleware that encodes the response (e.g., `GZipMiddleware`):
43
+ Add to your middleware early. For example:
44
44
 
45
45
  ```python
46
46
  MIDDLEWARE = [
47
47
  "django.middleware.security.SecurityMiddleware",
48
- "django_devbar.DevBarMiddleware",
49
48
  # ...
50
49
  ]
50
+
51
+ if DEBUG:
52
+ idx = MIDDLEWARE.index("django.middleware.security.SecurityMiddleware")
53
+ MIDDLEWARE.insert(idx + 1, "django_devbar.DevBarMiddleware")
51
54
  ```
52
55
 
56
+ This keeps the middleware active only in development and avoids import errors if the package isn't installed in production.
57
+
53
58
  ## Configuration
54
59
 
55
60
  All settings are optional. Configure via a `DEVBAR` dict in your Django settings:
@@ -59,6 +64,8 @@ DEVBAR = {
59
64
  "POSITION": "bottom-right", # bottom-right (default), bottom-left, top-right, top-left
60
65
  "SHOW_BAR": None, # follows DEBUG; set True/False to override
61
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
62
69
  }
63
70
  ```
64
71
 
@@ -71,6 +78,9 @@ Django DevBar adds HTTP response headers with performance metrics:
71
78
  An additional header is included by default in DEBUG mode:
72
79
 
73
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`
74
84
 
75
85
  This is useful for:
76
86
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "django-devbar"
3
- version = "0.6.0"
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 = [
@@ -85,7 +85,7 @@ def _record(sql, params, duration):
85
85
  query_log = _query_log.get()
86
86
  query_log.append(
87
87
  {
88
- "sql": sql,
88
+ "sql": str(sql),
89
89
  "duration": round(duration, 2),
90
90
  "params_hash": params_hash,
91
91
  }