debugbundle-python 0.1.3__tar.gz → 0.1.5__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.
Files changed (38) hide show
  1. {debugbundle_python-0.1.3/src/debugbundle_python.egg-info → debugbundle_python-0.1.5}/PKG-INFO +1 -1
  2. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/pyproject.toml +1 -1
  3. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/config.py +24 -0
  4. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/core.py +12 -2
  5. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5/src/debugbundle_python.egg-info}/PKG-INFO +1 -1
  6. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_remote_config.py +47 -1
  7. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/LICENSE +0 -0
  8. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/README.md +0 -0
  9. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/setup.cfg +0 -0
  10. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/__init__.py +0 -0
  11. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/__init__.py +0 -0
  12. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/common.py +0 -0
  13. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/django.py +0 -0
  14. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/fastapi.py +0 -0
  15. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/flask.py +0 -0
  16. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_django.py +0 -0
  17. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_fastapi.py +0 -0
  18. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/integrations/relay_flask.py +0 -0
  19. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/logger_integrations.py +0 -0
  20. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/py.typed +0 -0
  21. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/redaction.py +0 -0
  22. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/relay.py +0 -0
  23. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/suppression.py +0 -0
  24. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/transport.py +0 -0
  25. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle/trigger_token.py +0 -0
  26. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
  27. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
  28. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/requires.txt +0 -0
  29. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/src/debugbundle_python.egg-info/top_level.txt +0 -0
  30. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_contracts.py +0 -0
  31. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_framework_integrations.py +0 -0
  32. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_hooks.py +0 -0
  33. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_http_integration.py +0 -0
  34. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_relay.py +0 -0
  35. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_repository_metadata.py +0 -0
  36. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_schema_validation.py +0 -0
  37. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_sdk.py +0 -0
  38. {debugbundle_python-0.1.3 → debugbundle_python-0.1.5}/tests/test_trigger_token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "debugbundle-python"
7
- version = "0.1.3"
7
+ version = "0.1.5"
8
8
  description = "DebugBundle SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -13,6 +13,7 @@ class CapturePolicy:
13
13
  capture_request_events: str
14
14
  capture_breadcrumbs: str
15
15
  capture_probe_events: str
16
+ immediate_client_error_statuses: tuple[int, ...]
16
17
 
17
18
 
18
19
  @dataclass(frozen=True)
@@ -40,6 +41,7 @@ BALANCED_CAPTURE_POLICY = CapturePolicy(
40
41
  capture_request_events="failures_only",
41
42
  capture_breadcrumbs="exception_only",
42
43
  capture_probe_events="buffer_only",
44
+ immediate_client_error_statuses=(),
43
45
  )
44
46
 
45
47
  MINIMAL_CAPTURE_POLICY = CapturePolicy(
@@ -48,6 +50,7 @@ MINIMAL_CAPTURE_POLICY = CapturePolicy(
48
50
  capture_request_events="failures_only",
49
51
  capture_breadcrumbs="local_only",
50
52
  capture_probe_events="buffer_only",
53
+ immediate_client_error_statuses=(),
51
54
  )
52
55
 
53
56
 
@@ -117,6 +120,9 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
117
120
  capture_request_events = _as_non_empty_string(payload.get("capture_request_events"))
118
121
  capture_breadcrumbs = _as_non_empty_string(payload.get("capture_breadcrumbs"))
119
122
  capture_probe_events = _as_non_empty_string(payload.get("capture_probe_events"))
123
+ immediate_client_error_statuses = _parse_immediate_client_error_statuses(
124
+ payload.get("immediate_client_error_statuses")
125
+ )
120
126
 
121
127
  if capture_logs not in {"off", "error", "warning", "info"}:
122
128
  return None
@@ -126,6 +132,8 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
126
132
  return None
127
133
  if capture_probe_events not in {"buffer_only", "standalone_when_activated"}:
128
134
  return None
135
+ if immediate_client_error_statuses is None:
136
+ return None
129
137
 
130
138
  return CapturePolicy(
131
139
  preset=preset,
@@ -133,9 +141,25 @@ def _parse_capture_policy(payload: object) -> CapturePolicy | None:
133
141
  capture_request_events=capture_request_events,
134
142
  capture_breadcrumbs=capture_breadcrumbs,
135
143
  capture_probe_events=capture_probe_events,
144
+ immediate_client_error_statuses=immediate_client_error_statuses,
136
145
  )
137
146
 
138
147
 
148
+ def _parse_immediate_client_error_statuses(value: object) -> tuple[int, ...] | None:
149
+ if value is None:
150
+ return ()
151
+ if not isinstance(value, list) or len(value) > 12:
152
+ return None
153
+
154
+ statuses: list[int] = []
155
+ for item in value:
156
+ if not isinstance(item, int) or isinstance(item, bool) or item < 400 or item > 499:
157
+ return None
158
+ statuses.append(item)
159
+
160
+ return tuple(sorted(set(statuses)))
161
+
162
+
139
163
  def _parse_directive(payload: object) -> RemoteProbeDirective | None:
140
164
  if not isinstance(payload, dict):
141
165
  return None
@@ -609,7 +609,11 @@ class DebugBundleSdk:
609
609
  candidate = response.get("status_code") or response.get("response_status")
610
610
  if isinstance(candidate, int):
611
611
  status_code = candidate
612
- if _is_immediate_request_incident_status(status_code, self._capture_policy.preset):
612
+ if _is_immediate_request_incident_status(
613
+ status_code,
614
+ self._capture_policy.preset,
615
+ self._capture_policy.immediate_client_error_statuses,
616
+ ):
613
617
  return True
614
618
  if policy == "off":
615
619
  return False
@@ -851,11 +855,17 @@ def _iso_now(time_provider: Callable[[], float]) -> str:
851
855
  return datetime.fromtimestamp(time_provider(), tz=timezone.utc).isoformat().replace("+00:00", "Z")
852
856
 
853
857
 
854
- def _is_immediate_request_incident_status(status_code: int | None, preset: str) -> bool:
858
+ def _is_immediate_request_incident_status(
859
+ status_code: int | None,
860
+ preset: str,
861
+ immediate_client_error_statuses: tuple[int, ...],
862
+ ) -> bool:
855
863
  if status_code is None:
856
864
  return False
857
865
  if status_code >= 500:
858
866
  return True
867
+ if status_code in immediate_client_error_statuses:
868
+ return True
859
869
  if preset == "investigative":
860
870
  return status_code in INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES
861
871
  if preset == "balanced":
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 0.1.3
3
+ Version: 0.1.5
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -310,4 +310,50 @@ def test_investigative_capture_policy_promotes_409_even_when_request_capture_is_
310
310
  sdk.flush()
311
311
 
312
312
  events = _events(transport)
313
- assert [event["payload"]["response_status"] for event in events if event["event_type"] == "request_event"] == [409]
313
+ assert [event["payload"]["response_status"] for event in events if event["event_type"] == "request_event"] == [409]
314
+
315
+
316
+ def test_capture_policy_promotes_configured_client_error_statuses_when_request_capture_is_off() -> None:
317
+ clock = ManualClock()
318
+ fetch = FakeFetch(
319
+ responses=[
320
+ FakeConfigResponse(
321
+ 200,
322
+ {
323
+ "probes_enabled": True,
324
+ "remote_probes_enabled": True,
325
+ "active_probes": [],
326
+ "poll_interval_ms": 15000,
327
+ "capture_policy": {
328
+ "preset": "minimal",
329
+ "capture_logs": "error",
330
+ "capture_request_events": "off",
331
+ "capture_breadcrumbs": "local_only",
332
+ "capture_probe_events": "buffer_only",
333
+ "immediate_client_error_statuses": [422, 403, 403],
334
+ },
335
+ },
336
+ )
337
+ ]
338
+ )
339
+ transport = FakeTransport()
340
+ sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
341
+ sdk.init(
342
+ project_token="dbundle_proj_test",
343
+ service="checkout-api",
344
+ environment="production",
345
+ fetch_impl=fetch,
346
+ )
347
+
348
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 403})
349
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 404})
350
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 422})
351
+ sdk.flush()
352
+
353
+ events = _events(transport)
354
+ request_statuses = [
355
+ event["payload"]["response_status"]
356
+ for event in events
357
+ if event["event_type"] == "request_event"
358
+ ]
359
+ assert request_statuses == [403, 422]