debugbundle-python 2.1.0__tar.gz → 2.1.1__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 (82) hide show
  1. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/PKG-INFO +7 -6
  2. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/README.md +6 -5
  3. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/pyproject.toml +1 -1
  4. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/capture_hooks.py +14 -6
  5. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/exception_capture.py +8 -2
  6. debugbundle_python-2.1.1/src/debugbundle/exception_identity.py +47 -0
  7. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/logger_integrations.py +16 -0
  8. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/structlog_integration.py +21 -0
  9. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/PKG-INFO +7 -6
  10. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/SOURCES.txt +2 -0
  11. debugbundle_python-2.1.1/tests/test_logging_exception.py +110 -0
  12. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/LICENSE +0 -0
  13. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/setup.cfg +0 -0
  14. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/__init__.py +0 -0
  15. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/acknowledgement.py +0 -0
  16. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/before_send.py +0 -0
  17. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/canonical_event.py +0 -0
  18. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/config.py +0 -0
  19. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/core.py +0 -0
  20. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/delivery_hooks.py +0 -0
  21. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/diagnostic_budget.py +0 -0
  22. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/event-envelope.schema.json +0 -0
  23. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/event_support.py +0 -0
  24. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_delivery.py +0 -0
  25. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_http.py +0 -0
  26. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_json.py +0 -0
  27. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/evidence_wire.py +0 -0
  28. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/exception_evidence.py +0 -0
  29. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/exception_snapshot.py +0 -0
  30. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/flush_aggregates.py +0 -0
  31. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/flush_delivery.py +0 -0
  32. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/ingestion_wire.py +0 -0
  33. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/__init__.py +0 -0
  34. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/common.py +0 -0
  35. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/django.py +0 -0
  36. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/fastapi.py +0 -0
  37. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/flask.py +0 -0
  38. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_django.py +0 -0
  39. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_fastapi.py +0 -0
  40. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/integrations/relay_flask.py +0 -0
  41. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/operation_context.py +0 -0
  42. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/probe_capture.py +0 -0
  43. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/process_support.py +0 -0
  44. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/py.typed +0 -0
  45. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/queue_admission.py +0 -0
  46. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/recovery_capture.py +0 -0
  47. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/redaction.py +0 -0
  48. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/relay.py +0 -0
  49. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/relay_delivery.py +0 -0
  50. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/remote_config_client.py +0 -0
  51. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/request_capture_policy.py +0 -0
  52. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/scope_ownership.py +0 -0
  53. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/suppression.py +0 -0
  54. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/transport.py +0 -0
  55. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle/trigger_token.py +0 -0
  56. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
  57. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/requires.txt +0 -0
  58. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/src/debugbundle_python.egg-info/top_level.txt +0 -0
  59. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_before_send.py +0 -0
  60. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_contracts.py +0 -0
  61. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_delivery_hooks.py +0 -0
  62. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_evidence_capture.py +0 -0
  63. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_evidence_wire.py +0 -0
  64. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_exception_evidence.py +0 -0
  65. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_exception_snapshot.py +0 -0
  66. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_framework_integrations.py +0 -0
  67. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_hooks.py +0 -0
  68. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_http_integration.py +0 -0
  69. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_optional_imports.py +0 -0
  70. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_privacy_conformance.py +0 -0
  71. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_relay.py +0 -0
  72. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_relay_delivery.py +0 -0
  73. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_relay_evidence.py +0 -0
  74. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_remote_config.py +0 -0
  75. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_repository_metadata.py +0 -0
  76. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_schema_validation.py +0 -0
  77. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_sdk.py +0 -0
  78. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_smoke_script.py +0 -0
  79. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_structlog_filtering.py +0 -0
  80. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_suppression.py +0 -0
  81. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_transport_reliability.py +0 -0
  82. {debugbundle_python-2.1.0 → debugbundle_python-2.1.1}/tests/test_trigger_token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: Apache-2.0
@@ -41,6 +41,8 @@ Dynamic: license-file
41
41
 
42
42
  # debugbundle-python
43
43
 
44
+ Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
45
+
44
46
  Python SDK for DebugBundle.
45
47
 
46
48
  After `make install-docker`, `make evidence-performance` compares identical
@@ -59,16 +61,15 @@ Use this package to capture Python backend exceptions, request metadata, structu
59
61
 
60
62
  Requires Python 3.10 or newer.
61
63
 
62
- ## Evidence candidate (unreleased)
64
+ ## Evidence in 2.1.0
63
65
 
64
- The source candidate adds `capture_exception_evidence=True` and
66
+ Version 2.1.0 adds `capture_exception_evidence=True` and
65
67
  `capture_recovery_observations=False` installation options. Collection requires
66
68
  verified delivery/reader support, discovered on existing config/ACK traffic with
67
69
  `debugbundle_evidence=1`. No capture call performs discovery or adds polling.
68
70
  `evidence_capabilities` is an optional descriptor pin asserting the **entire**
69
71
  path, including local readers, relays and wrappers; later discovery can revoke it.
70
- Without proof, exception events retain their legacy shape. These additions are
71
- not available in the installed 2.0.1 release.
72
+ Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
72
73
 
73
74
  Native cause, implicit context and Python 3.11+ exception-group evidence excludes
74
75
  locals, source excerpts, arbitrary renderers and application properties. Python
@@ -89,7 +90,7 @@ Application results and exceptions pass through the context manager unchanged.
89
90
  The existing background sender remains best-effort at immediate process exit.
90
91
  The uncaught hook collects bounded native evidence without waiting for transport;
91
92
  applications needing a delivery attempt use explicit `flush()` in a controlled
92
- shutdown window. This candidate does not add default crash-file persistence or
93
+ shutdown window. This release does not add default crash-file persistence or
93
94
  synchronous capture transport.
94
95
 
95
96
  Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
@@ -1,5 +1,7 @@
1
1
  # debugbundle-python
2
2
 
3
+ Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
4
+
3
5
  Python SDK for DebugBundle.
4
6
 
5
7
  After `make install-docker`, `make evidence-performance` compares identical
@@ -18,16 +20,15 @@ Use this package to capture Python backend exceptions, request metadata, structu
18
20
 
19
21
  Requires Python 3.10 or newer.
20
22
 
21
- ## Evidence candidate (unreleased)
23
+ ## Evidence in 2.1.0
22
24
 
23
- The source candidate adds `capture_exception_evidence=True` and
25
+ Version 2.1.0 adds `capture_exception_evidence=True` and
24
26
  `capture_recovery_observations=False` installation options. Collection requires
25
27
  verified delivery/reader support, discovered on existing config/ACK traffic with
26
28
  `debugbundle_evidence=1`. No capture call performs discovery or adds polling.
27
29
  `evidence_capabilities` is an optional descriptor pin asserting the **entire**
28
30
  path, including local readers, relays and wrappers; later discovery can revoke it.
29
- Without proof, exception events retain their legacy shape. These additions are
30
- not available in the installed 2.0.1 release.
31
+ Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
31
32
 
32
33
  Native cause, implicit context and Python 3.11+ exception-group evidence excludes
33
34
  locals, source excerpts, arbitrary renderers and application properties. Python
@@ -48,7 +49,7 @@ Application results and exceptions pass through the context manager unchanged.
48
49
  The existing background sender remains best-effort at immediate process exit.
49
50
  The uncaught hook collects bounded native evidence without waiting for transport;
50
51
  applications needing a delivery attempt use explicit `flush()` in a controlled
51
- shutdown window. This candidate does not add default crash-file persistence or
52
+ shutdown window. This release does not add default crash-file persistence or
52
53
  synchronous capture transport.
53
54
 
54
55
  Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "debugbundle-python"
7
- version = "2.1.0"
7
+ version = "2.1.1"
8
8
  description = "DebugBundle SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -30,14 +30,19 @@ class DebugBundleLogHandler(logging.Handler):
30
30
  def emit(self, record: logging.LogRecord) -> None:
31
31
  if not self._sdk._log_level_eligible(record.levelname.lower()):
32
32
  return
33
+ context = {"logger_name": record.name, "pathname": record.pathname, "lineno": record.lineno}
34
+ if (
35
+ record.levelno >= logging.ERROR
36
+ and type(record.exc_info) is tuple
37
+ and len(record.exc_info) == 3
38
+ and isinstance(record.exc_info[1], BaseException)
39
+ ):
40
+ self._sdk.capture_exception(record.exc_info[1], {**context, "log_message": safe_log_message(record)})
41
+ return
33
42
  self._sdk.capture_log(
34
43
  safe_log_message(record),
35
44
  level=record.levelname.lower(),
36
- context={
37
- "logger_name": record.name,
38
- "pathname": record.pathname,
39
- "lineno": record.lineno,
40
- },
45
+ context=context,
41
46
  )
42
47
 
43
48
 
@@ -55,7 +60,10 @@ def safe_log_message(record: logging.LogRecord) -> str:
55
60
  if type(args) is not tuple or len(args) > 8:
56
61
  return template + " [log arguments withheld]"
57
62
  safe_args = [
58
- value[:1024] if type(value) is str else str(value) if type(value) in (int, float, bool)
63
+ value[:1024]
64
+ if type(value) is str
65
+ else str(value)
66
+ if type(value) in (int, float, bool)
59
67
  else "[unsupported argument]"
60
68
  for value in args
61
69
  ]
@@ -14,6 +14,7 @@ from .event_support import (
14
14
  runtime_process_facts,
15
15
  )
16
16
  from .exception_evidence import snapshot_exception
17
+ from .exception_identity import admit as admit_identity
17
18
  from .exception_snapshot import exception_details
18
19
  from .process_support import ensure_current_process
19
20
  from .queue_admission import may_prepare_event
@@ -35,6 +36,9 @@ def capture_exception(
35
36
  if not sdk._enabled or not may_prepare_event(sdk, True, max_pending_events, max_pending_bytes):
36
37
  return
37
38
  try:
39
+ automatic = fatal or type(context) is dict and "logger_name" in context
40
+ if not admit_identity(sdk, error, automatic):
41
+ return
38
42
  name, message, stack = exception_details(error)
39
43
  redacted_context = redact_mapping(dict(context or {}), sdk._redact_fields)
40
44
  request_payload = backend_exception_request_payload(redacted_context.get("request"))
@@ -84,8 +88,10 @@ def capture_exception(
84
88
  return
85
89
  try:
86
90
  if sdk._enabled and sdk._suppression.should_capture(suppression_key, sdk._time_provider()):
87
- if sdk._offer_protected_event(event, event_bytes) and fatal:
88
- sdk._hook_bypass.add(id(event))
91
+ if sdk._offer_protected_event(event, event_bytes):
92
+ admit_identity(sdk, error, automatic, accepted=True)
93
+ if fatal:
94
+ sdk._hook_bypass.add(id(event))
89
95
  finally:
90
96
  sdk._lock.release()
91
97
  except BaseException:
@@ -0,0 +1,47 @@
1
+ """Opaque receipts on native exceptions; no raw exception retention or equality hooks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ _SLOT = "_debugbundle_capture_receipt"
9
+ _DICT = BaseException.__dict__["__dict__"]
10
+
11
+
12
+ def admit(sdk: Any, error: BaseException, automatic: bool, *, accepted: bool = False) -> bool:
13
+ try:
14
+ # BaseException is not universally weak-referenceable. Store only an opaque
15
+ # owner, operation ID and time on its native dictionary, bypassing overrides.
16
+ if sdk._before_send is not None:
17
+ return True
18
+ fields = _DICT.__get__(error)
19
+ owner = getattr(sdk, "_exception_identity_owner", None)
20
+ if owner is None:
21
+ owner = sdk._exception_identity_owner = object()
22
+ operation = sdk._operations.current()
23
+ scope = operation.scope["operation_id"] if operation is not None else None
24
+ if scope is None:
25
+ return True
26
+ now = time.monotonic()
27
+ previous = fields.get(_SLOT)
28
+ if type(previous) is tuple and len(previous) == 4:
29
+ token, prior_scope, at, prior_automatic = previous
30
+ if (
31
+ not accepted
32
+ and token is owner
33
+ and type(prior_scope) is str
34
+ and scope == prior_scope
35
+ and type(at) is float
36
+ and type(prior_automatic) is bool
37
+ and now - at < 30
38
+ and automatic != prior_automatic
39
+ ):
40
+ fields.pop(_SLOT, None)
41
+ return False
42
+ if accepted:
43
+ fields[_SLOT] = (owner, scope, now, automatic)
44
+ except BaseException:
45
+ # Unavailable identity must preserve the failure, never infer sameness.
46
+ pass
47
+ return True
@@ -75,11 +75,27 @@ def _attach_loguru(
75
75
  return None
76
76
 
77
77
  try:
78
+
78
79
  def sink(message: Any) -> None:
79
80
  record = getattr(message, "record", None)
80
81
  if not isinstance(record, dict):
81
82
  return
82
83
  context = dict(record.get("extra") or {})
84
+ exception = record.get("exception")
85
+ error = exception.value if exception is not None else None
86
+ level = _normalize_level(str(getattr(record.get("level"), "name", "warning")))
87
+ if (
88
+ level in {"error", "critical"}
89
+ and isinstance(error, BaseException)
90
+ and hasattr(sdk, "capture_exception")
91
+ ):
92
+ eligible = getattr(sdk, "_log_level_eligible", None)
93
+ if callable(eligible) and not eligible(level):
94
+ return
95
+ sdk.capture_exception(
96
+ error, {**context, "logger_name": "loguru", "log_message": str(record.get("message") or "")}
97
+ )
98
+ return
83
99
  sdk.capture_log(
84
100
  str(record.get("message") or ""),
85
101
  level=_normalize_level(str(getattr(record.get("level"), "name", "warning"))),
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import copy
4
4
  import inspect
5
5
  import logging
6
+ import sys
6
7
  from contextvars import ContextVar
7
8
  from typing import Any
8
9
 
@@ -19,6 +20,17 @@ def _capture(sdk: Any, active: list[bool], name: str, record: dict[str, Any]) ->
19
20
  context = dict(record)
20
21
  message = str(context.pop("event", ""))
21
22
  level = {"warn": "warning", "exception": "error", "fatal": "critical"}.get(name, name)
23
+ info = context.get("exc_info")
24
+ if info is True:
25
+ info = sys.exc_info()
26
+ error = info[1] if type(info) is tuple and len(info) == 3 else info
27
+ if level in {"error", "critical"} and isinstance(error, BaseException) and hasattr(sdk, "capture_exception"):
28
+ eligible = getattr(sdk, "_log_level_eligible", None)
29
+ if callable(eligible) and not eligible(level):
30
+ return
31
+ context.pop("exc_info", None)
32
+ sdk.capture_exception(error, {**context, "logger_name": "structlog", "log_message": message})
33
+ return
22
34
  sdk.capture_log(message, level=level, context=context or None)
23
35
  except Exception:
24
36
  # A logger adapter must not throw into the application's logging path.
@@ -61,8 +73,10 @@ class StructlogLoggerProxy:
61
73
  if not callable(attribute):
62
74
  return attribute
63
75
  if name in _BIND_METHODS:
76
+
64
77
  def bind(*args: Any, **kwargs: Any) -> StructlogLoggerProxy:
65
78
  return StructlogLoggerProxy(attribute(*args, **kwargs), self._sdk, self._active)
79
+
66
80
  return bind
67
81
  method_name = name[1:] if name.startswith("a") else name
68
82
  if method_name not in _LOG_METHODS:
@@ -84,6 +98,7 @@ class StructlogLoggerProxy:
84
98
  if args:
85
99
  record["event"] = args[1] if method_name == "log" and len(args) > 1 else args[0]
86
100
  processors = list(bound._processors)
101
+ native_info = kwargs.get("exc_info", sys.exc_info() if method_name == "exception" else None)
87
102
  if processors:
88
103
  last = processors[-1]
89
104
 
@@ -96,6 +111,8 @@ class StructlogLoggerProxy:
96
111
  if isinstance(snapshot, dict):
97
112
  record.clear()
98
113
  record.update(snapshot)
114
+ if native_info is not None:
115
+ record["exc_info"] = native_info
99
116
  return result
100
117
 
101
118
  processors[-1] = observe
@@ -107,12 +124,15 @@ class StructlogLoggerProxy:
107
124
  return attribute
108
125
 
109
126
  if inspect.iscoroutinefunction(attribute):
127
+
110
128
  async def async_log(*args: Any, **kwargs: Any) -> Any:
111
129
  return await prepare(args, kwargs)(*args, **kwargs)
130
+
112
131
  return async_log
113
132
 
114
133
  def log(*args: Any, **kwargs: Any) -> Any:
115
134
  return prepare(args, kwargs)(*args, **kwargs)
135
+
116
136
  return log
117
137
 
118
138
  def _ensure_stdlib_handler(self, sink: Any) -> None:
@@ -136,4 +156,5 @@ class StructlogLoggerProxy:
136
156
  record = {**kwargs, **{f"arg_{i}": value for i, value in enumerate(args)}, "event": event or ""}
137
157
  _capture(self._sdk, self._active, name, record)
138
158
  return result
159
+
139
160
  return log
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 2.1.0
3
+ Version: 2.1.1
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: Apache-2.0
@@ -41,6 +41,8 @@ Dynamic: license-file
41
41
 
42
42
  # debugbundle-python
43
43
 
44
+ Patch 2.1.1 corrects native logger exception evidence and capture safety while preserving public APIs and event schemas. Upgrade the DebugBundle distribution to adopt the correction.
45
+
44
46
  Python SDK for DebugBundle.
45
47
 
46
48
  After `make install-docker`, `make evidence-performance` compares identical
@@ -59,16 +61,15 @@ Use this package to capture Python backend exceptions, request metadata, structu
59
61
 
60
62
  Requires Python 3.10 or newer.
61
63
 
62
- ## Evidence candidate (unreleased)
64
+ ## Evidence in 2.1.0
63
65
 
64
- The source candidate adds `capture_exception_evidence=True` and
66
+ Version 2.1.0 adds `capture_exception_evidence=True` and
65
67
  `capture_recovery_observations=False` installation options. Collection requires
66
68
  verified delivery/reader support, discovered on existing config/ACK traffic with
67
69
  `debugbundle_evidence=1`. No capture call performs discovery or adds polling.
68
70
  `evidence_capabilities` is an optional descriptor pin asserting the **entire**
69
71
  path, including local readers, relays and wrappers; later discovery can revoke it.
70
- Without proof, exception events retain their legacy shape. These additions are
71
- not available in the installed 2.0.1 release.
72
+ Without proof, exception events retain their legacy shape. These additions require upgrading from 2.0.1 to 2.1.0.
72
73
 
73
74
  Native cause, implicit context and Python 3.11+ exception-group evidence excludes
74
75
  locals, source excerpts, arbitrary renderers and application properties. Python
@@ -89,7 +90,7 @@ Application results and exceptions pass through the context manager unchanged.
89
90
  The existing background sender remains best-effort at immediate process exit.
90
91
  The uncaught hook collects bounded native evidence without waiting for transport;
91
92
  applications needing a delivery attempt use explicit `flush()` in a controlled
92
- shutdown window. This candidate does not add default crash-file persistence or
93
+ shutdown window. This release does not add default crash-file persistence or
93
94
  synchronous capture transport.
94
95
 
95
96
  Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
@@ -18,6 +18,7 @@ src/debugbundle/evidence_json.py
18
18
  src/debugbundle/evidence_wire.py
19
19
  src/debugbundle/exception_capture.py
20
20
  src/debugbundle/exception_evidence.py
21
+ src/debugbundle/exception_identity.py
21
22
  src/debugbundle/exception_snapshot.py
22
23
  src/debugbundle/flush_aggregates.py
23
24
  src/debugbundle/flush_delivery.py
@@ -62,6 +63,7 @@ tests/test_exception_snapshot.py
62
63
  tests/test_framework_integrations.py
63
64
  tests/test_hooks.py
64
65
  tests/test_http_integration.py
66
+ tests/test_logging_exception.py
65
67
  tests/test_optional_imports.py
66
68
  tests/test_privacy_conformance.py
67
69
  tests/test_relay.py
@@ -0,0 +1,110 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+
5
+ import pytest
6
+ import structlog
7
+
8
+ from debugbundle.capture_hooks import DebugBundleLogHandler
9
+ from debugbundle.core import DebugBundleSdk
10
+ from debugbundle.transport import TransportResponse
11
+
12
+ _ORIGINAL_GET_LOGGER = structlog.get_logger
13
+
14
+
15
+ @pytest.mark.parametrize("drop_first", [False, True])
16
+ def test_repeated_native_records_and_capture_after_rejected_hook(drop_first):
17
+ events = []
18
+ calls = []
19
+
20
+ def hook(event):
21
+ calls.append(event)
22
+ return None if len(calls) == 1 else event
23
+
24
+ sdk = DebugBundleSdk(transport=lambda request: (events.extend(request["events"]), TransportResponse(202))[1])
25
+ sdk.init("synthetic-token", batch_size=1000, flush_interval=3600, before_send=hook if drop_first else None)
26
+ error = RuntimeError("reused object")
27
+ try:
28
+ with sdk.operation_context({"kind": "request"}):
29
+ sdk.capture_exception(error, context={"logger_name": "synthetic"})
30
+ sdk.flush()
31
+ sdk.capture_exception(error, context={} if drop_first else {"logger_name": "synthetic"})
32
+ sdk.flush()
33
+ assert len(events) == (1 if drop_first else 2)
34
+ finally:
35
+ sdk.dispose()
36
+
37
+
38
+ def test_stdlib_exception_preserves_traceback_and_cause_without_frame_logs():
39
+ events = []
40
+
41
+ def send(request):
42
+ events.extend(request["events"])
43
+ return TransportResponse(202)
44
+
45
+ sdk = DebugBundleSdk(transport=send)
46
+ sdk.init("synthetic-token", batch_size=1000, flush_interval=3600)
47
+ logger = logging.Logger("synthetic", level=logging.ERROR)
48
+ logger.addHandler(DebugBundleLogHandler(sdk))
49
+ try:
50
+ try:
51
+ try:
52
+ raise ValueError("synthetic cause")
53
+ except ValueError as cause:
54
+ raise RuntimeError("synthetic outer") from cause
55
+ except RuntimeError:
56
+ logger.exception("operation failed")
57
+ logger.error("ordinary error")
58
+ sdk.flush()
59
+ assert [event["event_type"] for event in events] == ["backend_exception", "log_event"]
60
+ assert "synthetic cause" in events[0]["payload"]["stack"]
61
+ assert "test_stdlib_exception" in events[0]["payload"]["stack"]
62
+ finally:
63
+ sdk.dispose()
64
+
65
+
66
+ def test_logger_and_explicit_capture_share_identity_only_inside_owned_operation():
67
+ events = []
68
+ sdk = DebugBundleSdk(transport=lambda request: (events.extend(request["events"]), TransportResponse(202))[1])
69
+ sdk.init("synthetic-token", batch_size=1000, flush_interval=3600)
70
+ logger = logging.Logger("synthetic", level=logging.ERROR)
71
+ logger.addHandler(DebugBundleLogHandler(sdk))
72
+ error = RuntimeError("same object")
73
+ try:
74
+ for _ in range(2):
75
+ with sdk.operation_context({"kind": "request"}):
76
+ logger.error("failed", exc_info=(RuntimeError, error, None))
77
+ sdk.capture_exception(error)
78
+ sdk.flush()
79
+ assert len(events) == 2
80
+ finally:
81
+ sdk.dispose()
82
+
83
+
84
+ def test_loguru_and_structlog_preserve_native_cause_after_formatters(monkeypatch):
85
+ import structlog
86
+ from loguru import logger
87
+
88
+ events = []
89
+ sdk = DebugBundleSdk(transport=lambda request: (events.extend(request["events"]), TransportResponse(202))[1])
90
+ sdk.init("synthetic-token", batch_size=1000, flush_interval=3600)
91
+ previous = structlog.get_config()
92
+ monkeypatch.setattr(structlog, "get_logger", _ORIGINAL_GET_LOGGER)
93
+ structlog.configure(
94
+ processors=[structlog.processors.format_exc_info, structlog.processors.JSONRenderer()],
95
+ logger_factory=structlog.ReturnLoggerFactory(),
96
+ )
97
+ sdk.capture_logging(logger=logging.Logger("synthetic-integrations"))
98
+ try:
99
+ try:
100
+ raise ValueError("native cause")
101
+ except ValueError:
102
+ logger.exception("loguru failure")
103
+ structlog.get_logger().exception("structlog failure")
104
+ sdk.flush()
105
+ assert len(events) == 2
106
+ assert all(event["event_type"] == "backend_exception" for event in events)
107
+ assert all("native cause" in event["payload"]["stack"] for event in events)
108
+ finally:
109
+ structlog.configure(**previous)
110
+ sdk.dispose()