debugbundle-python 2.0.0__tar.gz → 2.0.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.
- {debugbundle_python-2.0.0/src/debugbundle_python.egg-info → debugbundle_python-2.0.1}/PKG-INFO +7 -1
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/README.md +6 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/pyproject.toml +1 -1
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/acknowledgement.py +3 -1
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/core.py +11 -9
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/event_support.py +1 -1
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/transport.py +30 -7
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1/src/debugbundle_python.egg-info}/PKG-INFO +7 -1
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle_python.egg-info/SOURCES.txt +1 -0
- debugbundle_python-2.0.1/tests/test_transport_reliability.py +144 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/LICENSE +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/setup.cfg +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/__init__.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/before_send.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/capture_hooks.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/config.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/delivery_hooks.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/exception_snapshot.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/flush_aggregates.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/relay_django.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/relay_fastapi.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/relay_flask.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/logger_integrations.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/probe_capture.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/process_support.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/queue_admission.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/redaction.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/relay.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/relay_delivery.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/request_capture_policy.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/structlog_integration.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle_python.egg-info/requires.txt +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_before_send.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_contracts.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_delivery_hooks.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_exception_snapshot.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_framework_integrations.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_hooks.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_http_integration.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_optional_imports.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_privacy_conformance.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_relay.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_relay_delivery.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_remote_config.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_repository_metadata.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_sdk.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_smoke_script.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_structlog_filtering.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_suppression.py +0 -0
- {debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/tests/test_trigger_token.py +0 -0
{debugbundle_python-2.0.0/src/debugbundle_python.egg-info → debugbundle_python-2.0.1}/PKG-INFO
RENAMED
|
@@ -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.
|
|
@@ -316,3 +316,9 @@ CI validates Ruff, mypy, pytest, package build, event schema fixtures, and cover
|
|
|
316
316
|
## License
|
|
317
317
|
|
|
318
318
|
Apache-2.0. See `LICENSE`.
|
|
319
|
+
|
|
320
|
+
## Delivery acknowledgement and retry limits
|
|
321
|
+
|
|
322
|
+
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`.
|
|
323
|
+
|
|
324
|
+
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.
|
|
@@ -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")
|
|
@@ -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:
|
|
@@ -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()
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1/src/debugbundle_python.egg-info}/PKG-INFO
RENAMED
|
@@ -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.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta, timezone
|
|
4
|
+
from email.utils import format_datetime
|
|
5
|
+
from types import SimpleNamespace
|
|
6
|
+
|
|
7
|
+
import httpx
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
from debugbundle.core import DebugBundleSdk
|
|
11
|
+
from debugbundle.transport import HttpTransport, TransportResponse
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@pytest.mark.parametrize("hint", [float("nan"), float("inf"), "1e100", True])
|
|
15
|
+
def test_invalid_custom_retry_hint_uses_safe_backoff_without_losing_events(hint: object) -> None:
|
|
16
|
+
now = 1_700_000_000.0
|
|
17
|
+
sdk = DebugBundleSdk(
|
|
18
|
+
transport=lambda _: SimpleNamespace(status_code=429, retry_after_ms=hint), time_provider=lambda: now
|
|
19
|
+
)
|
|
20
|
+
sdk.init(project_token="dbundle_proj_test", batch_size=100, flush_interval=3600)
|
|
21
|
+
try:
|
|
22
|
+
sdk.capture_message("retain", level="error")
|
|
23
|
+
sdk.flush()
|
|
24
|
+
assert sdk._retry_after == now + 1
|
|
25
|
+
assert len(sdk._buffer) == 1
|
|
26
|
+
assert sdk.last_event_at is None
|
|
27
|
+
finally:
|
|
28
|
+
sdk.dispose()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@pytest.mark.parametrize(
|
|
32
|
+
"body",
|
|
33
|
+
[
|
|
34
|
+
"",
|
|
35
|
+
'{"accepted":null,"rejected":2,"errors":[{"index":0,"reason":"rate_limited"},{"index":1,"reason":"rate_limited"}]}',
|
|
36
|
+
'{"accepted":2,"rejected":0,"errors":null}',
|
|
37
|
+
'{"accepted":1,"rejected":1,"errors":[{"index":4294967296,"reason":"rate_limited"}]}',
|
|
38
|
+
'{"accepted":1,"rejected":1,"errors":{"one":{"index":1,"reason":"rate_limited"}}}',
|
|
39
|
+
"<html>proxy</html>",
|
|
40
|
+
"{}",
|
|
41
|
+
"[]",
|
|
42
|
+
"null",
|
|
43
|
+
'{"accepted":1,"rejected":0,"errors":[]}',
|
|
44
|
+
'{"accepted":0,"rejected":2,"errors":[{"index":0,"reason":"rate_limited"},{"index":0,"reason":"rate_limited"}]}',
|
|
45
|
+
'{"accepted":1,"rejected":1,"errors":[{"index":2,"reason":"rate_limited"}]}',
|
|
46
|
+
],
|
|
47
|
+
)
|
|
48
|
+
def test_builtin_http_retains_unacknowledged_batch_and_recovers(body: str) -> None:
|
|
49
|
+
batches: list[object] = []
|
|
50
|
+
|
|
51
|
+
def respond(request: httpx.Request) -> httpx.Response:
|
|
52
|
+
import json
|
|
53
|
+
|
|
54
|
+
batches.append(json.loads(request.content)["events"])
|
|
55
|
+
return httpx.Response(
|
|
56
|
+
202,
|
|
57
|
+
text=body if len(batches) == 1 else '{"accepted":2,"rejected":0,"errors":[]}',
|
|
58
|
+
headers={"Retry-After": "300"},
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
transport = HttpTransport("https://example.invalid/events")
|
|
62
|
+
transport._client.close()
|
|
63
|
+
transport._client = httpx.Client(transport=httpx.MockTransport(respond))
|
|
64
|
+
now = [1_700_000_000.0]
|
|
65
|
+
sdk = DebugBundleSdk(transport=transport, time_provider=lambda: now[0])
|
|
66
|
+
sdk.init(project_token="dbundle_proj_test", batch_size=100, flush_interval=3600)
|
|
67
|
+
try:
|
|
68
|
+
sdk.capture_message("first", level="error")
|
|
69
|
+
sdk.capture_message("second", level="error")
|
|
70
|
+
sdk.flush()
|
|
71
|
+
assert sdk.last_event_at is None
|
|
72
|
+
assert len(sdk._buffer) == 2
|
|
73
|
+
sdk.flush()
|
|
74
|
+
assert len(batches) == 1
|
|
75
|
+
now[0] += 301
|
|
76
|
+
sdk.flush()
|
|
77
|
+
assert len(batches) == 2 and batches[0] == batches[1]
|
|
78
|
+
assert sdk.last_event_at is not None
|
|
79
|
+
finally:
|
|
80
|
+
sdk.dispose()
|
|
81
|
+
transport.close()
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize(
|
|
85
|
+
"header,expected",
|
|
86
|
+
[
|
|
87
|
+
("0.25", 250),
|
|
88
|
+
("999999", 300_000),
|
|
89
|
+
("1e300", 300_000),
|
|
90
|
+
("-1", 0),
|
|
91
|
+
("NaN", None),
|
|
92
|
+
("Infinity", None),
|
|
93
|
+
("nope", None),
|
|
94
|
+
(format_datetime(datetime.now(timezone.utc) + timedelta(days=1), usegmt=True), 300_000),
|
|
95
|
+
("Sun, 06 Nov 1994 08:49:37 GMT", 0),
|
|
96
|
+
],
|
|
97
|
+
)
|
|
98
|
+
def test_http_retry_after_is_bounded(header: str, expected: int | None) -> None:
|
|
99
|
+
transport = HttpTransport("https://example.invalid/events")
|
|
100
|
+
transport._client.close()
|
|
101
|
+
transport._client = httpx.Client(
|
|
102
|
+
transport=httpx.MockTransport(lambda _: httpx.Response(429, headers={"Retry-After": header}))
|
|
103
|
+
)
|
|
104
|
+
try:
|
|
105
|
+
assert transport({"project_token": "dbundle_proj_test", "events": []}).retry_after_ms == expected
|
|
106
|
+
finally:
|
|
107
|
+
transport.close()
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@pytest.mark.parametrize(
|
|
111
|
+
"body,status",
|
|
112
|
+
[
|
|
113
|
+
(None, 429),
|
|
114
|
+
(None, 503),
|
|
115
|
+
({}, 202),
|
|
116
|
+
({"accepted": 0, "rejected": 1, "errors": [{"index": 0, "reason": "rate_limited"}]}, 202),
|
|
117
|
+
],
|
|
118
|
+
)
|
|
119
|
+
def test_custom_retry_hint_cannot_suppress_delivery_beyond_five_minutes(body: object, status: int) -> None:
|
|
120
|
+
calls: list[object] = []
|
|
121
|
+
now = [1_700_000_000.0]
|
|
122
|
+
|
|
123
|
+
def send(request: object) -> TransportResponse:
|
|
124
|
+
calls.append(request)
|
|
125
|
+
now[0] += 10 # The delay starts when the response arrives, not when sending began.
|
|
126
|
+
return TransportResponse(status, 10**100, body)
|
|
127
|
+
|
|
128
|
+
# An empty custom ACK is explicitly legacy-compatible; use malformed counts for protocol failure.
|
|
129
|
+
if body == {}:
|
|
130
|
+
body = {"accepted": 2, "rejected": 0, "errors": []}
|
|
131
|
+
sdk = DebugBundleSdk(transport=send, time_provider=lambda: now[0])
|
|
132
|
+
sdk.init(project_token="dbundle_proj_test", batch_size=100, flush_interval=3600)
|
|
133
|
+
try:
|
|
134
|
+
sdk.capture_message("retry", level="error")
|
|
135
|
+
sdk.flush()
|
|
136
|
+
assert sdk._retry_after == now[0] + 300
|
|
137
|
+
now[0] += 299
|
|
138
|
+
sdk.flush()
|
|
139
|
+
assert len(calls) == 1
|
|
140
|
+
now[0] += 2
|
|
141
|
+
sdk.flush()
|
|
142
|
+
assert len(calls) == 2
|
|
143
|
+
finally:
|
|
144
|
+
sdk.dispose()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/__init__.py
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/common.py
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/django.py
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/relay_django.py
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/relay_fastapi.py
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/integrations/relay_flask.py
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/logger_integrations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/request_capture_policy.py
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle/structlog_integration.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{debugbundle_python-2.0.0 → debugbundle_python-2.0.1}/src/debugbundle_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|