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.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +81 -19
  8. sentry_sdk/_types.py +311 -11
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +35 -28
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +20 -11
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +71 -60
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {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
- from sentry_sdk.hub import Hub, _should_send_default_pii
4
- from sentry_sdk.utils import AnnotatedValue
5
- from sentry_sdk._compat import text_type, iteritems
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
- from sentry_sdk._types import MYPY
9
+ try:
10
+ from django.http.request import RawPostDataException
11
+ except ImportError:
12
+ RawPostDataException = None
8
13
 
9
- if MYPY:
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.Client], int) -> bool
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["request_bodies"]
67
+ bodies = client.options["max_request_body_size"]
39
68
  return not (
40
69
  bodies == "never"
41
- or (bodies == "small" and content_length > 10 ** 3)
42
- or (bodies == "medium" and content_length > 10 ** 4)
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(object):
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: (Dict[str, Any]) -> None
53
- client = Hub.current.client
54
- if client is None:
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 _should_send_default_pii():
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 self.raw_data():
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: () -> Dict[str, Any]
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
- form = self.form()
109
- files = self.files()
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 = dict(iteritems(form))
112
- for k, v in iteritems(files):
113
- size = self.size_of_file(v)
114
- data[k] = AnnotatedValue(
115
- "", {"len": size, "rem": [["!raw", "x", 0, size]]}
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
- raw_data = self.raw_data()
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, text_type):
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: (Dict[str, str]) -> Dict[str, str]
170
- if _should_send_default_pii():
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("", {"rem": [["!config", "x", 0, len(v)]]})
234
+ else AnnotatedValue.removed_because_over_size_limit()
178
235
  )
179
- for k, v in iteritems(headers)
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
- from sentry_sdk._compat import reraise
5
- from sentry_sdk.hub import Hub
6
- from sentry_sdk.integrations import Integration, DidNotEnable
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 Transaction
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 sentry_sdk._types import MYPY
50
+ from typing import TYPE_CHECKING
31
51
 
32
- if MYPY:
52
+ if TYPE_CHECKING:
33
53
  from aiohttp.web_request import Request
34
- from aiohttp.abc import AbstractMatchInfo
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
- try:
54
- version = tuple(map(int, AIOHTTP_VERSION.split(".")[:2]))
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
- hub = Hub.current
78
- if hub.get_integration(AioHttpIntegration) is None:
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 Hub(Hub.current) as hub:
84
- # Scope data will not leak between requests because aiohttp
85
- # create a task to wrap each request.
86
- with hub.configure_scope() as scope:
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
- transaction = Transaction.continue_from_headers(
91
- request.headers,
92
- op="http.server",
93
- # If this transaction name makes it to the UI, AIOHTTP's
94
- # URL resolver did not find a route or died trying.
95
- name="generic AIOHTTP request",
96
- )
97
-
98
- with hub.start_transaction(transaction):
99
- try:
100
- response = await old_handle(self, request)
101
- except HTTPException as e:
102
- transaction.set_http_status(e.status_code)
103
- raise
104
- except asyncio.CancelledError:
105
- transaction.set_status("cancelled")
106
- raise
107
- except Exception:
108
- # This will probably map to a 500 but seems like we
109
- # have no way to tell. Do not set span status.
110
- reraise(*_capture_exception(hub))
111
-
112
- transaction.set_http_status(response.status)
113
- return response
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) -> AbstractMatchInfo
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
- name = transaction_from_function(rv.handler)
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
- with Hub.current.configure_scope() as scope:
132
- scope.transaction = name
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: (Callable[[], Request]) -> EventProcessor
294
+ # type: (weakref.ReferenceType[Request]) -> EventProcessor
141
295
  def aiohttp_processor(
142
- event, # type: Dict[str, Any]
143
- hint, # type: Dict[str, Tuple[type, BaseException, Any]]
296
+ event, # type: Event
297
+ hint, # type: dict[str, Tuple[type, BaseException, Any]]
144
298
  ):
145
- # type: (...) -> Dict[str, Any]
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(hub, request)
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(hub):
177
- # type: (Hub) -> ExcInfo
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=hub.client.options, # type: ignore
333
+ client_options=sentry_sdk.get_client().options,
182
334
  mechanism={"type": "aiohttp", "handled": False},
183
335
  )
184
- hub.capture_event(event, hint=hint)
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(hub, request):
192
- # type: (Hub, Request) -> Union[Optional[str], AnnotatedValue]
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(hub.client, len(bytes_body)):
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