debugbundle-python 2.0.0__py3-none-any.whl → 2.0.1__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/acknowledgement.py +3 -1
- debugbundle/core.py +11 -9
- debugbundle/event_support.py +1 -1
- debugbundle/transport.py +30 -7
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.0.1.dist-info}/METADATA +7 -1
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.0.1.dist-info}/RECORD +9 -9
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.0.1.dist-info}/WHEEL +0 -0
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.0.1.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-2.0.0.dist-info → debugbundle_python-2.0.1.dist-info}/top_level.txt +0 -0
debugbundle/acknowledgement.py
CHANGED
|
@@ -19,8 +19,10 @@ class AcknowledgementDecision:
|
|
|
19
19
|
reason: str | None = None
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
def decide_acknowledgement(body: object | None, batch_length: int) -> AcknowledgementDecision:
|
|
22
|
+
def decide_acknowledgement(body: object | None, batch_length: int, required: bool = False) -> AcknowledgementDecision:
|
|
23
23
|
if not isinstance(body, dict) or not any(key in body for key in ("accepted", "rejected", "errors")):
|
|
24
|
+
if required:
|
|
25
|
+
return AcknowledgementDecision(kind="protocol_failure", reason="missing_acknowledgement")
|
|
24
26
|
return AcknowledgementDecision(kind="legacy")
|
|
25
27
|
|
|
26
28
|
accepted = body.get("accepted")
|
debugbundle/core.py
CHANGED
|
@@ -61,7 +61,7 @@ from .redaction import (
|
|
|
61
61
|
)
|
|
62
62
|
from .request_capture_policy import should_capture_request_event
|
|
63
63
|
from .suppression import EventSuppressionTracker
|
|
64
|
-
from .transport import HttpTransport, Transport, coerce_transport_response
|
|
64
|
+
from .transport import HttpTransport, Transport, bounded_retry_after_ms, coerce_transport_response
|
|
65
65
|
from .trigger_token import resolve_request_trigger_directives
|
|
66
66
|
|
|
67
67
|
DEFAULT_BATCH_SIZE = 25
|
|
@@ -499,11 +499,13 @@ class DebugBundleSdk:
|
|
|
499
499
|
return
|
|
500
500
|
|
|
501
501
|
if 200 <= response.status_code < 300:
|
|
502
|
-
acknowledgement = decide_acknowledgement(
|
|
502
|
+
acknowledgement = decide_acknowledgement(
|
|
503
|
+
response.body, len(batch), isinstance(transport, HttpTransport)
|
|
504
|
+
)
|
|
503
505
|
if acknowledgement.kind == "protocol_failure":
|
|
504
506
|
self._consecutive_failures += 1
|
|
505
|
-
retry_after_ms = response.retry_after_ms
|
|
506
|
-
self._retry_after =
|
|
507
|
+
retry_after_ms = bounded_retry_after_ms(response.retry_after_ms)
|
|
508
|
+
self._retry_after = self._time_provider() + (retry_after_ms / 1000)
|
|
507
509
|
self._emit_diagnostic(
|
|
508
510
|
"ingestion_acknowledgement_invalid",
|
|
509
511
|
"sdk-python retained a batch after an invalid ingestion acknowledgement",
|
|
@@ -535,8 +537,8 @@ class DebugBundleSdk:
|
|
|
535
537
|
self._last_event_at = self._time_provider() * 1000
|
|
536
538
|
if acknowledgement.retryable_indices:
|
|
537
539
|
self._consecutive_failures += 1
|
|
538
|
-
retry_after_ms = response.retry_after_ms
|
|
539
|
-
self._retry_after =
|
|
540
|
+
retry_after_ms = bounded_retry_after_ms(response.retry_after_ms)
|
|
541
|
+
self._retry_after = self._time_provider() + (retry_after_ms / 1000)
|
|
540
542
|
self._schedule_flush_locked(delay=retry_after_ms / 1000)
|
|
541
543
|
return
|
|
542
544
|
self._retry_after = 0.0
|
|
@@ -544,9 +546,9 @@ class DebugBundleSdk:
|
|
|
544
546
|
return
|
|
545
547
|
|
|
546
548
|
self._consecutive_failures += 1
|
|
547
|
-
if response.status_code == 429:
|
|
548
|
-
retry_after_ms = response.retry_after_ms
|
|
549
|
-
self._retry_after =
|
|
549
|
+
if response.status_code == 429 or (response.status_code >= 500 and response.retry_after_ms is not None):
|
|
550
|
+
retry_after_ms = bounded_retry_after_ms(response.retry_after_ms)
|
|
551
|
+
self._retry_after = self._time_provider() + (retry_after_ms / 1000)
|
|
550
552
|
self._schedule_flush_locked(delay=retry_after_ms / 1000)
|
|
551
553
|
return
|
|
552
554
|
if 400 <= response.status_code < 500:
|
debugbundle/event_support.py
CHANGED
debugbundle/transport.py
CHANGED
|
@@ -1,11 +1,40 @@
|
|
|
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
|
+
MAX_RETRY_AFTER_MS = 300_000
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def bounded_retry_after_ms(value: object, default: int = 1_000) -> int:
|
|
16
|
+
# Clamp before conversion so arbitrarily large integer hints cannot overflow a float deadline.
|
|
17
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
18
|
+
return default
|
|
19
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
20
|
+
return default
|
|
21
|
+
return int(min(MAX_RETRY_AFTER_MS, max(0, value)))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _parse_retry_after(value: str | None) -> int | None:
|
|
25
|
+
if value is None:
|
|
26
|
+
return None
|
|
27
|
+
try:
|
|
28
|
+
seconds = float(value)
|
|
29
|
+
except ValueError:
|
|
30
|
+
try:
|
|
31
|
+
seconds = parsedate_to_datetime(value).timestamp() - time.time()
|
|
32
|
+
except (ValueError, TypeError, OverflowError):
|
|
33
|
+
return None
|
|
34
|
+
if not math.isfinite(seconds):
|
|
35
|
+
return None
|
|
36
|
+
return bounded_retry_after_ms(min(300, max(0, seconds)) * 1_000)
|
|
37
|
+
|
|
9
38
|
|
|
10
39
|
@dataclass
|
|
11
40
|
class TransportResponse:
|
|
@@ -34,13 +63,7 @@ class HttpTransport:
|
|
|
34
63
|
json={"events": request["events"]},
|
|
35
64
|
headers=headers,
|
|
36
65
|
)
|
|
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
|
|
66
|
+
retry_after_ms = _parse_retry_after(response.headers.get("retry-after"))
|
|
44
67
|
|
|
45
68
|
try:
|
|
46
69
|
body: object | None = response.json()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.1
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -357,3 +357,9 @@ CI validates Ruff, mypy, pytest, package build, event schema fixtures, and cover
|
|
|
357
357
|
## License
|
|
358
358
|
|
|
359
359
|
Apache-2.0. See `LICENSE`.
|
|
360
|
+
|
|
361
|
+
## Delivery acknowledgement and retry limits
|
|
362
|
+
|
|
363
|
+
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`.
|
|
364
|
+
|
|
365
|
+
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.
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
debugbundle/__init__.py,sha256=QH5RY2Cdp3Ry5PNSAhiV9akOMoS2pERRqowFnZ94roc,5131
|
|
2
|
-
debugbundle/acknowledgement.py,sha256=
|
|
2
|
+
debugbundle/acknowledgement.py,sha256=2miOWbuIfElrbkktJHEuL7a2-IqKYUZCpO1UjSaouYw,2461
|
|
3
3
|
debugbundle/before_send.py,sha256=_tmV6ovHUBIxfQPMQ8a0C6r91IAnd4DVs8UcjsKhqX0,7792
|
|
4
4
|
debugbundle/capture_hooks.py,sha256=70Q6ske8JulyuYpeE9FohvMreytf4DUrj4QW9f2-o2Q,1940
|
|
5
5
|
debugbundle/config.py,sha256=ENRSR5jUI7xYdBqUakWws3GIjBk00GWFmwXVE_iQp2Y,9296
|
|
6
|
-
debugbundle/core.py,sha256=
|
|
6
|
+
debugbundle/core.py,sha256=uu2_VPvXwmccwwCcK1Y1kJgfehMOCrhFjV4L14X0cok,39467
|
|
7
7
|
debugbundle/delivery_hooks.py,sha256=BUqtHuEv1MDOSLv0sUP6D582c06JofsbXhi7I7Y5IGc,2691
|
|
8
|
-
debugbundle/event_support.py,sha256=
|
|
8
|
+
debugbundle/event_support.py,sha256=keOpma92Hxer2mOD9ELw-w5MRFYNOsj5A9C9goLdkDE,9046
|
|
9
9
|
debugbundle/exception_snapshot.py,sha256=_2p55F572sL7_ogBL6Raa137HRyUqvSaHVMwpKiC8fs,2831
|
|
10
10
|
debugbundle/flush_aggregates.py,sha256=ukHsIUykeGp4YJsesA_9NCUMarg_BAXVuzO4bqJKH3Y,1464
|
|
11
11
|
debugbundle/logger_integrations.py,sha256=T6LR10_DgDS8C-CUPxtC1kZz3qfX5j0B2YUV7LxDovk,3572
|
|
@@ -19,7 +19,7 @@ debugbundle/relay_delivery.py,sha256=VL-nIgJR6mYXqKy-EbA0USWeY_iS_uAr1KBzrnwBnnk
|
|
|
19
19
|
debugbundle/request_capture_policy.py,sha256=fl1EilWAnBQCdGTafRpY0tMlYUoSJK_7hZyJmPHLkeI,1583
|
|
20
20
|
debugbundle/structlog_integration.py,sha256=hDwCGXTQ8GAjnx2jwlRbpnvLE0y798kFFMsSB3iqOrM,6061
|
|
21
21
|
debugbundle/suppression.py,sha256=V_JvDvcV7tN3EHkfpEJGK1WB2G4nR3GhXvaKd400iFQ,4418
|
|
22
|
-
debugbundle/transport.py,sha256=
|
|
22
|
+
debugbundle/transport.py,sha256=_6L6XdjSSm-YzWsQLH8E5xku6yMS8wBYFpOwuYtY2cg,2764
|
|
23
23
|
debugbundle/trigger_token.py,sha256=YUwIWnnxu1klAVaB8Ltgk_PwWW_PPjq9rzmEbWXeFeM,4455
|
|
24
24
|
debugbundle/integrations/__init__.py,sha256=Jr87ImWXzUXOuFP4WlGleK14VARajSSdGZN_nHt5emw,1161
|
|
25
25
|
debugbundle/integrations/common.py,sha256=iiwf5wDlpujFuWfbO-ziFTn4MtiHBXSIZNXtamhCavg,2014
|
|
@@ -29,8 +29,8 @@ debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-
|
|
|
29
29
|
debugbundle/integrations/relay_django.py,sha256=Wj8BE9D5otU2KmtjgqdKIJ-BvXVcagCKgpLYvnPZ6WE,2281
|
|
30
30
|
debugbundle/integrations/relay_fastapi.py,sha256=-Zl6bvhYMLii__mP6-UvSdwxS5VKsb9OZvk-yy-3hEw,2227
|
|
31
31
|
debugbundle/integrations/relay_flask.py,sha256=VFHkDTJy4LkZzL_Ry_Ba-e_gW6UTG4PBwnLcvB_oXuQ,2011
|
|
32
|
-
debugbundle_python-2.0.
|
|
33
|
-
debugbundle_python-2.0.
|
|
34
|
-
debugbundle_python-2.0.
|
|
35
|
-
debugbundle_python-2.0.
|
|
36
|
-
debugbundle_python-2.0.
|
|
32
|
+
debugbundle_python-2.0.1.dist-info/licenses/LICENSE,sha256=z8d0m5b2O9McPEK1xHG_dWgUBT6EfBDz6wA0F7xSPTA,11358
|
|
33
|
+
debugbundle_python-2.0.1.dist-info/METADATA,sha256=5el7IjAErunxEkDXAZNfD8eIirCrVCgB5I4kAYBxC88,16205
|
|
34
|
+
debugbundle_python-2.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
35
|
+
debugbundle_python-2.0.1.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
|
|
36
|
+
debugbundle_python-2.0.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|