debugbundle-python 0.1.1__tar.gz → 0.1.3__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.1 → debugbundle_python-0.1.3}/PKG-INFO +22 -2
  2. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/README.md +21 -1
  3. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/pyproject.toml +1 -1
  4. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/core.py +84 -3
  5. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle_python.egg-info/PKG-INFO +22 -2
  6. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_remote_config.py +85 -1
  7. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_sdk.py +13 -1
  8. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/LICENSE +0 -0
  9. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/setup.cfg +0 -0
  10. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/__init__.py +0 -0
  11. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/config.py +0 -0
  12. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/__init__.py +0 -0
  13. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/common.py +0 -0
  14. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/django.py +0 -0
  15. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/fastapi.py +0 -0
  16. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/flask.py +0 -0
  17. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/relay_django.py +0 -0
  18. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/relay_fastapi.py +0 -0
  19. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/integrations/relay_flask.py +0 -0
  20. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/logger_integrations.py +0 -0
  21. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/py.typed +0 -0
  22. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/redaction.py +0 -0
  23. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/relay.py +0 -0
  24. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/suppression.py +0 -0
  25. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/transport.py +0 -0
  26. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle/trigger_token.py +0 -0
  27. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
  28. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
  29. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle_python.egg-info/requires.txt +0 -0
  30. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/src/debugbundle_python.egg-info/top_level.txt +0 -0
  31. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_contracts.py +0 -0
  32. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_framework_integrations.py +0 -0
  33. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_hooks.py +0 -0
  34. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_http_integration.py +0 -0
  35. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_relay.py +0 -0
  36. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_repository_metadata.py +0 -0
  37. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/tests/test_schema_validation.py +0 -0
  38. {debugbundle_python-0.1.1 → debugbundle_python-0.1.3}/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.1
3
+ Version: 0.1.3
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.1"
7
+ version = "0.1.3"
8
8
  description = "DebugBundle SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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,
@@ -44,6 +53,11 @@ LEVEL_RANKS = {
44
53
  "error": 40,
45
54
  "critical": 50,
46
55
  }
56
+ BALANCED_IMMEDIATE_REQUEST_STATUSES = {408, 423, 424, 425, 429}
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
47
61
 
48
62
 
49
63
  @dataclass
@@ -225,7 +239,7 @@ class DebugBundleSdk:
225
239
  "handled": handled,
226
240
  "request": request_payload,
227
241
  "response": response_payload,
228
- "runtime": {"version": platform.python_version()},
242
+ "runtime": _runtime_process_facts(),
229
243
  }
230
244
  if self._probe_flush_on_error:
231
245
  probe_data = self._build_probe_data()
@@ -595,7 +609,7 @@ class DebugBundleSdk:
595
609
  candidate = response.get("status_code") or response.get("response_status")
596
610
  if isinstance(candidate, int):
597
611
  status_code = candidate
598
- if status_code is not None and status_code >= 500:
612
+ if _is_immediate_request_incident_status(status_code, self._capture_policy.preset):
599
613
  return True
600
614
  if policy == "off":
601
615
  return False
@@ -606,7 +620,7 @@ class DebugBundleSdk:
606
620
  if status_code is None:
607
621
  return policy == "filtered"
608
622
  if policy == "failures_only":
609
- return False
623
+ return _is_request_anomaly_candidate_status(status_code, self._capture_policy.preset)
610
624
  if policy == "filtered":
611
625
  return False
612
626
  return True
@@ -699,6 +713,51 @@ def _redact_mapping(value: object, redact_fields: set[str]) -> Any:
699
713
  return value
700
714
 
701
715
 
716
+ def _runtime_process_facts() -> dict[str, object]:
717
+ return {
718
+ "version": platform.python_version(),
719
+ "platform": sys.platform,
720
+ "arch": platform.machine() or None,
721
+ "pid": os.getpid(),
722
+ "cwd": _safe_cwd(),
723
+ "uptime_sec": round(max(0.0, time.monotonic() - PROCESS_START_MONOTONIC), 3),
724
+ "hostname": _safe_hostname(),
725
+ "thread_id": threading.get_ident(),
726
+ "memory": _memory_facts(),
727
+ }
728
+
729
+
730
+ def _safe_cwd() -> str | None:
731
+ try:
732
+ return os.getcwd()
733
+ except OSError:
734
+ return None
735
+
736
+
737
+ def _safe_hostname() -> str | None:
738
+ try:
739
+ return socket.gethostname()
740
+ except OSError:
741
+ return None
742
+
743
+
744
+ def _memory_facts() -> dict[str, object]:
745
+ memory: dict[str, object] = {
746
+ "rss": None,
747
+ "heap_total": None,
748
+ "heap_used": None,
749
+ "external": None,
750
+ "peak": None,
751
+ }
752
+ if resource is None:
753
+ return memory
754
+
755
+ usage = resource.getrusage(resource.RUSAGE_SELF)
756
+ # ru_maxrss is KiB on Linux and bytes on macOS/BSD.
757
+ memory["peak"] = usage.ru_maxrss if sys.platform == "darwin" else usage.ru_maxrss * 1024
758
+ return memory
759
+
760
+
702
761
  def _backend_exception_request_payload(candidate: object | None) -> dict[str, object]:
703
762
  mapping = _dict_from_object(candidate)
704
763
  payload: dict[str, object] = {
@@ -792,6 +851,28 @@ def _iso_now(time_provider: Callable[[], float]) -> str:
792
851
  return datetime.fromtimestamp(time_provider(), tz=timezone.utc).isoformat().replace("+00:00", "Z")
793
852
 
794
853
 
854
+ def _is_immediate_request_incident_status(status_code: int | None, preset: str) -> bool:
855
+ if status_code is None:
856
+ return False
857
+ if status_code >= 500:
858
+ return True
859
+ if preset == "investigative":
860
+ return status_code in INVESTIGATIVE_IMMEDIATE_REQUEST_STATUSES
861
+ if preset == "balanced":
862
+ return status_code in BALANCED_IMMEDIATE_REQUEST_STATUSES
863
+ return False
864
+
865
+
866
+ def _is_request_anomaly_candidate_status(status_code: int | None, preset: str) -> bool:
867
+ if status_code is None or status_code < 400 or status_code >= 500:
868
+ return False
869
+ if preset == "investigative":
870
+ return status_code in INVESTIGATIVE_ANOMALY_STATUSES
871
+ if preset == "balanced":
872
+ return status_code in BALANCED_STANDARD_ANOMALY_STATUSES or status_code in BALANCED_HIGH_VOLUME_ANOMALY_STATUSES
873
+ return False
874
+
875
+
795
876
  def _time_now() -> float:
796
877
  return datetime.now(tz=timezone.utc).timestamp()
797
878
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 0.1.1
3
+ Version: 0.1.3
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
@@ -226,4 +226,88 @@ def test_capture_policy_filters_logs_and_request_events_from_remote_config() ->
226
226
  events = _events(transport)
227
227
  assert [event["event_type"] for event in events] == ["log_event", "request_event"]
228
228
  assert events[0]["payload"]["message"] == "error kept"
229
- assert events[1]["payload"]["response_status"] == 503
229
+ assert events[1]["payload"]["response_status"] == 503
230
+
231
+
232
+ def test_balanced_capture_policy_keeps_request_failure_anomaly_candidates() -> None:
233
+ clock = ManualClock()
234
+ fetch = FakeFetch(
235
+ responses=[
236
+ FakeConfigResponse(
237
+ 200,
238
+ {
239
+ "probes_enabled": True,
240
+ "remote_probes_enabled": True,
241
+ "active_probes": [],
242
+ "poll_interval_ms": 15000,
243
+ "capture_policy": {
244
+ "preset": "balanced",
245
+ "capture_logs": "warning",
246
+ "capture_request_events": "failures_only",
247
+ "capture_breadcrumbs": "exception_only",
248
+ "capture_probe_events": "buffer_only",
249
+ },
250
+ },
251
+ )
252
+ ]
253
+ )
254
+ transport = FakeTransport()
255
+ sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
256
+ sdk.init(
257
+ project_token="dbundle_proj_test",
258
+ service="checkout-api",
259
+ environment="production",
260
+ fetch_impl=fetch,
261
+ )
262
+
263
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 429})
264
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 404})
265
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 409})
266
+ sdk.flush()
267
+
268
+ events = _events(transport)
269
+ request_statuses = [
270
+ event["payload"]["response_status"]
271
+ for event in events
272
+ if event["event_type"] == "request_event"
273
+ ]
274
+ assert request_statuses == [429, 404, 409]
275
+
276
+
277
+ def test_investigative_capture_policy_promotes_409_even_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": "investigative",
290
+ "capture_logs": "info",
291
+ "capture_request_events": "off",
292
+ "capture_breadcrumbs": "standalone",
293
+ "capture_probe_events": "standalone_when_activated",
294
+ },
295
+ },
296
+ )
297
+ ]
298
+ )
299
+ transport = FakeTransport()
300
+ sdk = DebugBundleSdk(transport=transport, time_provider=clock.time)
301
+ sdk.init(
302
+ project_token="dbundle_proj_test",
303
+ service="checkout-api",
304
+ environment="production",
305
+ fetch_impl=fetch,
306
+ )
307
+
308
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 409})
309
+ sdk.capture_request({"method": "POST", "path": "/checkout", "headers": {}}, {"status_code": 404})
310
+ sdk.flush()
311
+
312
+ events = _events(transport)
313
+ assert [event["payload"]["response_status"] for event in events if event["event_type"] == "request_event"] == [409]
@@ -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: