insider-python 0.1.4__py3-none-any.whl → 0.3.0__py3-none-any.whl

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/_version.py CHANGED
@@ -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.4"
8
+ __version__ = "0.3.0"
@@ -2,24 +2,29 @@
2
2
  Sentry-style Django integration.
3
3
 
4
4
  Install with `insider.init(..., integrations=[DjangoIntegration()])` in
5
- `wsgi.py` / `asgi.py` before `get_wsgi_application()`. No middleware or
6
- `INSTALLED_APPS` wiring required.
5
+ `wsgi.py` / `asgi.py` before `get_wsgi_application()` or
6
+ `get_asgi_application()`. No middleware or `INSTALLED_APPS` wiring required.
7
7
 
8
8
  Hooks installed (each once per process):
9
9
 
10
10
  - `got_request_exception` → auto-capture unhandled view errors
11
- - `BaseHandler.get_response` → request context on the SDK scope
12
- - `WSGIHandler.__call__` → capture catastrophic escapes
11
+ - `BaseHandler.get_response` → request context on the SDK scope (WSGI + ASGI HTTP)
12
+ - `WSGIHandler.__call__` → capture catastrophic WSGI escapes
13
+ - `ASGIHandler.__call__` → capture catastrophic ASGI escapes
13
14
  - `APIView.initial` (when DRF is present) → DRF request body access
14
15
  - Auto `kind=request` beacon per HTTP request (optional, default on)
16
+
17
+ Channels / `ProtocolTypeRouter`: use `wrap_asgi_application()` on the HTTP
18
+ branch with `DjangoIntegration(auto_perf=False)` — see `asgi.py`.
15
19
  """
16
20
 
17
21
  from __future__ import annotations
18
22
 
19
23
  import threading
24
+ from typing import Any, Dict
20
25
 
21
- from ...safety import debug, safe
22
- from . import drf, handler, signals, wsgi
26
+ from ...safety import debug
27
+ from . import asgi, drf, handler, signals, wsgi
23
28
 
24
29
 
25
30
  class DjangoIntegration:
@@ -34,27 +39,83 @@ class DjangoIntegration:
34
39
  """
35
40
  Args:
36
41
  auto_perf: When True (default), emit one `kind=request` beacon
37
- after every HTTP request. Disable for high-traffic apps until
38
- server-side sampling lands.
42
+ after every HTTP request via the `get_response` patch.
43
+ Set False when using `wrap_asgi_application()` on the HTTP
44
+ branch of a Channels router (avoids double capture).
39
45
  """
40
46
  self.auto_perf = auto_perf
41
47
 
42
- @safe
43
48
  def setup_once(self) -> None:
44
49
  cls = type(self)
45
50
  with cls._lock:
46
51
  if cls._installed:
47
52
  return
48
- cls._installed = True
49
53
 
50
54
  try:
51
55
  import django # noqa: F401
52
56
  except ImportError:
53
57
  debug("DjangoIntegration: django is not installed")
54
- cls._installed = False
55
58
  return
56
59
 
57
- signals.install()
60
+ # Patch get_response first. Optional hooks below must not block this.
58
61
  handler.install(auto_perf=self.auto_perf)
59
- wsgi.install()
60
- drf.install()
62
+ if not handler._patched:
63
+ debug(
64
+ "DjangoIntegration: get_response patch failed — "
65
+ "call insider.init() before get_wsgi_application() / "
66
+ "get_asgi_application()"
67
+ )
68
+ return
69
+
70
+ with cls._lock:
71
+ cls._installed = True
72
+
73
+ for label, install in (
74
+ ("signals", signals.install),
75
+ ("wsgi", wsgi.install),
76
+ ("asgi", asgi.install),
77
+ ("drf", drf.install),
78
+ ):
79
+ try:
80
+ install()
81
+ except Exception as exc:
82
+ debug(f"DjangoIntegration: {label} hook failed: {exc}")
83
+
84
+ _log_integration_status()
85
+
86
+ @classmethod
87
+ def reset_for_tests(cls) -> None:
88
+ """Test helper — allow `setup_once()` to run again in the same process."""
89
+ with cls._lock:
90
+ cls._installed = False
91
+
92
+
93
+ def get_integration_status() -> Dict[str, Any]:
94
+ """Return which Django hooks are active in this process (tests + debug)."""
95
+ return {
96
+ "handler": handler._patched,
97
+ "handler_auto_perf": handler._auto_perf,
98
+ "wsgi": wsgi._patched,
99
+ "asgi_handler": asgi._handler_patched,
100
+ "signals": signals._connected,
101
+ "response_for_exception": signals._rfe_patched,
102
+ "drf": drf._patched,
103
+ }
104
+
105
+
106
+ def _log_integration_status() -> None:
107
+ status = get_integration_status()
108
+ debug(
109
+ "DjangoIntegration: "
110
+ + ", ".join(f"{key}={value}" for key, value in status.items())
111
+ )
112
+
113
+
114
+ __all__ = [
115
+ "DjangoIntegration",
116
+ "get_integration_status",
117
+ "wrap_asgi_application",
118
+ ]
119
+
120
+ # Re-export for `from insider.integrations.django.asgi import ...`
121
+ from .asgi import wrap_asgi_application # noqa: E402
@@ -0,0 +1,123 @@
1
+ """
2
+ ASGI support for Django — escape capture and optional HTTP wrapper.
3
+
4
+ `install()` patches `ASGIHandler` (mirrors `wsgi.install()`).
5
+
6
+ `wrap_asgi_application()` wraps the HTTP branch of a Channels
7
+ `ProtocolTypeRouter` (or plain `get_asgi_application()`). Use with
8
+ `DjangoIntegration(auto_perf=False)` so footprints are not emitted twice.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import time
14
+ import uuid
15
+ from typing import Any, Callable, Dict, MutableMapping, Optional
16
+
17
+ from ... import capture_exception
18
+ from ...client import _client
19
+ from ...safety import debug, safe
20
+ from ...stacktrace import exception_payload
21
+ from .perf import emit_http_footprint
22
+ from .request import build_request_ctx_from_scope
23
+
24
+ ASGIApp = Callable[..., Any]
25
+
26
+ _handler_patched = False
27
+
28
+
29
+ def install() -> None:
30
+ """Patch ASGIHandler to capture exceptions that escape Django entirely."""
31
+ global _handler_patched
32
+ if _handler_patched:
33
+ return
34
+ try:
35
+ from django.core.handlers.asgi import ASGIHandler
36
+ except ImportError:
37
+ debug("django ASGIHandler unavailable; skipping ASGI patch")
38
+ return
39
+
40
+ old_call = ASGIHandler.__call__
41
+
42
+ @safe
43
+ async def patched_call(
44
+ self: Any,
45
+ scope: MutableMapping[str, Any],
46
+ receive: Callable[..., Any],
47
+ send: Callable[..., Any],
48
+ ) -> Any:
49
+ if _client() is None:
50
+ return await old_call(self, scope, receive, send)
51
+ try:
52
+ return await old_call(self, scope, receive, send)
53
+ except BaseException as exc:
54
+ capture_exception(exc)
55
+ raise
56
+
57
+ ASGIHandler.__call__ = patched_call # type: ignore[method-assign]
58
+ _handler_patched = True
59
+
60
+
61
+ def wrap_asgi_application(application: ASGIApp) -> ASGIApp:
62
+ """
63
+ Wrap a Django (or other) ASGI HTTP app to emit one footprint per request.
64
+
65
+ Pass the return value of `get_asgi_application()` or the ``"http"`` branch
66
+ of a ``ProtocolTypeRouter``. Pair with ``DjangoIntegration(auto_perf=False)``.
67
+ """
68
+ return _InsiderAsgiHttpWrapper(application)
69
+
70
+
71
+ class _InsiderAsgiHttpWrapper:
72
+ def __init__(self, app: ASGIApp) -> None:
73
+ self.app = app
74
+
75
+ async def __call__(
76
+ self,
77
+ scope: MutableMapping[str, Any],
78
+ receive: Callable[..., Any],
79
+ send: Callable[..., Any],
80
+ ) -> None:
81
+ if scope.get("type") != "http":
82
+ await self.app(scope, receive, send)
83
+ return
84
+
85
+ client = _client()
86
+ if client is None:
87
+ await self.app(scope, receive, send)
88
+ return
89
+
90
+ trace_id = uuid.uuid4().hex
91
+ client.scope.set_trace_id(trace_id)
92
+ client.scope.set_request(
93
+ build_request_ctx_from_scope(scope, client.send_default_pii)
94
+ )
95
+
96
+ start = time.perf_counter()
97
+ status_code: Optional[int] = None
98
+
99
+ async def send_wrapper(message: Dict[str, Any]) -> None:
100
+ nonlocal status_code
101
+ if message.get("type") == "http.response.start":
102
+ status_code = int(message.get("status", 500))
103
+ await send(message)
104
+
105
+ try:
106
+ await self.app(scope, receive, send_wrapper)
107
+ except BaseException as exc:
108
+ block = exception_payload(
109
+ exc, in_app_include=client.scope.static.in_app_include
110
+ )
111
+ client.scope.set_pending_exception(block)
112
+ raise
113
+ finally:
114
+ path = str(scope.get("path") or "/")
115
+ method = str(scope.get("method") or "GET")
116
+ emit_http_footprint(
117
+ path=path,
118
+ method=method,
119
+ duration_ms=(time.perf_counter() - start) * 1000.0,
120
+ status_code=status_code,
121
+ trace_id=trace_id,
122
+ )
123
+ client.scope.clear_request_cycle()
@@ -16,6 +16,19 @@ from ...stacktrace import exception_payload
16
16
  from .request import build_request_ctx
17
17
 
18
18
  _CAPTURED_ATTR = "_insider_exception_captured"
19
+ _PENDING_BLOCK_ATTR = "_insider_pending_exception_block"
20
+
21
+
22
+ def sync_pending_from_request(request: Any) -> None:
23
+ """Copy exception buffered on the request (async worker thread) onto scope."""
24
+ client = _client()
25
+ if client is None:
26
+ return
27
+ if client.scope.current_pending_exception() is not None:
28
+ return
29
+ block = getattr(request, _PENDING_BLOCK_ATTR, None)
30
+ if block is not None:
31
+ client.scope.set_pending_exception(block)
19
32
 
20
33
 
21
34
  @safe
@@ -42,4 +55,5 @@ def capture_request_exception(request: Any, exception: BaseException) -> None:
42
55
  block = exception_payload(
43
56
  exception, in_app_include=client.scope.static.in_app_include
44
57
  )
58
+ setattr(request, _PENDING_BLOCK_ATTR, block)
45
59
  client.scope.set_pending_exception(block)
@@ -1,13 +1,17 @@
1
1
  """
2
- Patch `BaseHandler.get_response` for request context and auto perf timing.
2
+ Patch `BaseHandler.get_response` / `get_response_async` for request context
3
+ and auto perf timing.
3
4
 
4
- We patch `get_response` (not middleware) because:
5
+ We patch the handler (not middleware) because:
5
6
 
6
- - Customers already init in `wsgi.py` without touching INSTALLED_APPS.
7
+ - Customers already init in `wsgi.py` / `asgi.py` without touching INSTALLED_APPS.
7
8
  - The patch wraps the entire handler, including middleware inside Django's
8
9
  request cycle.
9
10
  - Perf timing in `finally` runs once per request whether the view returns
10
11
  200 or raises (converted to 500 by Django).
12
+
13
+ Django 4.1+ ASGI uses `get_response_async` for the async request path
14
+ (Daphne, `AsyncClient`). Both sync and async entrypoints are patched.
11
15
  """
12
16
 
13
17
  from __future__ import annotations
@@ -19,12 +23,39 @@ from typing import Any, Callable
19
23
  from ...client import _client
20
24
  from ...safety import debug, safe
21
25
  from .perf import emit_request_envelope
22
- from .request import build_request_ctx
26
+ from .capture import sync_pending_from_request
23
27
 
24
28
  _patched = False
25
29
  _auto_perf = True
26
30
 
27
31
 
32
+ def _finalize_request_cycle(
33
+ request: Any,
34
+ *,
35
+ start: float,
36
+ response: Any,
37
+ status_code: int | None,
38
+ trace_id: str,
39
+ ) -> None:
40
+ client = _client()
41
+ if client is None:
42
+ return
43
+ duration_ms = (time.perf_counter() - start) * 1000.0
44
+ if status_code is None and response is not None:
45
+ status_code = getattr(response, "status_code", None)
46
+ sync_pending_from_request(request)
47
+ if _auto_perf:
48
+ emit_request_envelope(
49
+ request,
50
+ duration_ms=duration_ms,
51
+ status_code=status_code,
52
+ trace_id=trace_id,
53
+ )
54
+ client.scope.clear_request_cycle()
55
+ # When auto_perf=False (e.g. wrap_asgi_application), leave scope intact
56
+ # for the outer ASGI wrapper to emit one combined footprint.
57
+
58
+
28
59
  def install(*, auto_perf: bool = True) -> None:
29
60
  global _patched, _auto_perf
30
61
  _auto_perf = auto_perf
@@ -37,6 +68,7 @@ def install(*, auto_perf: bool = True) -> None:
37
68
  return
38
69
 
39
70
  old_get_response = BaseHandler.get_response
71
+ old_get_response_async = BaseHandler.get_response_async
40
72
 
41
73
  @safe
42
74
  def patched_get_response(self: Any, request: Any) -> Any:
@@ -46,6 +78,8 @@ def install(*, auto_perf: bool = True) -> None:
46
78
 
47
79
  trace_id = uuid.uuid4().hex
48
80
  client.scope.set_trace_id(trace_id)
81
+ from .request import build_request_ctx
82
+
49
83
  ctx = build_request_ctx(request, client.send_default_pii)
50
84
  client.scope.set_request(ctx)
51
85
 
@@ -57,15 +91,43 @@ def install(*, auto_perf: bool = True) -> None:
57
91
  status_code = getattr(response, "status_code", None)
58
92
  return response
59
93
  finally:
60
- duration_ms = (time.perf_counter() - start) * 1000.0
61
- if _auto_perf:
62
- emit_request_envelope(
63
- request,
64
- duration_ms=duration_ms,
65
- status_code=status_code,
66
- trace_id=trace_id,
67
- )
68
- client.scope.clear_request_cycle()
94
+ _finalize_request_cycle(
95
+ request,
96
+ start=start,
97
+ response=response,
98
+ status_code=status_code,
99
+ trace_id=trace_id,
100
+ )
101
+
102
+ @safe
103
+ async def patched_get_response_async(self: Any, request: Any) -> Any:
104
+ client = _client()
105
+ if client is None:
106
+ return await old_get_response_async(self, request)
107
+
108
+ trace_id = uuid.uuid4().hex
109
+ client.scope.set_trace_id(trace_id)
110
+ from .request import build_request_ctx
111
+
112
+ ctx = build_request_ctx(request, client.send_default_pii)
113
+ client.scope.set_request(ctx)
114
+
115
+ start = time.perf_counter()
116
+ response = None
117
+ status_code: int | None = None
118
+ try:
119
+ response = await old_get_response_async(self, request)
120
+ status_code = getattr(response, "status_code", None)
121
+ return response
122
+ finally:
123
+ _finalize_request_cycle(
124
+ request,
125
+ start=start,
126
+ response=response,
127
+ status_code=status_code,
128
+ trace_id=trace_id,
129
+ )
69
130
 
70
131
  BaseHandler.get_response = patched_get_response # type: ignore[method-assign]
132
+ BaseHandler.get_response_async = patched_get_response_async # type: ignore[method-assign]
71
133
  _patched = True
@@ -14,9 +14,10 @@ from ...safety import safe
14
14
 
15
15
 
16
16
  @safe
17
- def emit_request_envelope(
18
- request: Any,
17
+ def emit_http_footprint(
19
18
  *,
19
+ path: str,
20
+ method: Optional[str],
20
21
  duration_ms: float,
21
22
  status_code: Optional[int],
22
23
  trace_id: Optional[str],
@@ -26,9 +27,7 @@ def emit_request_envelope(
26
27
  if client is None:
27
28
  return
28
29
 
29
- method = getattr(request, "method", None)
30
- op = getattr(request, "path", None) or "unknown"
31
-
30
+ op = path or "unknown"
32
31
  client.capture_request(
33
32
  duration_ms=duration_ms,
34
33
  op=op,
@@ -36,3 +35,21 @@ def emit_request_envelope(
36
35
  method=method,
37
36
  trace_id=trace_id,
38
37
  )
38
+
39
+
40
+ @safe
41
+ def emit_request_envelope(
42
+ request: Any,
43
+ *,
44
+ duration_ms: float,
45
+ status_code: Optional[int],
46
+ trace_id: Optional[str],
47
+ ) -> None:
48
+ """Build and ship a single `kind=request` beacon from a Django request."""
49
+ emit_http_footprint(
50
+ path=getattr(request, "path", None) or "unknown",
51
+ method=getattr(request, "method", None),
52
+ duration_ms=duration_ms,
53
+ status_code=status_code,
54
+ trace_id=trace_id,
55
+ )
@@ -39,6 +39,47 @@ def attach_drf_request_backref(django_request: Any, drf_request: Any) -> None:
39
39
  debug(f"drf request backref failed: {exc}")
40
40
 
41
41
 
42
+ def build_request_ctx_from_scope(
43
+ scope: Any,
44
+ send_default_pii: bool,
45
+ ) -> Dict[str, Any]:
46
+ """Build request context from a raw ASGI HTTP scope."""
47
+ try:
48
+ headers: Dict[str, str] = {}
49
+ for key, value in scope.get("headers") or []:
50
+ try:
51
+ name = key.decode("latin-1").lower().replace("_", "-")
52
+ val = value.decode("latin-1")
53
+ except Exception:
54
+ continue
55
+ if name not in _SAFE_HEADERS and len(val) > 4096:
56
+ continue
57
+ headers[name] = val
58
+
59
+ query_raw = scope.get("query_string") or b""
60
+ query = (
61
+ query_raw.decode("latin-1", errors="replace") if query_raw else None
62
+ )
63
+ path = scope.get("path")
64
+ method = scope.get("method")
65
+ client = scope.get("client")
66
+ ip = client[0] if client else None
67
+
68
+ ctx: Dict[str, Any] = {
69
+ "method": method,
70
+ "path": path,
71
+ "query_string": query,
72
+ "headers": headers,
73
+ "ip": ip,
74
+ }
75
+ if send_default_pii and ip:
76
+ ctx["ip_address"] = ip
77
+ return ctx
78
+ except Exception as exc:
79
+ debug(f"asgi scope ctx build failed: {exc}")
80
+ return {}
81
+
82
+
42
83
  def build_request_ctx(request: Any, send_default_pii: bool) -> Dict[str, Any]:
43
84
  try:
44
85
  request = _resolve_drf_request(request)
@@ -1,30 +1,70 @@
1
1
  """
2
- Connect Django's `got_request_exception` signal to Insider capture.
2
+ Connect Django's exception path to Insider capture.
3
+
4
+ Patches ``response_for_exception`` so the exception object is captured
5
+ reliably on both WSGI and ASGI (``got_request_exception`` alone is not
6
+ enough — ``sys.exc_info()`` is often cleared before the signal handler runs).
3
7
  """
4
8
 
5
9
  from __future__ import annotations
6
10
 
7
11
  import sys
8
- from typing import Any
12
+ from typing import Any, Tuple, Type
9
13
 
10
14
  from ...safety import debug, safe
11
15
  from .capture import capture_request_exception
12
16
 
13
17
  _connected = False
18
+ _rfe_patched = False
19
+
20
+ # These are converted to 4xx responses without emitting got_request_exception.
21
+ _HANDLED_QUIETLY: Tuple[Type[BaseException], ...] = ()
14
22
 
15
23
 
16
24
  def install() -> None:
17
- global _connected
18
- if _connected:
25
+ global _connected, _rfe_patched, _HANDLED_QUIETLY
26
+ if _connected and _rfe_patched:
19
27
  return
20
28
  try:
21
29
  from django.core import signals
30
+ from django.core.exceptions import (
31
+ BadRequest,
32
+ PermissionDenied,
33
+ SuspiciousOperation,
34
+ )
35
+ from django.core.handlers import exception as exception_module
36
+ from django.http import Http404
37
+ from django.http.multipartparser import MultiPartParserError
22
38
  except ImportError:
23
- debug("django signals unavailable; skipping got_request_exception hook")
39
+ debug("django signals unavailable; skipping exception hooks")
24
40
  return
25
41
 
26
- signals.got_request_exception.connect(_on_got_request_exception)
27
- _connected = True
42
+ _HANDLED_QUIETLY = (
43
+ Http404,
44
+ PermissionDenied,
45
+ MultiPartParserError,
46
+ BadRequest,
47
+ SuspiciousOperation,
48
+ )
49
+
50
+ if not _rfe_patched:
51
+ old_rfe = exception_module.response_for_exception
52
+
53
+ @safe
54
+ def patched_rfe(request: Any, exc: BaseException) -> Any:
55
+ response = old_rfe(request, exc)
56
+ if not isinstance(exc, _HANDLED_QUIETLY) and getattr(
57
+ response, "status_code", 500
58
+ ) >= 500:
59
+ capture_request_exception(request, exc)
60
+ return response
61
+
62
+ exception_module.response_for_exception = patched_rfe # type: ignore[attr-defined]
63
+ _rfe_patched = True
64
+
65
+ if not _connected:
66
+ signals.got_request_exception.connect(_on_got_request_exception)
67
+ _connected = True
28
68
 
29
69
 
30
70
  @safe
insider/safety.py CHANGED
@@ -50,6 +50,9 @@ def safe(fn: F) -> F:
50
50
  the boundary functions catch them.
51
51
  """
52
52
 
53
+ if asyncio_iscoroutinefunction(fn):
54
+ return safe_async(fn) # type: ignore[return-value]
55
+
53
56
  @functools.wraps(fn)
54
57
  def wrapper(*args: Any, **kwargs: Any) -> Any:
55
58
  try:
@@ -59,3 +62,26 @@ def safe(fn: F) -> F:
59
62
  return None
60
63
 
61
64
  return wrapper # type: ignore[return-value]
65
+
66
+
67
+ def asyncio_iscoroutinefunction(fn: Any) -> bool:
68
+ try:
69
+ import asyncio
70
+
71
+ return asyncio.iscoroutinefunction(fn)
72
+ except Exception:
73
+ return False
74
+
75
+
76
+ def safe_async(fn: F) -> F:
77
+ """Like `@safe` for async functions — preserves the coroutine contract."""
78
+
79
+ @functools.wraps(fn)
80
+ async def wrapper(*args: Any, **kwargs: Any) -> Any:
81
+ try:
82
+ return await fn(*args, **kwargs)
83
+ except Exception as exc:
84
+ debug(f"swallowed {type(exc).__name__} in {fn.__qualname__}: {exc}")
85
+ return None
86
+
87
+ return wrapper # type: ignore[return-value]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: insider-python
3
- Version: 0.1.4
3
+ Version: 0.3.0
4
4
  Summary: Python SDK for Insider — ship Beacons to your Insider server.
5
5
  Author: Insider
6
6
  License-Expression: MIT
@@ -70,7 +70,10 @@ Out-of-band events (background jobs, explicit calls) use standalone beacons:
70
70
 
71
71
  ### Django
72
72
 
73
- Initialize in `wsgi.py` (or `asgi.py`) before `get_wsgi_application()`:
73
+ Initialize in `wsgi.py` or `asgi.py` **before** `get_wsgi_application()` /
74
+ `get_asgi_application()`:
75
+
76
+ #### WSGI (Gunicorn)
74
77
 
75
78
  ```python
76
79
  import os
@@ -78,6 +81,8 @@ import insider
78
81
  from insider.integrations.django import DjangoIntegration
79
82
  from insider.integrations.logging import LoggingIntegration
80
83
 
84
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
85
+
81
86
  insider.init(
82
87
  dsn=os.environ.get("INSIDER_DSN"),
83
88
  environment="production",
@@ -90,6 +95,65 @@ from django.core.wsgi import get_wsgi_application
90
95
  application = get_wsgi_application()
91
96
  ```
92
97
 
98
+ #### ASGI (Daphne / Uvicorn — plain Django)
99
+
100
+ ```python
101
+ import os
102
+ import insider
103
+ from insider.integrations.django import DjangoIntegration
104
+ from insider.integrations.logging import LoggingIntegration
105
+
106
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
107
+
108
+ insider.init(
109
+ dsn=os.environ.get("INSIDER_DSN"),
110
+ environment="production",
111
+ release="1.2.3",
112
+ enable_logs=True,
113
+ integrations=[DjangoIntegration(), LoggingIntegration()],
114
+ )
115
+
116
+ from django.core.asgi import get_asgi_application
117
+ application = get_asgi_application()
118
+ ```
119
+
120
+ #### ASGI + Channels (`ProtocolTypeRouter`)
121
+
122
+ Wrap only the HTTP branch; disable handler auto-perf to avoid double capture:
123
+
124
+ ```python
125
+ import os
126
+ import django
127
+
128
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings")
129
+
130
+ import django
131
+
132
+ django.setup()
133
+
134
+ import insider
135
+ from insider.integrations.django import DjangoIntegration
136
+ from insider.integrations.django.asgi import wrap_asgi_application
137
+ from insider.integrations.logging import LoggingIntegration
138
+
139
+ insider.init(
140
+ dsn=os.environ.get("INSIDER_DSN"),
141
+ environment="production",
142
+ enable_logs=True,
143
+ integrations=[DjangoIntegration(auto_perf=False), LoggingIntegration()],
144
+ )
145
+
146
+ from channels.routing import ProtocolTypeRouter, URLRouter
147
+ from django.core.asgi import get_asgi_application
148
+
149
+ application = ProtocolTypeRouter({
150
+ "http": wrap_asgi_application(get_asgi_application()),
151
+ "websocket": URLRouter(websocket_urlpatterns),
152
+ })
153
+ ```
154
+
155
+ Set `INSIDER_DEBUG=true` to print which hooks installed at startup.
156
+
93
157
  That's the whole setup. **Every HTTP request** emits **one** `kind=request`
94
158
  beacon containing:
95
159
 
@@ -1,11 +1,11 @@
1
1
  insider/__init__.py,sha256=5vkZk0y10671uwRx4bvZB05b6CNUsH5jYEsBSOoGiw0,1011
2
2
  insider/_envelope.py,sha256=g7VQh4fBRVStzU4n7E_3bxij_x_Kw3p_IaAo9Fh94RE,6690
3
3
  insider/_footprint.py,sha256=d123PGVSbmvy5uEXEfQ44h6-V28uGMWjiDBfVcc2Du8,2072
4
- insider/_version.py,sha256=U8iF9nQf84cNN1x6CZXSB-I7SAnu3eG05meavOEi67c,246
4
+ insider/_version.py,sha256=UEiwKKTe6TALAXOWbaJXXvR3Tsia52swt986qSpnCig,246
5
5
  insider/client.py,sha256=Bn8f0JQ7igzTjyEyig2mctBzb-hUyIxut6wRJW-RYeI,22085
6
6
  insider/dsn.py,sha256=S8BbRhmKKKU1N6W8cpMSQoyDO_EuUJ1PkcjhGrrf5hw,3009
7
7
  insider/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
- insider/safety.py,sha256=mtzzWLujuvmd24W2VtVcMGO_81GPBbB2tYuVqU2KsuI,1910
8
+ insider/safety.py,sha256=OqvVbCLMDANzk7HmtPjr19CAzbi6FoX0fRbL6BOvJf8,2626
9
9
  insider/scope.py,sha256=k6FeNTKhkBjR7AwAfvWBTZ4BnvKMoe3OkSX55DgRiO8,6105
10
10
  insider/scrubbing.py,sha256=U8N3MS5VtDUNpIWN4PZ8bvrPqmjRRXq5hWGXxzoF5Gw,2853
11
11
  insider/stacktrace.py,sha256=tgdS9dQ-bVyz59_874BstuCYaP6rGn2k6HMo9sW8Ks8,5310
@@ -15,18 +15,19 @@ insider/contrib/django/__init__.py,sha256=UMGrJgVO-FqszZo9Cviw7oWS8PLdXVHt3hwXZ_
15
15
  insider/contrib/django/apps.py,sha256=Ct584Pa-S9I82omP3t_Xsn4NrdlqfoNf0eUN9xFa7Hs,2036
16
16
  insider/contrib/django/middleware.py,sha256=lX5sGDZw4rJ6gsNEcOpqHspkvzqilxTlUxOszQxwzpI,2077
17
17
  insider/integrations/__init__.py,sha256=Obf8BJS2kngbCeHCBPCik0jY30_F-sk3qHMCJ1heIFY,616
18
- insider/integrations/django/__init__.py,sha256=AIT4_mUUkQDBZzsSAmLAu38u_zZ705sJ1aj0FI4KJsA,1760
19
- insider/integrations/django/capture.py,sha256=X5X0_3Ruh3Qfnsr7BRJk5nPPW2UdsTUPMRAgXxTkhsA,1305
18
+ insider/integrations/django/__init__.py,sha256=HhVeMP3erdA5TYStTIJv4nQN3aAVwjaUiooI-Jl2cF0,3830
19
+ insider/integrations/django/asgi.py,sha256=s9eCAtGC45PDvS1yPhKr9I103aicSmjq83G2WpUW_Nk,3801
20
+ insider/integrations/django/capture.py,sha256=22tea2oSFZQ03M4pKNKxTotoqXG9svi1QPbBP_20K4M,1818
20
21
  insider/integrations/django/drf.py,sha256=MXY4erP9gPBiEvf8Hi0CLrUBczXY1o7Y5wVCjSsQeCY,899
21
- insider/integrations/django/handler.py,sha256=AqkYyzpuILfAIi43QUdyVqEn0g0Oc64wWlE_WFaZEAc,2174
22
- insider/integrations/django/perf.py,sha256=2B0hvCQuuWQubDdGcAetF1pzkdTLvQWDDAxLwT-htDU,916
23
- insider/integrations/django/request.py,sha256=pOuQxnFWeA3KPAc0eVaRtkxGRyr2PetGKEg7OlTnqF4,4181
24
- insider/integrations/django/signals.py,sha256=kwH7npDG3AC9MILB9f4YUYofy5iwOosapGkBm_3dZY4,802
22
+ insider/integrations/django/handler.py,sha256=04fH28hBJzDS5n2pmwZkkBA4is9Sk29rMCh_XjEWAK0,4154
23
+ insider/integrations/django/perf.py,sha256=i5pp3Y3niw4MEFsdRdCOatyrhcxC1imunnePyBVq-xQ,1331
24
+ insider/integrations/django/request.py,sha256=8XhzQgG9Zb2p8UFNt_XEyTDGX9ibSEBnC4WGJSv_XHE,5441
25
+ insider/integrations/django/signals.py,sha256=KFyZ3A0_Jpp0461lAaB_PmsNMAKExQOy73vvRNbWUso,2271
25
26
  insider/integrations/django/wsgi.py,sha256=kJ5qo5LUdRYJtp4NkiD38DZzbb__idJU6FAltYhgtus,1044
26
27
  insider/integrations/logging/__init__.py,sha256=vjsVWFmC1j6v-ZIcWtOP24JkvbnmvgHYlgpYP2jwR4g,1805
27
28
  insider/integrations/logging/handler.py,sha256=s_RfCuVr02I9D6igzgju2vnx0x1qUENyVFtwP5eguFs,2019
28
29
  insider/integrations/logging/levels.py,sha256=yYkt_vhJLMigiCj0Bk7UYf3hWpYO90JMp-pvRdQSan8,646
29
- insider_python-0.1.4.dist-info/METADATA,sha256=CtXFkqSPjtWNkIOiUmNciPnTohQZ-Rri4EyWoPGvwxo,4690
30
- insider_python-0.1.4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
31
- insider_python-0.1.4.dist-info/top_level.txt,sha256=g_YKp2jCaaefmasZ2nOa9capm0X8q2sAWI_eEClKIos,8
32
- insider_python-0.1.4.dist-info/RECORD,,
30
+ insider_python-0.3.0.dist-info/METADATA,sha256=NtDP6hxdo9FkCOOILKGd3fNKaER6QVg4PrQicFUtgPU,6346
31
+ insider_python-0.3.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
32
+ insider_python-0.3.0.dist-info/top_level.txt,sha256=g_YKp2jCaaefmasZ2nOa9capm0X8q2sAWI_eEClKIos,8
33
+ insider_python-0.3.0.dist-info/RECORD,,