insider-python 0.1.0__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.0/PKG-INFO +125 -0
- insider_python-0.1.0/README.md +99 -0
- insider_python-0.1.0/pyproject.toml +50 -0
- insider_python-0.1.0/setup.cfg +4 -0
- insider_python-0.1.0/src/insider/__init__.py +37 -0
- insider_python-0.1.0/src/insider/_envelope.py +189 -0
- insider_python-0.1.0/src/insider/_version.py +8 -0
- insider_python-0.1.0/src/insider/client.py +344 -0
- insider_python-0.1.0/src/insider/contrib/__init__.py +0 -0
- insider_python-0.1.0/src/insider/contrib/django/__init__.py +27 -0
- insider_python-0.1.0/src/insider/contrib/django/apps.py +60 -0
- insider_python-0.1.0/src/insider/contrib/django/middleware.py +164 -0
- insider_python-0.1.0/src/insider/dsn.py +92 -0
- insider_python-0.1.0/src/insider/py.typed +0 -0
- insider_python-0.1.0/src/insider/safety.py +61 -0
- insider_python-0.1.0/src/insider/scope.py +54 -0
- insider_python-0.1.0/src/insider/scrubbing.py +91 -0
- insider_python-0.1.0/src/insider/stacktrace.py +153 -0
- insider_python-0.1.0/src/insider/transport.py +213 -0
- insider_python-0.1.0/src/insider_python.egg-info/PKG-INFO +125 -0
- insider_python-0.1.0/src/insider_python.egg-info/SOURCES.txt +31 -0
- insider_python-0.1.0/src/insider_python.egg-info/dependency_links.txt +1 -0
- insider_python-0.1.0/src/insider_python.egg-info/requires.txt +9 -0
- insider_python-0.1.0/src/insider_python.egg-info/top_level.txt +1 -0
- insider_python-0.1.0/tests/test_capture.py +107 -0
- insider_python-0.1.0/tests/test_django.py +101 -0
- insider_python-0.1.0/tests/test_dsn.py +57 -0
- insider_python-0.1.0/tests/test_envelope.py +95 -0
- insider_python-0.1.0/tests/test_never_crash.py +138 -0
- insider_python-0.1.0/tests/test_safety.py +44 -0
- insider_python-0.1.0/tests/test_scrubbing.py +55 -0
- insider_python-0.1.0/tests/test_stacktrace.py +99 -0
- insider_python-0.1.0/tests/test_transport.py +124 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: insider-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for Insider — ship Beacons to your Insider server.
|
|
5
|
+
Author: Insider
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Insider-Inc/insider-python
|
|
8
|
+
Keywords: insider,telemetry,errors,monitoring,sdk
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
Requires-Dist: urllib3>=1.26
|
|
20
|
+
Provides-Extra: django
|
|
21
|
+
Requires-Dist: django>=4.2; extra == "django"
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
24
|
+
Requires-Dist: pytest-django>=4.8; extra == "dev"
|
|
25
|
+
Requires-Dist: django>=4.2; extra == "dev"
|
|
26
|
+
|
|
27
|
+
# insider-python
|
|
28
|
+
|
|
29
|
+
The Python SDK for [Insider](https://insider.moraks.cloud/).
|
|
30
|
+
|
|
31
|
+
Beam Beacons from your Python service to your Insider server with a
|
|
32
|
+
one-line setup. No runtime overhead on your request path. Never raises
|
|
33
|
+
into your code, no matter what.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install insider-python
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
For the Django integration:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install "insider-python[django]"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Quick start
|
|
48
|
+
|
|
49
|
+
### Plain Python
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import insider
|
|
53
|
+
|
|
54
|
+
insider.init(
|
|
55
|
+
dsn="https://<beacon_token>@insider.example.com/<project_uuid>",
|
|
56
|
+
environment="production",
|
|
57
|
+
release="1.2.3",
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
risky()
|
|
62
|
+
except Exception as exc:
|
|
63
|
+
insider.capture_exception(exc)
|
|
64
|
+
|
|
65
|
+
insider.capture_message("cache miss spiked", level="warning")
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Django
|
|
69
|
+
|
|
70
|
+
Add the integration to `INSTALLED_APPS` and configure via settings:
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
INSTALLED_APPS = [
|
|
74
|
+
# ...
|
|
75
|
+
"insider.contrib.django",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
MIDDLEWARE = [
|
|
79
|
+
# ...
|
|
80
|
+
"insider.contrib.django.middleware.InsiderMiddleware",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
INSIDER_DSN = "https://<beacon_token>@insider.example.com/<project_uuid>"
|
|
84
|
+
INSIDER_ENVIRONMENT = "production"
|
|
85
|
+
INSIDER_RELEASE = "1.2.3"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
That's the whole setup. Every unhandled exception in a view is now a
|
|
89
|
+
Beacon in your dashboard.
|
|
90
|
+
|
|
91
|
+
## Configuration
|
|
92
|
+
|
|
93
|
+
Order of precedence (first wins):
|
|
94
|
+
|
|
95
|
+
1. Keyword args to `insider.init(...)`.
|
|
96
|
+
2. `INSIDER_*` environment variables.
|
|
97
|
+
3. Django settings (when the Django integration is active).
|
|
98
|
+
4. Hard-coded defaults.
|
|
99
|
+
|
|
100
|
+
If no DSN is found anywhere, the SDK enters **disabled mode**: every
|
|
101
|
+
public call is a no-op, no thread starts, no socket opens.
|
|
102
|
+
|
|
103
|
+
| Option | Default | Notes |
|
|
104
|
+
|--------|---------|-------|
|
|
105
|
+
| `dsn` | env `INSIDER_DSN` | If absent, SDK is disabled |
|
|
106
|
+
| `environment` | `"production"` | Top-level Beacon field |
|
|
107
|
+
| `release` | `None` | Top-level Beacon field |
|
|
108
|
+
| `send_default_pii` | `False` | Required to capture `user.id`, request bodies |
|
|
109
|
+
| `before_send` | `None` | `(beacon) -> beacon | None` hook |
|
|
110
|
+
| `scrub_keys` | `None` | Extra keys to filter (added to the default deny-list) |
|
|
111
|
+
| `in_app_include` | `None` | Filename prefixes considered "your code" |
|
|
112
|
+
| `transport_queue_size` | `1000` | Bounded; drops on overflow |
|
|
113
|
+
| `transport_flush_timeout` | `2.0` | Seconds. Used by `close()` / `flush()` |
|
|
114
|
+
| `debug` | `False` | Print SDK's own warnings to stderr |
|
|
115
|
+
|
|
116
|
+
## Promise
|
|
117
|
+
|
|
118
|
+
The SDK never raises into your code. Every public function catches
|
|
119
|
+
`Exception` at its boundary; if something goes wrong inside the SDK,
|
|
120
|
+
you get nothing back and a debug log (if enabled). Your app keeps
|
|
121
|
+
running.
|
|
122
|
+
|
|
123
|
+
## License
|
|
124
|
+
|
|
125
|
+
MIT
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# insider-python
|
|
2
|
+
|
|
3
|
+
The Python SDK for [Insider](https://insider.moraks.cloud/).
|
|
4
|
+
|
|
5
|
+
Beam Beacons from your Python service to your Insider server with a
|
|
6
|
+
one-line setup. No runtime overhead on your request path. Never raises
|
|
7
|
+
into your code, no matter what.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pip install insider-python
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
For the Django integration:
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install "insider-python[django]"
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Quick start
|
|
22
|
+
|
|
23
|
+
### Plain Python
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import insider
|
|
27
|
+
|
|
28
|
+
insider.init(
|
|
29
|
+
dsn="https://<beacon_token>@insider.example.com/<project_uuid>",
|
|
30
|
+
environment="production",
|
|
31
|
+
release="1.2.3",
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
try:
|
|
35
|
+
risky()
|
|
36
|
+
except Exception as exc:
|
|
37
|
+
insider.capture_exception(exc)
|
|
38
|
+
|
|
39
|
+
insider.capture_message("cache miss spiked", level="warning")
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Django
|
|
43
|
+
|
|
44
|
+
Add the integration to `INSTALLED_APPS` and configure via settings:
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
INSTALLED_APPS = [
|
|
48
|
+
# ...
|
|
49
|
+
"insider.contrib.django",
|
|
50
|
+
]
|
|
51
|
+
|
|
52
|
+
MIDDLEWARE = [
|
|
53
|
+
# ...
|
|
54
|
+
"insider.contrib.django.middleware.InsiderMiddleware",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
INSIDER_DSN = "https://<beacon_token>@insider.example.com/<project_uuid>"
|
|
58
|
+
INSIDER_ENVIRONMENT = "production"
|
|
59
|
+
INSIDER_RELEASE = "1.2.3"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
That's the whole setup. Every unhandled exception in a view is now a
|
|
63
|
+
Beacon in your dashboard.
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Order of precedence (first wins):
|
|
68
|
+
|
|
69
|
+
1. Keyword args to `insider.init(...)`.
|
|
70
|
+
2. `INSIDER_*` environment variables.
|
|
71
|
+
3. Django settings (when the Django integration is active).
|
|
72
|
+
4. Hard-coded defaults.
|
|
73
|
+
|
|
74
|
+
If no DSN is found anywhere, the SDK enters **disabled mode**: every
|
|
75
|
+
public call is a no-op, no thread starts, no socket opens.
|
|
76
|
+
|
|
77
|
+
| Option | Default | Notes |
|
|
78
|
+
|--------|---------|-------|
|
|
79
|
+
| `dsn` | env `INSIDER_DSN` | If absent, SDK is disabled |
|
|
80
|
+
| `environment` | `"production"` | Top-level Beacon field |
|
|
81
|
+
| `release` | `None` | Top-level Beacon field |
|
|
82
|
+
| `send_default_pii` | `False` | Required to capture `user.id`, request bodies |
|
|
83
|
+
| `before_send` | `None` | `(beacon) -> beacon | None` hook |
|
|
84
|
+
| `scrub_keys` | `None` | Extra keys to filter (added to the default deny-list) |
|
|
85
|
+
| `in_app_include` | `None` | Filename prefixes considered "your code" |
|
|
86
|
+
| `transport_queue_size` | `1000` | Bounded; drops on overflow |
|
|
87
|
+
| `transport_flush_timeout` | `2.0` | Seconds. Used by `close()` / `flush()` |
|
|
88
|
+
| `debug` | `False` | Print SDK's own warnings to stderr |
|
|
89
|
+
|
|
90
|
+
## Promise
|
|
91
|
+
|
|
92
|
+
The SDK never raises into your code. Every public function catches
|
|
93
|
+
`Exception` at its boundary; if something goes wrong inside the SDK,
|
|
94
|
+
you get nothing back and a debug log (if enabled). Your app keeps
|
|
95
|
+
running.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "insider-python"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Python SDK for Insider — ship Beacons to your Insider server."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Insider" }]
|
|
13
|
+
keywords = ["insider", "telemetry", "errors", "monitoring", "sdk"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.9",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
23
|
+
]
|
|
24
|
+
dependencies = [
|
|
25
|
+
"urllib3>=1.26",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
django = ["django>=4.2"]
|
|
30
|
+
dev = [
|
|
31
|
+
"pytest>=8",
|
|
32
|
+
"pytest-django>=4.8",
|
|
33
|
+
"django>=4.2",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.urls]
|
|
37
|
+
Homepage = "https://github.com/Insider-Inc/insider-python"
|
|
38
|
+
|
|
39
|
+
[tool.setuptools.packages.find]
|
|
40
|
+
where = ["src"]
|
|
41
|
+
|
|
42
|
+
[tool.setuptools.package-data]
|
|
43
|
+
insider = ["py.typed"]
|
|
44
|
+
|
|
45
|
+
[tool.pytest.ini_options]
|
|
46
|
+
testpaths = ["tests"]
|
|
47
|
+
pythonpath = ["src", "."]
|
|
48
|
+
filterwarnings = ["ignore::DeprecationWarning"]
|
|
49
|
+
DJANGO_SETTINGS_MODULE = "tests.django_settings"
|
|
50
|
+
django_find_project = false
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Insider — Python SDK.
|
|
3
|
+
|
|
4
|
+
Public API:
|
|
5
|
+
|
|
6
|
+
insider.init(dsn=..., environment=..., release=..., ...)
|
|
7
|
+
insider.capture_exception(exc, level="error", tags=..., extra=...)
|
|
8
|
+
insider.capture_message("text", level="info", tags=..., extra=...)
|
|
9
|
+
insider.flush(timeout=2.0)
|
|
10
|
+
insider.close(timeout=2.0)
|
|
11
|
+
|
|
12
|
+
Nothing else is part of the public contract. Anything imported below
|
|
13
|
+
`_` is internal and may change without notice.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from ._version import __version__
|
|
17
|
+
from .client import (
|
|
18
|
+
Client,
|
|
19
|
+
capture_exception,
|
|
20
|
+
capture_message,
|
|
21
|
+
close,
|
|
22
|
+
flush,
|
|
23
|
+
init,
|
|
24
|
+
)
|
|
25
|
+
from .dsn import DSN, InvalidDSNError
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"Client",
|
|
29
|
+
"DSN",
|
|
30
|
+
"InvalidDSNError",
|
|
31
|
+
"__version__",
|
|
32
|
+
"capture_exception",
|
|
33
|
+
"capture_message",
|
|
34
|
+
"close",
|
|
35
|
+
"flush",
|
|
36
|
+
"init",
|
|
37
|
+
]
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Beacon envelope construction + size-budget enforcement.
|
|
3
|
+
|
|
4
|
+
`build_envelope` is called from the capture functions in `client.py`. It
|
|
5
|
+
takes the raw bits (kind, level, message, exception payload, scope,
|
|
6
|
+
tags, extra) and produces the top-level dict the transport will ship.
|
|
7
|
+
|
|
8
|
+
`enforce_size_budget` is the second-to-last step before submit. It
|
|
9
|
+
truncates progressively until the JSON-encoded envelope fits under the
|
|
10
|
+
server's 256 KB cap. The truncation order is intentional and matches
|
|
11
|
+
docs/python-sdk-plan -> "Payload size budget":
|
|
12
|
+
|
|
13
|
+
1. message capped to 8 KB
|
|
14
|
+
2. frame `vars` capped to 2 KB each (v1 has no vars, future-proof)
|
|
15
|
+
3. request.body capped to 32 KB
|
|
16
|
+
4. request.headers capped to 4 KB (after deny-listed keys are masked
|
|
17
|
+
by scrub.py upstream)
|
|
18
|
+
5. drop frames from the outermost end of the stack until envelope fits,
|
|
19
|
+
keeping the innermost frames (closest to the error)
|
|
20
|
+
6. drop payload.request entirely
|
|
21
|
+
7. ship minimal envelope with payload.truncated = True
|
|
22
|
+
|
|
23
|
+
Step 7's existence is the property: we ship *something* truthful rather
|
|
24
|
+
than nothing.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import json
|
|
30
|
+
from datetime import datetime, timezone
|
|
31
|
+
from typing import Any, Dict, Iterable, List, Optional
|
|
32
|
+
|
|
33
|
+
from .safety import debug
|
|
34
|
+
|
|
35
|
+
MAX_ENVELOPE_BYTES = 256 * 1024
|
|
36
|
+
MAX_MESSAGE_BYTES = 8 * 1024
|
|
37
|
+
MAX_REQUEST_BODY_BYTES = 32 * 1024
|
|
38
|
+
MAX_REQUEST_HEADERS_BYTES = 4 * 1024
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _now_iso() -> str:
|
|
42
|
+
"""ISO-8601 UTC timestamp with microseconds, used as `occurred_at`."""
|
|
43
|
+
return datetime.now(timezone.utc).isoformat()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def build_envelope(
|
|
47
|
+
*,
|
|
48
|
+
kind: str,
|
|
49
|
+
level: str,
|
|
50
|
+
message: Optional[str],
|
|
51
|
+
source: Optional[str],
|
|
52
|
+
environment: str,
|
|
53
|
+
release: Optional[str],
|
|
54
|
+
trace_id: Optional[str],
|
|
55
|
+
payload: Optional[Dict[str, Any]] = None,
|
|
56
|
+
tags: Optional[Dict[str, Any]] = None,
|
|
57
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
58
|
+
occurred_at: Optional[str] = None,
|
|
59
|
+
) -> Dict[str, Any]:
|
|
60
|
+
"""Assemble the Beacon envelope. Pure: no I/O, no globals."""
|
|
61
|
+
body: Dict[str, Any] = dict(payload or {})
|
|
62
|
+
if tags:
|
|
63
|
+
body["tags"] = tags
|
|
64
|
+
if extra:
|
|
65
|
+
body["extra"] = extra
|
|
66
|
+
return {
|
|
67
|
+
"kind": kind,
|
|
68
|
+
"level": level,
|
|
69
|
+
"environment": environment,
|
|
70
|
+
"release": release,
|
|
71
|
+
"source": source,
|
|
72
|
+
"message": message,
|
|
73
|
+
"occurred_at": occurred_at or _now_iso(),
|
|
74
|
+
"trace_id": trace_id,
|
|
75
|
+
"payload": body,
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
# Size budget
|
|
81
|
+
# ---------------------------------------------------------------------------
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _byte_len(obj: Any) -> int:
|
|
85
|
+
"""Best-effort serialized size. Encode errors → infinity to force trim."""
|
|
86
|
+
try:
|
|
87
|
+
return len(json.dumps(obj, default=str, ensure_ascii=False).encode("utf-8"))
|
|
88
|
+
except Exception:
|
|
89
|
+
return 10**9
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _truncate_str_to_bytes(value: str, limit: int) -> str:
|
|
93
|
+
encoded = value.encode("utf-8")
|
|
94
|
+
if len(encoded) <= limit:
|
|
95
|
+
return value
|
|
96
|
+
return encoded[:limit].decode("utf-8", errors="ignore")
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def enforce_size_budget(envelope: Dict[str, Any]) -> Dict[str, Any]:
|
|
100
|
+
"""
|
|
101
|
+
Apply the truncation rules in order. Returns the same envelope dict,
|
|
102
|
+
mutated. The caller is expected to discard the original reference
|
|
103
|
+
after this call.
|
|
104
|
+
"""
|
|
105
|
+
# 1. message
|
|
106
|
+
msg = envelope.get("message")
|
|
107
|
+
if isinstance(msg, str):
|
|
108
|
+
envelope["message"] = _truncate_str_to_bytes(msg, MAX_MESSAGE_BYTES)
|
|
109
|
+
|
|
110
|
+
payload = envelope.get("payload") or {}
|
|
111
|
+
|
|
112
|
+
# 3. request.body
|
|
113
|
+
request_ctx = payload.get("request")
|
|
114
|
+
if isinstance(request_ctx, dict):
|
|
115
|
+
body = request_ctx.get("body")
|
|
116
|
+
if isinstance(body, str):
|
|
117
|
+
request_ctx["body"] = _truncate_str_to_bytes(body, MAX_REQUEST_BODY_BYTES)
|
|
118
|
+
elif body is not None:
|
|
119
|
+
# Non-string body: dump to string and cap.
|
|
120
|
+
try:
|
|
121
|
+
as_str = json.dumps(body, default=str)
|
|
122
|
+
except Exception:
|
|
123
|
+
as_str = str(body)
|
|
124
|
+
request_ctx["body"] = _truncate_str_to_bytes(as_str, MAX_REQUEST_BODY_BYTES)
|
|
125
|
+
|
|
126
|
+
# 4. request.headers
|
|
127
|
+
headers = request_ctx.get("headers")
|
|
128
|
+
if isinstance(headers, dict):
|
|
129
|
+
if _byte_len(headers) > MAX_REQUEST_HEADERS_BYTES:
|
|
130
|
+
# Drop the largest-value headers until under budget. We drop
|
|
131
|
+
# whole entries rather than truncating individual values to
|
|
132
|
+
# avoid producing partial / misleading header strings.
|
|
133
|
+
items = sorted(
|
|
134
|
+
headers.items(),
|
|
135
|
+
key=lambda kv: _byte_len(kv[1]),
|
|
136
|
+
reverse=True,
|
|
137
|
+
)
|
|
138
|
+
trimmed = dict(items)
|
|
139
|
+
while items and _byte_len(trimmed) > MAX_REQUEST_HEADERS_BYTES:
|
|
140
|
+
k, _ = items.pop(0)
|
|
141
|
+
trimmed.pop(k, None)
|
|
142
|
+
request_ctx["headers"] = trimmed
|
|
143
|
+
|
|
144
|
+
# 5. drop frames from the outside until envelope fits
|
|
145
|
+
exception = payload.get("exception")
|
|
146
|
+
if isinstance(exception, dict) and isinstance(exception.get("frames"), list):
|
|
147
|
+
while _byte_len(envelope) > MAX_ENVELOPE_BYTES and exception["frames"]:
|
|
148
|
+
# Keep the *innermost* frames (the end of the list).
|
|
149
|
+
exception["frames"].pop(0)
|
|
150
|
+
|
|
151
|
+
# 6. drop payload.request entirely if still too big
|
|
152
|
+
if _byte_len(envelope) > MAX_ENVELOPE_BYTES and "request" in payload:
|
|
153
|
+
payload.pop("request", None)
|
|
154
|
+
debug("size budget: dropped payload.request")
|
|
155
|
+
|
|
156
|
+
# 7. minimal envelope of last resort
|
|
157
|
+
if _byte_len(envelope) > MAX_ENVELOPE_BYTES:
|
|
158
|
+
minimal_exception: Optional[Dict[str, Any]] = None
|
|
159
|
+
if isinstance(exception, dict):
|
|
160
|
+
minimal_exception = {
|
|
161
|
+
"type": exception.get("type"),
|
|
162
|
+
"value": _truncate_str_to_bytes(
|
|
163
|
+
str(exception.get("value", "")), 1024
|
|
164
|
+
),
|
|
165
|
+
}
|
|
166
|
+
minimal_payload: Dict[str, Any] = {"truncated": True}
|
|
167
|
+
if minimal_exception is not None:
|
|
168
|
+
minimal_payload["exception"] = minimal_exception
|
|
169
|
+
envelope["payload"] = minimal_payload
|
|
170
|
+
debug("size budget: emitting minimal envelope")
|
|
171
|
+
|
|
172
|
+
return envelope
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def safe_frame_subset(
|
|
176
|
+
frames: List[Dict[str, Any]],
|
|
177
|
+
in_app_only: bool = False,
|
|
178
|
+
) -> List[Dict[str, Any]]:
|
|
179
|
+
"""Optional filter for dashboards; unused in v1 but kept for callers."""
|
|
180
|
+
if not in_app_only:
|
|
181
|
+
return frames
|
|
182
|
+
return [f for f in frames if f.get("in_app")]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
__all__: Iterable[str] = (
|
|
186
|
+
"MAX_ENVELOPE_BYTES",
|
|
187
|
+
"build_envelope",
|
|
188
|
+
"enforce_size_budget",
|
|
189
|
+
)
|