debugbundle-python 2.0.0__py3-none-any.whl → 2.1.0__py3-none-any.whl
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/__init__.py +16 -0
- debugbundle/acknowledgement.py +3 -1
- debugbundle/before_send.py +35 -191
- debugbundle/canonical_event.py +202 -0
- debugbundle/config.py +6 -0
- debugbundle/core.py +76 -316
- debugbundle/delivery_hooks.py +14 -0
- debugbundle/diagnostic_budget.py +191 -0
- debugbundle/event-envelope.schema.json +4904 -0
- debugbundle/event_support.py +1 -1
- debugbundle/evidence_delivery.py +115 -0
- debugbundle/evidence_http.py +45 -0
- debugbundle/evidence_json.py +68 -0
- debugbundle/evidence_wire.py +409 -0
- debugbundle/exception_capture.py +92 -0
- debugbundle/exception_evidence.py +199 -0
- debugbundle/exception_snapshot.py +1 -1
- debugbundle/flush_delivery.py +203 -0
- debugbundle/ingestion_wire.py +75 -0
- debugbundle/integrations/relay_django.py +4 -1
- debugbundle/integrations/relay_fastapi.py +10 -2
- debugbundle/integrations/relay_flask.py +4 -1
- debugbundle/operation_context.py +121 -0
- debugbundle/process_support.py +7 -0
- debugbundle/queue_admission.py +42 -2
- debugbundle/recovery_capture.py +43 -0
- debugbundle/relay.py +93 -36
- debugbundle/relay_delivery.py +108 -17
- debugbundle/remote_config_client.py +81 -0
- debugbundle/scope_ownership.py +99 -0
- debugbundle/transport.py +49 -20
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.1.0.dist-info}/METADATA +122 -1
- debugbundle_python-2.1.0.dist-info/RECORD +51 -0
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.1.0.dist-info}/WHEEL +1 -1
- debugbundle_python-2.0.0.dist-info/RECORD +0 -36
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.1.0.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.1.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Owned SDK boundary extracted without changing capture/delivery semantics."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from .config import MINIMAL_CAPTURE_POLICY, parse_remote_config
|
|
8
|
+
from .event_support import sdk_config_endpoint, serialize_error
|
|
9
|
+
from .evidence_http import evidence_url
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from .core import DebugBundleSdk
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def refresh_remote_config(sdk: DebugBundleSdk, initial: bool = False) -> None:
|
|
16
|
+
with sdk._lock:
|
|
17
|
+
if not sdk._enabled or sdk._fetch_impl is None:
|
|
18
|
+
return
|
|
19
|
+
request_headers: dict[str, str] = {"authorization": f"Bearer {sdk._project_token}"}
|
|
20
|
+
if sdk._remote_config_etag is not None:
|
|
21
|
+
request_headers["if-none-match"] = sdk._remote_config_etag
|
|
22
|
+
fetch = sdk._fetch_impl
|
|
23
|
+
endpoint = evidence_url(sdk_config_endpoint(sdk._endpoint))
|
|
24
|
+
poll_interval = sdk._configured_probes_poll_interval_ms
|
|
25
|
+
generation = sdk._generation
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
response = fetch(endpoint, {"method": "GET", "headers": request_headers})
|
|
29
|
+
status_code = getattr(response, "status_code", None)
|
|
30
|
+
if status_code == 304:
|
|
31
|
+
snapshot = None
|
|
32
|
+
elif status_code == 200:
|
|
33
|
+
payload = response.json()
|
|
34
|
+
snapshot = parse_remote_config(
|
|
35
|
+
payload,
|
|
36
|
+
poll_interval,
|
|
37
|
+
int(sdk._time_provider() * 1000),
|
|
38
|
+
)
|
|
39
|
+
else:
|
|
40
|
+
raise RuntimeError(f"unexpected config status {status_code}")
|
|
41
|
+
headers = response.headers or {}
|
|
42
|
+
etag = headers.get("etag")
|
|
43
|
+
failure: BaseException | None = None
|
|
44
|
+
except BaseException as error:
|
|
45
|
+
status_code = None
|
|
46
|
+
snapshot = None
|
|
47
|
+
etag = None
|
|
48
|
+
failure = error
|
|
49
|
+
|
|
50
|
+
with sdk._lock:
|
|
51
|
+
if generation != sdk._generation or not sdk._enabled:
|
|
52
|
+
return
|
|
53
|
+
if failure is None and status_code == 304:
|
|
54
|
+
sdk._schedule_next_remote_config_refresh()
|
|
55
|
+
return
|
|
56
|
+
if failure is None:
|
|
57
|
+
sdk._evidence.observe(payload, True, snapshot is not None)
|
|
58
|
+
if snapshot is None:
|
|
59
|
+
sdk._emit_diagnostic(
|
|
60
|
+
"remote_probe_config_invalid",
|
|
61
|
+
"sdk-python received an invalid remote probe config payload",
|
|
62
|
+
)
|
|
63
|
+
if initial:
|
|
64
|
+
sdk._capture_policy = MINIMAL_CAPTURE_POLICY
|
|
65
|
+
sdk._schedule_next_remote_config_refresh(use_fallback=True)
|
|
66
|
+
return
|
|
67
|
+
|
|
68
|
+
sdk._remote_config_snapshot = snapshot
|
|
69
|
+
sdk._capture_policy = snapshot.capture_policy
|
|
70
|
+
if isinstance(etag, str) and len(etag) > 0:
|
|
71
|
+
sdk._remote_config_etag = etag
|
|
72
|
+
sdk._schedule_next_remote_config_refresh()
|
|
73
|
+
else:
|
|
74
|
+
sdk._emit_diagnostic(
|
|
75
|
+
"remote_probe_config_failed",
|
|
76
|
+
"sdk-python failed to refresh remote probe config",
|
|
77
|
+
metadata={"error": serialize_error(failure) if isinstance(failure, Exception) else "fetch_failed"},
|
|
78
|
+
)
|
|
79
|
+
if initial:
|
|
80
|
+
sdk._capture_policy = MINIMAL_CAPTURE_POLICY
|
|
81
|
+
sdk._schedule_next_remote_config_refresh(use_fallback=True)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Revoke copied ContextVar values when their request/project owner ends."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from contextvars import Token
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import TYPE_CHECKING, Any, cast
|
|
8
|
+
|
|
9
|
+
from .config import RemoteProbeDirective
|
|
10
|
+
from .operation_context import Operation
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from .core import DebugBundleSdk
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Lease:
|
|
18
|
+
generation: int
|
|
19
|
+
parent: Lease | None = None
|
|
20
|
+
active: bool = True
|
|
21
|
+
token_id: int = 0
|
|
22
|
+
operation: Token[Operation | None] | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class ScopedContext(dict[str, object]):
|
|
26
|
+
lease: Lease
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class ScopedDirectives(list[RemoteProbeDirective]):
|
|
30
|
+
lease: Lease
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def alive(value: object, generation: int) -> bool:
|
|
34
|
+
if type(value) is not ScopedContext and type(value) is not ScopedDirectives:
|
|
35
|
+
return False
|
|
36
|
+
lease: Lease | None = value.lease
|
|
37
|
+
for _ in range(16):
|
|
38
|
+
if lease is None:
|
|
39
|
+
return True
|
|
40
|
+
if not lease.active or lease.generation != generation:
|
|
41
|
+
return False
|
|
42
|
+
lease = lease.parent
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def context_value(sdk: DebugBundleSdk) -> dict[str, object]:
|
|
47
|
+
value = sdk._scoped_context.get()
|
|
48
|
+
return value if alive(value, sdk._generation) and value is not None else {}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def bind_context(sdk: DebugBundleSdk, protected: dict[str, object]) -> Token[dict[str, object] | None]:
|
|
52
|
+
parent = sdk._scoped_context.get()
|
|
53
|
+
value = ScopedContext(protected)
|
|
54
|
+
value.lease = Lease(
|
|
55
|
+
sdk._generation, parent.lease if type(parent) is ScopedContext and alive(parent, sdk._generation) else None
|
|
56
|
+
)
|
|
57
|
+
token = sdk._scoped_context.set(value)
|
|
58
|
+
value.lease.token_id = id(token)
|
|
59
|
+
return token
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def bind_directives(
|
|
63
|
+
sdk: DebugBundleSdk, directives: list[RemoteProbeDirective]
|
|
64
|
+
) -> Token[list[RemoteProbeDirective] | None]:
|
|
65
|
+
parent = sdk._request_trigger_directives.get()
|
|
66
|
+
value = ScopedDirectives(directives)
|
|
67
|
+
value.lease = Lease(
|
|
68
|
+
sdk._generation,
|
|
69
|
+
parent.lease if type(parent) is ScopedDirectives and alive(parent, sdk._generation) else None,
|
|
70
|
+
operation=sdk._operations.begin({"kind": "request"}),
|
|
71
|
+
)
|
|
72
|
+
token = sdk._request_trigger_directives.set(value)
|
|
73
|
+
value.lease.token_id = id(token)
|
|
74
|
+
return token
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def release(
|
|
78
|
+
sdk: DebugBundleSdk,
|
|
79
|
+
token: Token[dict[str, object] | None] | Token[list[RemoteProbeDirective] | None],
|
|
80
|
+
*,
|
|
81
|
+
directives: bool = False,
|
|
82
|
+
) -> None:
|
|
83
|
+
variable = sdk._request_trigger_directives if directives else sdk._scoped_context
|
|
84
|
+
value = variable.get()
|
|
85
|
+
lease = value.lease if type(value) is ScopedContext or type(value) is ScopedDirectives else None
|
|
86
|
+
current_lease = lease
|
|
87
|
+
for _ in range(16):
|
|
88
|
+
if lease is None:
|
|
89
|
+
return
|
|
90
|
+
if lease.token_id == id(token):
|
|
91
|
+
if lease is current_lease:
|
|
92
|
+
# Preserve native Token context checks; old completion never clears
|
|
93
|
+
# the currently active newer owner.
|
|
94
|
+
cast(Any, variable).reset(token)
|
|
95
|
+
lease.active = False
|
|
96
|
+
if directives:
|
|
97
|
+
sdk._operations.end(lease.operation)
|
|
98
|
+
return
|
|
99
|
+
lease = lease.parent
|
debugbundle/transport.py
CHANGED
|
@@ -1,11 +1,43 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import math
|
|
4
|
+
import time
|
|
3
5
|
from collections.abc import Mapping
|
|
4
6
|
from dataclasses import dataclass
|
|
7
|
+
from email.utils import parsedate_to_datetime
|
|
5
8
|
from typing import Any, Protocol
|
|
6
9
|
|
|
7
10
|
import httpx
|
|
8
11
|
|
|
12
|
+
from .evidence_http import bounded_response_json, evidence_url
|
|
13
|
+
from .ingestion_wire import MAX_BODY_BYTES, encode_body
|
|
14
|
+
|
|
15
|
+
MAX_RETRY_AFTER_MS = 300_000
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def bounded_retry_after_ms(value: object, default: int = 1_000) -> int:
|
|
19
|
+
# Clamp before conversion so arbitrarily large integer hints cannot overflow a float deadline.
|
|
20
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
21
|
+
return default
|
|
22
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
23
|
+
return default
|
|
24
|
+
return int(min(MAX_RETRY_AFTER_MS, max(0, value)))
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _parse_retry_after(value: str | None) -> int | None:
|
|
28
|
+
if value is None:
|
|
29
|
+
return None
|
|
30
|
+
try:
|
|
31
|
+
seconds = float(value)
|
|
32
|
+
except ValueError:
|
|
33
|
+
try:
|
|
34
|
+
seconds = parsedate_to_datetime(value).timestamp() - time.time()
|
|
35
|
+
except (ValueError, TypeError, OverflowError):
|
|
36
|
+
return None
|
|
37
|
+
if not math.isfinite(seconds):
|
|
38
|
+
return None
|
|
39
|
+
return bounded_retry_after_ms(min(300, max(0, seconds)) * 1_000)
|
|
40
|
+
|
|
9
41
|
|
|
10
42
|
@dataclass
|
|
11
43
|
class TransportResponse:
|
|
@@ -15,8 +47,7 @@ class TransportResponse:
|
|
|
15
47
|
|
|
16
48
|
|
|
17
49
|
class Transport(Protocol):
|
|
18
|
-
def __call__(self, request: Mapping[str, object]) -> TransportResponse:
|
|
19
|
-
...
|
|
50
|
+
def __call__(self, request: Mapping[str, object]) -> TransportResponse: ...
|
|
20
51
|
|
|
21
52
|
|
|
22
53
|
class HttpTransport:
|
|
@@ -25,28 +56,26 @@ class HttpTransport:
|
|
|
25
56
|
self._client = httpx.Client(timeout=5.0)
|
|
26
57
|
|
|
27
58
|
def __call__(self, request: Mapping[str, object]) -> TransportResponse:
|
|
59
|
+
return self.send_until(request, time.monotonic() + 5.0)
|
|
60
|
+
|
|
61
|
+
def send_until(self, request: Mapping[str, object], deadline: float) -> TransportResponse:
|
|
62
|
+
remaining = min(5.0, deadline - time.monotonic())
|
|
63
|
+
if remaining <= 0:
|
|
64
|
+
raise TimeoutError("ingestion deadline exhausted")
|
|
65
|
+
body_bytes = encode_body(request["events"])
|
|
66
|
+
if len(body_bytes) > MAX_BODY_BYTES:
|
|
67
|
+
raise ValueError("ingestion request exceeds byte limit")
|
|
28
68
|
headers = {
|
|
29
69
|
"authorization": f"Bearer {request['project_token']}",
|
|
30
70
|
"content-type": "application/json",
|
|
71
|
+
"accept-encoding": "identity",
|
|
31
72
|
}
|
|
32
|
-
|
|
33
|
-
self._endpoint,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
retry_after_ms = None
|
|
39
|
-
if retry_after_header is not None:
|
|
40
|
-
try:
|
|
41
|
-
retry_after_ms = int(float(retry_after_header) * 1000)
|
|
42
|
-
except ValueError:
|
|
43
|
-
retry_after_ms = None
|
|
44
|
-
|
|
45
|
-
try:
|
|
46
|
-
body: object | None = response.json()
|
|
47
|
-
except (ValueError, TypeError):
|
|
48
|
-
body = None
|
|
49
|
-
return TransportResponse(status_code=response.status_code, retry_after_ms=retry_after_ms, body=body)
|
|
73
|
+
with self._client.stream(
|
|
74
|
+
"POST", evidence_url(self._endpoint), content=body_bytes, headers=headers, timeout=remaining
|
|
75
|
+
) as response:
|
|
76
|
+
retry_after_ms = _parse_retry_after(response.headers.get("retry-after"))
|
|
77
|
+
body = bounded_response_json(response, deadline)
|
|
78
|
+
return TransportResponse(status_code=response.status_code, retry_after_ms=retry_after_ms, body=body)
|
|
50
79
|
|
|
51
80
|
def close(self) -> None:
|
|
52
81
|
self._client.close()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -43,6 +43,14 @@ Dynamic: license-file
|
|
|
43
43
|
|
|
44
44
|
Python SDK for DebugBundle.
|
|
45
45
|
|
|
46
|
+
After `make install-docker`, `make evidence-performance` compares identical
|
|
47
|
+
exception/recovery calls with evidence disabled and enabled, including a full
|
|
48
|
+
warning queue, with networking disabled. It reports capture timing separately from
|
|
49
|
+
Python `tracemalloc` peak/live allocation measurements, verifies native causes and
|
|
50
|
+
original-error priority, and checks existing retention ceilings. Setup and cleanup
|
|
51
|
+
are excluded; traced allocations are Python-managed memory, not cumulative churn
|
|
52
|
+
or total process RSS.
|
|
53
|
+
|
|
46
54
|

|
|
47
55
|

|
|
48
56
|

|
|
@@ -51,6 +59,94 @@ Use this package to capture Python backend exceptions, request metadata, structu
|
|
|
51
59
|
|
|
52
60
|
Requires Python 3.10 or newer.
|
|
53
61
|
|
|
62
|
+
## Evidence candidate (unreleased)
|
|
63
|
+
|
|
64
|
+
The source candidate adds `capture_exception_evidence=True` and
|
|
65
|
+
`capture_recovery_observations=False` installation options. Collection requires
|
|
66
|
+
verified delivery/reader support, discovered on existing config/ACK traffic with
|
|
67
|
+
`debugbundle_evidence=1`. No capture call performs discovery or adds polling.
|
|
68
|
+
`evidence_capabilities` is an optional descriptor pin asserting the **entire**
|
|
69
|
+
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
|
+
|
|
73
|
+
Native cause, implicit context and Python 3.11+ exception-group evidence excludes
|
|
74
|
+
locals, source excerpts, arbitrary renderers and application properties. Python
|
|
75
|
+
3.10 has no native exception groups. Graphs retain at most eight nodes, depth
|
|
76
|
+
four, five children and 32 frames per node; all optional exception additions fit
|
|
77
|
+
32 KiB. Uncaught native capture bypasses `before_send` and uses two nodes, eight
|
|
78
|
+
frames and a 4-KiB total including execution/reference metadata. The original
|
|
79
|
+
sanitised stack remains separate.
|
|
80
|
+
|
|
81
|
+
`with sdk.operation_context({"kind": "job", "name": "invoice.calculate",
|
|
82
|
+
"limit_ms": 5000}):` supplies a fresh operation scope and monotonic elapsed time
|
|
83
|
+
to synchronous or asynchronous work inside the block. Kinds are `request`,
|
|
84
|
+
`command`, `job` and `unknown`. The limit is a known configured limit, not time
|
|
85
|
+
remaining. Framework requests use their existing begin/end lifecycle. Completed
|
|
86
|
+
owners revoke copied async context; reinitialisation/fork discard prior ownership.
|
|
87
|
+
Application results and exceptions pass through the context manager unchanged.
|
|
88
|
+
|
|
89
|
+
The existing background sender remains best-effort at immediate process exit.
|
|
90
|
+
The uncaught hook collects bounded native evidence without waiting for transport;
|
|
91
|
+
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
|
+
synchronous capture transport.
|
|
94
|
+
|
|
95
|
+
Pass a fresh UUID v4 `failure_ref` in exception/log/request context. A valid
|
|
96
|
+
`evidence_scope` can specify a fresh page or operation UUID; an active operation
|
|
97
|
+
provides the default. `capture_recovery({"failure_ref": failure_ref,
|
|
98
|
+
"attempt_id": attempt_id, "state": "attempt_completed"})` records an explicit
|
|
99
|
+
application observation. Optional fields are `evidence_scope`, technical
|
|
100
|
+
`operation`, `status_code`, `duration_ms` and primitive `correlation` metadata.
|
|
101
|
+
Page scopes require a nonblank session ID, including the valid string `"0"`.
|
|
102
|
+
Only explicit `recovery_observed` claims semantic recovery; HTTP 200 and
|
|
103
|
+
`attempt_completed` leave it unknown. The other states are `attempt_started`,
|
|
104
|
+
`attempt_failed` and `failure_persisted`.
|
|
105
|
+
|
|
106
|
+
Standalone recovery requires its local opt-in, `recovery_v1` support and server
|
|
107
|
+
`capture_diagnostic_context`; minimal policy disables it. Privacy, sampling,
|
|
108
|
+
resulting hook-event policy and queue limits still apply. Fifty failure windows,
|
|
109
|
+
five attempts and twenty observations per failure share 64 KiB of tracker and
|
|
110
|
+
pending-payload ownership, retained until ACK/drop even offline. Canonical hooks
|
|
111
|
+
may change any allowed identity/type field; ownership follows the replacement.
|
|
112
|
+
The complete closed envelope is checked against the vendored accepting Draft 7
|
|
113
|
+
schema, followed by evidence semantics and mandatory privacy. Only native JSON
|
|
114
|
+
containers/scalars are copied; custom serializers and container subclasses are
|
|
115
|
+
never invoked by hook validation. Invalid or throwing hooks keep the protected
|
|
116
|
+
original. Exhausted validation or an unfit valid replacement is withheld, so an
|
|
117
|
+
application redaction cannot be undone by a budget fallback.
|
|
118
|
+
|
|
119
|
+
Native HTTP requests select an ordered prefix by the actual compact UTF-8 JSON
|
|
120
|
+
serializer and remain within the receiving 256-KiB limit. Unsent finalized tails
|
|
121
|
+
stay charged and continue through the existing sender without repeating their
|
|
122
|
+
hooks. Optional SDK evidence is reduced before withholding an unfit primary;
|
|
123
|
+
explicit custom/local transports retain their established budgets. Browser
|
|
124
|
+
relays measure after server identity/token expansion, split downstream requests
|
|
125
|
+
and keep incomplete spools. A legacy custom success remains accepted but cannot
|
|
126
|
+
mark a durable spool delivered; that requires complete canonical acknowledgements.
|
|
127
|
+
Relay chunks share a five-second monotonic forwarding deadline, and spool markers
|
|
128
|
+
are created exclusively with private permissions without following symlinks.
|
|
129
|
+
|
|
130
|
+
Native HTTP ACK reads stop at 256 KiB and close the response. Requests ask for
|
|
131
|
+
identity encoding; unexpected compressed ACKs are withheld. The five-second
|
|
132
|
+
deadline is checked between streamed reads; HTTPX's synchronous per-read/connect/
|
|
133
|
+
write timeouts remain advisory and an in-progress native read can overrun that
|
|
134
|
+
deadline. Explicit custom transports/config fetchers retain their own timeout,
|
|
135
|
+
allocation and cancellation responsibilities. No additional timer thread or
|
|
136
|
+
inline capture transport is introduced.
|
|
137
|
+
|
|
138
|
+
Browser relays and Django/Flask/FastAPI relay factories accept the same optional
|
|
139
|
+
`evidence_capabilities` pin. They preserve closed DTOs, protected application
|
|
140
|
+
context and server-owned credentials, and advertise only verified downstream
|
|
141
|
+
support. Opted-in replies keep string `errors` and add indexed `evidence_errors`;
|
|
142
|
+
partial batches return 202, while legacy partial responses retain 400. A durable
|
|
143
|
+
spool accepts locally but is marked delivered only after a valid complete ACK.
|
|
144
|
+
Malformed ACKs cannot grant proof. Only negotiated `invalid_event` rejection of
|
|
145
|
+
an original exception can retry its stripped legacy form once; standalone
|
|
146
|
+
context, policy and authentication rejections do not use this fallback.
|
|
147
|
+
Framework relay adapters read at most the configured body limit plus one byte
|
|
148
|
+
before parsing, and preserve the negotiation query/header through the handler.
|
|
149
|
+
|
|
54
150
|
Version 2.0.0 protects capture callers with bounded, priority-aware background delivery. Review the [2.0 migration guide](MIGRATION-2.0.md) before upgrading from 1.x.
|
|
55
151
|
|
|
56
152
|
## Automatic capture and application filtering
|
|
@@ -357,3 +453,28 @@ CI validates Ruff, mypy, pytest, package build, event schema fixtures, and cover
|
|
|
357
453
|
## License
|
|
358
454
|
|
|
359
455
|
Apache-2.0. See `LICENSE`.
|
|
456
|
+
|
|
457
|
+
## Delivery acknowledgement and retry limits
|
|
458
|
+
|
|
459
|
+
The built-in HTTP transport requires the canonical ingestion acknowledgement (`accepted`, `rejected`, and `errors`). Empty responses, unrelated JSON, malformed counts, or invalid rejection indices retain the full batch with backoff. Valid acknowledgements remove accepted and terminally rejected events and retry only the indexed retryable rejections; an all-rejected batch does not advance `lastEventAt`.
|
|
460
|
+
|
|
461
|
+
File transports and explicitly supplied custom/legacy transports retain their documented bodyless success fallback. A custom transport that returns acknowledgement fields must return the complete canonical shape. Retry hints (delay seconds or HTTP dates) are bounded to five minutes and measured from response receipt, including partial/protocol acknowledgements and retryable server failures. Without a server hint, existing retry timing is preserved. Failures remain contained within the SDK's existing delivery path.
|
|
462
|
+
|
|
463
|
+
## Probe checkpoint guidance
|
|
464
|
+
|
|
465
|
+
Application instrumentation supplies probe labels and bounded data. Deploy a new
|
|
466
|
+
checkpoint before activating it. Remote activation affects future matching calls
|
|
467
|
+
when policy permits; it does not inject code or recover unretained measurements.
|
|
468
|
+
Built-in runtime/error/device collectors keep their documented SDK settings.
|
|
469
|
+
|
|
470
|
+
Use fixed labels and allowlisted technical states: `media.preview.checkpoint`
|
|
471
|
+
(stage/input kind/attempt), `auth.session.checkpoint` (observed session presence
|
|
472
|
+
and result), `job.dependency.checkpoint` (named dependency/attempt/result), or
|
|
473
|
+
`cache.preview.checkpoint` (hit/miss/coarse age bucket). Omit customer identifiers,
|
|
474
|
+
tokens/cookies, cache keys, URLs and raw payloads. HTTP 200 or attempt completion
|
|
475
|
+
alone does not establish semantic recovery.
|
|
476
|
+
|
|
477
|
+
Empty probe data means no matching retained observation for the selected sample
|
|
478
|
+
window. It does not confirm activation delivery to every SDK or execution of a
|
|
479
|
+
label. Check the deployed SDK/wrapper version, instrumentation, scope, expiry,
|
|
480
|
+
policy, sampling and delivery receipts. See the [probe recipes](https://debugbundle.com/docs/probes#bounded-checkpoint-recipes).
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
debugbundle/__init__.py,sha256=OFqqIibB3YlNeuTSynunM_XZ0cuts3Xoh15yNYDVEFs,5685
|
|
2
|
+
debugbundle/acknowledgement.py,sha256=2miOWbuIfElrbkktJHEuL7a2-IqKYUZCpO1UjSaouYw,2461
|
|
3
|
+
debugbundle/before_send.py,sha256=VpE056j_GRtJhhF9er-aLvR665fagPkvUlyXIJxrgbE,2124
|
|
4
|
+
debugbundle/canonical_event.py,sha256=qMwKCMsF-rHnYMnNVB2obDe66ykTXlM5S6chHSJODBI,8054
|
|
5
|
+
debugbundle/capture_hooks.py,sha256=70Q6ske8JulyuYpeE9FohvMreytf4DUrj4QW9f2-o2Q,1940
|
|
6
|
+
debugbundle/config.py,sha256=3ZOYk35dEnZMqJOI_hvTGOzC17seVPUGxjnX05J15Do,9571
|
|
7
|
+
debugbundle/core.py,sha256=_oLk0FwW9vj1bxHDvW6cs47i0q6v4cjCsuA7k6N3Gr8,28285
|
|
8
|
+
debugbundle/delivery_hooks.py,sha256=hIXmFcKbHDw-3004Mc-pyz4iZGZNR-ZDiBw2xYKCTn4,3455
|
|
9
|
+
debugbundle/diagnostic_budget.py,sha256=dEfse2fqwYgR7IL6FASjqZ8cnq9lLlcwnLJ38utN0G0,7032
|
|
10
|
+
debugbundle/event-envelope.schema.json,sha256=QSAmE8Be_e91T_4_bDrBScdctEXABHpcsjMqyxZKeSI,148624
|
|
11
|
+
debugbundle/event_support.py,sha256=keOpma92Hxer2mOD9ELw-w5MRFYNOsj5A9C9goLdkDE,9046
|
|
12
|
+
debugbundle/evidence_delivery.py,sha256=cadrVo0lq5hNBLhl5woJof_HjAlrqrSgYjvBGfoUWVY,4781
|
|
13
|
+
debugbundle/evidence_http.py,sha256=k490I2Mh9L62A37Le1pnACb-9jVKIMFjsp9u0mYMhuA,1769
|
|
14
|
+
debugbundle/evidence_json.py,sha256=hAiENRzJBKUV-IujHDmK4qEEDtBkb8ZsBo9vyBmGwEo,2349
|
|
15
|
+
debugbundle/evidence_wire.py,sha256=nUY9SiiKoP9LhE31y6q_vyPE5_JieI-ZFDRYL_DX0dQ,16259
|
|
16
|
+
debugbundle/exception_capture.py,sha256=fHUuDxVuY5Uq7nLhtaGVd-vghIZAI1Tdbh5bDsNluwg,3797
|
|
17
|
+
debugbundle/exception_evidence.py,sha256=cclp6aIuuqCM6HHKl4nHezZeswsQQhFZQn2s9fNtSVc,7517
|
|
18
|
+
debugbundle/exception_snapshot.py,sha256=Q3yX-rPuwyiP5tiyMCHnUE2r5Y5lne7bLb8s8abXtDw,2863
|
|
19
|
+
debugbundle/flush_aggregates.py,sha256=ukHsIUykeGp4YJsesA_9NCUMarg_BAXVuzO4bqJKH3Y,1464
|
|
20
|
+
debugbundle/flush_delivery.py,sha256=0gtluLskyKFWaKMPQSfd16tV256_rCvQrfV5muQOXdw,9009
|
|
21
|
+
debugbundle/ingestion_wire.py,sha256=SGyzAw4uGr9YRsB5hClVXb7il_nn3z0rm0Ofziw51qo,2537
|
|
22
|
+
debugbundle/logger_integrations.py,sha256=T6LR10_DgDS8C-CUPxtC1kZz3qfX5j0B2YUV7LxDovk,3572
|
|
23
|
+
debugbundle/operation_context.py,sha256=oeY0W5su8VG1kWa4L8XrJZcNiAUZsGspjC5y-WGMXJQ,3783
|
|
24
|
+
debugbundle/probe_capture.py,sha256=c1a-ZKHyNeu6CMS5zgi8_wIfpLs5-pe12GEXNMpMTVM,3969
|
|
25
|
+
debugbundle/process_support.py,sha256=rKV_afnK-_tEcPO9Fzv2xvjN5Xt1sGwrZoF9yCsQ6_w,2273
|
|
26
|
+
debugbundle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
27
|
+
debugbundle/queue_admission.py,sha256=NtXwpB8lZIu0sbtxJUNHXQp_AcgVNDnavEM80rz3raY,2970
|
|
28
|
+
debugbundle/recovery_capture.py,sha256=IOCarb9vmJ3ylwoYnD7GFTRwbra17dx7JCvuCA1isNY,1663
|
|
29
|
+
debugbundle/redaction.py,sha256=mxJgtILpVDUaj8LAhzswfaX1M4Ppyx87lfkhFqbMR8M,9663
|
|
30
|
+
debugbundle/relay.py,sha256=FVEvwrmMxdEDkTk3WDoNHFD0-_2w1OiYYHWAwQFVu9c,17664
|
|
31
|
+
debugbundle/relay_delivery.py,sha256=2EowBh_s5aF6ruPs1PjOXFAZ7IULRe5SBBg8v-bDf9o,9355
|
|
32
|
+
debugbundle/remote_config_client.py,sha256=WrAq-YzixEomJ98pTB05qo2ImfJKayc-HSYRYO1VJg0,3206
|
|
33
|
+
debugbundle/request_capture_policy.py,sha256=fl1EilWAnBQCdGTafRpY0tMlYUoSJK_7hZyJmPHLkeI,1583
|
|
34
|
+
debugbundle/scope_ownership.py,sha256=uB3dpG9blEwFcUbQNAu0jjHoxJlRoliRx7bx8F1YNVE,3141
|
|
35
|
+
debugbundle/structlog_integration.py,sha256=hDwCGXTQ8GAjnx2jwlRbpnvLE0y798kFFMsSB3iqOrM,6061
|
|
36
|
+
debugbundle/suppression.py,sha256=V_JvDvcV7tN3EHkfpEJGK1WB2G4nR3GhXvaKd400iFQ,4418
|
|
37
|
+
debugbundle/transport.py,sha256=-MkMFf2rjPrDEGLVe-LHewFtkvVlA40X7XDaHKxVeqE,3343
|
|
38
|
+
debugbundle/trigger_token.py,sha256=YUwIWnnxu1klAVaB8Ltgk_PwWW_PPjq9rzmEbWXeFeM,4455
|
|
39
|
+
debugbundle/integrations/__init__.py,sha256=Jr87ImWXzUXOuFP4WlGleK14VARajSSdGZN_nHt5emw,1161
|
|
40
|
+
debugbundle/integrations/common.py,sha256=iiwf5wDlpujFuWfbO-ziFTn4MtiHBXSIZNXtamhCavg,2014
|
|
41
|
+
debugbundle/integrations/django.py,sha256=vDQGc0ObcHCe4NQbC0ijcXUM-y9GzOVBjDtcHzxyrxw,1828
|
|
42
|
+
debugbundle/integrations/fastapi.py,sha256=-054Z6MogkZIqKR8DHR4jm2nNC5yyZ5vtanpw6g-voE,3414
|
|
43
|
+
debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-Fbyo,2390
|
|
44
|
+
debugbundle/integrations/relay_django.py,sha256=KCo9-5eumVZZKEmzZemL4srCRN7inMB5aeOSUqeEEXE,2378
|
|
45
|
+
debugbundle/integrations/relay_fastapi.py,sha256=oVL5m8SFsbcbxzmDVefGsBk75Ha2DOSymJwDPmHOI-o,2551
|
|
46
|
+
debugbundle/integrations/relay_flask.py,sha256=GPiXNR3feH-KU5GJ8tguOvt0Rr9b776GY8O2SjiLwQs,2169
|
|
47
|
+
debugbundle_python-2.1.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
48
|
+
debugbundle_python-2.1.0.dist-info/METADATA,sha256=-l6UHUrrbNukdvKQ3jPLvpkvJNtKPBZBktmpQizU99s,23754
|
|
49
|
+
debugbundle_python-2.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
50
|
+
debugbundle_python-2.1.0.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
|
|
51
|
+
debugbundle_python-2.1.0.dist-info/RECORD,,
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
debugbundle/__init__.py,sha256=QH5RY2Cdp3Ry5PNSAhiV9akOMoS2pERRqowFnZ94roc,5131
|
|
2
|
-
debugbundle/acknowledgement.py,sha256=tyI8XLRG9N3-Weo6uiGrAlf_O4tjptVe1x1cG7RjrMc,2314
|
|
3
|
-
debugbundle/before_send.py,sha256=_tmV6ovHUBIxfQPMQ8a0C6r91IAnd4DVs8UcjsKhqX0,7792
|
|
4
|
-
debugbundle/capture_hooks.py,sha256=70Q6ske8JulyuYpeE9FohvMreytf4DUrj4QW9f2-o2Q,1940
|
|
5
|
-
debugbundle/config.py,sha256=ENRSR5jUI7xYdBqUakWws3GIjBk00GWFmwXVE_iQp2Y,9296
|
|
6
|
-
debugbundle/core.py,sha256=XgRx92jSzrP6PjvHHRI0NlwdD53cRmBQ6qN4UtnQqIA,39310
|
|
7
|
-
debugbundle/delivery_hooks.py,sha256=BUqtHuEv1MDOSLv0sUP6D582c06JofsbXhi7I7Y5IGc,2691
|
|
8
|
-
debugbundle/event_support.py,sha256=MtPg2CO7B91U7-zE9XGro0-fynaEnJZmQ6Ms9arfRlg,9046
|
|
9
|
-
debugbundle/exception_snapshot.py,sha256=_2p55F572sL7_ogBL6Raa137HRyUqvSaHVMwpKiC8fs,2831
|
|
10
|
-
debugbundle/flush_aggregates.py,sha256=ukHsIUykeGp4YJsesA_9NCUMarg_BAXVuzO4bqJKH3Y,1464
|
|
11
|
-
debugbundle/logger_integrations.py,sha256=T6LR10_DgDS8C-CUPxtC1kZz3qfX5j0B2YUV7LxDovk,3572
|
|
12
|
-
debugbundle/probe_capture.py,sha256=c1a-ZKHyNeu6CMS5zgi8_wIfpLs5-pe12GEXNMpMTVM,3969
|
|
13
|
-
debugbundle/process_support.py,sha256=D1aFhaP1wOGfMdzkdtEhO54R3XWa8dLCZlfWHWN3qA4,1962
|
|
14
|
-
debugbundle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
|
-
debugbundle/queue_admission.py,sha256=QtFengGZ4KEVbu1UqSp3u8eLucQeS9dGIwrzq62A5cY,1433
|
|
16
|
-
debugbundle/redaction.py,sha256=mxJgtILpVDUaj8LAhzswfaX1M4Ppyx87lfkhFqbMR8M,9663
|
|
17
|
-
debugbundle/relay.py,sha256=9HkEEVZpqrebzH6A-sSuFVuFjyLg07JuTMpMG8pu7SY,14726
|
|
18
|
-
debugbundle/relay_delivery.py,sha256=VL-nIgJR6mYXqKy-EbA0USWeY_iS_uAr1KBzrnwBnnk,4676
|
|
19
|
-
debugbundle/request_capture_policy.py,sha256=fl1EilWAnBQCdGTafRpY0tMlYUoSJK_7hZyJmPHLkeI,1583
|
|
20
|
-
debugbundle/structlog_integration.py,sha256=hDwCGXTQ8GAjnx2jwlRbpnvLE0y798kFFMsSB3iqOrM,6061
|
|
21
|
-
debugbundle/suppression.py,sha256=V_JvDvcV7tN3EHkfpEJGK1WB2G4nR3GhXvaKd400iFQ,4418
|
|
22
|
-
debugbundle/transport.py,sha256=V289FwFWL6KV5gyzOsesluEMbpRobm_FHu3x8BpOYKU,2014
|
|
23
|
-
debugbundle/trigger_token.py,sha256=YUwIWnnxu1klAVaB8Ltgk_PwWW_PPjq9rzmEbWXeFeM,4455
|
|
24
|
-
debugbundle/integrations/__init__.py,sha256=Jr87ImWXzUXOuFP4WlGleK14VARajSSdGZN_nHt5emw,1161
|
|
25
|
-
debugbundle/integrations/common.py,sha256=iiwf5wDlpujFuWfbO-ziFTn4MtiHBXSIZNXtamhCavg,2014
|
|
26
|
-
debugbundle/integrations/django.py,sha256=vDQGc0ObcHCe4NQbC0ijcXUM-y9GzOVBjDtcHzxyrxw,1828
|
|
27
|
-
debugbundle/integrations/fastapi.py,sha256=-054Z6MogkZIqKR8DHR4jm2nNC5yyZ5vtanpw6g-voE,3414
|
|
28
|
-
debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-Fbyo,2390
|
|
29
|
-
debugbundle/integrations/relay_django.py,sha256=Wj8BE9D5otU2KmtjgqdKIJ-BvXVcagCKgpLYvnPZ6WE,2281
|
|
30
|
-
debugbundle/integrations/relay_fastapi.py,sha256=-Zl6bvhYMLii__mP6-UvSdwxS5VKsb9OZvk-yy-3hEw,2227
|
|
31
|
-
debugbundle/integrations/relay_flask.py,sha256=VFHkDTJy4LkZzL_Ry_Ba-e_gW6UTG4PBwnLcvB_oXuQ,2011
|
|
32
|
-
debugbundle_python-2.0.0.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
33
|
-
debugbundle_python-2.0.0.dist-info/METADATA,sha256=oX9_gQfmoPs8PCSNucy0tTany71DEiuHsu71IPmYBmg,15231
|
|
34
|
-
debugbundle_python-2.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
35
|
-
debugbundle_python-2.0.0.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
|
|
36
|
-
debugbundle_python-2.0.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|