debugbundle-python 0.1.9__py3-none-any.whl → 1.1.0__py3-none-any.whl
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.
- debugbundle/config.py +65 -1
- debugbundle/core.py +64 -13
- {debugbundle_python-0.1.9.dist-info → debugbundle_python-1.1.0.dist-info}/METADATA +2 -2
- {debugbundle_python-0.1.9.dist-info → debugbundle_python-1.1.0.dist-info}/RECORD +7 -7
- {debugbundle_python-0.1.9.dist-info → debugbundle_python-1.1.0.dist-info}/WHEEL +0 -0
- {debugbundle_python-0.1.9.dist-info → debugbundle_python-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-0.1.9.dist-info → debugbundle_python-1.1.0.dist-info}/top_level.txt +0 -0
debugbundle/config.py
CHANGED
|
@@ -6,6 +6,13 @@ from typing import Any
|
|
|
6
6
|
DEFAULT_PROBES_POLL_INTERVAL_MS = 60_000
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class ImmediateClientErrorPathRule:
|
|
11
|
+
status_code: int
|
|
12
|
+
path_pattern: str
|
|
13
|
+
methods: tuple[str, ...]
|
|
14
|
+
|
|
15
|
+
|
|
9
16
|
@dataclass(frozen=True)
|
|
10
17
|
class CapturePolicy:
|
|
11
18
|
preset: str
|
|
@@ -14,6 +21,7 @@ class CapturePolicy:
|
|
|
14
21
|
capture_breadcrumbs: str
|
|
15
22
|
capture_probe_events: str
|
|
16
23
|
immediate_client_error_statuses: tuple[int, ...]
|
|
24
|
+
immediate_client_error_path_rules: tuple[ImmediateClientErrorPathRule, ...] = ()
|
|
17
25
|
|
|
18
26
|
|
|
19
27
|
@dataclass(frozen=True)
|
|
@@ -42,6 +50,7 @@ BALANCED_CAPTURE_POLICY = CapturePolicy(
|
|
|
42
50
|
capture_breadcrumbs="exception_only",
|
|
43
51
|
capture_probe_events="buffer_only",
|
|
44
52
|
immediate_client_error_statuses=(),
|
|
53
|
+
immediate_client_error_path_rules=(),
|
|
45
54
|
)
|
|
46
55
|
|
|
47
56
|
MINIMAL_CAPTURE_POLICY = CapturePolicy(
|
|
@@ -51,6 +60,7 @@ MINIMAL_CAPTURE_POLICY = CapturePolicy(
|
|
|
51
60
|
capture_breadcrumbs="local_only",
|
|
52
61
|
capture_probe_events="buffer_only",
|
|
53
62
|
immediate_client_error_statuses=(),
|
|
63
|
+
immediate_client_error_path_rules=(),
|
|
54
64
|
)
|
|
55
65
|
|
|
56
66
|
|
|
@@ -123,6 +133,9 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
|
123
133
|
immediate_client_error_statuses = _parse_immediate_client_error_statuses(
|
|
124
134
|
payload.get("immediate_client_error_statuses")
|
|
125
135
|
)
|
|
136
|
+
immediate_client_error_path_rules = _parse_immediate_client_error_path_rules(
|
|
137
|
+
payload.get("immediate_client_error_path_rules")
|
|
138
|
+
)
|
|
126
139
|
|
|
127
140
|
if capture_logs not in {"off", "error", "warning", "info"}:
|
|
128
141
|
return None
|
|
@@ -132,7 +145,7 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
|
132
145
|
return None
|
|
133
146
|
if capture_probe_events not in {"buffer_only", "standalone_when_activated"}:
|
|
134
147
|
return None
|
|
135
|
-
if immediate_client_error_statuses is None:
|
|
148
|
+
if immediate_client_error_statuses is None or immediate_client_error_path_rules is None:
|
|
136
149
|
return None
|
|
137
150
|
|
|
138
151
|
return CapturePolicy(
|
|
@@ -142,6 +155,7 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
|
|
|
142
155
|
capture_breadcrumbs=capture_breadcrumbs,
|
|
143
156
|
capture_probe_events=capture_probe_events,
|
|
144
157
|
immediate_client_error_statuses=immediate_client_error_statuses,
|
|
158
|
+
immediate_client_error_path_rules=immediate_client_error_path_rules,
|
|
145
159
|
)
|
|
146
160
|
|
|
147
161
|
|
|
@@ -160,6 +174,56 @@ def _parse_immediate_client_error_statuses(value: object) -> tuple[int, ...] | N
|
|
|
160
174
|
return tuple(sorted(set(statuses)))
|
|
161
175
|
|
|
162
176
|
|
|
177
|
+
def _parse_immediate_client_error_path_rules(value: object) -> tuple[ImmediateClientErrorPathRule, ...] | None:
|
|
178
|
+
if value is None:
|
|
179
|
+
return ()
|
|
180
|
+
if not isinstance(value, list) or len(value) > 25:
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
rules: list[ImmediateClientErrorPathRule] = []
|
|
184
|
+
for item in value:
|
|
185
|
+
if not isinstance(item, dict):
|
|
186
|
+
return None
|
|
187
|
+
status_code = item.get("status_code")
|
|
188
|
+
path_pattern = item.get("path_pattern")
|
|
189
|
+
raw_methods = item.get("methods", [])
|
|
190
|
+
if (
|
|
191
|
+
not isinstance(status_code, int)
|
|
192
|
+
or isinstance(status_code, bool)
|
|
193
|
+
or status_code < 400
|
|
194
|
+
or status_code > 499
|
|
195
|
+
or not isinstance(path_pattern, str)
|
|
196
|
+
or not _is_valid_path_pattern(path_pattern)
|
|
197
|
+
or not isinstance(raw_methods, list)
|
|
198
|
+
or len(raw_methods) > 7
|
|
199
|
+
):
|
|
200
|
+
return None
|
|
201
|
+
|
|
202
|
+
methods: list[str] = []
|
|
203
|
+
for raw_method in raw_methods:
|
|
204
|
+
method = raw_method.upper() if isinstance(raw_method, str) else ""
|
|
205
|
+
if method not in {"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}:
|
|
206
|
+
return None
|
|
207
|
+
if method not in methods:
|
|
208
|
+
methods.append(method)
|
|
209
|
+
rules.append(
|
|
210
|
+
ImmediateClientErrorPathRule(
|
|
211
|
+
status_code=status_code,
|
|
212
|
+
path_pattern=path_pattern,
|
|
213
|
+
methods=tuple(methods),
|
|
214
|
+
)
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
return tuple(rules)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _is_valid_path_pattern(value: str) -> bool:
|
|
221
|
+
if not value.startswith("/") or len(value) == 0 or len(value) > 256 or "?" in value or "#" in value:
|
|
222
|
+
return False
|
|
223
|
+
wildcard_index = value.find("*")
|
|
224
|
+
return wildcard_index == -1 or wildcard_index == len(value) - 1
|
|
225
|
+
|
|
226
|
+
|
|
163
227
|
def _parse_directive(payload: object) -> RemoteProbeDirective | None:
|
|
164
228
|
if not isinstance(payload, dict):
|
|
165
229
|
return None
|
debugbundle/core.py
CHANGED
|
@@ -55,9 +55,6 @@ LEVEL_RANKS = {
|
|
|
55
55
|
}
|
|
56
56
|
BALANCED_IMMEDIATE_REQUEST_STATUSES = {408, 423, 424, 425, 429}
|
|
57
57
|
INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES = BALANCED_IMMEDIATE_REQUEST_STATUSES | {409}
|
|
58
|
-
BALANCED_STANDARD_ANOMALY_STATUSES = {401, 403, 404, 409, 422}
|
|
59
|
-
BALANCED_HIGH_VOLUME_ANOMALY_STATUSES = {400, 410}
|
|
60
|
-
INVESTIGATIVE_ANOMALY_STATUSES = BALANCED_STANDARD_ANOMALY_STATUSES | BALANCED_HIGH_VOLUME_ANOMALY_STATUSES
|
|
61
58
|
|
|
62
59
|
|
|
63
60
|
@dataclass
|
|
@@ -286,7 +283,12 @@ class DebugBundleSdk:
|
|
|
286
283
|
context: Mapping[str, object] | None = None,
|
|
287
284
|
) -> None:
|
|
288
285
|
with self._lock:
|
|
289
|
-
|
|
286
|
+
should_capture = (
|
|
287
|
+
self._enabled
|
|
288
|
+
and self._passes_sample_rate()
|
|
289
|
+
and self._should_capture_request_event(request, response)
|
|
290
|
+
)
|
|
291
|
+
if not should_capture:
|
|
290
292
|
return
|
|
291
293
|
payload = _request_event_payload(
|
|
292
294
|
_redact_mapping(dict(request), self._redact_fields),
|
|
@@ -602,17 +604,31 @@ class DebugBundleSdk:
|
|
|
602
604
|
policy_threshold = self._capture_policy.capture_logs
|
|
603
605
|
return self._log_level if LEVEL_RANKS[self._log_level] >= LEVEL_RANKS[policy_threshold] else policy_threshold
|
|
604
606
|
|
|
605
|
-
def _should_capture_request_event(
|
|
607
|
+
def _should_capture_request_event(
|
|
608
|
+
self,
|
|
609
|
+
request: Mapping[str, object] | None,
|
|
610
|
+
response: Mapping[str, object] | None,
|
|
611
|
+
) -> bool:
|
|
606
612
|
policy = self._capture_policy.capture_request_events
|
|
607
613
|
status_code = None
|
|
608
614
|
if response is not None:
|
|
609
615
|
candidate = response.get("status_code") or response.get("response_status")
|
|
610
616
|
if isinstance(candidate, int):
|
|
611
617
|
status_code = candidate
|
|
618
|
+
request_path = None
|
|
619
|
+
http_method = None
|
|
620
|
+
if request is not None:
|
|
621
|
+
path_candidate = request.get("path") or request.get("url")
|
|
622
|
+
method_candidate = request.get("method")
|
|
623
|
+
request_path = path_candidate if isinstance(path_candidate, str) else None
|
|
624
|
+
http_method = method_candidate if isinstance(method_candidate, str) else None
|
|
612
625
|
if _is_immediate_request_incident_status(
|
|
613
626
|
status_code,
|
|
614
627
|
self._capture_policy.preset,
|
|
615
628
|
self._capture_policy.immediate_client_error_statuses,
|
|
629
|
+
request_path,
|
|
630
|
+
http_method,
|
|
631
|
+
self._capture_policy.immediate_client_error_path_rules,
|
|
616
632
|
):
|
|
617
633
|
return True
|
|
618
634
|
if policy == "off":
|
|
@@ -624,7 +640,7 @@ class DebugBundleSdk:
|
|
|
624
640
|
if status_code is None:
|
|
625
641
|
return policy == "filtered"
|
|
626
642
|
if policy == "failures_only":
|
|
627
|
-
return
|
|
643
|
+
return status_code >= 500
|
|
628
644
|
if policy == "filtered":
|
|
629
645
|
return False
|
|
630
646
|
return True
|
|
@@ -859,6 +875,9 @@ def _is_immediate_request_incident_status(
|
|
|
859
875
|
status_code: int | None,
|
|
860
876
|
preset: str,
|
|
861
877
|
immediate_client_error_statuses: tuple[int, ...],
|
|
878
|
+
request_path: str | None = None,
|
|
879
|
+
http_method: str | None = None,
|
|
880
|
+
immediate_client_error_path_rules: tuple[object, ...] = (),
|
|
862
881
|
) -> bool:
|
|
863
882
|
if status_code is None:
|
|
864
883
|
return False
|
|
@@ -866,6 +885,13 @@ def _is_immediate_request_incident_status(
|
|
|
866
885
|
return True
|
|
867
886
|
if status_code in immediate_client_error_statuses:
|
|
868
887
|
return True
|
|
888
|
+
if _matches_immediate_client_error_path_rule(
|
|
889
|
+
status_code,
|
|
890
|
+
request_path,
|
|
891
|
+
http_method,
|
|
892
|
+
immediate_client_error_path_rules,
|
|
893
|
+
):
|
|
894
|
+
return True
|
|
869
895
|
if preset == "investigative":
|
|
870
896
|
return status_code in INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES
|
|
871
897
|
if preset == "balanced":
|
|
@@ -873,16 +899,41 @@ def _is_immediate_request_incident_status(
|
|
|
873
899
|
return False
|
|
874
900
|
|
|
875
901
|
|
|
876
|
-
def
|
|
877
|
-
|
|
902
|
+
def _matches_immediate_client_error_path_rule(
|
|
903
|
+
status_code: int,
|
|
904
|
+
request_path: str | None,
|
|
905
|
+
http_method: str | None,
|
|
906
|
+
rules: tuple[object, ...],
|
|
907
|
+
) -> bool:
|
|
908
|
+
if status_code < 400 or status_code > 499 or request_path is None:
|
|
878
909
|
return False
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
910
|
+
normalized_path = _normalize_request_path(request_path)
|
|
911
|
+
normalized_method = http_method.upper() if isinstance(http_method, str) else None
|
|
912
|
+
for rule in rules:
|
|
913
|
+
rule_status = getattr(rule, "status_code", None)
|
|
914
|
+
path_pattern = getattr(rule, "path_pattern", None)
|
|
915
|
+
methods = getattr(rule, "methods", ())
|
|
916
|
+
if rule_status != status_code or not isinstance(path_pattern, str):
|
|
917
|
+
continue
|
|
918
|
+
if methods and (normalized_method is None or normalized_method not in methods):
|
|
919
|
+
continue
|
|
920
|
+
if path_pattern.endswith("*"):
|
|
921
|
+
if normalized_path.startswith(path_pattern[:-1]):
|
|
922
|
+
return True
|
|
923
|
+
elif normalized_path == path_pattern:
|
|
924
|
+
return True
|
|
883
925
|
return False
|
|
884
926
|
|
|
885
927
|
|
|
928
|
+
def _normalize_request_path(value: str) -> str:
|
|
929
|
+
from urllib.parse import urlparse
|
|
930
|
+
|
|
931
|
+
parsed = urlparse(value)
|
|
932
|
+
if parsed.path:
|
|
933
|
+
return parsed.path
|
|
934
|
+
return value.split("?", 1)[0].split("#", 1)[0] if value.startswith("/") else "/"
|
|
935
|
+
|
|
936
|
+
|
|
886
937
|
def _time_now() -> float:
|
|
887
938
|
return datetime.now(tz=timezone.utc).timestamp()
|
|
888
939
|
|
|
@@ -891,7 +942,7 @@ def _sdk_version() -> str:
|
|
|
891
942
|
try:
|
|
892
943
|
return metadata.version("debugbundle-python")
|
|
893
944
|
except metadata.PackageNotFoundError:
|
|
894
|
-
return "
|
|
945
|
+
return "1.1.0"
|
|
895
946
|
|
|
896
947
|
|
|
897
948
|
def _sdk_config_endpoint(events_endpoint: str) -> str:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
@@ -8,7 +8,7 @@ Project-URL: Homepage, https://debugbundle.com/docs/sdks/python
|
|
|
8
8
|
Project-URL: Repository, https://github.com/debugbundle/debugbundle-python
|
|
9
9
|
Project-URL: Issues, https://github.com/debugbundle/debugbundle-python/issues
|
|
10
10
|
Keywords: debugbundle,debugging,ai-agent,error-tracking
|
|
11
|
-
Classifier: Development Status ::
|
|
11
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
12
12
|
Classifier: Framework :: Django
|
|
13
13
|
Classifier: Framework :: FastAPI
|
|
14
14
|
Classifier: Framework :: Flask
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
debugbundle/__init__.py,sha256=mqibduUrdcr_fnb6k3Gwf93H7iqVBS8zd6SceX-U3Q4,5011
|
|
2
|
-
debugbundle/config.py,sha256=
|
|
3
|
-
debugbundle/core.py,sha256=
|
|
2
|
+
debugbundle/config.py,sha256=ENRSR5jUI7xYdBqUakWws3GIjBk00GWFmwXVE_iQp2Y,9296
|
|
3
|
+
debugbundle/core.py,sha256=oeCuJgaHPk3LNObrD54IDB_YrzuZFYlvYJUmTi-2PfE,37322
|
|
4
4
|
debugbundle/logger_integrations.py,sha256=RuTNaD9RRVmiE-BBkksAXWVEGaMzLrWavVpQdgGZBpE,4564
|
|
5
5
|
debugbundle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
debugbundle/redaction.py,sha256=QTaPkSsYv54yR1mz8SB6Q4vWjvp7Ddcui4WXaH2RVz8,736
|
|
@@ -17,8 +17,8 @@ debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-
|
|
|
17
17
|
debugbundle/integrations/relay_django.py,sha256=Wj8BE9D5otU2KmtjgqdKIJ-BvXVcagCKgpLYvnPZ6WE,2281
|
|
18
18
|
debugbundle/integrations/relay_fastapi.py,sha256=-Zl6bvhYMLii__mP6-UvSdwxS5VKsb9OZvk-yy-3hEw,2227
|
|
19
19
|
debugbundle/integrations/relay_flask.py,sha256=VFHkDTJy4LkZzL_Ry_Ba-e_gW6UTG4PBwnLcvB_oXuQ,2011
|
|
20
|
-
debugbundle_python-
|
|
21
|
-
debugbundle_python-
|
|
22
|
-
debugbundle_python-
|
|
23
|
-
debugbundle_python-
|
|
24
|
-
debugbundle_python-
|
|
20
|
+
debugbundle_python-1.1.0.dist-info/licenses/LICENSE,sha256=AKZZ5DQAHrOKGwt24VoRd-SXJLM9OloxlsX8ZgENdEY,735
|
|
21
|
+
debugbundle_python-1.1.0.dist-info/METADATA,sha256=dJKpk66J95YPT9BuTuzJKIgZXJsflZGUdE5p9U1QDM4,13841
|
|
22
|
+
debugbundle_python-1.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
debugbundle_python-1.1.0.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
|
|
24
|
+
debugbundle_python-1.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|