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 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
- if not self._enabled or not self._passes_sample_rate() or not self._should_capture_request_event(response):
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(self, response: Mapping[str, object] | None) -> bool:
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 _is_request_anomaly_candidate_status(status_code, self._capture_policy.preset)
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 _is_request_anomaly_candidate_status(status_code: int | None, preset: str) -> bool:
877
- if status_code is None or status_code < 400 or status_code >= 500:
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
- if preset == "investigative":
880
- return status_code in INVESTIGATIVE_ANOMALY_STATUSES
881
- if preset == "balanced":
882
- return status_code in BALANCED_STANDARD_ANOMALY_STATUSES or status_code in BALANCED_HIGH_VOLUME_ANOMALY_STATUSES
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 "0.1.0"
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: 0.1.9
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 :: 3 - Alpha
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=Cty7Wkj2nIRf5bumsI-dexrvQTFwVICPKrcx6LkhU4Q,6960
3
- debugbundle/core.py,sha256=YQFQax6EhjnN7SKFMXq_AohHZyFqxx18Az1ezIZiaDg,35772
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-0.1.9.dist-info/licenses/LICENSE,sha256=AKZZ5DQAHrOKGwt24VoRd-SXJLM9OloxlsX8ZgENdEY,735
21
- debugbundle_python-0.1.9.dist-info/METADATA,sha256=xDpBNECSnuPc1kSNDE-5Zd9SQAd5C-iNwZUWU1ISFXM,13829
22
- debugbundle_python-0.1.9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
- debugbundle_python-0.1.9.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
24
- debugbundle_python-0.1.9.dist-info/RECORD,,
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,,