debugbundle-python 0.1.5__py3-none-any.whl → 0.1.6__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/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.5.dist-info → debugbundle_python-0.1.6.dist-info}/METADATA +20 -4
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.6.dist-info}/RECORD +10 -9
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.6.dist-info}/WHEEL +0 -0
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {debugbundle_python-0.1.5.dist-info → debugbundle_python-0.1.6.dist-info}/top_level.txt +0 -0
|
@@ -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
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debugbundle-python
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.6
|
|
4
4
|
Summary: DebugBundle SDK for Python
|
|
5
5
|
Author: DebugBundle
|
|
6
6
|
License-Expression: AGPL-3.0-only
|
|
7
|
-
Project-URL: Homepage, https://debugbundle.com/docs/
|
|
7
|
+
Project-URL: Homepage, https://debugbundle.com/docs/sdks/python
|
|
8
8
|
Project-URL: Repository, https://github.com/debugbundle/debugbundle-python
|
|
9
9
|
Project-URL: Issues, https://github.com/debugbundle/debugbundle-python/issues
|
|
10
10
|
Keywords: debugbundle,debugging,ai-agent,error-tracking
|
|
@@ -59,7 +59,7 @@ debugbundle.flush()
|
|
|
59
59
|
|
|
60
60
|
## Status
|
|
61
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.
|
|
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, full browser relay handler parity with Django/Flask/FastAPI helpers plus local-only and connected delivery modes, and safe backend runtime process facts on exception payloads without reading environment variables.
|
|
63
63
|
|
|
64
64
|
## Runtime Context
|
|
65
65
|
|
|
@@ -77,9 +77,25 @@ Backend exception events now include safe runtime process facts when the host ex
|
|
|
77
77
|
|
|
78
78
|
The SDK does not read or emit environment variables in this runtime block.
|
|
79
79
|
|
|
80
|
+
## Browser Relay
|
|
81
|
+
|
|
82
|
+
The Python SDK includes a framework-agnostic `BrowserRelayHandler` plus framework helpers for the contract-required `POST /debugbundle/browser` endpoint in Python servers that also load `@debugbundle/sdk-browser`.
|
|
83
|
+
|
|
84
|
+
- `create_django_relay_view()` returns a Django view for the relay route.
|
|
85
|
+
- `create_flask_relay_handler()` registers the relay route on a Flask app.
|
|
86
|
+
- `create_fastapi_relay_handler()` registers the relay route on a FastAPI app.
|
|
87
|
+
|
|
88
|
+
The relay handler enforces same-origin or configured allowed origins, requires `Content-Type: application/json`, accepts the canonical `batch` body shape only, rejects bodies larger than `256 KB`, applies per-IP rate limiting, accepts only supported browser event types, strips trust-sensitive headers and fields, forces `sdk_name` to `@debugbundle/sdk-browser`, and preserves browser correlation fields (`request_id`, `trace_id`, `session_id`, and `user_id_hash`) when they are strings or `null`.
|
|
89
|
+
|
|
90
|
+
Delivery behavior matches the shared relay contract across the shipped server SDKs:
|
|
91
|
+
|
|
92
|
+
- `project_mode="local-only"` writes accepted browser events to local event files for CLI processing.
|
|
93
|
+
- `project_mode="connected"` with the default `durable_write=True` writes a durable relay spool record and then forwards to the ingestion API with the server-side project token.
|
|
94
|
+
- `project_mode="connected"` with `durable_write=False` uses the lower-latency forward-only path.
|
|
95
|
+
|
|
80
96
|
## Docs
|
|
81
97
|
|
|
82
|
-
https://debugbundle.com/docs/
|
|
98
|
+
https://debugbundle.com/docs/sdks/python
|
|
83
99
|
|
|
84
100
|
## License
|
|
85
101
|
|
|
@@ -4,7 +4,8 @@ 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
|
|
@@ -13,11 +14,11 @@ debugbundle/integrations/common.py,sha256=iiwf5wDlpujFuWfbO-ziFTn4MtiHBXSIZNXtam
|
|
|
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.6.dist-info/licenses/LICENSE,sha256=AKZZ5DQAHrOKGwt24VoRd-SXJLM9OloxlsX8ZgENdEY,735
|
|
21
|
+
debugbundle_python-0.1.6.dist-info/METADATA,sha256=hIx0qILB2fF-SpIlyisBe5DRtUadKnNihPDRTuiSP5c,5130
|
|
22
|
+
debugbundle_python-0.1.6.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
23
|
+
debugbundle_python-0.1.6.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
|
|
24
|
+
debugbundle_python-0.1.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|