debugbundle-python 0.1.0__tar.gz → 0.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.
Files changed (38) hide show
  1. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/PKG-INFO +22 -2
  2. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/README.md +21 -1
  3. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/pyproject.toml +1 -1
  4. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/config.py +5 -5
  5. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/core.py +65 -5
  6. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/relay.py +1 -0
  7. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle_python.egg-info/PKG-INFO +22 -2
  8. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_framework_integrations.py +6 -6
  9. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_relay.py +25 -1
  10. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_remote_config.py +8 -5
  11. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_repository_metadata.py +9 -0
  12. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_sdk.py +13 -1
  13. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/LICENSE +0 -0
  14. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/setup.cfg +0 -0
  15. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/__init__.py +0 -0
  16. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/__init__.py +0 -0
  17. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/common.py +0 -0
  18. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/django.py +0 -0
  19. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/fastapi.py +0 -0
  20. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/flask.py +0 -0
  21. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/relay_django.py +0 -0
  22. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/relay_fastapi.py +0 -0
  23. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/integrations/relay_flask.py +0 -0
  24. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/logger_integrations.py +0 -0
  25. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/py.typed +0 -0
  26. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/redaction.py +0 -0
  27. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/suppression.py +0 -0
  28. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/transport.py +0 -0
  29. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle/trigger_token.py +0 -0
  30. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
  31. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
  32. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle_python.egg-info/requires.txt +0 -0
  33. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/src/debugbundle_python.egg-info/top_level.txt +0 -0
  34. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_contracts.py +0 -0
  35. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_hooks.py +0 -0
  36. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_http_integration.py +0 -0
  37. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/tests/test_schema_validation.py +0 -0
  38. {debugbundle_python-0.1.0 → debugbundle_python-0.1.2}/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.0
3
+ Version: 0.1.2
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -59,8 +59,28 @@ debugbundle.flush()
59
59
 
60
60
  ## Status
61
61
 
62
- This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, and request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking.
62
+ This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking, and safe backend runtime process facts on exception payloads without reading environment variables.
63
+
64
+ ## Runtime Context
65
+
66
+ Backend exception events now include safe runtime process facts when the host exposes them, including:
67
+
68
+ - Python version
69
+ - platform
70
+ - architecture
71
+ - pid
72
+ - cwd
73
+ - uptime
74
+ - hostname
75
+ - thread id
76
+ - best-effort memory metadata
77
+
78
+ The SDK does not read or emit environment variables in this runtime block.
63
79
 
64
80
  ## Docs
65
81
 
66
82
  https://debugbundle.com/docs/sdk-python
83
+
84
+ ## License
85
+
86
+ AGPL-3.0-only
@@ -20,8 +20,28 @@ debugbundle.flush()
20
20
 
21
21
  ## Status
22
22
 
23
- This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, and request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking.
23
+ This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking, and safe backend runtime process facts on exception payloads without reading environment variables.
24
+
25
+ ## Runtime Context
26
+
27
+ Backend exception events now include safe runtime process facts when the host exposes them, including:
28
+
29
+ - Python version
30
+ - platform
31
+ - architecture
32
+ - pid
33
+ - cwd
34
+ - uptime
35
+ - hostname
36
+ - thread id
37
+ - best-effort memory metadata
38
+
39
+ The SDK does not read or emit environment variables in this runtime block.
24
40
 
25
41
  ## Docs
26
42
 
27
43
  https://debugbundle.com/docs/sdk-python
44
+
45
+ ## License
46
+
47
+ 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.0"
7
+ version = "0.1.2"
8
8
  description = "DebugBundle SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -37,15 +37,15 @@ class RemoteConfigSnapshot:
37
37
  BALANCED_CAPTURE_POLICY = CapturePolicy(
38
38
  preset="balanced",
39
39
  capture_logs="warning",
40
- capture_request_events="all",
41
- capture_breadcrumbs="local_only",
42
- capture_probe_events="standalone_when_activated",
40
+ capture_request_events="failures_only",
41
+ capture_breadcrumbs="exception_only",
42
+ capture_probe_events="buffer_only",
43
43
  )
44
44
 
45
45
  MINIMAL_CAPTURE_POLICY = CapturePolicy(
46
46
  preset="minimal",
47
- capture_logs="warning",
48
- capture_request_events="off",
47
+ capture_logs="error",
48
+ capture_request_events="failures_only",
49
49
  capture_breadcrumbs="local_only",
50
50
  capture_probe_events="buffer_only",
51
51
  )
@@ -2,9 +2,12 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
+ import os
5
6
  import platform
7
+ import socket
6
8
  import sys
7
9
  import threading
10
+ import time
8
11
  import traceback
9
12
  import uuid
10
13
  from collections import deque
@@ -32,10 +35,16 @@ from .suppression import EventSuppressionTracker
32
35
  from .transport import HttpTransport, Transport, coerce_transport_response
33
36
  from .trigger_token import resolve_request_trigger_directives
34
37
 
38
+ try:
39
+ import resource
40
+ except ImportError: # pragma: no cover - resource is unavailable on some platforms.
41
+ resource = None # type: ignore[assignment]
42
+
35
43
  DEFAULT_BATCH_SIZE = 25
36
44
  DEFAULT_FLUSH_INTERVAL = 5.0
37
45
  DEFAULT_ENDPOINT = "https://api.debugbundle.com/v1/events"
38
46
  DEFAULT_LOG_LEVEL = "warning"
47
+ PROCESS_START_MONOTONIC = time.monotonic()
39
48
  SCHEMA_VERSION = "2026-03-01"
40
49
  LEVEL_RANKS = {
41
50
  "debug": 10,
@@ -225,7 +234,7 @@ class DebugBundleSdk:
225
234
  "handled": handled,
226
235
  "request": request_payload,
227
236
  "response": response_payload,
228
- "runtime": {"version": platform.python_version()},
237
+ "runtime": _runtime_process_facts(),
229
238
  }
230
239
  if self._probe_flush_on_error:
231
240
  probe_data = self._build_probe_data()
@@ -590,19 +599,25 @@ class DebugBundleSdk:
590
599
 
591
600
  def _should_capture_request_event(self, response: Mapping[str, object] | None) -> bool:
592
601
  policy = self._capture_policy.capture_request_events
602
+ status_code = None
603
+ if response is not None:
604
+ candidate = response.get("status_code") or response.get("response_status")
605
+ if isinstance(candidate, int):
606
+ status_code = candidate
607
+ if status_code is not None and status_code >= 500:
608
+ return True
593
609
  if policy == "off":
594
610
  return False
595
611
  if policy == "all":
596
612
  return True
597
613
  if response is None:
598
614
  return policy == "filtered"
599
- status_code = response.get("status_code") or response.get("response_status")
600
- if not isinstance(status_code, int):
615
+ if status_code is None:
601
616
  return policy == "filtered"
602
617
  if policy == "failures_only":
603
- return status_code >= 500
618
+ return False
604
619
  if policy == "filtered":
605
- return status_code >= 500
620
+ return False
606
621
  return True
607
622
 
608
623
  def _emit_probe_events(self, label: str, data: dict[str, object], directives: list[RemoteProbeDirective]) -> None:
@@ -693,6 +708,51 @@ def _redact_mapping(value: object, redact_fields: set[str]) -> Any:
693
708
  return value
694
709
 
695
710
 
711
+ def _runtime_process_facts() -> dict[str, object]:
712
+ return {
713
+ "version": platform.python_version(),
714
+ "platform": sys.platform,
715
+ "arch": platform.machine() or None,
716
+ "pid": os.getpid(),
717
+ "cwd": _safe_cwd(),
718
+ "uptime_sec": round(max(0.0, time.monotonic() - PROCESS_START_MONOTONIC), 3),
719
+ "hostname": _safe_hostname(),
720
+ "thread_id": threading.get_ident(),
721
+ "memory": _memory_facts(),
722
+ }
723
+
724
+
725
+ def _safe_cwd() -> str | None:
726
+ try:
727
+ return os.getcwd()
728
+ except OSError:
729
+ return None
730
+
731
+
732
+ def _safe_hostname() -> str | None:
733
+ try:
734
+ return socket.gethostname()
735
+ except OSError:
736
+ return None
737
+
738
+
739
+ def _memory_facts() -> dict[str, object]:
740
+ memory: dict[str, object] = {
741
+ "rss": None,
742
+ "heap_total": None,
743
+ "heap_used": None,
744
+ "external": None,
745
+ "peak": None,
746
+ }
747
+ if resource is None:
748
+ return memory
749
+
750
+ usage = resource.getrusage(resource.RUSAGE_SELF)
751
+ # ru_maxrss is KiB on Linux and bytes on macOS/BSD.
752
+ memory["peak"] = usage.ru_maxrss if sys.platform == "darwin" else usage.ru_maxrss * 1024
753
+ return memory
754
+
755
+
696
756
  def _backend_exception_request_payload(candidate: object | None) -> dict[str, object]:
697
757
  mapping = _dict_from_object(candidate)
698
758
  payload: dict[str, object] = {
@@ -15,6 +15,7 @@ ACCEPTED_EVENT_TYPES = frozenset(
15
15
  "frontend_exception",
16
16
  "error_suppressed",
17
17
  "frontend_breadcrumb",
18
+ "request_event",
18
19
  "probe_event",
19
20
  }
20
21
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -59,8 +59,28 @@ debugbundle.flush()
59
59
 
60
60
  ## Status
61
61
 
62
- This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, and request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking.
62
+ This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking, and safe backend runtime process facts on exception payloads without reading environment variables.
63
+
64
+ ## Runtime Context
65
+
66
+ Backend exception events now include safe runtime process facts when the host exposes them, including:
67
+
68
+ - Python version
69
+ - platform
70
+ - architecture
71
+ - pid
72
+ - cwd
73
+ - uptime
74
+ - hostname
75
+ - thread id
76
+ - best-effort memory metadata
77
+
78
+ The SDK does not read or emit environment variables in this runtime block.
63
79
 
64
80
  ## Docs
65
81
 
66
82
  https://debugbundle.com/docs/sdk-python
83
+
84
+ ## License
85
+
86
+ AGPL-3.0-only
@@ -178,14 +178,14 @@ def test_django_middleware_captures_requests_logs_and_errors() -> None:
178
178
 
179
179
  factory = RequestFactory()
180
180
 
181
- def ok_response(request: object) -> HttpResponse:
182
- logging.getLogger("django-test").warning("django ok warning")
183
- return HttpResponse("ok")
181
+ def server_error_response(request: object) -> HttpResponse:
182
+ logging.getLogger("django-test").warning("django server warning")
183
+ return HttpResponse("failed", status=503)
184
184
 
185
- middleware = DebugBundleDjangoMiddleware(ok_response, sdk=sdk)
186
- request = factory.get("/ok", HTTP_X_DEBUGBUNDLE_TRACE_ID="trace-django")
185
+ middleware = DebugBundleDjangoMiddleware(server_error_response, sdk=sdk)
186
+ request = factory.get("/failed", HTTP_X_DEBUGBUNDLE_TRACE_ID="trace-django")
187
187
  response = middleware(request)
188
- assert response.status_code == 200
188
+ assert response.status_code == 503
189
189
 
190
190
  raising_middleware = DebugBundleDjangoMiddleware(
191
191
  lambda request: (_ for _ in ()).throw(RuntimeError("django failure")),
@@ -166,7 +166,7 @@ def test_preserves_correlation_trace_id() -> None:
166
166
 
167
167
  def test_accepts_all_supported_event_types() -> None:
168
168
  handler = BrowserRelayHandler(allowed_origins=["https://example.com"])
169
- for event_type in ("frontend_exception", "error_suppressed", "frontend_breadcrumb", "probe_event"):
169
+ for event_type in ("frontend_exception", "error_suppressed", "frontend_breadcrumb", "request_event", "probe_event"):
170
170
  evt = _valid_event(event_type)
171
171
  response = handler.handle(
172
172
  _make_request(body={"batch": [evt]}, ip_address=f"10.0.0.{hash(event_type) % 254 + 1}")
@@ -174,6 +174,30 @@ def test_accepts_all_supported_event_types() -> None:
174
174
  assert response.status == 202, f"Failed for event type: {event_type}"
175
175
 
176
176
 
177
+ def test_accepts_browser_request_event_payloads() -> None:
178
+ accepted: list[BrowserRelayAcceptedBatch] = []
179
+ handler = BrowserRelayHandler(
180
+ allowed_origins=["https://example.com"],
181
+ on_accept=lambda batch: accepted.append(batch),
182
+ )
183
+ event = _valid_event("request_event")
184
+ event["payload"] = {
185
+ "method": "POST",
186
+ "path": "/v1/billing/checkout",
187
+ "query": {"plan": "team"},
188
+ "headers": {},
189
+ "response_status": 503,
190
+ "duration_ms": 84,
191
+ }
192
+
193
+ response = handler.handle(_make_request(body={"batch": [event]}))
194
+
195
+ assert response.status == 202
196
+ assert len(accepted) == 1
197
+ assert accepted[0].events[0]["event_type"] == "request_event"
198
+ assert accepted[0].events[0]["payload"]["response_status"] == 503
199
+
200
+
177
201
  def test_rejects_event_with_missing_required_fields() -> None:
178
202
  handler = BrowserRelayHandler(allowed_origins=["https://example.com"])
179
203
  bad_event = {"event_type": "frontend_exception"}
@@ -111,8 +111,8 @@ def test_remote_config_uses_etag_and_activates_heavy_probes_only_while_directive
111
111
  "capture_policy": {
112
112
  "preset": "balanced",
113
113
  "capture_logs": "warning",
114
- "capture_request_events": "all",
115
- "capture_breadcrumbs": "local_only",
114
+ "capture_request_events": "failures_only",
115
+ "capture_breadcrumbs": "exception_only",
116
116
  "capture_probe_events": "standalone_when_activated",
117
117
  },
118
118
  },
@@ -173,14 +173,17 @@ def test_failed_init_config_fetch_falls_back_to_minimal_policy() -> None:
173
173
  probes_poll_interval=25000,
174
174
  )
175
175
 
176
- sdk.capture_message("warning still allowed", level="warning")
176
+ sdk.capture_message("warning blocked", level="warning")
177
+ sdk.capture_message("error still allowed", level="error")
177
178
  sdk.capture_message("info blocked", level="info")
178
179
  sdk.capture_request({"method": "GET", "path": "/ok", "headers": {}}, {"status_code": 200})
180
+ sdk.capture_request({"method": "GET", "path": "/boom", "headers": {}}, {"status_code": 503})
179
181
  sdk.flush()
180
182
 
181
183
  events = _events(transport)
182
- assert [event["event_type"] for event in events] == ["log_event"]
183
- assert events[0]["payload"]["message"] == "warning still allowed"
184
+ assert [event["event_type"] for event in events] == ["log_event", "request_event"]
185
+ assert events[0]["payload"]["message"] == "error still allowed"
186
+ assert events[1]["payload"]["response_status"] == 503
184
187
 
185
188
 
186
189
  def test_capture_policy_filters_logs_and_request_events_from_remote_config() -> None:
@@ -25,6 +25,15 @@ def test_pyproject_points_to_standalone_repository_urls() -> None:
25
25
  assert 'Issues = "https://github.com/debugbundle/debugbundle-python/issues"' in pyproject
26
26
 
27
27
 
28
+ def test_standalone_changelog_and_security_policy_are_launch_ready() -> None:
29
+ changelog = (REPO_ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
30
+ security = (REPO_ROOT / "SECURITY.md").read_text(encoding="utf-8")
31
+
32
+ assert "## [Unreleased]" in changelog
33
+ assert "## [0.1.0] - 2026-05-07" in changelog
34
+ assert "https://github.com/debugbundle/debugbundle-python/security/advisories/new" in security
35
+
36
+
28
37
  def test_standalone_ci_workflow_covers_python_sdk_checks() -> None:
29
38
  workflow = (REPO_ROOT / ".github/workflows/ci.yml").read_text(encoding="utf-8")
30
39
 
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import platform
4
+ import sys
4
5
  import uuid
5
6
  from dataclasses import dataclass
6
7
 
@@ -234,7 +235,18 @@ def test_emits_contract_compliant_event_envelopes() -> None:
234
235
  assert exception_event["payload"]["handled"] is True
235
236
  assert exception_event["payload"]["request"]["path"] == "/checkout"
236
237
  assert exception_event["payload"]["response"]["status_code"] == 500
237
- assert exception_event["payload"]["runtime"] == {"version": platform.python_version()}
238
+ runtime = exception_event["payload"]["runtime"]
239
+ assert runtime["version"] == platform.python_version()
240
+ assert runtime["platform"] == sys.platform
241
+ assert isinstance(runtime["arch"], str)
242
+ assert isinstance(runtime["pid"], int)
243
+ assert isinstance(runtime["cwd"], str)
244
+ assert isinstance(runtime["uptime_sec"], (int, float))
245
+ assert runtime["uptime_sec"] >= 0
246
+ assert isinstance(runtime["hostname"], str)
247
+ assert isinstance(runtime["thread_id"], int)
248
+ assert isinstance(runtime["memory"], dict)
249
+ assert "environment" not in runtime
238
250
 
239
251
 
240
252
  def test_suppresses_duplicate_exceptions_after_the_first_three() -> None: