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
sentry_sdk/integrations/asgi.py
CHANGED
|
@@ -1,33 +1,49 @@
|
|
|
1
1
|
"""
|
|
2
2
|
An ASGI middleware.
|
|
3
3
|
|
|
4
|
-
Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi
|
|
4
|
+
Based on Tom Christie's `sentry-asgi <https://github.com/encode/sentry-asgi>`.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
7
|
import asyncio
|
|
8
8
|
import inspect
|
|
9
|
-
import
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
from sentry_sdk.
|
|
14
|
-
from sentry_sdk.
|
|
9
|
+
from copy import deepcopy
|
|
10
|
+
from functools import partial
|
|
11
|
+
|
|
12
|
+
import sentry_sdk
|
|
13
|
+
from sentry_sdk.api import continue_trace
|
|
14
|
+
from sentry_sdk.consts import OP
|
|
15
|
+
from sentry_sdk.integrations._asgi_common import (
|
|
16
|
+
_get_headers,
|
|
17
|
+
_get_request_data,
|
|
18
|
+
_get_url,
|
|
19
|
+
)
|
|
20
|
+
from sentry_sdk.integrations._wsgi_common import (
|
|
21
|
+
DEFAULT_HTTP_METHODS_TO_CAPTURE,
|
|
22
|
+
nullcontext,
|
|
23
|
+
)
|
|
24
|
+
from sentry_sdk.sessions import track_session
|
|
25
|
+
from sentry_sdk.tracing import (
|
|
26
|
+
SOURCE_FOR_STYLE,
|
|
27
|
+
TransactionSource,
|
|
28
|
+
)
|
|
15
29
|
from sentry_sdk.utils import (
|
|
16
30
|
ContextVar,
|
|
17
31
|
event_from_exception,
|
|
18
|
-
transaction_from_function,
|
|
19
32
|
HAS_REAL_CONTEXTVARS,
|
|
20
33
|
CONTEXTVARS_ERROR_MESSAGE,
|
|
34
|
+
logger,
|
|
35
|
+
transaction_from_function,
|
|
36
|
+
_get_installed_modules,
|
|
21
37
|
)
|
|
22
38
|
from sentry_sdk.tracing import Transaction
|
|
23
39
|
|
|
24
|
-
|
|
25
|
-
|
|
40
|
+
from typing import TYPE_CHECKING
|
|
41
|
+
|
|
42
|
+
if TYPE_CHECKING:
|
|
26
43
|
from typing import Any
|
|
44
|
+
from typing import Dict
|
|
27
45
|
from typing import Optional
|
|
28
|
-
from typing import
|
|
29
|
-
|
|
30
|
-
from typing_extensions import Literal
|
|
46
|
+
from typing import Tuple
|
|
31
47
|
|
|
32
48
|
from sentry_sdk._types import Event, Hint
|
|
33
49
|
|
|
@@ -36,18 +52,18 @@ _asgi_middleware_applied = ContextVar("sentry_asgi_middleware_applied")
|
|
|
36
52
|
|
|
37
53
|
_DEFAULT_TRANSACTION_NAME = "generic ASGI request"
|
|
38
54
|
|
|
55
|
+
TRANSACTION_STYLE_VALUES = ("endpoint", "url")
|
|
39
56
|
|
|
40
|
-
def _capture_exception(hub, exc):
|
|
41
|
-
# type: (Hub, Any) -> None
|
|
42
57
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
58
|
+
def _capture_exception(exc, mechanism_type="asgi"):
|
|
59
|
+
# type: (Any, str) -> None
|
|
60
|
+
|
|
61
|
+
event, hint = event_from_exception(
|
|
62
|
+
exc,
|
|
63
|
+
client_options=sentry_sdk.get_client().options,
|
|
64
|
+
mechanism={"type": mechanism_type, "handled": False},
|
|
65
|
+
)
|
|
66
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
51
67
|
|
|
52
68
|
|
|
53
69
|
def _looks_like_asgi3(app):
|
|
@@ -67,10 +83,26 @@ def _looks_like_asgi3(app):
|
|
|
67
83
|
|
|
68
84
|
|
|
69
85
|
class SentryAsgiMiddleware:
|
|
70
|
-
__slots__ = (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
86
|
+
__slots__ = (
|
|
87
|
+
"app",
|
|
88
|
+
"__call__",
|
|
89
|
+
"transaction_style",
|
|
90
|
+
"mechanism_type",
|
|
91
|
+
"span_origin",
|
|
92
|
+
"http_methods_to_capture",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
app, # type: Any
|
|
98
|
+
unsafe_context_data=False, # type: bool
|
|
99
|
+
transaction_style="endpoint", # type: str
|
|
100
|
+
mechanism_type="asgi", # type: str
|
|
101
|
+
span_origin="manual", # type: str
|
|
102
|
+
http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
|
|
103
|
+
asgi_version=None, # type: Optional[int]
|
|
104
|
+
):
|
|
105
|
+
# type: (...) -> None
|
|
74
106
|
"""
|
|
75
107
|
Instrument an ASGI application with Sentry. Provides HTTP/websocket
|
|
76
108
|
data to sent events and basic handling for exceptions bubbling up
|
|
@@ -78,7 +110,6 @@ class SentryAsgiMiddleware:
|
|
|
78
110
|
|
|
79
111
|
:param unsafe_context_data: Disable errors when a proper contextvars installation could not be found. We do not recommend changing this from the default.
|
|
80
112
|
"""
|
|
81
|
-
|
|
82
113
|
if not unsafe_context_data and not HAS_REAL_CONTEXTVARS:
|
|
83
114
|
# We better have contextvars or we're going to leak state between
|
|
84
115
|
# requests.
|
|
@@ -86,154 +117,225 @@ class SentryAsgiMiddleware:
|
|
|
86
117
|
"The ASGI middleware for Sentry requires Python 3.7+ "
|
|
87
118
|
"or the aiocontextvars package." + CONTEXTVARS_ERROR_MESSAGE
|
|
88
119
|
)
|
|
120
|
+
if transaction_style not in TRANSACTION_STYLE_VALUES:
|
|
121
|
+
raise ValueError(
|
|
122
|
+
"Invalid value for transaction_style: %s (must be in %s)"
|
|
123
|
+
% (transaction_style, TRANSACTION_STYLE_VALUES)
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
asgi_middleware_while_using_starlette_or_fastapi = (
|
|
127
|
+
mechanism_type == "asgi" and "starlette" in _get_installed_modules()
|
|
128
|
+
)
|
|
129
|
+
if asgi_middleware_while_using_starlette_or_fastapi:
|
|
130
|
+
logger.warning(
|
|
131
|
+
"The Sentry Python SDK can now automatically support ASGI frameworks like Starlette and FastAPI. "
|
|
132
|
+
"Please remove 'SentryAsgiMiddleware' from your project. "
|
|
133
|
+
"See https://docs.sentry.io/platforms/python/guides/asgi/ for more information."
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
self.transaction_style = transaction_style
|
|
137
|
+
self.mechanism_type = mechanism_type
|
|
138
|
+
self.span_origin = span_origin
|
|
89
139
|
self.app = app
|
|
140
|
+
self.http_methods_to_capture = http_methods_to_capture
|
|
141
|
+
|
|
142
|
+
if asgi_version is None:
|
|
143
|
+
if _looks_like_asgi3(app):
|
|
144
|
+
asgi_version = 3
|
|
145
|
+
else:
|
|
146
|
+
asgi_version = 2
|
|
147
|
+
|
|
148
|
+
if asgi_version == 3:
|
|
149
|
+
self.__call__ = self._run_asgi3
|
|
150
|
+
elif asgi_version == 2:
|
|
151
|
+
self.__call__ = self._run_asgi2 # type: ignore
|
|
90
152
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
153
|
+
def _capture_lifespan_exception(self, exc):
|
|
154
|
+
# type: (Exception) -> None
|
|
155
|
+
"""Capture exceptions raise in application lifespan handlers.
|
|
156
|
+
|
|
157
|
+
The separate function is needed to support overriding in derived integrations that use different catching mechanisms.
|
|
158
|
+
"""
|
|
159
|
+
return _capture_exception(exc=exc, mechanism_type=self.mechanism_type)
|
|
160
|
+
|
|
161
|
+
def _capture_request_exception(self, exc):
|
|
162
|
+
# type: (Exception) -> None
|
|
163
|
+
"""Capture exceptions raised in incoming request handlers.
|
|
164
|
+
|
|
165
|
+
The separate function is needed to support overriding in derived integrations that use different catching mechanisms.
|
|
166
|
+
"""
|
|
167
|
+
return _capture_exception(exc=exc, mechanism_type=self.mechanism_type)
|
|
95
168
|
|
|
96
169
|
def _run_asgi2(self, scope):
|
|
97
170
|
# type: (Any) -> Any
|
|
98
171
|
async def inner(receive, send):
|
|
99
172
|
# type: (Any, Any) -> Any
|
|
100
|
-
return await self._run_app(scope,
|
|
173
|
+
return await self._run_app(scope, receive, send, asgi_version=2)
|
|
101
174
|
|
|
102
175
|
return inner
|
|
103
176
|
|
|
104
177
|
async def _run_asgi3(self, scope, receive, send):
|
|
105
178
|
# type: (Any, Any, Any) -> Any
|
|
106
|
-
return await self._run_app(scope,
|
|
179
|
+
return await self._run_app(scope, receive, send, asgi_version=3)
|
|
107
180
|
|
|
108
|
-
async def _run_app(self, scope,
|
|
109
|
-
# type: (Any, Any) -> Any
|
|
181
|
+
async def _run_app(self, scope, receive, send, asgi_version):
|
|
182
|
+
# type: (Any, Any, Any, int) -> Any
|
|
110
183
|
is_recursive_asgi_middleware = _asgi_middleware_applied.get(False)
|
|
111
|
-
|
|
112
|
-
if is_recursive_asgi_middleware:
|
|
184
|
+
is_lifespan = scope["type"] == "lifespan"
|
|
185
|
+
if is_recursive_asgi_middleware or is_lifespan:
|
|
113
186
|
try:
|
|
114
|
-
|
|
187
|
+
if asgi_version == 2:
|
|
188
|
+
return await self.app(scope)(receive, send)
|
|
189
|
+
else:
|
|
190
|
+
return await self.app(scope, receive, send)
|
|
191
|
+
|
|
115
192
|
except Exception as exc:
|
|
116
|
-
|
|
193
|
+
self._capture_lifespan_exception(exc)
|
|
117
194
|
raise exc from None
|
|
118
195
|
|
|
119
196
|
_asgi_middleware_applied.set(True)
|
|
120
197
|
try:
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
with hub.configure_scope() as sentry_scope:
|
|
198
|
+
with sentry_sdk.isolation_scope() as sentry_scope:
|
|
199
|
+
with track_session(sentry_scope, session_mode="request"):
|
|
124
200
|
sentry_scope.clear_breadcrumbs()
|
|
125
201
|
sentry_scope._name = "asgi"
|
|
126
202
|
processor = partial(self.event_processor, asgi_scope=scope)
|
|
127
203
|
sentry_scope.add_event_processor(processor)
|
|
128
204
|
|
|
129
|
-
|
|
205
|
+
ty = scope["type"]
|
|
206
|
+
(
|
|
207
|
+
transaction_name,
|
|
208
|
+
transaction_source,
|
|
209
|
+
) = self._get_transaction_name_and_source(
|
|
210
|
+
self.transaction_style,
|
|
211
|
+
scope,
|
|
212
|
+
)
|
|
130
213
|
|
|
131
|
-
|
|
132
|
-
transaction =
|
|
133
|
-
|
|
134
|
-
|
|
214
|
+
method = scope.get("method", "").upper()
|
|
215
|
+
transaction = None
|
|
216
|
+
if ty in ("http", "websocket"):
|
|
217
|
+
if ty == "websocket" or method in self.http_methods_to_capture:
|
|
218
|
+
transaction = continue_trace(
|
|
219
|
+
_get_headers(scope),
|
|
220
|
+
op="{}.server".format(ty),
|
|
221
|
+
name=transaction_name,
|
|
222
|
+
source=transaction_source,
|
|
223
|
+
origin=self.span_origin,
|
|
224
|
+
)
|
|
225
|
+
else:
|
|
226
|
+
transaction = Transaction(
|
|
227
|
+
op=OP.HTTP_SERVER,
|
|
228
|
+
name=transaction_name,
|
|
229
|
+
source=transaction_source,
|
|
230
|
+
origin=self.span_origin,
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
if transaction:
|
|
234
|
+
transaction.set_tag("asgi.type", ty)
|
|
235
|
+
|
|
236
|
+
transaction_context = (
|
|
237
|
+
sentry_sdk.start_transaction(
|
|
238
|
+
transaction,
|
|
239
|
+
custom_sampling_context={"asgi_scope": scope},
|
|
240
|
+
)
|
|
241
|
+
if transaction is not None
|
|
242
|
+
else nullcontext()
|
|
135
243
|
)
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
244
|
+
with transaction_context:
|
|
245
|
+
try:
|
|
246
|
+
|
|
247
|
+
async def _sentry_wrapped_send(event):
|
|
248
|
+
# type: (Dict[str, Any]) -> Any
|
|
249
|
+
if transaction is not None:
|
|
250
|
+
is_http_response = (
|
|
251
|
+
event.get("type") == "http.response.start"
|
|
252
|
+
and "status" in event
|
|
253
|
+
)
|
|
254
|
+
if is_http_response:
|
|
255
|
+
transaction.set_http_status(event["status"])
|
|
256
|
+
|
|
257
|
+
return await send(event)
|
|
258
|
+
|
|
259
|
+
if asgi_version == 2:
|
|
260
|
+
return await self.app(scope)(
|
|
261
|
+
receive, _sentry_wrapped_send
|
|
262
|
+
)
|
|
263
|
+
else:
|
|
264
|
+
return await self.app(
|
|
265
|
+
scope, receive, _sentry_wrapped_send
|
|
266
|
+
)
|
|
267
|
+
except Exception as exc:
|
|
268
|
+
self._capture_request_exception(exc)
|
|
269
|
+
raise exc from None
|
|
151
270
|
finally:
|
|
152
271
|
_asgi_middleware_applied.set(False)
|
|
153
272
|
|
|
154
273
|
def event_processor(self, event, hint, asgi_scope):
|
|
155
274
|
# type: (Event, Hint, Any) -> Optional[Event]
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
275
|
+
request_data = event.get("request", {})
|
|
276
|
+
request_data.update(_get_request_data(asgi_scope))
|
|
277
|
+
event["request"] = deepcopy(request_data)
|
|
278
|
+
|
|
279
|
+
# Only set transaction name if not already set by Starlette or FastAPI (or other frameworks)
|
|
280
|
+
transaction = event.get("transaction")
|
|
281
|
+
transaction_source = (event.get("transaction_info") or {}).get("source")
|
|
282
|
+
already_set = (
|
|
283
|
+
transaction is not None
|
|
284
|
+
and transaction != _DEFAULT_TRANSACTION_NAME
|
|
285
|
+
and transaction_source
|
|
286
|
+
in [
|
|
287
|
+
TransactionSource.COMPONENT,
|
|
288
|
+
TransactionSource.ROUTE,
|
|
289
|
+
TransactionSource.CUSTOM,
|
|
290
|
+
]
|
|
291
|
+
)
|
|
292
|
+
if not already_set:
|
|
293
|
+
name, source = self._get_transaction_name_and_source(
|
|
294
|
+
self.transaction_style, asgi_scope
|
|
168
295
|
)
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if client and _should_send_default_pii():
|
|
172
|
-
request_info["env"] = {"REMOTE_ADDR": client[0]}
|
|
173
|
-
|
|
174
|
-
if (
|
|
175
|
-
event.get("transaction", _DEFAULT_TRANSACTION_NAME)
|
|
176
|
-
== _DEFAULT_TRANSACTION_NAME
|
|
177
|
-
):
|
|
178
|
-
endpoint = asgi_scope.get("endpoint")
|
|
179
|
-
# Webframeworks like Starlette mutate the ASGI env once routing is
|
|
180
|
-
# done, which is sometime after the request has started. If we have
|
|
181
|
-
# an endpoint, overwrite our generic transaction name.
|
|
182
|
-
if endpoint:
|
|
183
|
-
event["transaction"] = transaction_from_function(endpoint)
|
|
184
|
-
|
|
185
|
-
event["request"] = request_info
|
|
296
|
+
event["transaction"] = name
|
|
297
|
+
event["transaction_info"] = {"source": source}
|
|
186
298
|
|
|
187
299
|
return event
|
|
188
300
|
|
|
189
|
-
# Helper functions
|
|
301
|
+
# Helper functions.
|
|
190
302
|
#
|
|
191
303
|
# Note: Those functions are not public API. If you want to mutate request
|
|
192
304
|
# data to your liking it's recommended to use the `before_send` callback
|
|
193
305
|
# for that.
|
|
194
306
|
|
|
195
|
-
def
|
|
196
|
-
# type: (
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
""
|
|
200
|
-
scheme = scope.get("scheme", default_scheme)
|
|
201
|
-
|
|
202
|
-
server = scope.get("server", None)
|
|
203
|
-
path = scope.get("root_path", "") + scope.get("path", "")
|
|
204
|
-
|
|
205
|
-
if host:
|
|
206
|
-
return "%s://%s%s" % (scheme, host, path)
|
|
307
|
+
def _get_transaction_name_and_source(self, transaction_style, asgi_scope):
|
|
308
|
+
# type: (SentryAsgiMiddleware, str, Any) -> Tuple[str, str]
|
|
309
|
+
name = None
|
|
310
|
+
source = SOURCE_FOR_STYLE[transaction_style]
|
|
311
|
+
ty = asgi_scope.get("type")
|
|
207
312
|
|
|
208
|
-
if
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
313
|
+
if transaction_style == "endpoint":
|
|
314
|
+
endpoint = asgi_scope.get("endpoint")
|
|
315
|
+
# Webframeworks like Starlette mutate the ASGI env once routing is
|
|
316
|
+
# done, which is sometime after the request has started. If we have
|
|
317
|
+
# an endpoint, overwrite our generic transaction name.
|
|
318
|
+
if endpoint:
|
|
319
|
+
name = transaction_from_function(endpoint) or ""
|
|
320
|
+
else:
|
|
321
|
+
name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None)
|
|
322
|
+
source = TransactionSource.URL
|
|
323
|
+
|
|
324
|
+
elif transaction_style == "url":
|
|
325
|
+
# FastAPI includes the route object in the scope to let Sentry extract the
|
|
326
|
+
# path from it for the transaction name
|
|
327
|
+
route = asgi_scope.get("route")
|
|
328
|
+
if route:
|
|
329
|
+
path = getattr(route, "path", None)
|
|
330
|
+
if path is not None:
|
|
331
|
+
name = path
|
|
332
|
+
else:
|
|
333
|
+
name = _get_url(asgi_scope, "http" if ty == "http" else "ws", host=None)
|
|
334
|
+
source = TransactionSource.URL
|
|
215
335
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
"""
|
|
221
|
-
qs = scope.get("query_string")
|
|
222
|
-
if not qs:
|
|
223
|
-
return None
|
|
224
|
-
return urllib.parse.unquote(qs.decode("latin-1"))
|
|
336
|
+
if name is None:
|
|
337
|
+
name = _DEFAULT_TRANSACTION_NAME
|
|
338
|
+
source = TransactionSource.ROUTE
|
|
339
|
+
return name, source
|
|
225
340
|
|
|
226
|
-
|
|
227
|
-
# type: (Any) -> Dict[str, str]
|
|
228
|
-
"""
|
|
229
|
-
Extract headers from the ASGI scope, in the format that the Sentry protocol expects.
|
|
230
|
-
"""
|
|
231
|
-
headers = {} # type: Dict[str, str]
|
|
232
|
-
for raw_key, raw_value in scope["headers"]:
|
|
233
|
-
key = raw_key.decode("latin-1")
|
|
234
|
-
value = raw_value.decode("latin-1")
|
|
235
|
-
if key in headers:
|
|
236
|
-
headers[key] = headers[key] + ", " + value
|
|
237
|
-
else:
|
|
238
|
-
headers[key] = value
|
|
239
|
-
return headers
|
|
341
|
+
return name, source
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
import functools
|
|
3
|
+
|
|
4
|
+
import sentry_sdk
|
|
5
|
+
from sentry_sdk.consts import OP
|
|
6
|
+
from sentry_sdk.integrations import Integration, DidNotEnable
|
|
7
|
+
from sentry_sdk.utils import event_from_exception, logger, reraise
|
|
8
|
+
|
|
9
|
+
try:
|
|
10
|
+
import asyncio
|
|
11
|
+
from asyncio.tasks import Task
|
|
12
|
+
except ImportError:
|
|
13
|
+
raise DidNotEnable("asyncio not available")
|
|
14
|
+
|
|
15
|
+
from typing import cast, TYPE_CHECKING
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from typing import Any, Callable, TypeVar
|
|
19
|
+
from collections.abc import Coroutine
|
|
20
|
+
|
|
21
|
+
from sentry_sdk._types import ExcInfo
|
|
22
|
+
|
|
23
|
+
T = TypeVar("T", bound=Callable[..., Any])
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def get_name(coro):
|
|
27
|
+
# type: (Any) -> str
|
|
28
|
+
return (
|
|
29
|
+
getattr(coro, "__qualname__", None)
|
|
30
|
+
or getattr(coro, "__name__", None)
|
|
31
|
+
or "coroutine without __name__"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _wrap_coroutine(wrapped):
|
|
36
|
+
# type: (Coroutine[Any, Any, Any]) -> Callable[[T], T]
|
|
37
|
+
# Only __name__ and __qualname__ are copied from function to coroutine in CPython
|
|
38
|
+
return functools.partial(
|
|
39
|
+
functools.update_wrapper,
|
|
40
|
+
wrapped=wrapped, # type: ignore
|
|
41
|
+
assigned=("__name__", "__qualname__"),
|
|
42
|
+
updated=(),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def patch_asyncio():
|
|
47
|
+
# type: () -> None
|
|
48
|
+
orig_task_factory = None
|
|
49
|
+
try:
|
|
50
|
+
loop = asyncio.get_running_loop()
|
|
51
|
+
orig_task_factory = loop.get_task_factory()
|
|
52
|
+
|
|
53
|
+
def _sentry_task_factory(loop, coro, **kwargs):
|
|
54
|
+
# type: (asyncio.AbstractEventLoop, Coroutine[Any, Any, Any], Any) -> asyncio.Future[Any]
|
|
55
|
+
|
|
56
|
+
@_wrap_coroutine(coro)
|
|
57
|
+
async def _task_with_sentry_span_creation():
|
|
58
|
+
# type: () -> Any
|
|
59
|
+
result = None
|
|
60
|
+
|
|
61
|
+
with sentry_sdk.isolation_scope():
|
|
62
|
+
with sentry_sdk.start_span(
|
|
63
|
+
op=OP.FUNCTION,
|
|
64
|
+
name=get_name(coro),
|
|
65
|
+
origin=AsyncioIntegration.origin,
|
|
66
|
+
):
|
|
67
|
+
try:
|
|
68
|
+
result = await coro
|
|
69
|
+
except StopAsyncIteration as e:
|
|
70
|
+
raise e from None
|
|
71
|
+
except Exception:
|
|
72
|
+
reraise(*_capture_exception())
|
|
73
|
+
|
|
74
|
+
return result
|
|
75
|
+
|
|
76
|
+
task = None
|
|
77
|
+
|
|
78
|
+
# Trying to use user set task factory (if there is one)
|
|
79
|
+
if orig_task_factory:
|
|
80
|
+
task = orig_task_factory(
|
|
81
|
+
loop, _task_with_sentry_span_creation(), **kwargs
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if task is None:
|
|
85
|
+
# The default task factory in `asyncio` does not have its own function
|
|
86
|
+
# but is just a couple of lines in `asyncio.base_events.create_task()`
|
|
87
|
+
# Those lines are copied here.
|
|
88
|
+
|
|
89
|
+
# WARNING:
|
|
90
|
+
# If the default behavior of the task creation in asyncio changes,
|
|
91
|
+
# this will break!
|
|
92
|
+
task = Task(_task_with_sentry_span_creation(), loop=loop, **kwargs)
|
|
93
|
+
if task._source_traceback: # type: ignore
|
|
94
|
+
del task._source_traceback[-1] # type: ignore
|
|
95
|
+
|
|
96
|
+
# Set the task name to include the original coroutine's name
|
|
97
|
+
try:
|
|
98
|
+
cast("asyncio.Task[Any]", task).set_name(
|
|
99
|
+
f"{get_name(coro)} (Sentry-wrapped)"
|
|
100
|
+
)
|
|
101
|
+
except AttributeError:
|
|
102
|
+
# set_name might not be available in all Python versions
|
|
103
|
+
pass
|
|
104
|
+
|
|
105
|
+
return task
|
|
106
|
+
|
|
107
|
+
loop.set_task_factory(_sentry_task_factory) # type: ignore
|
|
108
|
+
|
|
109
|
+
except RuntimeError:
|
|
110
|
+
# When there is no running loop, we have nothing to patch.
|
|
111
|
+
logger.warning(
|
|
112
|
+
"There is no running asyncio loop so there is nothing Sentry can patch. "
|
|
113
|
+
"Please make sure you call sentry_sdk.init() within a running "
|
|
114
|
+
"asyncio loop for the AsyncioIntegration to work. "
|
|
115
|
+
"See https://docs.sentry.io/platforms/python/integrations/asyncio/"
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _capture_exception():
|
|
120
|
+
# type: () -> ExcInfo
|
|
121
|
+
exc_info = sys.exc_info()
|
|
122
|
+
|
|
123
|
+
client = sentry_sdk.get_client()
|
|
124
|
+
|
|
125
|
+
integration = client.get_integration(AsyncioIntegration)
|
|
126
|
+
if integration is not None:
|
|
127
|
+
event, hint = event_from_exception(
|
|
128
|
+
exc_info,
|
|
129
|
+
client_options=client.options,
|
|
130
|
+
mechanism={"type": "asyncio", "handled": False},
|
|
131
|
+
)
|
|
132
|
+
sentry_sdk.capture_event(event, hint=hint)
|
|
133
|
+
|
|
134
|
+
return exc_info
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
class AsyncioIntegration(Integration):
|
|
138
|
+
identifier = "asyncio"
|
|
139
|
+
origin = f"auto.function.{identifier}"
|
|
140
|
+
|
|
141
|
+
@staticmethod
|
|
142
|
+
def setup_once():
|
|
143
|
+
# type: () -> None
|
|
144
|
+
patch_asyncio()
|