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.
@@ -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(response.body, len(batch))
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 if response.retry_after_ms is not None else 1_000
506
- self._retry_after = now + (retry_after_ms / 1000)
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 if response.retry_after_ms is not None else 1_000
539
- self._retry_after = now + (retry_after_ms / 1000)
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 if response.retry_after_ms is not None else 1_000
549
- self._retry_after = now + (retry_after_ms / 1000)
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:
@@ -264,7 +264,7 @@ def sdk_version() -> str:
264
264
  try:
265
265
  return metadata.version("debugbundle-python")
266
266
  except metadata.PackageNotFoundError:
267
- return "2.0.0"
267
+ return "2.0.1"
268
268
 
269
269
 
270
270
  def sdk_config_endpoint(events_endpoint: str) -> str:
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
- retry_after_header = response.headers.get("retry-after")
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.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=tyI8XLRG9N3-Weo6uiGrAlf_O4tjptVe1x1cG7RjrMc,2314
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=XgRx92jSzrP6PjvHHRI0NlwdD53cRmBQ6qN4UtnQqIA,39310
6
+ debugbundle/core.py,sha256=uu2_VPvXwmccwwCcK1Y1kJgfehMOCrhFjV4L14X0cok,39467
7
7
  debugbundle/delivery_hooks.py,sha256=BUqtHuEv1MDOSLv0sUP6D582c06JofsbXhi7I7Y5IGc,2691
8
- debugbundle/event_support.py,sha256=MtPg2CO7B91U7-zE9XGro0-fynaEnJZmQ6Ms9arfRlg,9046
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=V289FwFWL6KV5gyzOsesluEMbpRobm_FHu3x8BpOYKU,2014
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.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,,
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,,