debugbundle-python 0.1.8__tar.gz → 0.1.9__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {debugbundle_python-0.1.8/src/debugbundle_python.egg-info → debugbundle_python-0.1.9}/PKG-INFO +1 -1
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/pyproject.toml +1 -1
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_django.py +7 -2
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_fastapi.py +3 -3
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_flask.py +2 -1
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/relay.py +39 -19
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9/src/debugbundle_python.egg-info}/PKG-INFO +1 -1
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_relay.py +23 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/LICENSE +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/README.md +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/setup.cfg +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/__init__.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/config.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/core.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/__init__.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/common.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/django.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/fastapi.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/flask.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/logger_integrations.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/py.typed +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/redaction.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/relay_delivery.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/suppression.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/transport.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/trigger_token.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/requires.txt +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/top_level.txt +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_contracts.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_framework_integrations.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_hooks.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_http_integration.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_optional_imports.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_relay_delivery.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_remote_config.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_repository_metadata.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_schema_validation.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_sdk.py +0 -0
- {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_trigger_token.py +0 -0
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_django.py
RENAMED
|
@@ -54,9 +54,14 @@ def create_django_relay_view(
|
|
|
54
54
|
)
|
|
55
55
|
|
|
56
56
|
if response.body is not None:
|
|
57
|
-
|
|
57
|
+
result = JsonResponse(response.body, status=response.status, safe=False)
|
|
58
|
+
else:
|
|
59
|
+
result = JsonResponse({}, status=response.status)
|
|
58
60
|
|
|
59
|
-
|
|
61
|
+
for key, value in response.headers.items():
|
|
62
|
+
result[key] = value
|
|
63
|
+
|
|
64
|
+
return result
|
|
60
65
|
|
|
61
66
|
return view
|
|
62
67
|
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_fastapi.py
RENAMED
|
@@ -42,7 +42,7 @@ def create_fastapi_relay_handler(
|
|
|
42
42
|
)
|
|
43
43
|
|
|
44
44
|
def register(app: Any) -> None:
|
|
45
|
-
@app.
|
|
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
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_flask.py
RENAMED
|
@@ -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
|
|
|
@@ -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:
|
|
@@ -78,6 +78,29 @@ def test_rejects_non_post() -> None:
|
|
|
78
78
|
assert response.status == 405
|
|
79
79
|
|
|
80
80
|
|
|
81
|
+
def test_answers_allowed_cross_origin_preflight() -> None:
|
|
82
|
+
handler = BrowserRelayHandler(allowed_origins=["https://web.example.com"])
|
|
83
|
+
response = handler.handle(
|
|
84
|
+
_make_request(
|
|
85
|
+
body="",
|
|
86
|
+
method="OPTIONS",
|
|
87
|
+
origin="https://web.example.com",
|
|
88
|
+
host="api.example.com",
|
|
89
|
+
)
|
|
90
|
+
)
|
|
91
|
+
assert response.status == 204
|
|
92
|
+
assert response.headers["access-control-allow-origin"] == "https://web.example.com"
|
|
93
|
+
assert response.headers["access-control-allow-methods"] == "POST, OPTIONS"
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_adds_cors_headers_to_accepted_cross_origin_posts() -> None:
|
|
97
|
+
handler = BrowserRelayHandler(allowed_origins=["https://web.example.com"])
|
|
98
|
+
response = handler.handle(_make_request(origin="https://web.example.com", host="api.example.com"))
|
|
99
|
+
assert response.status == 202
|
|
100
|
+
assert response.headers["access-control-allow-origin"] == "https://web.example.com"
|
|
101
|
+
assert response.headers["vary"] == "Origin"
|
|
102
|
+
|
|
103
|
+
|
|
81
104
|
def test_rejects_invalid_origin() -> None:
|
|
82
105
|
handler = BrowserRelayHandler(allowed_origins=["https://allowed.com"])
|
|
83
106
|
response = handler.handle(_make_request(origin="https://attacker.com"))
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/__init__.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/common.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/django.py
RENAMED
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/fastapi.py
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/logger_integrations.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/SOURCES.txt
RENAMED
|
File without changes
|
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/requires.txt
RENAMED
|
File without changes
|
{debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|