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.
Files changed (41) hide show
  1. {debugbundle_python-0.1.8/src/debugbundle_python.egg-info → debugbundle_python-0.1.9}/PKG-INFO +1 -1
  2. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/pyproject.toml +1 -1
  3. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_django.py +7 -2
  4. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_fastapi.py +3 -3
  5. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/relay_flask.py +2 -1
  6. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/relay.py +39 -19
  7. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9/src/debugbundle_python.egg-info}/PKG-INFO +1 -1
  8. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_relay.py +23 -0
  9. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/LICENSE +0 -0
  10. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/README.md +0 -0
  11. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/setup.cfg +0 -0
  12. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/__init__.py +0 -0
  13. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/config.py +0 -0
  14. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/core.py +0 -0
  15. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/__init__.py +0 -0
  16. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/common.py +0 -0
  17. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/django.py +0 -0
  18. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/fastapi.py +0 -0
  19. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/integrations/flask.py +0 -0
  20. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/logger_integrations.py +0 -0
  21. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/py.typed +0 -0
  22. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/redaction.py +0 -0
  23. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/relay_delivery.py +0 -0
  24. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/suppression.py +0 -0
  25. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/transport.py +0 -0
  26. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle/trigger_token.py +0 -0
  27. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/SOURCES.txt +0 -0
  28. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/dependency_links.txt +0 -0
  29. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/requires.txt +0 -0
  30. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/src/debugbundle_python.egg-info/top_level.txt +0 -0
  31. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_contracts.py +0 -0
  32. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_framework_integrations.py +0 -0
  33. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_hooks.py +0 -0
  34. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_http_integration.py +0 -0
  35. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_optional_imports.py +0 -0
  36. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_relay_delivery.py +0 -0
  37. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_remote_config.py +0 -0
  38. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_repository_metadata.py +0 -0
  39. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_schema_validation.py +0 -0
  40. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_sdk.py +0 -0
  41. {debugbundle_python-0.1.8 → debugbundle_python-0.1.9}/tests/test_trigger_token.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "debugbundle-python"
7
- version = "0.1.8"
7
+ version = "0.1.9"
8
8
  description = "DebugBundle SDK for Python"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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
 
@@ -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:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: debugbundle-python
3
- Version: 0.1.8
3
+ Version: 0.1.9
4
4
  Summary: DebugBundle SDK for Python
5
5
  Author: DebugBundle
6
6
  License-Expression: AGPL-3.0-only
@@ -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"))