debugbundle-python 1.0.0__tar.gz → 1.1.2__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-1.0.0/src/debugbundle_python.egg-info → debugbundle_python-1.1.2}/PKG-INFO +1 -1
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/pyproject.toml +1 -1
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/config.py +65 -1
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/core.py +79 -16
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2/src/debugbundle_python.egg-info}/PKG-INFO +1 -1
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_remote_config.py +48 -2
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_repository_metadata.py +13 -2
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_smoke_script.py +2 -2
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/LICENSE +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/README.md +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/setup.cfg +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/__init__.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_django.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_fastapi.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_flask.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/logger_integrations.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/redaction.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/relay.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/relay_delivery.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/transport.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/requires.txt +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_contracts.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_framework_integrations.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_hooks.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_http_integration.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_optional_imports.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_relay.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_relay_delivery.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_sdk.py +0 -0
- {debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/tests/test_trigger_token.py +0 -0
|
@@ -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),
|
|
@@ -526,23 +528,27 @@ class DebugBundleSdk:
|
|
|
526
528
|
payload: dict[str, object],
|
|
527
529
|
context: Mapping[str, object] | None = None,
|
|
528
530
|
) -> dict[str, object]:
|
|
529
|
-
|
|
531
|
+
merged_context = self._merged_context(context)
|
|
532
|
+
event: dict[str, object] = {
|
|
530
533
|
"schema_version": SCHEMA_VERSION,
|
|
531
534
|
"event_id": str(uuid.uuid4()),
|
|
532
535
|
"event_type": event_type,
|
|
533
536
|
"occurred_at": _iso_now(self._time_provider),
|
|
534
537
|
"sdk_name": "debugbundle-python",
|
|
535
538
|
"sdk_version": _sdk_version(),
|
|
536
|
-
"sdk_language": "python",
|
|
537
539
|
"service": {
|
|
538
540
|
"name": self._service,
|
|
539
541
|
"runtime": "python",
|
|
540
542
|
"framework": None,
|
|
541
543
|
"environment": self._environment,
|
|
542
544
|
},
|
|
543
|
-
"correlation": _correlation_payload(
|
|
545
|
+
"correlation": _correlation_payload(merged_context),
|
|
544
546
|
"payload": payload,
|
|
545
547
|
}
|
|
548
|
+
envelope_context = _event_context(merged_context)
|
|
549
|
+
if envelope_context:
|
|
550
|
+
event["context"] = envelope_context
|
|
551
|
+
return event
|
|
546
552
|
|
|
547
553
|
def _merged_context(self, context: Mapping[str, object] | None = None) -> dict[str, object]:
|
|
548
554
|
merged = dict(self._context)
|
|
@@ -602,17 +608,31 @@ class DebugBundleSdk:
|
|
|
602
608
|
policy_threshold = self._capture_policy.capture_logs
|
|
603
609
|
return self._log_level if LEVEL_RANKS[self._log_level] >= LEVEL_RANKS[policy_threshold] else policy_threshold
|
|
604
610
|
|
|
605
|
-
def _should_capture_request_event(
|
|
611
|
+
def _should_capture_request_event(
|
|
612
|
+
self,
|
|
613
|
+
request: Mapping[str, object] | None,
|
|
614
|
+
response: Mapping[str, object] | None,
|
|
615
|
+
) -> bool:
|
|
606
616
|
policy = self._capture_policy.capture_request_events
|
|
607
617
|
status_code = None
|
|
608
618
|
if response is not None:
|
|
609
619
|
candidate = response.get("status_code") or response.get("response_status")
|
|
610
620
|
if isinstance(candidate, int):
|
|
611
621
|
status_code = candidate
|
|
622
|
+
request_path = None
|
|
623
|
+
http_method = None
|
|
624
|
+
if request is not None:
|
|
625
|
+
path_candidate = request.get("path") or request.get("url")
|
|
626
|
+
method_candidate = request.get("method")
|
|
627
|
+
request_path = path_candidate if isinstance(path_candidate, str) else None
|
|
628
|
+
http_method = method_candidate if isinstance(method_candidate, str) else None
|
|
612
629
|
if _is_immediate_request_incident_status(
|
|
613
630
|
status_code,
|
|
614
631
|
self._capture_policy.preset,
|
|
615
632
|
self._capture_policy.immediate_client_error_statuses,
|
|
633
|
+
request_path,
|
|
634
|
+
http_method,
|
|
635
|
+
self._capture_policy.immediate_client_error_path_rules,
|
|
616
636
|
):
|
|
617
637
|
return True
|
|
618
638
|
if policy == "off":
|
|
@@ -624,7 +644,7 @@ class DebugBundleSdk:
|
|
|
624
644
|
if status_code is None:
|
|
625
645
|
return policy == "filtered"
|
|
626
646
|
if policy == "failures_only":
|
|
627
|
-
return
|
|
647
|
+
return status_code >= 500
|
|
628
648
|
if policy == "filtered":
|
|
629
649
|
return False
|
|
630
650
|
return True
|
|
@@ -845,6 +865,14 @@ def _correlation_payload(context: Mapping[str, object]) -> dict[str, str | None]
|
|
|
845
865
|
}
|
|
846
866
|
|
|
847
867
|
|
|
868
|
+
def _event_context(context: Mapping[str, object]) -> dict[str, object]:
|
|
869
|
+
return {
|
|
870
|
+
str(key): value
|
|
871
|
+
for key, value in context.items()
|
|
872
|
+
if key not in {"request", "response", "correlation", "request_id", "trace_id", "session_id", "user_id_hash"}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
|
|
848
876
|
def _coerce_optional_string(value: object) -> str | None:
|
|
849
877
|
if value is None:
|
|
850
878
|
return None
|
|
@@ -859,6 +887,9 @@ def _is_immediate_request_incident_status(
|
|
|
859
887
|
status_code: int | None,
|
|
860
888
|
preset: str,
|
|
861
889
|
immediate_client_error_statuses: tuple[int, ...],
|
|
890
|
+
request_path: str | None = None,
|
|
891
|
+
http_method: str | None = None,
|
|
892
|
+
immediate_client_error_path_rules: tuple[object, ...] = (),
|
|
862
893
|
) -> bool:
|
|
863
894
|
if status_code is None:
|
|
864
895
|
return False
|
|
@@ -866,6 +897,13 @@ def _is_immediate_request_incident_status(
|
|
|
866
897
|
return True
|
|
867
898
|
if status_code in immediate_client_error_statuses:
|
|
868
899
|
return True
|
|
900
|
+
if _matches_immediate_client_error_path_rule(
|
|
901
|
+
status_code,
|
|
902
|
+
request_path,
|
|
903
|
+
http_method,
|
|
904
|
+
immediate_client_error_path_rules,
|
|
905
|
+
):
|
|
906
|
+
return True
|
|
869
907
|
if preset == "investigative":
|
|
870
908
|
return status_code in INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES
|
|
871
909
|
if preset == "balanced":
|
|
@@ -873,16 +911,41 @@ def _is_immediate_request_incident_status(
|
|
|
873
911
|
return False
|
|
874
912
|
|
|
875
913
|
|
|
876
|
-
def
|
|
877
|
-
|
|
914
|
+
def _matches_immediate_client_error_path_rule(
|
|
915
|
+
status_code: int,
|
|
916
|
+
request_path: str | None,
|
|
917
|
+
http_method: str | None,
|
|
918
|
+
rules: tuple[object, ...],
|
|
919
|
+
) -> bool:
|
|
920
|
+
if status_code < 400 or status_code > 499 or request_path is None:
|
|
878
921
|
return False
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
922
|
+
normalized_path = _normalize_request_path(request_path)
|
|
923
|
+
normalized_method = http_method.upper() if isinstance(http_method, str) else None
|
|
924
|
+
for rule in rules:
|
|
925
|
+
rule_status = getattr(rule, "status_code", None)
|
|
926
|
+
path_pattern = getattr(rule, "path_pattern", None)
|
|
927
|
+
methods = getattr(rule, "methods", ())
|
|
928
|
+
if rule_status != status_code or not isinstance(path_pattern, str):
|
|
929
|
+
continue
|
|
930
|
+
if methods and (normalized_method is None or normalized_method not in methods):
|
|
931
|
+
continue
|
|
932
|
+
if path_pattern.endswith("*"):
|
|
933
|
+
if normalized_path.startswith(path_pattern[:-1]):
|
|
934
|
+
return True
|
|
935
|
+
elif normalized_path == path_pattern:
|
|
936
|
+
return True
|
|
883
937
|
return False
|
|
884
938
|
|
|
885
939
|
|
|
940
|
+
def _normalize_request_path(value: str) -> str:
|
|
941
|
+
from urllib.parse import urlparse
|
|
942
|
+
|
|
943
|
+
parsed = urlparse(value)
|
|
944
|
+
if parsed.path:
|
|
945
|
+
return parsed.path
|
|
946
|
+
return value.split("?", 1)[0].split("#", 1)[0] if value.startswith("/") else "/"
|
|
947
|
+
|
|
948
|
+
|
|
886
949
|
def _time_now() -> float:
|
|
887
950
|
return datetime.now(tz=timezone.utc).timestamp()
|
|
888
951
|
|
|
@@ -891,7 +954,7 @@ def _sdk_version() -> str:
|
|
|
891
954
|
try:
|
|
892
955
|
return metadata.version("debugbundle-python")
|
|
893
956
|
except metadata.PackageNotFoundError:
|
|
894
|
-
return "1.
|
|
957
|
+
return "1.1.2"
|
|
895
958
|
|
|
896
959
|
|
|
897
960
|
def _sdk_config_endpoint(events_endpoint: str) -> str:
|
|
@@ -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,25 @@ 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 (
|
|
34
|
+
"## [1.1.2] - 2026-06-19\n\n"
|
|
35
|
+
"### Fixed\n"
|
|
36
|
+
"- Release packaging quality gates so the published Python SDK patch can ship cleanly "
|
|
37
|
+
"without changing runtime behavior.\n\n"
|
|
38
|
+
"## [1.1.1] - 2026-06-19\n\n"
|
|
39
|
+
"### Fixed\n"
|
|
40
|
+
"- Normalized canonical event-envelope emission so custom app context now stays in envelope `context`, "
|
|
41
|
+
"request events avoid legacy payload extras, and installed projects stop tripping malformed ingestion rejects "
|
|
42
|
+
"after upgrade.\n\n"
|
|
43
|
+
"## [1.1.0] - 2026-06-08" in changelog
|
|
44
|
+
)
|
|
34
45
|
assert "https://github.com/debugbundle/debugbundle-python/security/advisories/new" in security
|
|
35
46
|
|
|
36
47
|
|
|
37
48
|
def test_standalone_ci_workflow_covers_python_sdk_checks() -> None:
|
|
38
49
|
workflow = (REPO_ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8")
|
|
39
50
|
|
|
40
|
-
assert "actions/setup-python@
|
|
51
|
+
assert "actions/setup-python@v6" in workflow
|
|
41
52
|
assert 'python-version: "3.12"' in workflow
|
|
42
53
|
assert "python -m pip install -e .[dev]" in workflow
|
|
43
54
|
assert "ruff check src tests" in workflow
|
|
@@ -32,7 +32,7 @@ def test_install_with_retry_retries_until_success(monkeypatch) -> None:
|
|
|
32
32
|
monkeypatch.setattr(SMOKE.time, "sleep", sleeps.append)
|
|
33
33
|
|
|
34
34
|
SMOKE._install_with_retry(
|
|
35
|
-
["python", "-m", "pip", "install", "debugbundle-python==1.
|
|
35
|
+
["python", "-m", "pip", "install", "debugbundle-python==1.1.2"],
|
|
36
36
|
retries=3,
|
|
37
37
|
retry_delay_seconds=7,
|
|
38
38
|
)
|
|
@@ -50,7 +50,7 @@ def test_install_with_retry_raises_after_final_attempt(monkeypatch) -> None:
|
|
|
50
50
|
|
|
51
51
|
try:
|
|
52
52
|
SMOKE._install_with_retry(
|
|
53
|
-
["python", "-m", "pip", "install", "debugbundle-python==1.
|
|
53
|
+
["python", "-m", "pip", "install", "debugbundle-python==1.1.2"],
|
|
54
54
|
retries=2,
|
|
55
55
|
retry_delay_seconds=1,
|
|
56
56
|
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/__init__.py
RENAMED
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/common.py
RENAMED
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/django.py
RENAMED
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_django.py
RENAMED
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_fastapi.py
RENAMED
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle/integrations/relay_flask.py
RENAMED
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/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
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/src/debugbundle_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{debugbundle_python-1.0.0 → debugbundle_python-1.1.2}/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
|