insider-python 0.1.6__tar.gz → 0.1.7__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.6 → insider_python-0.1.7}/PKG-INFO +1 -1
  2. {insider_python-0.1.6 → insider_python-0.1.7}/pyproject.toml +1 -1
  3. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/_version.py +1 -1
  4. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/client.py +12 -1
  5. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/asgi.py +21 -1
  6. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/handler.py +7 -0
  7. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/request.py +41 -0
  8. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider_python.egg-info/PKG-INFO +1 -1
  9. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_django_integration.py +11 -0
  10. {insider_python-0.1.6 → insider_python-0.1.7}/README.md +0 -0
  11. {insider_python-0.1.6 → insider_python-0.1.7}/setup.cfg +0 -0
  12. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/__init__.py +0 -0
  13. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/_envelope.py +0 -0
  14. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/_footprint.py +0 -0
  15. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/contrib/__init__.py +0 -0
  16. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/contrib/django/__init__.py +0 -0
  17. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/contrib/django/apps.py +0 -0
  18. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/contrib/django/middleware.py +0 -0
  19. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/dsn.py +0 -0
  20. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/__init__.py +0 -0
  21. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/__init__.py +0 -0
  22. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/capture.py +0 -0
  23. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/drf.py +0 -0
  24. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/perf.py +0 -0
  25. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/signals.py +0 -0
  26. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/django/wsgi.py +0 -0
  27. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/logging/__init__.py +0 -0
  28. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/logging/handler.py +0 -0
  29. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/integrations/logging/levels.py +0 -0
  30. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/py.typed +0 -0
  31. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/safety.py +0 -0
  32. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/scope.py +0 -0
  33. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/scrubbing.py +0 -0
  34. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/stacktrace.py +0 -0
  35. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider/transport.py +0 -0
  36. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider_python.egg-info/SOURCES.txt +0 -0
  37. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider_python.egg-info/dependency_links.txt +0 -0
  38. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider_python.egg-info/requires.txt +0 -0
  39. {insider_python-0.1.6 → insider_python-0.1.7}/src/insider_python.egg-info/top_level.txt +0 -0
  40. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_asgi_integration.py +0 -0
  41. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_capture.py +0 -0
  42. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_capture_log.py +0 -0
  43. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_capture_perf.py +0 -0
  44. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_django.py +0 -0
  45. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_drf_integration.py +0 -0
  46. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_dsn.py +0 -0
  47. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_envelope.py +0 -0
  48. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_logging_integration.py +0 -0
  49. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_never_crash.py +0 -0
  50. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_safety.py +0 -0
  51. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_scrubbing.py +0 -0
  52. {insider_python-0.1.6 → insider_python-0.1.7}/tests/test_stacktrace.py +0 -0
  53. {insider_python-0.1.6 → insider_python-0.1.7}/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.6
3
+ Version: 0.1.7
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.6"
7
+ version = "0.1.7"
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.6"
8
+ __version__ = "0.1.7"
@@ -37,6 +37,16 @@ VALID_KINDS = {"error", "perf", "log", "custom", "request"}
37
37
  VALID_LEVELS = {"debug", "info", "warning", "error", "fatal"}
38
38
 
39
39
 
40
+ def _format_request_user(user_val: Any) -> str:
41
+ if not user_val:
42
+ return "anonymous"
43
+ if isinstance(user_val, dict):
44
+ uid = user_val.get("id")
45
+ if uid is not None:
46
+ return str(uid)
47
+ return str(user_val)
48
+
49
+
40
50
  def _byte_len_footprint(obj: Dict[str, Any]) -> int:
41
51
  try:
42
52
  return len(json.dumps(obj, default=str, ensure_ascii=False).encode("utf-8"))
@@ -290,8 +300,9 @@ class Client:
290
300
  request_id=trace_id or self.scope.current_trace_id(),
291
301
  request_path=op.strip(),
292
302
  request_method=method,
293
- request_user=str(request_ctx.get("user") or "anonymous"),
303
+ request_user=_format_request_user(request_ctx.get("user")),
294
304
  request_body=request_ctx.get("body"),
305
+ response_body=request_ctx.get("response_body"),
295
306
  response_time=float(duration_ms),
296
307
  status_code=code,
297
308
  system_logs=logs or None,
@@ -95,11 +95,24 @@ class _InsiderAsgiHttpWrapper:
95
95
 
96
96
  start = time.perf_counter()
97
97
  status_code: Optional[int] = None
98
+ response_chunks: list[bytes] = []
99
+ response_bytes = 0
100
+ max_response_bytes = 8192
98
101
 
99
102
  async def send_wrapper(message: Dict[str, Any]) -> None:
100
- nonlocal status_code
103
+ nonlocal status_code, response_bytes
101
104
  if message.get("type") == "http.response.start":
102
105
  status_code = int(message.get("status", 500))
106
+ elif (
107
+ client.send_default_pii
108
+ and message.get("type") == "http.response.body"
109
+ and response_bytes < max_response_bytes
110
+ ):
111
+ chunk = message.get("body") or b""
112
+ if chunk:
113
+ room = max_response_bytes - response_bytes
114
+ response_chunks.append(chunk[:room])
115
+ response_bytes += min(len(chunk), room)
103
116
  await send(message)
104
117
 
105
118
  try:
@@ -111,6 +124,13 @@ class _InsiderAsgiHttpWrapper:
111
124
  client.scope.set_pending_exception(block)
112
125
  raise
113
126
  finally:
127
+ if client.send_default_pii and response_chunks:
128
+ body = b"".join(response_chunks).decode("utf-8", errors="replace")
129
+ if response_bytes >= max_response_bytes:
130
+ body += "...[truncated]"
131
+ ctx = dict(client.scope.current_request() or {})
132
+ ctx["response_body"] = body
133
+ client.scope.set_request(ctx)
114
134
  path = str(scope.get("path") or "/")
115
135
  method = str(scope.get("method") or "GET")
116
136
  emit_http_footprint(
@@ -24,6 +24,7 @@ from ...client import _client
24
24
  from ...safety import debug, safe
25
25
  from .perf import emit_request_envelope
26
26
  from .capture import sync_pending_from_request
27
+ from .request import read_response_body
27
28
 
28
29
  _patched = False
29
30
  _auto_perf = True
@@ -45,6 +46,12 @@ def _finalize_request_cycle(
45
46
  status_code = getattr(response, "status_code", None)
46
47
  sync_pending_from_request(request)
47
48
  if _auto_perf:
49
+ if client.send_default_pii and response is not None:
50
+ body = read_response_body(response)
51
+ if body is not None:
52
+ ctx = dict(client.scope.current_request() or {})
53
+ ctx["response_body"] = body
54
+ client.scope.set_request(ctx)
48
55
  emit_request_envelope(
49
56
  request,
50
57
  duration_ms=duration_ms,
@@ -179,3 +179,44 @@ def _read_body(request: Any) -> Optional[str]:
179
179
  return str(raw)
180
180
  except Exception:
181
181
  return None
182
+
183
+
184
+ _RESPONSE_BODY_MAX_BYTES = 8192
185
+
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."""
189
+ if response is None:
190
+ return None
191
+ try:
192
+ from django.http import StreamingHttpResponse
193
+
194
+ if isinstance(response, StreamingHttpResponse):
195
+ return None
196
+ except Exception:
197
+ pass
198
+ try:
199
+ content = getattr(response, "content", None)
200
+ if content is None:
201
+ return None
202
+ if not isinstance(content, (bytes, bytearray)):
203
+ return str(content)
204
+ raw = bytes(content)
205
+ if len(raw) > max_bytes:
206
+ raw = raw[:max_bytes]
207
+ suffix = "...[truncated]"
208
+ else:
209
+ suffix = ""
210
+ return raw.decode("utf-8", errors="replace") + suffix
211
+ except Exception:
212
+ return None
213
+
214
+
215
+ def format_request_user(user_val: Any) -> str:
216
+ if not user_val:
217
+ return "anonymous"
218
+ if isinstance(user_val, dict):
219
+ uid = user_val.get("id")
220
+ if uid is not None:
221
+ return str(uid)
222
+ return str(user_val)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: insider-python
3
- Version: 0.1.6
3
+ Version: 0.1.7
4
4
  Summary: Python SDK for Insider — ship Beacons to your Insider server.
5
5
  Author: Insider
6
6
  License-Expression: MIT
@@ -43,6 +43,17 @@ def test_integration_captures_view_exception(client, fake_transport):
43
43
  assert fp["stack_trace"]["value"] == "intentional explosion"
44
44
 
45
45
 
46
+ @pytest.mark.django_db
47
+ def test_integration_captures_response_body_when_pii_enabled(
48
+ sdk_client, client, fake_transport
49
+ ):
50
+ sdk_client.send_default_pii = True
51
+ response = client.get("/ok/")
52
+ assert response.status_code == 200
53
+ assert len(fake_transport.envelopes) == 1
54
+ assert fake_transport.envelopes[0].get("response_body") == "ok"
55
+
56
+
46
57
  @pytest.mark.django_db
47
58
  def test_integration_clean_request_emits_one_footprint(client, fake_transport):
48
59
  response = client.get("/ok/")
File without changes
File without changes