insider-python 0.1.5__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.
- {insider_python-0.1.5 → insider_python-0.1.7}/PKG-INFO +1 -1
- {insider_python-0.1.5 → insider_python-0.1.7}/pyproject.toml +1 -1
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/_version.py +1 -1
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/client.py +12 -1
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/asgi.py +21 -1
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/handler.py +7 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/request.py +41 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider_python.egg-info/PKG-INFO +1 -1
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_django_integration.py +11 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/README.md +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/setup.cfg +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/__init__.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/_envelope.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/_footprint.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/contrib/__init__.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/contrib/django/__init__.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/contrib/django/apps.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/contrib/django/middleware.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/dsn.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/__init__.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/__init__.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/capture.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/drf.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/perf.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/signals.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/django/wsgi.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/logging/__init__.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/logging/handler.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/integrations/logging/levels.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/py.typed +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/safety.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/scope.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/scrubbing.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/stacktrace.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider/transport.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider_python.egg-info/SOURCES.txt +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider_python.egg-info/dependency_links.txt +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider_python.egg-info/requires.txt +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/src/insider_python.egg-info/top_level.txt +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_asgi_integration.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_capture.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_capture_log.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_capture_perf.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_django.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_drf_integration.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_dsn.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_envelope.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_logging_integration.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_never_crash.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_safety.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_scrubbing.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_stacktrace.py +0 -0
- {insider_python-0.1.5 → insider_python-0.1.7}/tests/test_transport.py +0 -0
|
@@ -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=
|
|
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)
|
|
@@ -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
|
|
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
|
|
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
|
|
File without changes
|
{insider_python-0.1.5 → insider_python-0.1.7}/src/insider_python.egg-info/dependency_links.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|