debugbundle-python 0.1.6__py3-none-any.whl → 0.1.9__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.
@@ -1,9 +1,24 @@
1
- from .django import DebugBundleDjangoMiddleware
2
- from .fastapi import DebugBundleFastAPIMiddleware, instrument_fastapi
3
- from .flask import instrument_flask
4
- from .relay_django import create_django_relay_view
5
- from .relay_fastapi import create_fastapi_relay_handler
6
- from .relay_flask import create_flask_relay_handler
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",
@@ -54,9 +54,14 @@ def create_django_relay_view(
54
54
  )
55
55
 
56
56
  if response.body is not None:
57
- return JsonResponse(response.body, status=response.status, safe=False)
57
+ result = JsonResponse(response.body, status=response.status, safe=False)
58
+ else:
59
+ result = JsonResponse({}, status=response.status)
58
60
 
59
- return JsonResponse({}, status=response.status)
61
+ for key, value in response.headers.items():
62
+ result[key] = value
63
+
64
+ return result
60
65
 
61
66
  return view
62
67
 
@@ -42,7 +42,7 @@ def create_fastapi_relay_handler(
42
42
  )
43
43
 
44
44
  def register(app: Any) -> None:
45
- @app.post(route_path)
45
+ @app.api_route(route_path, methods=["POST", "OPTIONS"])
46
46
  async def debugbundle_browser_relay(request: Request) -> Response:
47
47
  body = await request.body()
48
48
  headers = {str(key): str(value) for key, value in request.headers.items()}
@@ -58,7 +58,7 @@ def create_fastapi_relay_handler(
58
58
  )
59
59
 
60
60
  if response.body is not None:
61
- return JSONResponse(content=response.body, status_code=response.status)
62
- return Response(status_code=response.status)
61
+ return JSONResponse(content=response.body, status_code=response.status, headers=response.headers)
62
+ return Response(status_code=response.status, headers=response.headers)
63
63
 
64
64
  return register
@@ -41,7 +41,7 @@ def create_flask_relay_handler(
41
41
  def register(app: Any) -> None:
42
42
  from flask import Response, request
43
43
 
44
- @app.route(route_path, methods=["POST"])
44
+ @app.route(route_path, methods=["POST", "OPTIONS"])
45
45
  def debugbundle_browser_relay() -> Response:
46
46
  response = handler.handle(
47
47
  {
@@ -58,6 +58,7 @@ def create_flask_relay_handler(
58
58
  return Response(
59
59
  body,
60
60
  status=response.status,
61
+ headers=response.headers,
61
62
  content_type="application/json",
62
63
  )
63
64
 
debugbundle/relay.py CHANGED
@@ -33,6 +33,7 @@ ACCEPTED_EVENT_TYPES = frozenset(
33
33
  class BrowserRelayResponse:
34
34
  status: int
35
35
  body: dict[str, Any] | None = None
36
+ headers: dict[str, str] = field(default_factory=dict)
36
37
 
37
38
 
38
39
  @dataclass(frozen=True)
@@ -78,47 +79,56 @@ class BrowserRelayHandler:
78
79
 
79
80
  def handle(self, request: dict[str, Any]) -> BrowserRelayResponse:
80
81
  method = str(request.get("method", "POST")).upper()
81
- if method != "POST":
82
- return BrowserRelayResponse(405)
83
-
84
82
  headers = _normalize_headers(request.get("headers") or {})
83
+ source_origin = _source_origin(headers)
85
84
  if not self._is_origin_allowed(headers):
86
85
  return BrowserRelayResponse(403)
87
86
 
87
+ response_headers = _cors_headers(source_origin) if source_origin else {}
88
+
89
+ def with_headers(response: BrowserRelayResponse) -> BrowserRelayResponse:
90
+ return BrowserRelayResponse(response.status, response.body, {**response_headers, **response.headers})
91
+
92
+ if method == "OPTIONS":
93
+ return with_headers(BrowserRelayResponse(204))
94
+
95
+ if method != "POST":
96
+ return with_headers(BrowserRelayResponse(405))
97
+
88
98
  if not _is_supported_content_type(headers.get("content-type")):
89
- return BrowserRelayResponse(
99
+ return with_headers(BrowserRelayResponse(
90
100
  400,
91
101
  {"accepted": 0, "rejected": 0, "errors": ["Relay requests must use Content-Type: application/json."]},
92
- )
102
+ ))
93
103
 
94
104
  body: str = request.get("body", "")
95
105
  if len(body.encode("utf-8") if isinstance(body, str) else body) > self.max_body_bytes:
96
- return BrowserRelayResponse(413)
106
+ return with_headers(BrowserRelayResponse(413))
97
107
 
98
108
  ip_address: str | None = request.get("ipAddress") or request.get("ip_address")
99
109
  if self._is_rate_limited(ip_address):
100
- return BrowserRelayResponse(429)
110
+ return with_headers(BrowserRelayResponse(429))
101
111
 
102
112
  try:
103
113
  decoded = json.loads(body)
104
114
  except (json.JSONDecodeError, TypeError, ValueError):
105
- return BrowserRelayResponse(
115
+ return with_headers(BrowserRelayResponse(
106
116
  400,
107
117
  {"accepted": 0, "rejected": 0, "errors": ["Relay request body must be valid JSON."]},
108
- )
118
+ ))
109
119
 
110
120
  if not isinstance(decoded, dict):
111
- return BrowserRelayResponse(
121
+ return with_headers(BrowserRelayResponse(
112
122
  400,
113
123
  {"accepted": 0, "rejected": 0, "errors": ["Relay request body must be valid JSON."]},
114
- )
124
+ ))
115
125
 
116
126
  batch = decoded.get("batch")
117
127
  if not isinstance(batch, list):
118
- return BrowserRelayResponse(
128
+ return with_headers(BrowserRelayResponse(
119
129
  400,
120
130
  {"accepted": 0, "rejected": 0, "errors": ["Relay request body must include a batch array."]},
121
- )
131
+ ))
122
132
 
123
133
  accepted_events: list[dict[str, Any]] = []
124
134
  errors: list[str] = []
@@ -144,7 +154,7 @@ class BrowserRelayHandler:
144
154
  if accepted_events:
145
155
  try:
146
156
  if not self._deliver_events(accepted_events):
147
- return BrowserRelayResponse(500)
157
+ return with_headers(BrowserRelayResponse(500))
148
158
 
149
159
  if self.on_accept is not None:
150
160
  self.on_accept(
@@ -156,18 +166,18 @@ class BrowserRelayHandler:
156
166
  )
157
167
  )
158
168
  except Exception:
159
- return BrowserRelayResponse(500)
169
+ return with_headers(BrowserRelayResponse(500))
160
170
 
161
171
  if errors:
162
- return BrowserRelayResponse(
172
+ return with_headers(BrowserRelayResponse(
163
173
  400,
164
174
  {"accepted": len(accepted_events), "rejected": len(errors), "errors": errors},
165
- )
175
+ ))
166
176
 
167
- return BrowserRelayResponse(
177
+ return with_headers(BrowserRelayResponse(
168
178
  202,
169
179
  {"accepted": len(accepted_events), "rejected": 0, "errors": []},
170
- )
180
+ ))
171
181
 
172
182
  def _deliver_events(self, accepted_events: list[dict[str, Any]]) -> bool:
173
183
  if self.project_mode is None:
@@ -261,6 +271,16 @@ def _is_supported_content_type(content_type: str | None) -> bool:
261
271
  return isinstance(content_type, str) and "application/json" in content_type.lower()
262
272
 
263
273
 
274
+ def _cors_headers(origin: str) -> dict[str, str]:
275
+ return {
276
+ "access-control-allow-origin": origin,
277
+ "access-control-allow-methods": "POST, OPTIONS",
278
+ "access-control-allow-headers": "content-type",
279
+ "access-control-max-age": "600",
280
+ "vary": "Origin",
281
+ }
282
+
283
+
264
284
  def _source_origin(headers: dict[str, str]) -> str | None:
265
285
  origin = (headers.get("origin") or "").strip()
266
286
  if origin:
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.4
2
+ Name: debugbundle-python
3
+ Version: 0.1.9
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
+ ![PyPI](https://img.shields.io/pypi/v/debugbundle-python?label=pypi)
45
+ ![CI](https://img.shields.io/github/actions/workflow/status/debugbundle/debugbundle-python/ci.yml?branch=main&label=ci)
46
+ ![License](https://img.shields.io/badge/license-AGPL--3.0--only-blue)
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,21 +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=BssBwjAp3usfZ3HPrFD1QpW7i7FPT_jf8fqyhocRZ_w,13143
7
+ debugbundle/relay.py,sha256=IVBLm4jqhaWpAoEYRNyy1dr19hp_aIOVjxTvTFXeBwA,14072
8
8
  debugbundle/relay_delivery.py,sha256=VL-nIgJR6mYXqKy-EbA0USWeY_iS_uAr1KBzrnwBnnk,4676
9
9
  debugbundle/suppression.py,sha256=XMn0GfF_-WZk2wHWk3KfbO5_u5QhEunues5h3jOInLs,4098
10
10
  debugbundle/transport.py,sha256=oOk0xazHxq9h4CneJdRODKtwDciwikvpHVJM_6iBYXU,1791
11
11
  debugbundle/trigger_token.py,sha256=YUwIWnnxu1klAVaB8Ltgk_PwWW_PPjq9rzmEbWXeFeM,4455
12
- debugbundle/integrations/__init__.py,sha256=VVNoL30a9yd26U9oH1CQMw0_0O0Apo-ZNK1dyrMjTQg,551
12
+ debugbundle/integrations/__init__.py,sha256=Jr87ImWXzUXOuFP4WlGleK14VARajSSdGZN_nHt5emw,1161
13
13
  debugbundle/integrations/common.py,sha256=iiwf5wDlpujFuWfbO-ziFTn4MtiHBXSIZNXtamhCavg,2014
14
14
  debugbundle/integrations/django.py,sha256=vDQGc0ObcHCe4NQbC0ijcXUM-y9GzOVBjDtcHzxyrxw,1828
15
15
  debugbundle/integrations/fastapi.py,sha256=-054Z6MogkZIqKR8DHR4jm2nNC5yyZ5vtanpw6g-voE,3414
16
16
  debugbundle/integrations/flask.py,sha256=cgyHbsAH96gpNPf_5IGJYzp2va28JdDgXROBsF-Fbyo,2390
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,,
17
+ debugbundle/integrations/relay_django.py,sha256=Wj8BE9D5otU2KmtjgqdKIJ-BvXVcagCKgpLYvnPZ6WE,2281
18
+ debugbundle/integrations/relay_fastapi.py,sha256=-Zl6bvhYMLii__mP6-UvSdwxS5VKsb9OZvk-yy-3hEw,2227
19
+ debugbundle/integrations/relay_flask.py,sha256=VFHkDTJy4LkZzL_Ry_Ba-e_gW6UTG4PBwnLcvB_oXuQ,2011
20
+ debugbundle_python-0.1.9.dist-info/licenses/LICENSE,sha256=AKZZ5DQAHrOKGwt24VoRd-SXJLM9OloxlsX8ZgENdEY,735
21
+ debugbundle_python-0.1.9.dist-info/METADATA,sha256=xDpBNECSnuPc1kSNDE-5Zd9SQAd5C-iNwZUWU1ISFXM,13829
22
+ debugbundle_python-0.1.9.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
23
+ debugbundle_python-0.1.9.dist-info/top_level.txt,sha256=RCB9STTFnl1OKdojxz-xhaks2zkRFs1meZXsKnm18LM,12
24
+ debugbundle_python-0.1.9.dist-info/RECORD,,
@@ -1,102 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: debugbundle-python
3
- Version: 0.1.6
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
- 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, 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
-
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
- ## 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
-
96
- ## Docs
97
-
98
- https://debugbundle.com/docs/sdks/python
99
-
100
- ## License
101
-
102
- AGPL-3.0-only