sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__py2.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.
- sentry_sdk/__init__.py +48 -6
- sentry_sdk/_compat.py +64 -56
- sentry_sdk/_init_implementation.py +84 -0
- sentry_sdk/_log_batcher.py +172 -0
- sentry_sdk/_lru_cache.py +47 -0
- sentry_sdk/_metrics_batcher.py +167 -0
- sentry_sdk/_queue.py +81 -19
- sentry_sdk/_types.py +311 -11
- sentry_sdk/_werkzeug.py +98 -0
- sentry_sdk/ai/__init__.py +7 -0
- sentry_sdk/ai/monitoring.py +137 -0
- sentry_sdk/ai/utils.py +144 -0
- sentry_sdk/api.py +409 -67
- sentry_sdk/attachments.py +75 -0
- sentry_sdk/client.py +849 -103
- sentry_sdk/consts.py +1389 -34
- sentry_sdk/crons/__init__.py +10 -0
- sentry_sdk/crons/api.py +62 -0
- sentry_sdk/crons/consts.py +4 -0
- sentry_sdk/crons/decorator.py +135 -0
- sentry_sdk/debug.py +12 -15
- sentry_sdk/envelope.py +112 -61
- sentry_sdk/feature_flags.py +71 -0
- sentry_sdk/hub.py +442 -386
- sentry_sdk/integrations/__init__.py +228 -58
- sentry_sdk/integrations/_asgi_common.py +108 -0
- sentry_sdk/integrations/_wsgi_common.py +131 -40
- sentry_sdk/integrations/aiohttp.py +221 -72
- sentry_sdk/integrations/anthropic.py +439 -0
- sentry_sdk/integrations/argv.py +4 -6
- sentry_sdk/integrations/ariadne.py +161 -0
- sentry_sdk/integrations/arq.py +247 -0
- sentry_sdk/integrations/asgi.py +237 -135
- sentry_sdk/integrations/asyncio.py +144 -0
- sentry_sdk/integrations/asyncpg.py +208 -0
- sentry_sdk/integrations/atexit.py +13 -18
- sentry_sdk/integrations/aws_lambda.py +233 -80
- sentry_sdk/integrations/beam.py +27 -35
- sentry_sdk/integrations/boto3.py +137 -0
- sentry_sdk/integrations/bottle.py +91 -69
- sentry_sdk/integrations/celery/__init__.py +529 -0
- sentry_sdk/integrations/celery/beat.py +293 -0
- sentry_sdk/integrations/celery/utils.py +43 -0
- sentry_sdk/integrations/chalice.py +35 -28
- sentry_sdk/integrations/clickhouse_driver.py +177 -0
- sentry_sdk/integrations/cloud_resource_context.py +280 -0
- sentry_sdk/integrations/cohere.py +274 -0
- sentry_sdk/integrations/dedupe.py +32 -8
- sentry_sdk/integrations/django/__init__.py +343 -89
- sentry_sdk/integrations/django/asgi.py +201 -22
- sentry_sdk/integrations/django/caching.py +204 -0
- sentry_sdk/integrations/django/middleware.py +80 -32
- sentry_sdk/integrations/django/signals_handlers.py +91 -0
- sentry_sdk/integrations/django/templates.py +69 -2
- sentry_sdk/integrations/django/transactions.py +39 -14
- sentry_sdk/integrations/django/views.py +69 -16
- sentry_sdk/integrations/dramatiq.py +226 -0
- sentry_sdk/integrations/excepthook.py +19 -13
- sentry_sdk/integrations/executing.py +5 -6
- sentry_sdk/integrations/falcon.py +128 -65
- sentry_sdk/integrations/fastapi.py +141 -0
- sentry_sdk/integrations/flask.py +114 -75
- sentry_sdk/integrations/gcp.py +67 -36
- sentry_sdk/integrations/gnu_backtrace.py +14 -22
- sentry_sdk/integrations/google_genai/__init__.py +301 -0
- sentry_sdk/integrations/google_genai/consts.py +16 -0
- sentry_sdk/integrations/google_genai/streaming.py +155 -0
- sentry_sdk/integrations/google_genai/utils.py +576 -0
- sentry_sdk/integrations/gql.py +162 -0
- sentry_sdk/integrations/graphene.py +151 -0
- sentry_sdk/integrations/grpc/__init__.py +168 -0
- sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
- sentry_sdk/integrations/grpc/aio/client.py +95 -0
- sentry_sdk/integrations/grpc/aio/server.py +100 -0
- sentry_sdk/integrations/grpc/client.py +91 -0
- sentry_sdk/integrations/grpc/consts.py +1 -0
- sentry_sdk/integrations/grpc/server.py +66 -0
- sentry_sdk/integrations/httpx.py +178 -0
- sentry_sdk/integrations/huey.py +174 -0
- sentry_sdk/integrations/huggingface_hub.py +378 -0
- sentry_sdk/integrations/langchain.py +1132 -0
- sentry_sdk/integrations/langgraph.py +337 -0
- sentry_sdk/integrations/launchdarkly.py +61 -0
- sentry_sdk/integrations/litellm.py +287 -0
- sentry_sdk/integrations/litestar.py +315 -0
- sentry_sdk/integrations/logging.py +261 -85
- sentry_sdk/integrations/loguru.py +213 -0
- sentry_sdk/integrations/mcp.py +566 -0
- sentry_sdk/integrations/modules.py +6 -33
- sentry_sdk/integrations/openai.py +725 -0
- sentry_sdk/integrations/openai_agents/__init__.py +61 -0
- sentry_sdk/integrations/openai_agents/consts.py +1 -0
- sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
- sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
- sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
- sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
- sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
- sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
- sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
- sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
- sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
- sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
- sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
- sentry_sdk/integrations/openai_agents/utils.py +199 -0
- sentry_sdk/integrations/openfeature.py +35 -0
- sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
- sentry_sdk/integrations/opentelemetry/consts.py +5 -0
- sentry_sdk/integrations/opentelemetry/integration.py +58 -0
- sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
- sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
- sentry_sdk/integrations/otlp.py +82 -0
- sentry_sdk/integrations/pure_eval.py +20 -11
- sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
- sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
- sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
- sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
- sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
- sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
- sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
- sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
- sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
- sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
- sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
- sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
- sentry_sdk/integrations/pymongo.py +214 -0
- sentry_sdk/integrations/pyramid.py +71 -60
- sentry_sdk/integrations/quart.py +237 -0
- sentry_sdk/integrations/ray.py +165 -0
- sentry_sdk/integrations/redis/__init__.py +48 -0
- sentry_sdk/integrations/redis/_async_common.py +116 -0
- sentry_sdk/integrations/redis/_sync_common.py +119 -0
- sentry_sdk/integrations/redis/consts.py +19 -0
- sentry_sdk/integrations/redis/modules/__init__.py +0 -0
- sentry_sdk/integrations/redis/modules/caches.py +118 -0
- sentry_sdk/integrations/redis/modules/queries.py +65 -0
- sentry_sdk/integrations/redis/rb.py +32 -0
- sentry_sdk/integrations/redis/redis.py +69 -0
- sentry_sdk/integrations/redis/redis_cluster.py +107 -0
- sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
- sentry_sdk/integrations/redis/utils.py +148 -0
- sentry_sdk/integrations/rq.py +62 -52
- sentry_sdk/integrations/rust_tracing.py +284 -0
- sentry_sdk/integrations/sanic.py +248 -114
- sentry_sdk/integrations/serverless.py +13 -22
- sentry_sdk/integrations/socket.py +96 -0
- sentry_sdk/integrations/spark/spark_driver.py +115 -62
- sentry_sdk/integrations/spark/spark_worker.py +42 -50
- sentry_sdk/integrations/sqlalchemy.py +82 -37
- sentry_sdk/integrations/starlette.py +737 -0
- sentry_sdk/integrations/starlite.py +292 -0
- sentry_sdk/integrations/statsig.py +37 -0
- sentry_sdk/integrations/stdlib.py +100 -58
- sentry_sdk/integrations/strawberry.py +394 -0
- sentry_sdk/integrations/sys_exit.py +70 -0
- sentry_sdk/integrations/threading.py +142 -38
- sentry_sdk/integrations/tornado.py +68 -53
- sentry_sdk/integrations/trytond.py +15 -20
- sentry_sdk/integrations/typer.py +60 -0
- sentry_sdk/integrations/unleash.py +33 -0
- sentry_sdk/integrations/unraisablehook.py +53 -0
- sentry_sdk/integrations/wsgi.py +126 -125
- sentry_sdk/logger.py +96 -0
- sentry_sdk/metrics.py +81 -0
- sentry_sdk/monitor.py +120 -0
- sentry_sdk/profiler/__init__.py +49 -0
- sentry_sdk/profiler/continuous_profiler.py +730 -0
- sentry_sdk/profiler/transaction_profiler.py +839 -0
- sentry_sdk/profiler/utils.py +195 -0
- sentry_sdk/scope.py +1542 -112
- sentry_sdk/scrubber.py +177 -0
- sentry_sdk/serializer.py +152 -210
- sentry_sdk/session.py +177 -0
- sentry_sdk/sessions.py +202 -179
- sentry_sdk/spotlight.py +242 -0
- sentry_sdk/tracing.py +1202 -294
- sentry_sdk/tracing_utils.py +1236 -0
- sentry_sdk/transport.py +693 -189
- sentry_sdk/types.py +52 -0
- sentry_sdk/utils.py +1395 -228
- sentry_sdk/worker.py +30 -17
- sentry_sdk-2.46.0.dist-info/METADATA +268 -0
- sentry_sdk-2.46.0.dist-info/RECORD +189 -0
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
- sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
- sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
- sentry_sdk/_functools.py +0 -66
- sentry_sdk/integrations/celery.py +0 -275
- sentry_sdk/integrations/redis.py +0 -103
- sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
- sentry_sdk-0.18.0.dist-info/METADATA +0 -66
- sentry_sdk-0.18.0.dist-info/RECORD +0 -65
- {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
|
@@ -1,18 +1,27 @@
|
|
|
1
|
+
from contextlib import contextmanager
|
|
1
2
|
import json
|
|
3
|
+
from copy import deepcopy
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
from sentry_sdk.
|
|
5
|
-
from sentry_sdk.
|
|
5
|
+
import sentry_sdk
|
|
6
|
+
from sentry_sdk.scope import should_send_default_pii
|
|
7
|
+
from sentry_sdk.utils import AnnotatedValue, logger
|
|
6
8
|
|
|
7
|
-
|
|
9
|
+
try:
|
|
10
|
+
from django.http.request import RawPostDataException
|
|
11
|
+
except ImportError:
|
|
12
|
+
RawPostDataException = None
|
|
8
13
|
|
|
9
|
-
|
|
10
|
-
import sentry_sdk
|
|
14
|
+
from typing import TYPE_CHECKING
|
|
11
15
|
|
|
16
|
+
if TYPE_CHECKING:
|
|
12
17
|
from typing import Any
|
|
13
18
|
from typing import Dict
|
|
19
|
+
from typing import Iterator
|
|
20
|
+
from typing import Mapping
|
|
21
|
+
from typing import MutableMapping
|
|
14
22
|
from typing import Optional
|
|
15
23
|
from typing import Union
|
|
24
|
+
from sentry_sdk._types import Event, HttpStatusCodeRange
|
|
16
25
|
|
|
17
26
|
|
|
18
27
|
SENSITIVE_ENV_KEYS = (
|
|
@@ -21,6 +30,7 @@ SENSITIVE_ENV_KEYS = (
|
|
|
21
30
|
"HTTP_SET_COOKIE",
|
|
22
31
|
"HTTP_COOKIE",
|
|
23
32
|
"HTTP_AUTHORIZATION",
|
|
33
|
+
"HTTP_X_API_KEY",
|
|
24
34
|
"HTTP_X_FORWARDED_FOR",
|
|
25
35
|
"HTTP_X_REAL_IP",
|
|
26
36
|
)
|
|
@@ -29,29 +39,57 @@ SENSITIVE_HEADERS = tuple(
|
|
|
29
39
|
x[len("HTTP_") :] for x in SENSITIVE_ENV_KEYS if x.startswith("HTTP_")
|
|
30
40
|
)
|
|
31
41
|
|
|
42
|
+
DEFAULT_HTTP_METHODS_TO_CAPTURE = (
|
|
43
|
+
"CONNECT",
|
|
44
|
+
"DELETE",
|
|
45
|
+
"GET",
|
|
46
|
+
# "HEAD", # do not capture HEAD requests by default
|
|
47
|
+
# "OPTIONS", # do not capture OPTIONS requests by default
|
|
48
|
+
"PATCH",
|
|
49
|
+
"POST",
|
|
50
|
+
"PUT",
|
|
51
|
+
"TRACE",
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# This noop context manager can be replaced with "from contextlib import nullcontext" when we drop Python 3.6 support
|
|
56
|
+
@contextmanager
|
|
57
|
+
def nullcontext():
|
|
58
|
+
# type: () -> Iterator[None]
|
|
59
|
+
yield
|
|
60
|
+
|
|
32
61
|
|
|
33
62
|
def request_body_within_bounds(client, content_length):
|
|
34
|
-
# type: (Optional[sentry_sdk.
|
|
63
|
+
# type: (Optional[sentry_sdk.client.BaseClient], int) -> bool
|
|
35
64
|
if client is None:
|
|
36
65
|
return False
|
|
37
66
|
|
|
38
|
-
bodies = client.options["
|
|
67
|
+
bodies = client.options["max_request_body_size"]
|
|
39
68
|
return not (
|
|
40
69
|
bodies == "never"
|
|
41
|
-
or (bodies == "small" and content_length > 10
|
|
42
|
-
or (bodies == "medium" and content_length > 10
|
|
70
|
+
or (bodies == "small" and content_length > 10**3)
|
|
71
|
+
or (bodies == "medium" and content_length > 10**4)
|
|
43
72
|
)
|
|
44
73
|
|
|
45
74
|
|
|
46
|
-
class RequestExtractor
|
|
75
|
+
class RequestExtractor:
|
|
76
|
+
"""
|
|
77
|
+
Base class for request extraction.
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
# It does not make sense to make this class an ABC because it is not used
|
|
81
|
+
# for typing, only so that child classes can inherit common methods from
|
|
82
|
+
# it. Only some child classes implement all methods that raise
|
|
83
|
+
# NotImplementedError in this class.
|
|
84
|
+
|
|
47
85
|
def __init__(self, request):
|
|
48
86
|
# type: (Any) -> None
|
|
49
87
|
self.request = request
|
|
50
88
|
|
|
51
89
|
def extract_into_event(self, event):
|
|
52
|
-
# type: (
|
|
53
|
-
client =
|
|
54
|
-
if client
|
|
90
|
+
# type: (Event) -> None
|
|
91
|
+
client = sentry_sdk.get_client()
|
|
92
|
+
if not client.is_active():
|
|
55
93
|
return
|
|
56
94
|
|
|
57
95
|
data = None # type: Optional[Union[AnnotatedValue, Dict[str, Any]]]
|
|
@@ -59,30 +97,36 @@ class RequestExtractor(object):
|
|
|
59
97
|
content_length = self.content_length()
|
|
60
98
|
request_info = event.get("request", {})
|
|
61
99
|
|
|
62
|
-
if
|
|
100
|
+
if should_send_default_pii():
|
|
63
101
|
request_info["cookies"] = dict(self.cookies())
|
|
64
102
|
|
|
65
103
|
if not request_body_within_bounds(client, content_length):
|
|
66
|
-
data = AnnotatedValue(
|
|
67
|
-
"",
|
|
68
|
-
{"rem": [["!config", "x", 0, content_length]], "len": content_length},
|
|
69
|
-
)
|
|
104
|
+
data = AnnotatedValue.removed_because_over_size_limit()
|
|
70
105
|
else:
|
|
106
|
+
# First read the raw body data
|
|
107
|
+
# It is important to read this first because if it is Django
|
|
108
|
+
# it will cache the body and then we can read the cached version
|
|
109
|
+
# again in parsed_body() (or json() or wherever).
|
|
110
|
+
raw_data = None
|
|
111
|
+
try:
|
|
112
|
+
raw_data = self.raw_data()
|
|
113
|
+
except (RawPostDataException, ValueError):
|
|
114
|
+
# If DjangoRestFramework is used it already read the body for us
|
|
115
|
+
# so reading it here will fail. We can ignore this.
|
|
116
|
+
pass
|
|
117
|
+
|
|
71
118
|
parsed_body = self.parsed_body()
|
|
72
119
|
if parsed_body is not None:
|
|
73
120
|
data = parsed_body
|
|
74
|
-
elif
|
|
75
|
-
data = AnnotatedValue(
|
|
76
|
-
"",
|
|
77
|
-
{"rem": [["!raw", "x", 0, content_length]], "len": content_length},
|
|
78
|
-
)
|
|
121
|
+
elif raw_data:
|
|
122
|
+
data = AnnotatedValue.removed_because_raw_data()
|
|
79
123
|
else:
|
|
80
124
|
data = None
|
|
81
125
|
|
|
82
126
|
if data is not None:
|
|
83
127
|
request_info["data"] = data
|
|
84
128
|
|
|
85
|
-
event["request"] = request_info
|
|
129
|
+
event["request"] = deepcopy(request_info)
|
|
86
130
|
|
|
87
131
|
def content_length(self):
|
|
88
132
|
# type: () -> int
|
|
@@ -92,7 +136,7 @@ class RequestExtractor(object):
|
|
|
92
136
|
return 0
|
|
93
137
|
|
|
94
138
|
def cookies(self):
|
|
95
|
-
# type: () ->
|
|
139
|
+
# type: () -> MutableMapping[str, Any]
|
|
96
140
|
raise NotImplementedError()
|
|
97
141
|
|
|
98
142
|
def raw_data(self):
|
|
@@ -105,15 +149,22 @@ class RequestExtractor(object):
|
|
|
105
149
|
|
|
106
150
|
def parsed_body(self):
|
|
107
151
|
# type: () -> Optional[Dict[str, Any]]
|
|
108
|
-
|
|
109
|
-
|
|
152
|
+
try:
|
|
153
|
+
form = self.form()
|
|
154
|
+
except Exception:
|
|
155
|
+
form = None
|
|
156
|
+
try:
|
|
157
|
+
files = self.files()
|
|
158
|
+
except Exception:
|
|
159
|
+
files = None
|
|
160
|
+
|
|
110
161
|
if form or files:
|
|
111
|
-
data =
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
162
|
+
data = {}
|
|
163
|
+
if form:
|
|
164
|
+
data = dict(form.items())
|
|
165
|
+
if files:
|
|
166
|
+
for key in files.keys():
|
|
167
|
+
data[key] = AnnotatedValue.removed_because_raw_data()
|
|
117
168
|
|
|
118
169
|
return data
|
|
119
170
|
|
|
@@ -129,11 +180,17 @@ class RequestExtractor(object):
|
|
|
129
180
|
if not self.is_json():
|
|
130
181
|
return None
|
|
131
182
|
|
|
132
|
-
|
|
183
|
+
try:
|
|
184
|
+
raw_data = self.raw_data()
|
|
185
|
+
except (RawPostDataException, ValueError):
|
|
186
|
+
# The body might have already been read, in which case this will
|
|
187
|
+
# fail
|
|
188
|
+
raw_data = None
|
|
189
|
+
|
|
133
190
|
if raw_data is None:
|
|
134
191
|
return None
|
|
135
192
|
|
|
136
|
-
if isinstance(raw_data,
|
|
193
|
+
if isinstance(raw_data, str):
|
|
137
194
|
return json.loads(raw_data)
|
|
138
195
|
else:
|
|
139
196
|
return json.loads(raw_data.decode("utf-8"))
|
|
@@ -166,15 +223,49 @@ def _is_json_content_type(ct):
|
|
|
166
223
|
|
|
167
224
|
|
|
168
225
|
def _filter_headers(headers):
|
|
169
|
-
# type: (
|
|
170
|
-
if
|
|
226
|
+
# type: (Mapping[str, str]) -> Mapping[str, Union[AnnotatedValue, str]]
|
|
227
|
+
if should_send_default_pii():
|
|
171
228
|
return headers
|
|
172
229
|
|
|
173
230
|
return {
|
|
174
231
|
k: (
|
|
175
232
|
v
|
|
176
233
|
if k.upper().replace("-", "_") not in SENSITIVE_HEADERS
|
|
177
|
-
else AnnotatedValue(
|
|
234
|
+
else AnnotatedValue.removed_because_over_size_limit()
|
|
178
235
|
)
|
|
179
|
-
for k, v in
|
|
236
|
+
for k, v in headers.items()
|
|
180
237
|
}
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _in_http_status_code_range(code, code_ranges):
|
|
241
|
+
# type: (object, list[HttpStatusCodeRange]) -> bool
|
|
242
|
+
for target in code_ranges:
|
|
243
|
+
if isinstance(target, int):
|
|
244
|
+
if code == target:
|
|
245
|
+
return True
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
try:
|
|
249
|
+
if code in target:
|
|
250
|
+
return True
|
|
251
|
+
except TypeError:
|
|
252
|
+
logger.warning(
|
|
253
|
+
"failed_request_status_codes has to be a list of integers or containers"
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class HttpCodeRangeContainer:
|
|
260
|
+
"""
|
|
261
|
+
Wrapper to make it possible to use list[HttpStatusCodeRange] as a Container[int].
|
|
262
|
+
Used for backwards compatibility with the old `failed_request_status_codes` option.
|
|
263
|
+
"""
|
|
264
|
+
|
|
265
|
+
def __init__(self, code_ranges):
|
|
266
|
+
# type: (list[HttpStatusCodeRange]) -> None
|
|
267
|
+
self._code_ranges = code_ranges
|
|
268
|
+
|
|
269
|
+
def __contains__(self, item):
|
|
270
|
+
# type: (object) -> bool
|
|
271
|
+
return _in_http_status_code_range(item, self._code_ranges)
|
|
@@ -1,21 +1,40 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
import weakref
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
from sentry_sdk.
|
|
3
|
+
from functools import wraps
|
|
4
|
+
|
|
5
|
+
import sentry_sdk
|
|
6
|
+
from sentry_sdk.api import continue_trace
|
|
7
|
+
from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA
|
|
8
|
+
from sentry_sdk.integrations import (
|
|
9
|
+
_DEFAULT_FAILED_REQUEST_STATUS_CODES,
|
|
10
|
+
_check_minimum_version,
|
|
11
|
+
Integration,
|
|
12
|
+
DidNotEnable,
|
|
13
|
+
)
|
|
7
14
|
from sentry_sdk.integrations.logging import ignore_logger
|
|
15
|
+
from sentry_sdk.sessions import track_session
|
|
8
16
|
from sentry_sdk.integrations._wsgi_common import (
|
|
9
17
|
_filter_headers,
|
|
10
18
|
request_body_within_bounds,
|
|
11
19
|
)
|
|
12
|
-
from sentry_sdk.tracing import
|
|
20
|
+
from sentry_sdk.tracing import (
|
|
21
|
+
BAGGAGE_HEADER_NAME,
|
|
22
|
+
SOURCE_FOR_STYLE,
|
|
23
|
+
TransactionSource,
|
|
24
|
+
)
|
|
25
|
+
from sentry_sdk.tracing_utils import should_propagate_trace, add_http_request_source
|
|
13
26
|
from sentry_sdk.utils import (
|
|
14
27
|
capture_internal_exceptions,
|
|
28
|
+
ensure_integration_enabled,
|
|
15
29
|
event_from_exception,
|
|
30
|
+
logger,
|
|
31
|
+
parse_url,
|
|
32
|
+
parse_version,
|
|
33
|
+
reraise,
|
|
16
34
|
transaction_from_function,
|
|
17
35
|
HAS_REAL_CONTEXTVARS,
|
|
18
36
|
CONTEXTVARS_ERROR_MESSAGE,
|
|
37
|
+
SENSITIVE_DATA_SUBSTITUTE,
|
|
19
38
|
AnnotatedValue,
|
|
20
39
|
)
|
|
21
40
|
|
|
@@ -23,42 +42,57 @@ try:
|
|
|
23
42
|
import asyncio
|
|
24
43
|
|
|
25
44
|
from aiohttp import __version__ as AIOHTTP_VERSION
|
|
45
|
+
from aiohttp import ClientSession, TraceConfig
|
|
26
46
|
from aiohttp.web import Application, HTTPException, UrlDispatcher
|
|
27
47
|
except ImportError:
|
|
28
48
|
raise DidNotEnable("AIOHTTP not installed")
|
|
29
49
|
|
|
30
|
-
from
|
|
50
|
+
from typing import TYPE_CHECKING
|
|
31
51
|
|
|
32
|
-
if
|
|
52
|
+
if TYPE_CHECKING:
|
|
33
53
|
from aiohttp.web_request import Request
|
|
34
|
-
from aiohttp.
|
|
54
|
+
from aiohttp.web_urldispatcher import UrlMappingMatchInfo
|
|
55
|
+
from aiohttp import TraceRequestStartParams, TraceRequestEndParams
|
|
56
|
+
|
|
57
|
+
from collections.abc import Set
|
|
58
|
+
from types import SimpleNamespace
|
|
35
59
|
from typing import Any
|
|
36
|
-
from typing import Dict
|
|
37
60
|
from typing import Optional
|
|
38
61
|
from typing import Tuple
|
|
39
|
-
from typing import Callable
|
|
40
62
|
from typing import Union
|
|
41
63
|
|
|
42
64
|
from sentry_sdk.utils import ExcInfo
|
|
43
|
-
from sentry_sdk._types import EventProcessor
|
|
65
|
+
from sentry_sdk._types import Event, EventProcessor
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
|
|
44
69
|
|
|
45
70
|
|
|
46
71
|
class AioHttpIntegration(Integration):
|
|
47
72
|
identifier = "aiohttp"
|
|
73
|
+
origin = f"auto.http.{identifier}"
|
|
74
|
+
|
|
75
|
+
def __init__(
|
|
76
|
+
self,
|
|
77
|
+
transaction_style="handler_name", # type: str
|
|
78
|
+
*,
|
|
79
|
+
failed_request_status_codes=_DEFAULT_FAILED_REQUEST_STATUS_CODES, # type: Set[int]
|
|
80
|
+
):
|
|
81
|
+
# type: (...) -> None
|
|
82
|
+
if transaction_style not in TRANSACTION_STYLE_VALUES:
|
|
83
|
+
raise ValueError(
|
|
84
|
+
"Invalid value for transaction_style: %s (must be in %s)"
|
|
85
|
+
% (transaction_style, TRANSACTION_STYLE_VALUES)
|
|
86
|
+
)
|
|
87
|
+
self.transaction_style = transaction_style
|
|
88
|
+
self._failed_request_status_codes = failed_request_status_codes
|
|
48
89
|
|
|
49
90
|
@staticmethod
|
|
50
91
|
def setup_once():
|
|
51
92
|
# type: () -> None
|
|
52
93
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
except (TypeError, ValueError):
|
|
56
|
-
raise DidNotEnable(
|
|
57
|
-
"AIOHTTP version unparseable: {}".format(AIOHTTP_VERSION)
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
if version < (3, 4):
|
|
61
|
-
raise DidNotEnable("AIOHTTP 3.4 or newer required.")
|
|
94
|
+
version = parse_version(AIOHTTP_VERSION)
|
|
95
|
+
_check_minimum_version(AioHttpIntegration, version)
|
|
62
96
|
|
|
63
97
|
if not HAS_REAL_CONTEXTVARS:
|
|
64
98
|
# We better have contextvars or we're going to leak state between
|
|
@@ -74,75 +108,195 @@ class AioHttpIntegration(Integration):
|
|
|
74
108
|
|
|
75
109
|
async def sentry_app_handle(self, request, *args, **kwargs):
|
|
76
110
|
# type: (Any, Request, *Any, **Any) -> Any
|
|
77
|
-
|
|
78
|
-
if
|
|
111
|
+
integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
|
|
112
|
+
if integration is None:
|
|
79
113
|
return await old_handle(self, request, *args, **kwargs)
|
|
80
114
|
|
|
81
115
|
weak_request = weakref.ref(request)
|
|
82
116
|
|
|
83
|
-
with
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
117
|
+
with sentry_sdk.isolation_scope() as scope:
|
|
118
|
+
with track_session(scope, session_mode="request"):
|
|
119
|
+
# Scope data will not leak between requests because aiohttp
|
|
120
|
+
# create a task to wrap each request.
|
|
121
|
+
scope.generate_propagation_context()
|
|
87
122
|
scope.clear_breadcrumbs()
|
|
88
123
|
scope.add_event_processor(_make_request_processor(weak_request))
|
|
89
124
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
125
|
+
headers = dict(request.headers)
|
|
126
|
+
transaction = continue_trace(
|
|
127
|
+
headers,
|
|
128
|
+
op=OP.HTTP_SERVER,
|
|
129
|
+
# If this transaction name makes it to the UI, AIOHTTP's
|
|
130
|
+
# URL resolver did not find a route or died trying.
|
|
131
|
+
name="generic AIOHTTP request",
|
|
132
|
+
source=TransactionSource.ROUTE,
|
|
133
|
+
origin=AioHttpIntegration.origin,
|
|
134
|
+
)
|
|
135
|
+
with sentry_sdk.start_transaction(
|
|
136
|
+
transaction,
|
|
137
|
+
custom_sampling_context={"aiohttp_request": request},
|
|
138
|
+
):
|
|
139
|
+
try:
|
|
140
|
+
response = await old_handle(self, request)
|
|
141
|
+
except HTTPException as e:
|
|
142
|
+
transaction.set_http_status(e.status_code)
|
|
143
|
+
|
|
144
|
+
if (
|
|
145
|
+
e.status_code
|
|
146
|
+
in integration._failed_request_status_codes
|
|
147
|
+
):
|
|
148
|
+
_capture_exception()
|
|
149
|
+
|
|
150
|
+
raise
|
|
151
|
+
except (asyncio.CancelledError, ConnectionResetError):
|
|
152
|
+
transaction.set_status(SPANSTATUS.CANCELLED)
|
|
153
|
+
raise
|
|
154
|
+
except Exception:
|
|
155
|
+
# This will probably map to a 500 but seems like we
|
|
156
|
+
# have no way to tell. Do not set span status.
|
|
157
|
+
reraise(*_capture_exception())
|
|
158
|
+
|
|
159
|
+
try:
|
|
160
|
+
# A valid response handler will return a valid response with a status. But, if the handler
|
|
161
|
+
# returns an invalid response (e.g. None), the line below will raise an AttributeError.
|
|
162
|
+
# Even though this is likely invalid, we need to handle this case to ensure we don't break
|
|
163
|
+
# the application.
|
|
164
|
+
response_status = response.status
|
|
165
|
+
except AttributeError:
|
|
166
|
+
pass
|
|
167
|
+
else:
|
|
168
|
+
transaction.set_http_status(response_status)
|
|
169
|
+
|
|
170
|
+
return response
|
|
114
171
|
|
|
115
172
|
Application._handle = sentry_app_handle
|
|
116
173
|
|
|
117
174
|
old_urldispatcher_resolve = UrlDispatcher.resolve
|
|
118
175
|
|
|
176
|
+
@wraps(old_urldispatcher_resolve)
|
|
119
177
|
async def sentry_urldispatcher_resolve(self, request):
|
|
120
|
-
# type: (UrlDispatcher, Request) ->
|
|
178
|
+
# type: (UrlDispatcher, Request) -> UrlMappingMatchInfo
|
|
121
179
|
rv = await old_urldispatcher_resolve(self, request)
|
|
122
180
|
|
|
181
|
+
integration = sentry_sdk.get_client().get_integration(AioHttpIntegration)
|
|
182
|
+
if integration is None:
|
|
183
|
+
return rv
|
|
184
|
+
|
|
123
185
|
name = None
|
|
124
186
|
|
|
125
187
|
try:
|
|
126
|
-
|
|
188
|
+
if integration.transaction_style == "handler_name":
|
|
189
|
+
name = transaction_from_function(rv.handler)
|
|
190
|
+
elif integration.transaction_style == "method_and_path_pattern":
|
|
191
|
+
route_info = rv.get_info()
|
|
192
|
+
pattern = route_info.get("path") or route_info.get("formatter")
|
|
193
|
+
name = "{} {}".format(request.method, pattern)
|
|
127
194
|
except Exception:
|
|
128
195
|
pass
|
|
129
196
|
|
|
130
197
|
if name is not None:
|
|
131
|
-
|
|
132
|
-
|
|
198
|
+
sentry_sdk.get_current_scope().set_transaction_name(
|
|
199
|
+
name,
|
|
200
|
+
source=SOURCE_FOR_STYLE[integration.transaction_style],
|
|
201
|
+
)
|
|
133
202
|
|
|
134
203
|
return rv
|
|
135
204
|
|
|
136
205
|
UrlDispatcher.resolve = sentry_urldispatcher_resolve
|
|
137
206
|
|
|
207
|
+
old_client_session_init = ClientSession.__init__
|
|
208
|
+
|
|
209
|
+
@ensure_integration_enabled(AioHttpIntegration, old_client_session_init)
|
|
210
|
+
def init(*args, **kwargs):
|
|
211
|
+
# type: (Any, Any) -> None
|
|
212
|
+
client_trace_configs = list(kwargs.get("trace_configs") or ())
|
|
213
|
+
trace_config = create_trace_config()
|
|
214
|
+
client_trace_configs.append(trace_config)
|
|
215
|
+
|
|
216
|
+
kwargs["trace_configs"] = client_trace_configs
|
|
217
|
+
return old_client_session_init(*args, **kwargs)
|
|
218
|
+
|
|
219
|
+
ClientSession.__init__ = init
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def create_trace_config():
|
|
223
|
+
# type: () -> TraceConfig
|
|
224
|
+
|
|
225
|
+
async def on_request_start(session, trace_config_ctx, params):
|
|
226
|
+
# type: (ClientSession, SimpleNamespace, TraceRequestStartParams) -> None
|
|
227
|
+
if sentry_sdk.get_client().get_integration(AioHttpIntegration) is None:
|
|
228
|
+
return
|
|
229
|
+
|
|
230
|
+
method = params.method.upper()
|
|
231
|
+
|
|
232
|
+
parsed_url = None
|
|
233
|
+
with capture_internal_exceptions():
|
|
234
|
+
parsed_url = parse_url(str(params.url), sanitize=False)
|
|
235
|
+
|
|
236
|
+
span = sentry_sdk.start_span(
|
|
237
|
+
op=OP.HTTP_CLIENT,
|
|
238
|
+
name="%s %s"
|
|
239
|
+
% (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
|
|
240
|
+
origin=AioHttpIntegration.origin,
|
|
241
|
+
)
|
|
242
|
+
span.set_data(SPANDATA.HTTP_METHOD, method)
|
|
243
|
+
if parsed_url is not None:
|
|
244
|
+
span.set_data("url", parsed_url.url)
|
|
245
|
+
span.set_data(SPANDATA.HTTP_QUERY, parsed_url.query)
|
|
246
|
+
span.set_data(SPANDATA.HTTP_FRAGMENT, parsed_url.fragment)
|
|
247
|
+
|
|
248
|
+
client = sentry_sdk.get_client()
|
|
249
|
+
|
|
250
|
+
if should_propagate_trace(client, str(params.url)):
|
|
251
|
+
for (
|
|
252
|
+
key,
|
|
253
|
+
value,
|
|
254
|
+
) in sentry_sdk.get_current_scope().iter_trace_propagation_headers(
|
|
255
|
+
span=span
|
|
256
|
+
):
|
|
257
|
+
logger.debug(
|
|
258
|
+
"[Tracing] Adding `{key}` header {value} to outgoing request to {url}.".format(
|
|
259
|
+
key=key, value=value, url=params.url
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
if key == BAGGAGE_HEADER_NAME and params.headers.get(
|
|
263
|
+
BAGGAGE_HEADER_NAME
|
|
264
|
+
):
|
|
265
|
+
# do not overwrite any existing baggage, just append to it
|
|
266
|
+
params.headers[key] += "," + value
|
|
267
|
+
else:
|
|
268
|
+
params.headers[key] = value
|
|
269
|
+
|
|
270
|
+
trace_config_ctx.span = span
|
|
271
|
+
|
|
272
|
+
async def on_request_end(session, trace_config_ctx, params):
|
|
273
|
+
# type: (ClientSession, SimpleNamespace, TraceRequestEndParams) -> None
|
|
274
|
+
if trace_config_ctx.span is None:
|
|
275
|
+
return
|
|
276
|
+
|
|
277
|
+
span = trace_config_ctx.span
|
|
278
|
+
span.set_http_status(int(params.response.status))
|
|
279
|
+
span.set_data("reason", params.response.reason)
|
|
280
|
+
span.finish()
|
|
281
|
+
|
|
282
|
+
with capture_internal_exceptions():
|
|
283
|
+
add_http_request_source(span)
|
|
284
|
+
|
|
285
|
+
trace_config = TraceConfig()
|
|
286
|
+
|
|
287
|
+
trace_config.on_request_start.append(on_request_start)
|
|
288
|
+
trace_config.on_request_end.append(on_request_end)
|
|
289
|
+
|
|
290
|
+
return trace_config
|
|
291
|
+
|
|
138
292
|
|
|
139
293
|
def _make_request_processor(weak_request):
|
|
140
|
-
# type: (
|
|
294
|
+
# type: (weakref.ReferenceType[Request]) -> EventProcessor
|
|
141
295
|
def aiohttp_processor(
|
|
142
|
-
event, # type:
|
|
143
|
-
hint, # type:
|
|
296
|
+
event, # type: Event
|
|
297
|
+
hint, # type: dict[str, Tuple[type, BaseException, Any]]
|
|
144
298
|
):
|
|
145
|
-
# type: (...) ->
|
|
299
|
+
# type: (...) -> Event
|
|
146
300
|
request = weak_request()
|
|
147
301
|
if request is None:
|
|
148
302
|
return event
|
|
@@ -159,47 +313,42 @@ def _make_request_processor(weak_request):
|
|
|
159
313
|
request_info["query_string"] = request.query_string
|
|
160
314
|
request_info["method"] = request.method
|
|
161
315
|
request_info["env"] = {"REMOTE_ADDR": request.remote}
|
|
162
|
-
|
|
163
|
-
hub = Hub.current
|
|
164
316
|
request_info["headers"] = _filter_headers(dict(request.headers))
|
|
165
317
|
|
|
166
318
|
# Just attach raw data here if it is within bounds, if available.
|
|
167
319
|
# Unfortunately there's no way to get structured data from aiohttp
|
|
168
320
|
# without awaiting on some coroutine.
|
|
169
|
-
request_info["data"] = get_aiohttp_request_data(
|
|
321
|
+
request_info["data"] = get_aiohttp_request_data(request)
|
|
170
322
|
|
|
171
323
|
return event
|
|
172
324
|
|
|
173
325
|
return aiohttp_processor
|
|
174
326
|
|
|
175
327
|
|
|
176
|
-
def _capture_exception(
|
|
177
|
-
# type: (
|
|
328
|
+
def _capture_exception():
|
|
329
|
+
# type: () -> ExcInfo
|
|
178
330
|
exc_info = sys.exc_info()
|
|
179
331
|
event, hint = event_from_exception(
|
|
180
332
|
exc_info,
|
|
181
|
-
client_options=
|
|
333
|
+
client_options=sentry_sdk.get_client().options,
|
|
182
334
|
mechanism={"type": "aiohttp", "handled": False},
|
|
183
335
|
)
|
|
184
|
-
|
|
336
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
185
337
|
return exc_info
|
|
186
338
|
|
|
187
339
|
|
|
188
340
|
BODY_NOT_READ_MESSAGE = "[Can't show request body due to implementation details.]"
|
|
189
341
|
|
|
190
342
|
|
|
191
|
-
def get_aiohttp_request_data(
|
|
192
|
-
# type: (
|
|
343
|
+
def get_aiohttp_request_data(request):
|
|
344
|
+
# type: (Request) -> Union[Optional[str], AnnotatedValue]
|
|
193
345
|
bytes_body = request._read_bytes
|
|
194
346
|
|
|
195
347
|
if bytes_body is not None:
|
|
196
348
|
# we have body to show
|
|
197
|
-
if not request_body_within_bounds(
|
|
349
|
+
if not request_body_within_bounds(sentry_sdk.get_client(), len(bytes_body)):
|
|
350
|
+
return AnnotatedValue.removed_because_over_size_limit()
|
|
198
351
|
|
|
199
|
-
return AnnotatedValue(
|
|
200
|
-
"",
|
|
201
|
-
{"rem": [["!config", "x", 0, len(bytes_body)]], "len": len(bytes_body)},
|
|
202
|
-
)
|
|
203
352
|
encoding = request.charset or "utf-8"
|
|
204
353
|
return bytes_body.decode(encoding, "replace")
|
|
205
354
|
|