debugbundle-python 0.1.9__tar.gz → 1.1.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.
- {debugbundle_python-0.1.9/src/debugbundle_python.egg-info → debugbundle_python-1.1.0}/PKG-INFO +2 -2
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/pyproject.toml +2 -2
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/config.py +65 -1
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/core.py +64 -13
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0/src/debugbundle_python.egg-info}/PKG-INFO +2 -2
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle_python.egg-info/SOURCES.txt +1 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_remote_config.py +48 -2
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_repository_metadata.py +2 -2
- debugbundle_python-1.1.0/tests/test_smoke_script.py +60 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/LICENSE +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/README.md +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/setup.cfg +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/__init__.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/relay_django.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/relay_fastapi.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/relay_flask.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/logger_integrations.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/redaction.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/relay.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/relay_delivery.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/transport.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle_python.egg-info/requires.txt +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_contracts.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_framework_integrations.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_hooks.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_http_integration.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_optional_imports.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_relay.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_relay_delivery.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_sdk.py +0 -0
- {debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/tests/test_trigger_token.py +0 -0
{debugbundle_python-0.1.9/src/debugbundle_python.egg-info → debugbundle_python-1.1.0}/PKG-INFO
RENAMED
|
@@ -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
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "debugbundle-python"
|
|
7
|
-
version = "
|
|
7
|
+
version = "1.1.0"
|
|
8
8
|
description = "DebugBundle SDK for Python"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -12,7 +12,7 @@ license = "AGPL-3.0-only"
|
|
|
12
12
|
authors = [{ name = "DebugBundle" }]
|
|
13
13
|
keywords = ["debugbundle", "debugging", "ai-agent", "error-tracking"]
|
|
14
14
|
classifiers = [
|
|
15
|
-
"Development Status ::
|
|
15
|
+
"Development Status :: 5 - Production/Stable",
|
|
16
16
|
"Framework :: Django",
|
|
17
17
|
"Framework :: FastAPI",
|
|
18
18
|
"Framework :: Flask",
|
|
@@ -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
|
|
@@ -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:
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0/src/debugbundle_python.egg-info}/PKG-INFO
RENAMED
|
@@ -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
|
|
@@ -229,7 +229,7 @@ def test_capture_policy_filters_logs_and_request_events_from_remote_config() ->
|
|
|
229
229
|
assert events[1]["payload"]["response_status"] == 503
|
|
230
230
|
|
|
231
231
|
|
|
232
|
-
def
|
|
232
|
+
def test_balanced_capture_policy_keeps_immediate_failures_but_not_unconfigured_4xx() -> None:
|
|
233
233
|
clock = ManualClock()
|
|
234
234
|
fetch = FakeFetch(
|
|
235
235
|
responses=[
|
|
@@ -271,7 +271,53 @@ def test_balanced_capture_policy_keeps_request_failure_anomaly_candidates() -> N
|
|
|
271
271
|
for event in events
|
|
272
272
|
if event["event_type"] == "request_event"
|
|
273
273
|
]
|
|
274
|
-
assert request_statuses == [429
|
|
274
|
+
assert request_statuses == [429]
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def test_capture_policy_promotes_configured_client_error_path_rules_when_request_capture_is_off() -> None:
|
|
278
|
+
clock = ManualClock()
|
|
279
|
+
fetch = FakeFetch(
|
|
280
|
+
responses=[
|
|
281
|
+
FakeConfigResponse(
|
|
282
|
+
200,
|
|
283
|
+
{
|
|
284
|
+
"probes_enabled": True,
|
|
285
|
+
"remote_probes_enabled": True,
|
|
286
|
+
"active_probes": [],
|
|
287
|
+
"poll_interval_ms": 15000,
|
|
288
|
+
"capture_policy": {
|
|
289
|
+
"preset": "minimal",
|
|
290
|
+
"capture_logs": "error",
|
|
291
|
+
"capture_request_events": "off",
|
|
292
|
+
"capture_breadcrumbs": "local_only",
|
|
293
|
+
"capture_probe_events": "buffer_only",
|
|
294
|
+
"immediate_client_error_path_rules": [
|
|
295
|
+
{"status_code": 404, "path_pattern": "/checkout/*", "methods": ["POST"]}
|
|
296
|
+
],
|
|
297
|
+
},
|
|
298
|
+
},
|
|
299
|
+
)
|
|
300
|
+
]
|
|
301
|
+
)
|
|
302
|
+
transport = FakeTransport()
|
|
303
|
+
sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
|
|
304
|
+
sdk.init(
|
|
305
|
+
project_token="dbundle_proj_test",
|
|
306
|
+
service="checkout-api",
|
|
307
|
+
environment="production",
|
|
308
|
+
fetch_impl=fetch,
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
sdk.capture_request({"method": "POST", "path": "/checkout/cart", "headers": {}}, {"status_code": 404})
|
|
312
|
+
sdk.capture_request({"method": "GET", "path": "/checkout/cart", "headers": {}}, {"status_code": 404})
|
|
313
|
+
sdk.capture_request({"method": "POST", "path": "/robots.txt", "headers": {}}, {"status_code": 404})
|
|
314
|
+
sdk.flush()
|
|
315
|
+
|
|
316
|
+
events = _events(transport)
|
|
317
|
+
request_events = [event for event in events if event["event_type"] == "request_event"]
|
|
318
|
+
assert len(request_events) == 1
|
|
319
|
+
assert request_events[0]["payload"]["path"] == "/checkout/cart"
|
|
320
|
+
assert request_events[0]["payload"]["response_status"] == 404
|
|
275
321
|
|
|
276
322
|
|
|
277
323
|
def test_investigative_capture_policy_promotes_409_even_when_request_capture_is_off() -> None:
|
|
@@ -30,14 +30,14 @@ def test_standalone_changelog_and_security_policy_are_launch_ready() -> None:
|
|
|
30
30
|
security = (REPO_ROOT / "SECURITY.md").read_text(encoding="utf-8")
|
|
31
31
|
|
|
32
32
|
assert "## [Unreleased]" in changelog
|
|
33
|
-
assert "## [
|
|
33
|
+
assert "## [1.1.0] - 2026-06-08" in changelog
|
|
34
34
|
assert "https://github.com/debugbundle/debugbundle-python/security/advisories/new" in security
|
|
35
35
|
|
|
36
36
|
|
|
37
37
|
def test_standalone_ci_workflow_covers_python_sdk_checks() -> None:
|
|
38
38
|
workflow = (REPO_ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8")
|
|
39
39
|
|
|
40
|
-
assert "actions/setup-python@
|
|
40
|
+
assert "actions/setup-python@v6" in workflow
|
|
41
41
|
assert 'python-version: "3.12"' in workflow
|
|
42
42
|
assert "python -m pip install -e .[dev]" in workflow
|
|
43
43
|
assert "ruff check src tests" in workflow
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
9
|
+
SMOKE_SCRIPT = REPO_ROOT / "smoke" / "run_app_driven_smoke.py"
|
|
10
|
+
SPEC = importlib.util.spec_from_file_location("debugbundle_python_smoke", SMOKE_SCRIPT)
|
|
11
|
+
assert SPEC is not None and SPEC.loader is not None
|
|
12
|
+
SMOKE = importlib.util.module_from_spec(SPEC)
|
|
13
|
+
sys.modules.setdefault("debugbundle_python_smoke", SMOKE)
|
|
14
|
+
SPEC.loader.exec_module(SMOKE)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_install_with_retry_retries_until_success(monkeypatch) -> None:
|
|
18
|
+
calls: list[list[str]] = []
|
|
19
|
+
sleeps: list[int] = []
|
|
20
|
+
outcomes = [
|
|
21
|
+
subprocess.CalledProcessError(returncode=1, cmd=["pip", "install"]),
|
|
22
|
+
None,
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
def fake_run_subprocess(command: list[str]) -> None:
|
|
26
|
+
calls.append(command)
|
|
27
|
+
outcome = outcomes.pop(0)
|
|
28
|
+
if outcome is not None:
|
|
29
|
+
raise outcome
|
|
30
|
+
|
|
31
|
+
monkeypatch.setattr(SMOKE, "_run_subprocess", fake_run_subprocess)
|
|
32
|
+
monkeypatch.setattr(SMOKE.time, "sleep", sleeps.append)
|
|
33
|
+
|
|
34
|
+
SMOKE._install_with_retry(
|
|
35
|
+
["python", "-m", "pip", "install", "debugbundle-python==1.1.0"],
|
|
36
|
+
retries=3,
|
|
37
|
+
retry_delay_seconds=7,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
assert len(calls) == 2
|
|
41
|
+
assert sleeps == [7]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_install_with_retry_raises_after_final_attempt(monkeypatch) -> None:
|
|
45
|
+
def fake_run_subprocess(command: list[str]) -> None:
|
|
46
|
+
raise subprocess.CalledProcessError(returncode=1, cmd=command)
|
|
47
|
+
|
|
48
|
+
monkeypatch.setattr(SMOKE, "_run_subprocess", fake_run_subprocess)
|
|
49
|
+
monkeypatch.setattr(SMOKE.time, "sleep", lambda seconds: None)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
SMOKE._install_with_retry(
|
|
53
|
+
["python", "-m", "pip", "install", "debugbundle-python==1.1.0"],
|
|
54
|
+
retries=2,
|
|
55
|
+
retry_delay_seconds=1,
|
|
56
|
+
)
|
|
57
|
+
except subprocess.CalledProcessError as error:
|
|
58
|
+
assert error.returncode == 1
|
|
59
|
+
else:
|
|
60
|
+
raise AssertionError("Expected install retry helper to re-raise after the final attempt.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/__init__.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/common.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/django.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/relay_django.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/relay_fastapi.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/integrations/relay_flask.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle/logger_integrations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{debugbundle_python-0.1.9 → debugbundle_python-1.1.0}/src/debugbundle_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|