insider-python 0.1.7__tar.gz → 0.1.8__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 (53) hide show
  1. {insider_python-0.1.7 → insider_python-0.1.8}/PKG-INFO +1 -1
  2. {insider_python-0.1.7 → insider_python-0.1.8}/pyproject.toml +1 -1
  3. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/_version.py +1 -1
  4. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/asgi.py +12 -5
  5. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/request.py +45 -8
  6. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider_python.egg-info/PKG-INFO +1 -1
  7. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_django_integration.py +14 -0
  8. {insider_python-0.1.7 → insider_python-0.1.8}/README.md +0 -0
  9. {insider_python-0.1.7 → insider_python-0.1.8}/setup.cfg +0 -0
  10. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/__init__.py +0 -0
  11. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/_envelope.py +0 -0
  12. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/_footprint.py +0 -0
  13. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/client.py +0 -0
  14. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/contrib/__init__.py +0 -0
  15. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/contrib/django/__init__.py +0 -0
  16. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/contrib/django/apps.py +0 -0
  17. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/contrib/django/middleware.py +0 -0
  18. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/dsn.py +0 -0
  19. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/__init__.py +0 -0
  20. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/__init__.py +0 -0
  21. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/capture.py +0 -0
  22. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/drf.py +0 -0
  23. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/handler.py +0 -0
  24. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/perf.py +0 -0
  25. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/signals.py +0 -0
  26. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/django/wsgi.py +0 -0
  27. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/logging/__init__.py +0 -0
  28. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/logging/handler.py +0 -0
  29. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/integrations/logging/levels.py +0 -0
  30. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/py.typed +0 -0
  31. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/safety.py +0 -0
  32. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/scope.py +0 -0
  33. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/scrubbing.py +0 -0
  34. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/stacktrace.py +0 -0
  35. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider/transport.py +0 -0
  36. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider_python.egg-info/SOURCES.txt +0 -0
  37. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider_python.egg-info/dependency_links.txt +0 -0
  38. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider_python.egg-info/requires.txt +0 -0
  39. {insider_python-0.1.7 → insider_python-0.1.8}/src/insider_python.egg-info/top_level.txt +0 -0
  40. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_asgi_integration.py +0 -0
  41. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_capture.py +0 -0
  42. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_capture_log.py +0 -0
  43. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_capture_perf.py +0 -0
  44. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_django.py +0 -0
  45. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_drf_integration.py +0 -0
  46. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_dsn.py +0 -0
  47. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_envelope.py +0 -0
  48. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_logging_integration.py +0 -0
  49. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_never_crash.py +0 -0
  50. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_safety.py +0 -0
  51. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_scrubbing.py +0 -0
  52. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_stacktrace.py +0 -0
  53. {insider_python-0.1.7 → insider_python-0.1.8}/tests/test_transport.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: insider-python
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Python SDK for Insider — ship Beacons to your Insider server.
5
5
  Author: Insider
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "insider-python"
7
- version = "0.1.7"
7
+ version = "0.1.8"
8
8
  description = "Python SDK for Insider — ship Beacons to your Insider server."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.8"
@@ -5,4 +5,4 @@ lookup on every beacon. Bump this and `[project].version` together when
5
5
  cutting a release.
6
6
  """
7
7
 
8
- __version__ = "0.1.7"
8
+ __version__ = "0.1.8"
@@ -19,7 +19,7 @@ from ...client import _client
19
19
  from ...safety import debug, safe
20
20
  from ...stacktrace import exception_payload
21
21
  from .perf import emit_http_footprint
22
- from .request import build_request_ctx_from_scope
22
+ from .request import build_request_ctx_from_scope, parse_response_body_text
23
23
 
24
24
  ASGIApp = Callable[..., Any]
25
25
 
@@ -95,14 +95,19 @@ class _InsiderAsgiHttpWrapper:
95
95
 
96
96
  start = time.perf_counter()
97
97
  status_code: Optional[int] = None
98
+ response_content_type = ""
98
99
  response_chunks: list[bytes] = []
99
100
  response_bytes = 0
100
101
  max_response_bytes = 8192
101
102
 
102
103
  async def send_wrapper(message: Dict[str, Any]) -> None:
103
- nonlocal status_code, response_bytes
104
+ nonlocal status_code, response_bytes, response_content_type
104
105
  if message.get("type") == "http.response.start":
105
106
  status_code = int(message.get("status", 500))
107
+ for key, value in message.get("headers") or []:
108
+ if key.lower() == b"content-type":
109
+ response_content_type = value.decode("latin-1")
110
+ break
106
111
  elif (
107
112
  client.send_default_pii
108
113
  and message.get("type") == "http.response.body"
@@ -125,11 +130,13 @@ class _InsiderAsgiHttpWrapper:
125
130
  raise
126
131
  finally:
127
132
  if client.send_default_pii and response_chunks:
128
- body = b"".join(response_chunks).decode("utf-8", errors="replace")
133
+ body_text = b"".join(response_chunks).decode("utf-8", errors="replace")
129
134
  if response_bytes >= max_response_bytes:
130
- body += "...[truncated]"
135
+ body_text += "...[truncated]"
131
136
  ctx = dict(client.scope.current_request() or {})
132
- ctx["response_body"] = body
137
+ ctx["response_body"] = parse_response_body_text(
138
+ body_text, response_content_type
139
+ )
133
140
  client.scope.set_request(ctx)
134
141
  path = str(scope.get("path") or "/")
135
142
  method = str(scope.get("method") or "GET")
@@ -184,8 +184,44 @@ def _read_body(request: Any) -> Optional[str]:
184
184
  _RESPONSE_BODY_MAX_BYTES = 8192
185
185
 
186
186
 
187
- def read_response_body(response: Any, *, max_bytes: int = _RESPONSE_BODY_MAX_BYTES) -> Optional[str]:
188
- """Return response content as text when already materialized on the response."""
187
+ def parse_response_body_text(text: str, content_type: str = "") -> Any:
188
+ """Decode captured body text; parse JSON API responses into objects."""
189
+ if not text:
190
+ return text
191
+ ct = content_type.lower()
192
+ if "application/json" in ct or ct.endswith("+json"):
193
+ try:
194
+ import json
195
+
196
+ return json.loads(text)
197
+ except json.JSONDecodeError:
198
+ pass
199
+ return text
200
+
201
+
202
+ def _response_content_type(response: Any) -> str:
203
+ try:
204
+ if hasattr(response, "get"):
205
+ value = response.get("Content-Type")
206
+ if value:
207
+ return str(value)
208
+ except Exception:
209
+ pass
210
+ try:
211
+ headers = getattr(response, "headers", None)
212
+ if headers is not None:
213
+ value = headers.get("Content-Type")
214
+ if value:
215
+ return str(value)
216
+ except Exception:
217
+ pass
218
+ return ""
219
+
220
+
221
+ def read_response_body(
222
+ response: Any, *, max_bytes: int = _RESPONSE_BODY_MAX_BYTES
223
+ ) -> Any:
224
+ """Return response content; JSON responses are parsed to dict/list."""
189
225
  if response is None:
190
226
  return None
191
227
  try:
@@ -200,14 +236,15 @@ def read_response_body(response: Any, *, max_bytes: int = _RESPONSE_BODY_MAX_BYT
200
236
  if content is None:
201
237
  return None
202
238
  if not isinstance(content, (bytes, bytearray)):
203
- return str(content)
239
+ return parse_response_body_text(str(content), _response_content_type(response))
204
240
  raw = bytes(content)
205
- if len(raw) > max_bytes:
241
+ truncated = len(raw) > max_bytes
242
+ if truncated:
206
243
  raw = raw[:max_bytes]
207
- suffix = "...[truncated]"
208
- else:
209
- suffix = ""
210
- return raw.decode("utf-8", errors="replace") + suffix
244
+ text = raw.decode("utf-8", errors="replace")
245
+ if truncated:
246
+ text += "...[truncated]"
247
+ return parse_response_body_text(text, _response_content_type(response))
211
248
  except Exception:
212
249
  return None
213
250
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: insider-python
3
- Version: 0.1.7
3
+ Version: 0.1.8
4
4
  Summary: Python SDK for Insider — ship Beacons to your Insider server.
5
5
  Author: Insider
6
6
  License-Expression: MIT
@@ -54,6 +54,20 @@ def test_integration_captures_response_body_when_pii_enabled(
54
54
  assert fake_transport.envelopes[0].get("response_body") == "ok"
55
55
 
56
56
 
57
+ @pytest.mark.django_db
58
+ def test_integration_captures_json_response_body_as_object(
59
+ sdk_client, client, fake_transport
60
+ ):
61
+ sdk_client.send_default_pii = True
62
+ response = client.get("/json/")
63
+ assert response.status_code == 200
64
+ assert len(fake_transport.envelopes) == 1
65
+ assert fake_transport.envelopes[0].get("response_body") == {
66
+ "ok": True,
67
+ "count": 2,
68
+ }
69
+
70
+
57
71
  @pytest.mark.django_db
58
72
  def test_integration_clean_request_emits_one_footprint(client, fake_transport):
59
73
  response = client.get("/ok/")
File without changes
File without changes