debugbundle-python 0.1.5__py3-none-any.whl → 0.1.8__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/integrations/__init__.py +21 -6
- debugbundle/integrations/relay_django.py +18 -0
- debugbundle/integrations/relay_fastapi.py +18 -0
- debugbundle/integrations/relay_flask.py +18 -0
- debugbundle/relay.py +122 -17
- debugbundle/relay_delivery.py +138 -0
- debugbundle_python-0.1.8.dist-info/METADATA +348 -0
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.8.dist-info}/RECORD +11 -10
- debugbundle_python-0.1.5.dist-info/METADATA +0 -86
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.8.dist-info}/WHEEL +0 -0
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.8.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.8.dist-info}/top_level.txt +0 -0
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import importlib
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
_OPTIONAL_EXPORTS = {
|
|
5
|
+
"DebugBundleDjangoMiddleware": (".django", "DebugBundleDjangoMiddleware"),
|
|
6
|
+
"DebugBundleFastAPIMiddleware": (".fastapi", "DebugBundleFastAPIMiddleware"),
|
|
7
|
+
"create_django_relay_view": (".relay_django", "create_django_relay_view"),
|
|
8
|
+
"create_fastapi_relay_handler": (".relay_fastapi", "create_fastapi_relay_handler"),
|
|
9
|
+
"create_flask_relay_handler": (".relay_flask", "create_flask_relay_handler"),
|
|
10
|
+
"instrument_fastapi": (".fastapi", "instrument_fastapi"),
|
|
11
|
+
"instrument_flask": (".flask", "instrument_flask"),
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def __getattr__(name: str) -> Any:
|
|
16
|
+
if name not in _OPTIONAL_EXPORTS:
|
|
17
|
+
raise AttributeError(f"module 'debugbundle.integrations' has no attribute {name!r}")
|
|
18
|
+
|
|
19
|
+
module_name, attribute_name = _OPTIONAL_EXPORTS[name]
|
|
20
|
+
module = importlib.import_module(module_name, __name__)
|
|
21
|
+
return getattr(module, attribute_name)
|
|
7
22
|
|
|
8
23
|
__all__ = [
|
|
9
24
|
"DebugBundleDjangoMiddleware",
|
|
@@ -11,12 +11,30 @@ def create_django_relay_view(
|
|
|
11
11
|
max_body_bytes: int = 262_144,
|
|
12
12
|
rate_limit_per_minute: int = 60,
|
|
13
13
|
on_accept: Any = None,
|
|
14
|
+
project_mode: str | None = None,
|
|
15
|
+
project_token: str | None = None,
|
|
16
|
+
endpoint: str | None = None,
|
|
17
|
+
local_events_dir: str | None = None,
|
|
18
|
+
spool_dir: str | None = None,
|
|
19
|
+
durable_write: bool = True,
|
|
20
|
+
service: str | None = None,
|
|
21
|
+
environment: str | None = None,
|
|
22
|
+
forward_transport: Any = None,
|
|
14
23
|
) -> Any:
|
|
15
24
|
handler = BrowserRelayHandler(
|
|
16
25
|
allowed_origins=allowed_origins or [],
|
|
17
26
|
max_body_bytes=max_body_bytes,
|
|
18
27
|
rate_limit_per_minute=rate_limit_per_minute,
|
|
19
28
|
on_accept=on_accept,
|
|
29
|
+
project_mode=project_mode,
|
|
30
|
+
project_token=project_token,
|
|
31
|
+
endpoint=endpoint,
|
|
32
|
+
local_events_dir=local_events_dir,
|
|
33
|
+
spool_dir=spool_dir,
|
|
34
|
+
durable_write=durable_write,
|
|
35
|
+
service=service,
|
|
36
|
+
environment=environment,
|
|
37
|
+
forward_transport=forward_transport,
|
|
20
38
|
)
|
|
21
39
|
|
|
22
40
|
def view(request: Any) -> Any:
|
|
@@ -14,6 +14,15 @@ def create_fastapi_relay_handler(
|
|
|
14
14
|
max_body_bytes: int = 262_144,
|
|
15
15
|
rate_limit_per_minute: int = 60,
|
|
16
16
|
on_accept: Any = None,
|
|
17
|
+
project_mode: str | None = None,
|
|
18
|
+
project_token: str | None = None,
|
|
19
|
+
endpoint: str | None = None,
|
|
20
|
+
local_events_dir: str | None = None,
|
|
21
|
+
spool_dir: str | None = None,
|
|
22
|
+
durable_write: bool = True,
|
|
23
|
+
service: str | None = None,
|
|
24
|
+
environment: str | None = None,
|
|
25
|
+
forward_transport: Any = None,
|
|
17
26
|
route_path: str = "/debugbundle/browser",
|
|
18
27
|
) -> Any:
|
|
19
28
|
handler = BrowserRelayHandler(
|
|
@@ -21,6 +30,15 @@ def create_fastapi_relay_handler(
|
|
|
21
30
|
max_body_bytes=max_body_bytes,
|
|
22
31
|
rate_limit_per_minute=rate_limit_per_minute,
|
|
23
32
|
on_accept=on_accept,
|
|
33
|
+
project_mode=project_mode,
|
|
34
|
+
project_token=project_token,
|
|
35
|
+
endpoint=endpoint,
|
|
36
|
+
local_events_dir=local_events_dir,
|
|
37
|
+
spool_dir=spool_dir,
|
|
38
|
+
durable_write=durable_write,
|
|
39
|
+
service=service,
|
|
40
|
+
environment=environment,
|
|
41
|
+
forward_transport=forward_transport,
|
|
24
42
|
)
|
|
25
43
|
|
|
26
44
|
def register(app: Any) -> None:
|
|
@@ -11,6 +11,15 @@ def create_flask_relay_handler(
|
|
|
11
11
|
max_body_bytes: int = 262_144,
|
|
12
12
|
rate_limit_per_minute: int = 60,
|
|
13
13
|
on_accept: Any = None,
|
|
14
|
+
project_mode: str | None = None,
|
|
15
|
+
project_token: str | None = None,
|
|
16
|
+
endpoint: str | None = None,
|
|
17
|
+
local_events_dir: str | None = None,
|
|
18
|
+
spool_dir: str | None = None,
|
|
19
|
+
durable_write: bool = True,
|
|
20
|
+
service: str | None = None,
|
|
21
|
+
environment: str | None = None,
|
|
22
|
+
forward_transport: Any = None,
|
|
14
23
|
route_path: str = "/debugbundle/browser",
|
|
15
24
|
) -> Any:
|
|
16
25
|
handler = BrowserRelayHandler(
|
|
@@ -18,6 +27,15 @@ def create_flask_relay_handler(
|
|
|
18
27
|
max_body_bytes=max_body_bytes,
|
|
19
28
|
rate_limit_per_minute=rate_limit_per_minute,
|
|
20
29
|
on_accept=on_accept,
|
|
30
|
+
project_mode=project_mode,
|
|
31
|
+
project_token=project_token,
|
|
32
|
+
endpoint=endpoint,
|
|
33
|
+
local_events_dir=local_events_dir,
|
|
34
|
+
spool_dir=spool_dir,
|
|
35
|
+
durable_write=durable_write,
|
|
36
|
+
service=service,
|
|
37
|
+
environment=environment,
|
|
38
|
+
forward_transport=forward_transport,
|
|
21
39
|
)
|
|
22
40
|
|
|
23
41
|
def register(app: Any) -> None:
|
debugbundle/relay.py
CHANGED
|
@@ -2,10 +2,18 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import json
|
|
4
4
|
import time
|
|
5
|
-
from collections.abc import Callable
|
|
5
|
+
from collections.abc import Callable, Mapping
|
|
6
6
|
from dataclasses import dataclass, field
|
|
7
7
|
from typing import Any
|
|
8
8
|
|
|
9
|
+
from .relay_delivery import (
|
|
10
|
+
AtomicRelayFileTransport,
|
|
11
|
+
RelayForwardTransport,
|
|
12
|
+
mark_spool_file_delivered,
|
|
13
|
+
resolve_default_local_events_dir,
|
|
14
|
+
resolve_default_relay_spool_dir,
|
|
15
|
+
)
|
|
16
|
+
|
|
9
17
|
DEFAULT_MAX_BODY_BYTES = 262_144
|
|
10
18
|
DEFAULT_RATE_LIMIT_PER_MINUTE = 60
|
|
11
19
|
BROWSER_SDK_NAME = "@debugbundle/sdk-browser"
|
|
@@ -41,12 +49,32 @@ class BrowserRelayHandler:
|
|
|
41
49
|
max_body_bytes: int = DEFAULT_MAX_BODY_BYTES
|
|
42
50
|
rate_limit_per_minute: int = DEFAULT_RATE_LIMIT_PER_MINUTE
|
|
43
51
|
on_accept: Callable[[BrowserRelayAcceptedBatch], None] | None = None
|
|
52
|
+
project_mode: str | None = None
|
|
53
|
+
project_token: str | None = None
|
|
54
|
+
endpoint: str | None = None
|
|
55
|
+
local_events_dir: str | None = None
|
|
56
|
+
spool_dir: str | None = None
|
|
57
|
+
durable_write: bool = True
|
|
58
|
+
service: str | None = None
|
|
59
|
+
environment: str | None = None
|
|
60
|
+
forward_transport: Callable[[Mapping[str, object]], object] | None = None
|
|
44
61
|
|
|
45
62
|
def __post_init__(self) -> None:
|
|
46
63
|
self.allowed_origins = [o for o in self.allowed_origins if o]
|
|
47
64
|
self.max_body_bytes = max(1, self.max_body_bytes)
|
|
48
65
|
self.rate_limit_per_minute = max(1, self.rate_limit_per_minute)
|
|
66
|
+
normalized_project_mode = (self.project_mode or "").strip().lower()
|
|
67
|
+
self.project_mode = normalized_project_mode or None
|
|
68
|
+
if self.project_mode not in {None, "connected", "local-only"}:
|
|
69
|
+
self.project_mode = None
|
|
49
70
|
self._rate_limit_state: dict[str, list[int]] = {}
|
|
71
|
+
self._local_transports: dict[str, AtomicRelayFileTransport] = {}
|
|
72
|
+
self._spool_transports: dict[str, AtomicRelayFileTransport] = {}
|
|
73
|
+
self._forwarder = (
|
|
74
|
+
RelayForwardTransport(self.endpoint, self.forward_transport)
|
|
75
|
+
if self.project_mode == "connected" and self.endpoint is not None
|
|
76
|
+
else None
|
|
77
|
+
)
|
|
50
78
|
|
|
51
79
|
def handle(self, request: dict[str, Any]) -> BrowserRelayResponse:
|
|
52
80
|
method = str(request.get("method", "POST")).upper()
|
|
@@ -106,22 +134,29 @@ class BrowserRelayHandler:
|
|
|
106
134
|
errors.append(f"batch[{index}]: Unsupported browser relay event type {type_label}.")
|
|
107
135
|
continue
|
|
108
136
|
|
|
109
|
-
sanitized = _sanitize_event(candidate)
|
|
137
|
+
sanitized = _sanitize_event(candidate, service_override=self.service, environment_override=self.environment)
|
|
110
138
|
if sanitized is None:
|
|
111
139
|
errors.append(f"batch[{index}]: Invalid browser relay event payload.")
|
|
112
140
|
continue
|
|
113
141
|
|
|
114
142
|
accepted_events.append(sanitized)
|
|
115
143
|
|
|
116
|
-
if accepted_events
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
144
|
+
if accepted_events:
|
|
145
|
+
try:
|
|
146
|
+
if not self._deliver_events(accepted_events):
|
|
147
|
+
return BrowserRelayResponse(500)
|
|
148
|
+
|
|
149
|
+
if self.on_accept is not None:
|
|
150
|
+
self.on_accept(
|
|
151
|
+
BrowserRelayAcceptedBatch(
|
|
152
|
+
events=accepted_events,
|
|
153
|
+
headers=_strip_sensitive_headers(headers),
|
|
154
|
+
ip_address=ip_address,
|
|
155
|
+
received_at=time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
except Exception:
|
|
159
|
+
return BrowserRelayResponse(500)
|
|
125
160
|
|
|
126
161
|
if errors:
|
|
127
162
|
return BrowserRelayResponse(
|
|
@@ -134,6 +169,54 @@ class BrowserRelayHandler:
|
|
|
134
169
|
{"accepted": len(accepted_events), "rejected": 0, "errors": []},
|
|
135
170
|
)
|
|
136
171
|
|
|
172
|
+
def _deliver_events(self, accepted_events: list[dict[str, Any]]) -> bool:
|
|
173
|
+
if self.project_mode is None:
|
|
174
|
+
return True
|
|
175
|
+
|
|
176
|
+
service_name = self.service or str(accepted_events[0]["service"]["name"])
|
|
177
|
+
|
|
178
|
+
if self.project_mode == "local-only":
|
|
179
|
+
local_transport = self._local_transports.get(service_name)
|
|
180
|
+
if local_transport is None:
|
|
181
|
+
local_transport = AtomicRelayFileTransport(
|
|
182
|
+
self.local_events_dir or resolve_default_local_events_dir(),
|
|
183
|
+
service_name,
|
|
184
|
+
)
|
|
185
|
+
self._local_transports[service_name] = local_transport
|
|
186
|
+
|
|
187
|
+
return local_transport.write(accepted_events).status_code == 202
|
|
188
|
+
|
|
189
|
+
if self.project_mode != "connected":
|
|
190
|
+
return True
|
|
191
|
+
|
|
192
|
+
if self.durable_write:
|
|
193
|
+
spool_transport = self._spool_transports.get(service_name)
|
|
194
|
+
if spool_transport is None:
|
|
195
|
+
spool_transport = AtomicRelayFileTransport(
|
|
196
|
+
self.spool_dir or resolve_default_relay_spool_dir(),
|
|
197
|
+
service_name,
|
|
198
|
+
)
|
|
199
|
+
self._spool_transports[service_name] = spool_transport
|
|
200
|
+
|
|
201
|
+
spool_write_result = spool_transport.write(accepted_events)
|
|
202
|
+
if spool_write_result.status_code != 202:
|
|
203
|
+
return False
|
|
204
|
+
|
|
205
|
+
configured, succeeded = self._forward_connected_events(accepted_events)
|
|
206
|
+
if succeeded and spool_write_result.written_file_path is not None:
|
|
207
|
+
mark_spool_file_delivered(spool_write_result.written_file_path)
|
|
208
|
+
|
|
209
|
+
return True if configured or spool_write_result.written_file_path is not None else False
|
|
210
|
+
|
|
211
|
+
configured, succeeded = self._forward_connected_events(accepted_events)
|
|
212
|
+
return configured and succeeded
|
|
213
|
+
|
|
214
|
+
def _forward_connected_events(self, accepted_events: list[dict[str, Any]]) -> tuple[bool, bool]:
|
|
215
|
+
if self._forwarder is None or not self.project_token:
|
|
216
|
+
return (False, False)
|
|
217
|
+
|
|
218
|
+
return self._forwarder.send(self.project_token, accepted_events)
|
|
219
|
+
|
|
137
220
|
def _is_origin_allowed(self, headers: dict[str, str]) -> bool:
|
|
138
221
|
origin = _source_origin(headers)
|
|
139
222
|
if origin is None:
|
|
@@ -210,7 +293,11 @@ def _strip_sensitive_headers(headers: dict[str, str]) -> dict[str, str]:
|
|
|
210
293
|
return sanitized
|
|
211
294
|
|
|
212
295
|
|
|
213
|
-
def _sanitize_event(
|
|
296
|
+
def _sanitize_event(
|
|
297
|
+
event: dict[str, Any],
|
|
298
|
+
service_override: str | None = None,
|
|
299
|
+
environment_override: str | None = None,
|
|
300
|
+
) -> dict[str, Any] | None:
|
|
214
301
|
schema_version = event.get("schema_version")
|
|
215
302
|
event_id = event.get("event_id")
|
|
216
303
|
event_type = event.get("event_type")
|
|
@@ -239,6 +326,11 @@ def _sanitize_event(event: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
239
326
|
if not isinstance(service_name, str) or not service_name or not isinstance(environment, str) or not environment:
|
|
240
327
|
return None
|
|
241
328
|
|
|
329
|
+
normalized_service_name = service_override or service_name
|
|
330
|
+
normalized_environment = environment_override or environment
|
|
331
|
+
if not normalized_service_name or not normalized_environment:
|
|
332
|
+
return None
|
|
333
|
+
|
|
242
334
|
sanitized: dict[str, Any] = {
|
|
243
335
|
"schema_version": schema_version,
|
|
244
336
|
"event_id": event_id,
|
|
@@ -247,16 +339,29 @@ def _sanitize_event(event: dict[str, Any]) -> dict[str, Any] | None:
|
|
|
247
339
|
"sdk_name": BROWSER_SDK_NAME,
|
|
248
340
|
"sdk_version": sdk_version,
|
|
249
341
|
"service": {
|
|
250
|
-
"name":
|
|
251
|
-
"environment":
|
|
342
|
+
"name": normalized_service_name,
|
|
343
|
+
"environment": normalized_environment,
|
|
252
344
|
},
|
|
253
345
|
"payload": payload,
|
|
254
346
|
}
|
|
255
347
|
|
|
348
|
+
runtime = service.get("runtime")
|
|
349
|
+
if isinstance(runtime, str) or runtime is None:
|
|
350
|
+
sanitized["service"]["runtime"] = runtime
|
|
351
|
+
|
|
352
|
+
framework = service.get("framework")
|
|
353
|
+
if isinstance(framework, str) or framework is None:
|
|
354
|
+
sanitized["service"]["framework"] = framework
|
|
355
|
+
|
|
256
356
|
correlation = event.get("correlation")
|
|
257
357
|
if isinstance(correlation, dict):
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
358
|
+
normalized_correlation: dict[str, Any] = {}
|
|
359
|
+
for key in ("request_id", "trace_id", "session_id", "user_id_hash"):
|
|
360
|
+
value = correlation.get(key)
|
|
361
|
+
if isinstance(value, str) or value is None:
|
|
362
|
+
normalized_correlation[key] = value
|
|
363
|
+
|
|
364
|
+
if normalized_correlation:
|
|
365
|
+
sanitized["correlation"] = normalized_correlation
|
|
261
366
|
|
|
262
367
|
return sanitized
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import secrets
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable, Mapping
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .transport import HttpTransport, coerce_transport_response
|
|
14
|
+
|
|
15
|
+
LOCAL_EVENTS_DIRECTORY_MODE = 0o700
|
|
16
|
+
LOCAL_EVENT_FILE_MODE = 0o600
|
|
17
|
+
RELAY_SPOOL_DELIVERED_MARKER_SUFFIX = ".delivered"
|
|
18
|
+
OPTIONAL_NOFOLLOW_FLAG = getattr(os, "O_NOFOLLOW", 0)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RelayWriteResult:
|
|
23
|
+
status_code: int
|
|
24
|
+
written_file_path: str | None = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def resolve_default_local_events_dir(cwd: str | None = None) -> str:
|
|
28
|
+
return os.path.join(cwd or os.getcwd(), ".debugbundle", "local", "events")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def resolve_default_relay_spool_dir(cwd: str | None = None) -> str:
|
|
32
|
+
return os.path.join(cwd or os.getcwd(), ".debugbundle", "local", "browser-relay-spool")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def attach_project_token(events: list[dict[str, Any]], project_token: str) -> list[dict[str, Any]]:
|
|
36
|
+
return [{**event, "project_token": project_token} for event in events]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def mark_spool_file_delivered(written_file_path: str) -> None:
|
|
40
|
+
try:
|
|
41
|
+
with open(f"{written_file_path}{RELAY_SPOOL_DELIVERED_MARKER_SUFFIX}", "w", encoding="utf-8"):
|
|
42
|
+
pass
|
|
43
|
+
except OSError:
|
|
44
|
+
# Durable acceptance already happened at the spool write; marker creation is maintenance metadata only.
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class AtomicRelayFileTransport:
|
|
49
|
+
def __init__(self, events_dir: str, service_name: str) -> None:
|
|
50
|
+
self._events_dir = os.path.abspath(os.path.normpath(events_dir))
|
|
51
|
+
self._service_name = _sanitize_service_name(service_name)
|
|
52
|
+
self._sequence = 0
|
|
53
|
+
self._dir_ensured = False
|
|
54
|
+
self._lock = threading.Lock()
|
|
55
|
+
|
|
56
|
+
def write(self, events: list[dict[str, Any]]) -> RelayWriteResult:
|
|
57
|
+
if not events:
|
|
58
|
+
return RelayWriteResult(status_code=202)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
with self._lock:
|
|
62
|
+
if not self._dir_ensured:
|
|
63
|
+
os.makedirs(self._events_dir, mode=LOCAL_EVENTS_DIRECTORY_MODE, exist_ok=True)
|
|
64
|
+
self._dir_ensured = True
|
|
65
|
+
|
|
66
|
+
timestamp = int(time.time() * 1000)
|
|
67
|
+
self._sequence += 1
|
|
68
|
+
filename = f"{timestamp}-{self._sequence}-{self._service_name}.events.json"
|
|
69
|
+
final_path = os.path.join(self._events_dir, filename)
|
|
70
|
+
tmp_path = f"{final_path}.tmp-{secrets.token_hex(8)}"
|
|
71
|
+
|
|
72
|
+
_assert_not_symlink(final_path)
|
|
73
|
+
_write_secure_temp_file(tmp_path, json.dumps(events, separators=(",", ":")))
|
|
74
|
+
os.replace(tmp_path, final_path)
|
|
75
|
+
return RelayWriteResult(status_code=202, written_file_path=final_path)
|
|
76
|
+
except OSError:
|
|
77
|
+
_cleanup_temp_files(self._events_dir)
|
|
78
|
+
return RelayWriteResult(status_code=500)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class RelayForwardTransport:
|
|
82
|
+
def __init__(self, endpoint: str, transport: Callable[[Mapping[str, object]], object] | None = None) -> None:
|
|
83
|
+
self._transport = transport or HttpTransport(endpoint)
|
|
84
|
+
|
|
85
|
+
def send(self, project_token: str, events: list[dict[str, Any]]) -> tuple[bool, bool]:
|
|
86
|
+
if not project_token:
|
|
87
|
+
return (False, False)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
response = coerce_transport_response(
|
|
91
|
+
self._transport(
|
|
92
|
+
{
|
|
93
|
+
"project_token": project_token,
|
|
94
|
+
"events": attach_project_token(events, project_token),
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
except Exception:
|
|
99
|
+
return (True, False)
|
|
100
|
+
|
|
101
|
+
return (True, 200 <= response.status_code < 300)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _sanitize_service_name(service_name: str) -> str:
|
|
105
|
+
normalized = re.sub(r"[^A-Za-z0-9._-]+", "-", service_name.strip())
|
|
106
|
+
normalized = re.sub(r"-+", "-", normalized).strip("-")
|
|
107
|
+
return normalized or "service"
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _assert_not_symlink(target_path: str) -> None:
|
|
111
|
+
try:
|
|
112
|
+
if os.path.islink(target_path):
|
|
113
|
+
raise OSError("symlink_path_rejected")
|
|
114
|
+
except OSError:
|
|
115
|
+
raise
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _write_secure_temp_file(tmp_path: str, payload: str) -> None:
|
|
119
|
+
flags = os.O_CREAT | os.O_EXCL | os.O_WRONLY | OPTIONAL_NOFOLLOW_FLAG
|
|
120
|
+
encoded = payload.encode("utf-8")
|
|
121
|
+
fd = os.open(tmp_path, flags, LOCAL_EVENT_FILE_MODE)
|
|
122
|
+
try:
|
|
123
|
+
os.write(fd, encoded)
|
|
124
|
+
finally:
|
|
125
|
+
os.close(fd)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _cleanup_temp_files(events_dir: str) -> None:
|
|
129
|
+
try:
|
|
130
|
+
for entry in os.listdir(events_dir):
|
|
131
|
+
if ".tmp-" not in entry:
|
|
132
|
+
continue
|
|
133
|
+
try:
|
|
134
|
+
os.remove(os.path.join(events_dir, entry))
|
|
135
|
+
except OSError:
|
|
136
|
+
continue
|
|
137
|
+
except OSError:
|
|
138
|
+
return
|
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: debugbundle-python
|
|
3
|
+
Version: 0.1.8
|
|
4
|
+
Summary: DebugBundle SDK for Python
|
|
5
|
+
Author: DebugBundle
|
|
6
|
+
License-Expression: AGPL-3.0-only
|
|
7
|
+
Project-URL: Homepage, https://debugbundle.com/docs/sdks/python
|
|
8
|
+
Project-URL: Repository, https://github.com/debugbundle/debugbundle-python
|
|
9
|
+
Project-URL: Issues, https://github.com/debugbundle/debugbundle-python/issues
|
|
10
|
+
Keywords: debugbundle,debugging,ai-agent,error-tracking
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Framework :: Django
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Framework :: Flask
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: httpx<0.29,>=0.27
|
|
25
|
+
Provides-Extra: dev
|
|
26
|
+
Requires-Dist: build<2,>=1; extra == "dev"
|
|
27
|
+
Requires-Dist: django<6,>=5; extra == "dev"
|
|
28
|
+
Requires-Dist: fastapi<1,>=0.115; extra == "dev"
|
|
29
|
+
Requires-Dist: flask<4,>=3; extra == "dev"
|
|
30
|
+
Requires-Dist: jsonschema<5,>=4.23; extra == "dev"
|
|
31
|
+
Requires-Dist: loguru<1,>=0.7; extra == "dev"
|
|
32
|
+
Requires-Dist: mypy<2,>=1.15; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest<9,>=8.3; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov<7,>=5; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff<0.12,>=0.11; extra == "dev"
|
|
36
|
+
Requires-Dist: structlog<26,>=24; extra == "dev"
|
|
37
|
+
Requires-Dist: twine<7,>=5; extra == "dev"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# debugbundle-python
|
|
41
|
+
|
|
42
|
+
Python SDK for DebugBundle.
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+

|
|
46
|
+

|
|
47
|
+
|
|
48
|
+
Use this package to capture Python backend exceptions, request metadata, structured logs, runtime context, and probe data. It supports vanilla Python plus Django, Flask, FastAPI, Python logging, structlog, loguru, and browser relay helpers.
|
|
49
|
+
|
|
50
|
+
Requires Python 3.10 or newer.
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install debugbundle-python
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Install the SDK alongside the framework you actually run:
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install debugbundle-python django
|
|
62
|
+
pip install debugbundle-python flask
|
|
63
|
+
pip install debugbundle-python fastapi uvicorn
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
For local development:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
pip install -e ".[dev]"
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Quick Start
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
import os
|
|
76
|
+
import debugbundle
|
|
77
|
+
|
|
78
|
+
debugbundle.init(
|
|
79
|
+
project_token=os.environ["DEBUGBUNDLE_PROJECT_TOKEN"],
|
|
80
|
+
service="checkout-api",
|
|
81
|
+
environment="production",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
debugbundle.capture_exceptions()
|
|
85
|
+
debugbundle.capture_logging()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Capture handled errors, logs, messages, and probes explicitly:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
debugbundle.capture_exception(error)
|
|
92
|
+
debugbundle.capture_log("payment retry failed", level="warning", context={"order_id": order_id})
|
|
93
|
+
debugbundle.capture_message("worker started")
|
|
94
|
+
debugbundle.probe("checkout.cart", {"item_count": len(cart.items)})
|
|
95
|
+
|
|
96
|
+
debugbundle.flush()
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Framework Integrations
|
|
100
|
+
|
|
101
|
+
| Framework | Integration |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| Django | `DebugBundleDjangoMiddleware` |
|
|
104
|
+
| Flask | `instrument_flask(app)` |
|
|
105
|
+
| FastAPI | `DebugBundleFastAPIMiddleware` or `instrument_fastapi(app)` |
|
|
106
|
+
| Python logging | `capture_logging()` |
|
|
107
|
+
| asyncio | `capture_async()` |
|
|
108
|
+
| structlog/loguru | Auto-detected when log capture is enabled and the libraries are installed |
|
|
109
|
+
|
|
110
|
+
## Browser Relay
|
|
111
|
+
|
|
112
|
+
Python backends can host the browser relay endpoint used by `@debugbundle/sdk-browser`.
|
|
113
|
+
|
|
114
|
+
| Framework | Helper |
|
|
115
|
+
| --- | --- |
|
|
116
|
+
| Django | `create_django_relay_view()` |
|
|
117
|
+
| Flask | `create_flask_relay_handler()` |
|
|
118
|
+
| FastAPI | `create_fastapi_relay_handler()` |
|
|
119
|
+
|
|
120
|
+
The relay validates JSON batches, enforces same-origin or allowed origins, strips trust-sensitive browser fields, keeps the server-side project token private, and supports both local-only file writes and connected forwarding.
|
|
121
|
+
|
|
122
|
+
Relay defaults and limits:
|
|
123
|
+
|
|
124
|
+
- Same-origin requests are allowed by default when `allowed_origins` is omitted.
|
|
125
|
+
- Split frontend/backend deployments should set explicit `allowed_origins` values.
|
|
126
|
+
- Relay requests must use `Content-Type: application/json` and stay below `max_body_bytes` (default `262144`).
|
|
127
|
+
- Relay rate limiting defaults to `60` requests per IP per minute.
|
|
128
|
+
- `project_mode="local-only"` writes accepted browser events to `.debugbundle/local/events` or your configured `local_events_dir`.
|
|
129
|
+
- `project_mode="connected"` writes durable spool files by default and forwards with the server-side `project_token` only.
|
|
130
|
+
- Leaving relay `project_mode` unset disables local writes and forwarding; accepted batches are only surfaced through `on_accept`.
|
|
131
|
+
- Connected relay mode without a usable `project_token` keeps accepting and optionally spooling events, but forwarding remains disabled until the server provides credentials.
|
|
132
|
+
|
|
133
|
+
## Configuration Reference
|
|
134
|
+
|
|
135
|
+
Configuration sources and precedence:
|
|
136
|
+
|
|
137
|
+
- The SDK only reads the keyword arguments passed to `debugbundle.init(...)`.
|
|
138
|
+
- Environment variables, Django settings, Flask config, or FastAPI settings are convenience sources that your application maps into `debugbundle.init(...)`; the SDK does not read them directly.
|
|
139
|
+
- Explicit `debugbundle.init(...)` arguments always win because they are the only configuration source the runtime consumes.
|
|
140
|
+
- Capture-policy fields are server-owned and are not accepted in local SDK config. The SDK learns capture policy through `GET /v1/sdk/config` and applies it locally before transport.
|
|
141
|
+
|
|
142
|
+
| Option | Default | Purpose |
|
|
143
|
+
| --- | --- | --- |
|
|
144
|
+
| `project_token` | required for connected capture | Write-only DebugBundle project token. Blank or missing tokens disable connected capture and leave the SDK status at `disconnected`. |
|
|
145
|
+
| `service` | auto/default service | Service name shown on incidents and bundles. |
|
|
146
|
+
| `environment` | `development` | Runtime environment such as `production`, `staging`, or `development`. |
|
|
147
|
+
| `endpoint` | `https://api.debugbundle.com/v1/events` | Ingestion endpoint for connected mode or self-hosting. |
|
|
148
|
+
| `enabled` | `True` | Disable all capture without removing instrumentation. |
|
|
149
|
+
| `log_level` | `warning` | Minimum captured log severity. |
|
|
150
|
+
| `sample_rate` | `1.0` | Fraction of events to keep before transport. |
|
|
151
|
+
| `batch_size` | `25` | Events per batch before flushing. |
|
|
152
|
+
| `flush_interval` | `5.0` | Flush interval in seconds. |
|
|
153
|
+
| `redact_fields` | common sensitive fields | Additional field names to redact. |
|
|
154
|
+
| `max_probe_labels` | `50` | Maximum distinct probe labels buffered in memory. |
|
|
155
|
+
| `max_probe_entries_per_label` | `10` | Maximum entries retained per probe label. |
|
|
156
|
+
| `probe_flush_on_error` | `True` | Attach buffered probe data to captured exceptions. |
|
|
157
|
+
| `probes_poll_interval` | `60000` | Remote probe config poll interval in milliseconds. |
|
|
158
|
+
| `fetch_impl` | internal HTTP fetch | Custom remote-config fetch function for tests or advanced routing. |
|
|
159
|
+
| `on_diagnostic` | none | Callback for SDK internal diagnostics. |
|
|
160
|
+
|
|
161
|
+
Framework-native wiring:
|
|
162
|
+
|
|
163
|
+
- Django: initialize the SDK during startup, then add `DebugBundleDjangoMiddleware` to `MIDDLEWARE`.
|
|
164
|
+
- Flask: initialize the SDK during app creation, then call `instrument_flask(app)`.
|
|
165
|
+
- FastAPI: initialize the SDK during startup, then add `DebugBundleFastAPIMiddleware` or call `instrument_fastapi(app)`.
|
|
166
|
+
- There is no separate package-manager plugin, settings loader, or framework-only config surface in V1.
|
|
167
|
+
|
|
168
|
+
## Install Examples by Mode
|
|
169
|
+
|
|
170
|
+
Vanilla Python:
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import os
|
|
174
|
+
import debugbundle
|
|
175
|
+
|
|
176
|
+
debugbundle.init(
|
|
177
|
+
project_token=os.environ["DEBUGBUNDLE_PROJECT_TOKEN"],
|
|
178
|
+
service="worker",
|
|
179
|
+
environment="production",
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
debugbundle.capture_exceptions()
|
|
183
|
+
debugbundle.capture_logging()
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Flask:
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
import os
|
|
190
|
+
from flask import Flask
|
|
191
|
+
import debugbundle
|
|
192
|
+
|
|
193
|
+
app = Flask(__name__)
|
|
194
|
+
debugbundle.init(
|
|
195
|
+
project_token=os.environ["DEBUGBUNDLE_PROJECT_TOKEN"],
|
|
196
|
+
service="checkout-api",
|
|
197
|
+
environment="production",
|
|
198
|
+
)
|
|
199
|
+
debugbundle.instrument_flask(app)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
FastAPI:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
import os
|
|
206
|
+
from fastapi import FastAPI
|
|
207
|
+
import debugbundle
|
|
208
|
+
|
|
209
|
+
app = FastAPI()
|
|
210
|
+
debugbundle.init(
|
|
211
|
+
project_token=os.environ["DEBUGBUNDLE_PROJECT_TOKEN"],
|
|
212
|
+
service="checkout-api",
|
|
213
|
+
environment="production",
|
|
214
|
+
)
|
|
215
|
+
debugbundle.instrument_fastapi(app)
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Logger integration:
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
import logging
|
|
222
|
+
import debugbundle
|
|
223
|
+
|
|
224
|
+
debugbundle.capture_logging(logging.getLogger("checkout"))
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
Connected browser relay:
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from flask import Flask
|
|
231
|
+
import debugbundle
|
|
232
|
+
|
|
233
|
+
app = Flask(__name__)
|
|
234
|
+
debugbundle.create_flask_relay_handler(
|
|
235
|
+
allowed_origins=["https://app.example.com"],
|
|
236
|
+
project_mode="connected",
|
|
237
|
+
project_token="dbundle_proj_...",
|
|
238
|
+
endpoint="https://api.debugbundle.com/v1/events",
|
|
239
|
+
)(app)
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
Local-only browser relay:
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
from flask import Flask
|
|
246
|
+
import debugbundle
|
|
247
|
+
|
|
248
|
+
app = Flask(__name__)
|
|
249
|
+
debugbundle.create_flask_relay_handler(
|
|
250
|
+
allowed_origins=["http://localhost:3000"],
|
|
251
|
+
project_mode="local-only",
|
|
252
|
+
local_events_dir=".debugbundle/local/events",
|
|
253
|
+
)(app)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
There is no zero-install fallback for the Python SDK itself in V1. The nearest low-friction path is the browser relay mounted on an existing Python web app.
|
|
257
|
+
|
|
258
|
+
## Runtime and Framework Support
|
|
259
|
+
|
|
260
|
+
| Surface | Minimum compatibility version | Recommended production version | Installed-base compatibility lane | Rolling CI lane | Out of scope |
|
|
261
|
+
| --- | --- | --- | --- | --- | --- |
|
|
262
|
+
| Python runtime | 3.10 | 3.12 | 3.10 and 3.11 remain supported for installed-base coverage | 3.12 | 3.9 and older |
|
|
263
|
+
| Django | 5.x | latest 5.x patch | 5.x compatibility support | repo release smoke and tests install Django 5.x | Django 4.x and older |
|
|
264
|
+
| Flask | 3.x | latest 3.x patch | 3.x compatibility support | repo release smoke installs Flask 3.x | Flask 2.x and older |
|
|
265
|
+
| FastAPI | 0.115+ | latest 0.115+ patch line | 0.115+ compatibility support | repo tests install FastAPI 0.115+ | standalone Starlette, older FastAPI lines |
|
|
266
|
+
|
|
267
|
+
Post-V1 planned expansions from `spec/sdk-language-targets.md` remain out of scope here: Celery, RQ, Dramatiq, standalone Starlette, Gunicorn/Uvicorn server hooks, and AWS Lambda Python.
|
|
268
|
+
|
|
269
|
+
## Dependency Alignment
|
|
270
|
+
|
|
271
|
+
`debugbundle-python` ships as one package in V1, so there is no multi-package version-alignment step like a BOM or plugin family lock.
|
|
272
|
+
|
|
273
|
+
- Pin one `debugbundle-python` version across your service and worker repos when you want identical SDK behavior everywhere.
|
|
274
|
+
- Keep framework dependencies inside the supported lanes above: Django 5.x, Flask 3.x, and FastAPI 0.115+.
|
|
275
|
+
- The packaged HTTP client dependency is `httpx>=0.27,<0.29`; if you override transport behavior in tests or wrappers, stay inside that range unless you retest the SDK.
|
|
276
|
+
|
|
277
|
+
## Safety Defaults
|
|
278
|
+
|
|
279
|
+
- SDK failures are caught internally and do not crash the host process.
|
|
280
|
+
- Sensitive fields are redacted before transport.
|
|
281
|
+
- Duplicate event storms are suppressed locally.
|
|
282
|
+
- Runtime context excludes environment variables.
|
|
283
|
+
- Browser relay requests cannot smuggle server-side credentials.
|
|
284
|
+
|
|
285
|
+
## Service Naming
|
|
286
|
+
|
|
287
|
+
- Use one stable backend service name per deployable, such as `checkout-api`, `billing-worker`, or `admin-api`.
|
|
288
|
+
- Keep browser relay traffic on the browser-owned service name by default, for example `checkout-web`; the Python relay preserves the browser service unless you explicitly override `service=` or `environment=` on the relay helper.
|
|
289
|
+
- When multiple Python deployables share one DebugBundle project, give each deployable its own `service` value instead of reusing one generic name.
|
|
290
|
+
- Reuse the same environment label across related surfaces, for example `production` on both `checkout-web` and `checkout-api`, so incident and bundle correlation stays readable.
|
|
291
|
+
|
|
292
|
+
## Safe Startup and Status
|
|
293
|
+
|
|
294
|
+
- The SDK never crashes the host process when configuration is invalid, transport calls fail, or remote config responses are malformed.
|
|
295
|
+
- `debugbundle.init(project_token="")` or any missing/blank connected token leaves capture disabled and `debugbundle.get_status()` returns `disconnected`.
|
|
296
|
+
- Rate-limited transports move the status to `degraded` until the retry window expires.
|
|
297
|
+
- Three consecutive transport failures also move the status to `disconnected` until a later successful flush.
|
|
298
|
+
- `debugbundle.get_last_event_at()` returns the Unix timestamp of the last successful delivery, or `None` before the first success.
|
|
299
|
+
|
|
300
|
+
## First-Event Verification
|
|
301
|
+
|
|
302
|
+
Use the repo-local smoke target to prove a fresh install end to end against a mock ingestion endpoint:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
make smoke
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
That command builds the wheel, installs it into a fresh virtualenv, runs a Flask app that emits an application-owned `capture_message()` event, sends a browser relay batch through `/debugbundle/browser`, validates the emitted Python events against the SDK event-envelope fixture, and confirms both paths reach the mock ingestion endpoint with the expected `service`, `environment`, SDK metadata, and correlation fields.
|
|
309
|
+
|
|
310
|
+
For a manual verification snippet inside your own app:
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
import os
|
|
314
|
+
import debugbundle
|
|
315
|
+
|
|
316
|
+
debugbundle.init(
|
|
317
|
+
project_token=os.environ["DEBUGBUNDLE_PROJECT_TOKEN"],
|
|
318
|
+
service="checkout-api",
|
|
319
|
+
environment="staging",
|
|
320
|
+
)
|
|
321
|
+
|
|
322
|
+
debugbundle.capture_message("debugbundle first-event verification", level="error")
|
|
323
|
+
debugbundle.flush()
|
|
324
|
+
print(debugbundle.get_status(), debugbundle.get_last_event_at())
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
## Development
|
|
328
|
+
|
|
329
|
+
```bash
|
|
330
|
+
pip install -e ".[dev]"
|
|
331
|
+
ruff check .
|
|
332
|
+
mypy src
|
|
333
|
+
pytest
|
|
334
|
+
python -m build
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
CI validates Ruff, mypy, pytest, package build, event schema fixtures, and coverage gates.
|
|
338
|
+
|
|
339
|
+
## Documentation
|
|
340
|
+
|
|
341
|
+
- Python SDK docs: <https://debugbundle.com/docs/sdks/python>
|
|
342
|
+
- SDK overview: <https://debugbundle.com/docs/sdks>
|
|
343
|
+
- Browser relay: <https://debugbundle.com/docs/sdks/browser-relay>
|
|
344
|
+
- Repository: <https://github.com/debugbundle/debugbundle-python>
|
|
345
|
+
|
|
346
|
+
## License
|
|
347
|
+
|
|
348
|
+
AGPL-3.0-only. See `LICENSE`.
|
|
@@ -4,20 +4,21 @@ debugbundle/core.py,sha256=YQFQax6EhjnN7SKFMXq_AohHZyFqxx18Az1ezIZiaDg,35772
|
|
|
4
4
|
debugbundle/logger_integrations.py,sha256=RuTNaD9RRVmiE-BBkksAXWVEGaMzLrWavVpQdgGZBpE,4564
|
|
5
5
|
debugbundle/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
6
6
|
debugbundle/redaction.py,sha256=QTaPkSsYv54yR1mz8SB6Q4vWjvp7Ddcui4WXaH2RVz8,736
|
|
7
|
-
debugbundle/relay.py,sha256=
|
|
7
|
+
debugbundle/relay.py,sha256=BssBwjAp3usfZ3HPrFD1QpW7i7FPT_jf8fqyhocRZ_w,13143
|
|
8
|
+
debugbundle/relay_delivery.py,sha256=VL-nIgJR6mYXqKy-EbA0USWeY_iS_uAr1KBzrnwBnnk,4676
|
|
8
9
|
debugbundle/suppression.py,sha256=XMn0GfF_-WZk2wHWk3KfbO5_u5QhEunues5h3jOInLs,4098
|
|
9
10
|
debugbundle/transport.py,sha256=oOk0xazHxq9h4CneJdRODKtwDciwikvpHVJM_6iBYXU,1791
|
|
10
11
|
debugbundle/trigger_token.py,sha256=YUwIWnnxu1klAVaB8Ltgk_PwWW_PPjq9rzmEbWXeFeM,4455
|
|
11
|
-
debugbundle/integrations/__init__.py,sha256=
|
|
12
|
+
debugbundle/integrations/__init__.py,sha256=Jr87ImWXzUXOuFP4WlGleK14VARajSSdGZN_nHt5emw,1161
|
|
12
13
|
debugbundle/integrations/common.py,sha256=iiwf5wDlpujFuWfbO-ziFTn4MtiHBXSIZNXtamhCavg,2014
|
|
13
14
|
debugbundle/integrations/django.py,sha256=vDQGc0ObcHCe4NQbC0ijcXUM-y9GzOVBjDtcHzxyrxw,1828
|
|
14
15
|
debugbundle/integrations/fastapi.py,sha256=-054Z6MogkZIqKR8DHR4jm2nNC5yyZ5vtanpw6g-voE,3414
|
|
15
16
|
debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-Fbyo,2390
|
|
16
|
-
debugbundle/integrations/relay_django.py,sha256=
|
|
17
|
-
debugbundle/integrations/relay_fastapi.py,sha256=
|
|
18
|
-
debugbundle/integrations/relay_flask.py,sha256=
|
|
19
|
-
debugbundle_python-0.1.
|
|
20
|
-
debugbundle_python-0.1.
|
|
21
|
-
debugbundle_python-0.1.
|
|
22
|
-
debugbundle_python-0.1.
|
|
23
|
-
debugbundle_python-0.1.
|
|
17
|
+
debugbundle/integrations/relay_django.py,sha256=MMuh1Grdfk5IPUkAHqny6rkHb0cPfUXQDOi7w6Rxkuk,2152
|
|
18
|
+
debugbundle/integrations/relay_fastapi.py,sha256=AW6XA1FzFvjh2mD_gmhdIRgXp5YRUuVV7Cdy7H7_92M,2141
|
|
19
|
+
debugbundle/integrations/relay_flask.py,sha256=oJY0KLDB5fnIHjbZfcRL0X2zzSbCmAagKYLwK8mD_Bo,1958
|
|
20
|
+
debugbundle_python-0.1.8.dist-info/licenses/LICENSE,sha256=AKZZ5DQAHrOKGwt24VoRd-SXJLM9OloxlsX8ZgENdEY,735
|
|
21
|
+
debugbundle_python-0.1.8.dist-info/METADATA,sha256=e34e1npCR_fWBRSuDgUzRTN3H6NItj_Qt-_qXLzdFSU,13829
|
|
22
|
+
debugbundle_python-0.1.8.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
debugbundle_python-0.1.8.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
|
|
24
|
+
debugbundle_python-0.1.8.dist-info/RECORD,,
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: debugbundle-python
|
|
3
|
-
Version: 0.1.5
|
|
4
|
-
Summary: DebugBundle SDK for Python
|
|
5
|
-
Author: DebugBundle
|
|
6
|
-
License-Expression: AGPL-3.0-only
|
|
7
|
-
Project-URL: Homepage, https://debugbundle.com/docs/sdk-python
|
|
8
|
-
Project-URL: Repository, https://github.com/debugbundle/debugbundle-python
|
|
9
|
-
Project-URL: Issues, https://github.com/debugbundle/debugbundle-python/issues
|
|
10
|
-
Keywords: debugbundle,debugging,ai-agent,error-tracking
|
|
11
|
-
Classifier: Development Status :: 3 - Alpha
|
|
12
|
-
Classifier: Framework :: Django
|
|
13
|
-
Classifier: Framework :: FastAPI
|
|
14
|
-
Classifier: Framework :: Flask
|
|
15
|
-
Classifier: Programming Language :: Python :: 3
|
|
16
|
-
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
-
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
-
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
-
Classifier: Typing :: Typed
|
|
21
|
-
Requires-Python: >=3.10
|
|
22
|
-
Description-Content-Type: text/markdown
|
|
23
|
-
License-File: LICENSE
|
|
24
|
-
Requires-Dist: httpx<0.29,>=0.27
|
|
25
|
-
Provides-Extra: dev
|
|
26
|
-
Requires-Dist: build<2,>=1; extra == "dev"
|
|
27
|
-
Requires-Dist: django<6,>=5; extra == "dev"
|
|
28
|
-
Requires-Dist: fastapi<1,>=0.115; extra == "dev"
|
|
29
|
-
Requires-Dist: flask<4,>=3; extra == "dev"
|
|
30
|
-
Requires-Dist: jsonschema<5,>=4.23; extra == "dev"
|
|
31
|
-
Requires-Dist: loguru<1,>=0.7; extra == "dev"
|
|
32
|
-
Requires-Dist: mypy<2,>=1.15; extra == "dev"
|
|
33
|
-
Requires-Dist: pytest<9,>=8.3; extra == "dev"
|
|
34
|
-
Requires-Dist: pytest-cov<7,>=5; extra == "dev"
|
|
35
|
-
Requires-Dist: ruff<0.12,>=0.11; extra == "dev"
|
|
36
|
-
Requires-Dist: structlog<26,>=24; extra == "dev"
|
|
37
|
-
Requires-Dist: twine<7,>=5; extra == "dev"
|
|
38
|
-
Dynamic: license-file
|
|
39
|
-
|
|
40
|
-
# debugbundle-python
|
|
41
|
-
|
|
42
|
-
DebugBundle SDK for Python.
|
|
43
|
-
|
|
44
|
-
## Installation
|
|
45
|
-
|
|
46
|
-
```bash
|
|
47
|
-
pip install debugbundle-python
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
## Quick Start
|
|
51
|
-
|
|
52
|
-
```python
|
|
53
|
-
import debugbundle
|
|
54
|
-
|
|
55
|
-
debugbundle.init(project_token="dbundle_proj_test", service="checkout-api")
|
|
56
|
-
debugbundle.capture_exception(RuntimeError("boom"))
|
|
57
|
-
debugbundle.flush()
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
## Status
|
|
61
|
-
|
|
62
|
-
This repository currently contains the full Phase 18 Python SDK scope in eleven implementation slices: core SDK surface, buffering, redaction, duplicate suppression, probe buffering, vanilla runtime hooks, framework integrations for Django, Flask, and FastAPI, remote config polling and capture-policy enforcement, optional `structlog` and `loguru` auto-detection when `capture_logging()` is enabled, contract-aligned `EventEnvelope` emission for log, request, exception, suppression, and probe payloads, explicit public wrapper signatures and a validated buildable typed package artifact, real HTTP integration coverage against a lightweight mock ingestion server, vendored machine-readable schema validation for all event types the Python SDK currently emits, a standalone CI workflow that validates Ruff, mypy, pytest, and package builds for the Python 3.10+ support floor actually used by the package, an enforced per-file coverage gate that keeps every shipped Python SDK module at or above the required 80% minimum, request-local framework correlation binding so `X-DebugBundle-Trace-Id` flows through Django, Flask, and FastAPI into the emitted event correlation metadata for cross-context linking, and safe backend runtime process facts on exception payloads without reading environment variables.
|
|
63
|
-
|
|
64
|
-
## Runtime Context
|
|
65
|
-
|
|
66
|
-
Backend exception events now include safe runtime process facts when the host exposes them, including:
|
|
67
|
-
|
|
68
|
-
- Python version
|
|
69
|
-
- platform
|
|
70
|
-
- architecture
|
|
71
|
-
- pid
|
|
72
|
-
- cwd
|
|
73
|
-
- uptime
|
|
74
|
-
- hostname
|
|
75
|
-
- thread id
|
|
76
|
-
- best-effort memory metadata
|
|
77
|
-
|
|
78
|
-
The SDK does not read or emit environment variables in this runtime block.
|
|
79
|
-
|
|
80
|
-
## Docs
|
|
81
|
-
|
|
82
|
-
https://debugbundle.com/docs/sdk-python
|
|
83
|
-
|
|
84
|
-
## License
|
|
85
|
-
|
|
86
|
-
AGPL-3.0-only
|
|
File without changes
|
|
File without changes
|
|
File without changes
|