sentry-sdk 2.30.0__py2.py3-none-any.whl → 3.0.0a2__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.

Potentially problematic release.


This version of sentry-sdk might be problematic. Click here for more details.

Files changed (109) hide show
  1. sentry_sdk/__init__.py +3 -8
  2. sentry_sdk/_compat.py +0 -1
  3. sentry_sdk/_init_implementation.py +6 -44
  4. sentry_sdk/_types.py +2 -64
  5. sentry_sdk/ai/monitoring.py +14 -10
  6. sentry_sdk/ai/utils.py +1 -1
  7. sentry_sdk/api.py +56 -169
  8. sentry_sdk/client.py +27 -72
  9. sentry_sdk/consts.py +60 -23
  10. sentry_sdk/debug.py +0 -10
  11. sentry_sdk/envelope.py +1 -3
  12. sentry_sdk/feature_flags.py +1 -1
  13. sentry_sdk/integrations/__init__.py +4 -2
  14. sentry_sdk/integrations/_asgi_common.py +5 -6
  15. sentry_sdk/integrations/_wsgi_common.py +11 -40
  16. sentry_sdk/integrations/aiohttp.py +104 -57
  17. sentry_sdk/integrations/anthropic.py +10 -7
  18. sentry_sdk/integrations/arq.py +24 -13
  19. sentry_sdk/integrations/asgi.py +102 -83
  20. sentry_sdk/integrations/asyncio.py +1 -0
  21. sentry_sdk/integrations/asyncpg.py +45 -30
  22. sentry_sdk/integrations/aws_lambda.py +109 -92
  23. sentry_sdk/integrations/boto3.py +38 -9
  24. sentry_sdk/integrations/bottle.py +1 -1
  25. sentry_sdk/integrations/celery/__init__.py +51 -41
  26. sentry_sdk/integrations/clickhouse_driver.py +59 -28
  27. sentry_sdk/integrations/cohere.py +2 -0
  28. sentry_sdk/integrations/django/__init__.py +25 -46
  29. sentry_sdk/integrations/django/asgi.py +6 -2
  30. sentry_sdk/integrations/django/caching.py +13 -22
  31. sentry_sdk/integrations/django/middleware.py +1 -0
  32. sentry_sdk/integrations/django/signals_handlers.py +3 -1
  33. sentry_sdk/integrations/django/templates.py +8 -12
  34. sentry_sdk/integrations/django/transactions.py +1 -6
  35. sentry_sdk/integrations/django/views.py +5 -2
  36. sentry_sdk/integrations/falcon.py +7 -25
  37. sentry_sdk/integrations/fastapi.py +3 -3
  38. sentry_sdk/integrations/flask.py +1 -1
  39. sentry_sdk/integrations/gcp.py +63 -38
  40. sentry_sdk/integrations/graphene.py +6 -13
  41. sentry_sdk/integrations/grpc/aio/client.py +14 -8
  42. sentry_sdk/integrations/grpc/aio/server.py +19 -21
  43. sentry_sdk/integrations/grpc/client.py +8 -6
  44. sentry_sdk/integrations/grpc/server.py +12 -14
  45. sentry_sdk/integrations/httpx.py +47 -12
  46. sentry_sdk/integrations/huey.py +26 -22
  47. sentry_sdk/integrations/huggingface_hub.py +1 -0
  48. sentry_sdk/integrations/langchain.py +22 -15
  49. sentry_sdk/integrations/litestar.py +4 -2
  50. sentry_sdk/integrations/logging.py +7 -2
  51. sentry_sdk/integrations/openai.py +2 -0
  52. sentry_sdk/integrations/pymongo.py +18 -25
  53. sentry_sdk/integrations/pyramid.py +1 -1
  54. sentry_sdk/integrations/quart.py +3 -3
  55. sentry_sdk/integrations/ray.py +23 -17
  56. sentry_sdk/integrations/redis/_async_common.py +29 -18
  57. sentry_sdk/integrations/redis/_sync_common.py +28 -19
  58. sentry_sdk/integrations/redis/modules/caches.py +13 -10
  59. sentry_sdk/integrations/redis/modules/queries.py +14 -11
  60. sentry_sdk/integrations/redis/rb.py +4 -4
  61. sentry_sdk/integrations/redis/redis.py +6 -6
  62. sentry_sdk/integrations/redis/redis_cluster.py +18 -18
  63. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +4 -4
  64. sentry_sdk/integrations/redis/utils.py +64 -24
  65. sentry_sdk/integrations/rq.py +68 -23
  66. sentry_sdk/integrations/rust_tracing.py +28 -43
  67. sentry_sdk/integrations/sanic.py +23 -13
  68. sentry_sdk/integrations/socket.py +9 -5
  69. sentry_sdk/integrations/sqlalchemy.py +8 -8
  70. sentry_sdk/integrations/starlette.py +11 -31
  71. sentry_sdk/integrations/starlite.py +4 -2
  72. sentry_sdk/integrations/stdlib.py +56 -9
  73. sentry_sdk/integrations/strawberry.py +40 -59
  74. sentry_sdk/integrations/threading.py +10 -26
  75. sentry_sdk/integrations/tornado.py +57 -18
  76. sentry_sdk/integrations/trytond.py +4 -1
  77. sentry_sdk/integrations/wsgi.py +84 -38
  78. sentry_sdk/opentelemetry/__init__.py +9 -0
  79. sentry_sdk/opentelemetry/consts.py +33 -0
  80. sentry_sdk/opentelemetry/contextvars_context.py +81 -0
  81. sentry_sdk/{integrations/opentelemetry → opentelemetry}/propagator.py +19 -28
  82. sentry_sdk/opentelemetry/sampler.py +326 -0
  83. sentry_sdk/opentelemetry/scope.py +218 -0
  84. sentry_sdk/opentelemetry/span_processor.py +335 -0
  85. sentry_sdk/opentelemetry/tracing.py +59 -0
  86. sentry_sdk/opentelemetry/utils.py +484 -0
  87. sentry_sdk/profiler/__init__.py +0 -40
  88. sentry_sdk/profiler/continuous_profiler.py +1 -30
  89. sentry_sdk/profiler/transaction_profiler.py +5 -56
  90. sentry_sdk/scope.py +108 -361
  91. sentry_sdk/sessions.py +0 -87
  92. sentry_sdk/tracing.py +415 -1161
  93. sentry_sdk/tracing_utils.py +130 -166
  94. sentry_sdk/transport.py +4 -104
  95. sentry_sdk/utils.py +169 -152
  96. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/METADATA +3 -5
  97. sentry_sdk-3.0.0a2.dist-info/RECORD +154 -0
  98. sentry_sdk-3.0.0a2.dist-info/entry_points.txt +2 -0
  99. sentry_sdk/hub.py +0 -739
  100. sentry_sdk/integrations/opentelemetry/__init__.py +0 -7
  101. sentry_sdk/integrations/opentelemetry/consts.py +0 -5
  102. sentry_sdk/integrations/opentelemetry/integration.py +0 -58
  103. sentry_sdk/integrations/opentelemetry/span_processor.py +0 -391
  104. sentry_sdk/metrics.py +0 -965
  105. sentry_sdk-2.30.0.dist-info/RECORD +0 -152
  106. sentry_sdk-2.30.0.dist-info/entry_points.txt +0 -2
  107. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/WHEEL +0 -0
  108. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/licenses/LICENSE +0 -0
  109. {sentry_sdk-2.30.0.dist-info → sentry_sdk-3.0.0a2.dist-info}/top_level.txt +0 -0
@@ -107,14 +107,6 @@ def _patch_schema_init():
107
107
  "False" if should_use_async_extension else "True",
108
108
  )
109
109
 
110
- # remove the built in strawberry sentry extension, if present
111
- extensions = [
112
- extension
113
- for extension in extensions
114
- if extension
115
- not in (StrawberrySentryAsyncExtension, StrawberrySentrySyncExtension)
116
- ]
117
-
118
110
  # add our extension
119
111
  extensions.append(
120
112
  SentryAsyncExtension if should_use_async_extension else SentrySyncExtension
@@ -184,58 +176,52 @@ class SentryAsyncExtension(SchemaExtension):
184
176
  event_processor = _make_request_event_processor(self.execution_context)
185
177
  scope.add_event_processor(event_processor)
186
178
 
187
- span = sentry_sdk.get_current_span()
188
- if span:
189
- self.graphql_span = span.start_child(
190
- op=op,
191
- name=description,
192
- origin=StrawberryIntegration.origin,
193
- )
194
- else:
195
- self.graphql_span = sentry_sdk.start_span(
196
- op=op,
197
- name=description,
198
- origin=StrawberryIntegration.origin,
199
- )
179
+ with sentry_sdk.start_span(
180
+ op=op,
181
+ name=description,
182
+ origin=StrawberryIntegration.origin,
183
+ only_if_parent=True,
184
+ ) as graphql_span:
185
+ graphql_span.set_attribute("graphql.operation.type", operation_type)
186
+ graphql_span.set_attribute("graphql.document", self.execution_context.query)
187
+ graphql_span.set_attribute("graphql.resource_name", self._resource_name)
188
+
189
+ yield
200
190
 
201
- self.graphql_span.set_data("graphql.operation.type", operation_type)
202
- self.graphql_span.set_data("graphql.operation.name", self._operation_name)
203
- self.graphql_span.set_data("graphql.document", self.execution_context.query)
204
- self.graphql_span.set_data("graphql.resource_name", self._resource_name)
191
+ # we might have a more accurate operation_name after the parsing
192
+ self._operation_name = self.execution_context.operation_name
205
193
 
206
- yield
194
+ if self._operation_name is not None:
195
+ graphql_span.set_attribute(
196
+ "graphql.operation.name", self._operation_name
197
+ )
207
198
 
208
- transaction = self.graphql_span.containing_transaction
209
- if transaction and self.execution_context.operation_name:
210
- transaction.name = self.execution_context.operation_name
211
- transaction.source = TransactionSource.COMPONENT
212
- transaction.op = op
199
+ sentry_sdk.get_current_scope().set_transaction_name(
200
+ self._operation_name,
201
+ source=TransactionSource.COMPONENT,
202
+ )
213
203
 
214
- self.graphql_span.finish()
204
+ root_span = graphql_span.root_span
205
+ if root_span:
206
+ root_span.op = op
215
207
 
216
208
  def on_validate(self):
217
209
  # type: () -> Generator[None, None, None]
218
- self.validation_span = self.graphql_span.start_child(
210
+ with sentry_sdk.start_span(
219
211
  op=OP.GRAPHQL_VALIDATE,
220
212
  name="validation",
221
213
  origin=StrawberryIntegration.origin,
222
- )
223
-
224
- yield
225
-
226
- self.validation_span.finish()
214
+ ):
215
+ yield
227
216
 
228
217
  def on_parse(self):
229
218
  # type: () -> Generator[None, None, None]
230
- self.parsing_span = self.graphql_span.start_child(
219
+ with sentry_sdk.start_span(
231
220
  op=OP.GRAPHQL_PARSE,
232
221
  name="parsing",
233
222
  origin=StrawberryIntegration.origin,
234
- )
235
-
236
- yield
237
-
238
- self.parsing_span.finish()
223
+ ):
224
+ yield
239
225
 
240
226
  def should_skip_tracing(self, _next, info):
241
227
  # type: (Callable[[Any, GraphQLResolveInfo, Any, Any], Any], GraphQLResolveInfo) -> bool
@@ -257,15 +243,15 @@ class SentryAsyncExtension(SchemaExtension):
257
243
 
258
244
  field_path = "{}.{}".format(info.parent_type, info.field_name)
259
245
 
260
- with self.graphql_span.start_child(
246
+ with sentry_sdk.start_span(
261
247
  op=OP.GRAPHQL_RESOLVE,
262
248
  name="resolving {}".format(field_path),
263
249
  origin=StrawberryIntegration.origin,
264
250
  ) as span:
265
- span.set_data("graphql.field_name", info.field_name)
266
- span.set_data("graphql.parent_type", info.parent_type.name)
267
- span.set_data("graphql.field_path", field_path)
268
- span.set_data("graphql.path", ".".join(map(str, info.path.as_list())))
251
+ span.set_attribute("graphql.field_name", info.field_name)
252
+ span.set_attribute("graphql.parent_type", info.parent_type.name)
253
+ span.set_attribute("graphql.field_path", field_path)
254
+ span.set_attribute("graphql.path", ".".join(map(str, info.path.as_list())))
269
255
 
270
256
  return await self._resolve(_next, root, info, *args, **kwargs)
271
257
 
@@ -278,15 +264,15 @@ class SentrySyncExtension(SentryAsyncExtension):
278
264
 
279
265
  field_path = "{}.{}".format(info.parent_type, info.field_name)
280
266
 
281
- with self.graphql_span.start_child(
267
+ with sentry_sdk.start_span(
282
268
  op=OP.GRAPHQL_RESOLVE,
283
269
  name="resolving {}".format(field_path),
284
270
  origin=StrawberryIntegration.origin,
285
271
  ) as span:
286
- span.set_data("graphql.field_name", info.field_name)
287
- span.set_data("graphql.parent_type", info.parent_type.name)
288
- span.set_data("graphql.field_path", field_path)
289
- span.set_data("graphql.path", ".".join(map(str, info.path.as_list())))
272
+ span.set_attribute("graphql.field_name", info.field_name)
273
+ span.set_attribute("graphql.parent_type", info.parent_type.name)
274
+ span.set_attribute("graphql.field_path", field_path)
275
+ span.set_attribute("graphql.path", ".".join(map(str, info.path.as_list())))
290
276
 
291
277
  return _next(root, info, *args, **kwargs)
292
278
 
@@ -383,11 +369,6 @@ def _make_response_event_processor(response_data):
383
369
 
384
370
  def _guess_if_using_async(extensions):
385
371
  # type: (List[SchemaExtension]) -> bool
386
- if StrawberrySentryAsyncExtension in extensions:
387
- return True
388
- elif StrawberrySentrySyncExtension in extensions:
389
- return False
390
-
391
372
  return bool(
392
373
  {"starlette", "starlite", "litestar", "fastapi"} & set(_get_installed_modules())
393
374
  )
@@ -4,12 +4,12 @@ from functools import wraps
4
4
  from threading import Thread, current_thread
5
5
 
6
6
  import sentry_sdk
7
+ from sentry_sdk import Scope
8
+ from sentry_sdk.scope import ScopeType
7
9
  from sentry_sdk.integrations import Integration
8
- from sentry_sdk.scope import use_isolation_scope, use_scope
9
10
  from sentry_sdk.utils import (
10
11
  event_from_exception,
11
12
  capture_internal_exceptions,
12
- logger,
13
13
  reraise,
14
14
  )
15
15
 
@@ -19,7 +19,6 @@ if TYPE_CHECKING:
19
19
  from typing import Any
20
20
  from typing import TypeVar
21
21
  from typing import Callable
22
- from typing import Optional
23
22
 
24
23
  from sentry_sdk._types import ExcInfo
25
24
 
@@ -29,22 +28,10 @@ if TYPE_CHECKING:
29
28
  class ThreadingIntegration(Integration):
30
29
  identifier = "threading"
31
30
 
32
- def __init__(self, propagate_hub=None, propagate_scope=True):
33
- # type: (Optional[bool], bool) -> None
34
- if propagate_hub is not None:
35
- logger.warning(
36
- "Deprecated: propagate_hub is deprecated. This will be removed in the future."
37
- )
38
-
39
- # Note: propagate_hub did not have any effect on propagation of scope data
40
- # scope data was always propagated no matter what the value of propagate_hub was
41
- # This is why the default for propagate_scope is True
42
-
31
+ def __init__(self, propagate_scope=True):
32
+ # type: (bool) -> None
43
33
  self.propagate_scope = propagate_scope
44
34
 
45
- if propagate_hub is not None:
46
- self.propagate_scope = propagate_hub
47
-
48
35
  @staticmethod
49
36
  def setup_once():
50
37
  # type: () -> None
@@ -89,8 +76,8 @@ class ThreadingIntegration(Integration):
89
76
  isolation_scope = sentry_sdk.get_isolation_scope().fork()
90
77
  current_scope = sentry_sdk.get_current_scope().fork()
91
78
  else:
92
- isolation_scope = None
93
- current_scope = None
79
+ isolation_scope = Scope(ty=ScopeType.ISOLATION)
80
+ current_scope = Scope(ty=ScopeType.CURRENT)
94
81
 
95
82
  # Patching instance methods in `start()` creates a reference cycle if
96
83
  # done in a naive way. See
@@ -112,7 +99,7 @@ class ThreadingIntegration(Integration):
112
99
 
113
100
 
114
101
  def _wrap_run(isolation_scope_to_use, current_scope_to_use, old_run_func):
115
- # type: (Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope], F) -> F
102
+ # type: (sentry_sdk.Scope, sentry_sdk.Scope, F) -> F
116
103
  @wraps(old_run_func)
117
104
  def run(*a, **kw):
118
105
  # type: (*Any, **Any) -> Any
@@ -124,12 +111,9 @@ def _wrap_run(isolation_scope_to_use, current_scope_to_use, old_run_func):
124
111
  except Exception:
125
112
  reraise(*_capture_exception())
126
113
 
127
- if isolation_scope_to_use is not None and current_scope_to_use is not None:
128
- with use_isolation_scope(isolation_scope_to_use):
129
- with use_scope(current_scope_to_use):
130
- return _run_old_run_func()
131
- else:
132
- return _run_old_run_func()
114
+ with sentry_sdk.use_isolation_scope(isolation_scope_to_use):
115
+ with sentry_sdk.use_scope(current_scope_to_use):
116
+ return _run_old_run_func()
133
117
 
134
118
  return run # type: ignore
135
119
 
@@ -3,7 +3,6 @@ import contextlib
3
3
  from inspect import iscoroutinefunction
4
4
 
5
5
  import sentry_sdk
6
- from sentry_sdk.api import continue_trace
7
6
  from sentry_sdk.consts import OP
8
7
  from sentry_sdk.scope import should_send_default_pii
9
8
  from sentry_sdk.tracing import TransactionSource
@@ -20,13 +19,15 @@ from sentry_sdk.integrations._wsgi_common import (
20
19
  RequestExtractor,
21
20
  _filter_headers,
22
21
  _is_json_content_type,
22
+ _request_headers_to_span_attributes,
23
23
  )
24
24
  from sentry_sdk.integrations.logging import ignore_logger
25
25
 
26
26
  try:
27
27
  from tornado import version_info as TORNADO_VERSION
28
- from tornado.web import RequestHandler, HTTPError
29
28
  from tornado.gen import coroutine
29
+ from tornado.httputil import HTTPServerRequest
30
+ from tornado.web import RequestHandler, HTTPError
30
31
  except ImportError:
31
32
  raise DidNotEnable("Tornado not installed")
32
33
 
@@ -42,6 +43,14 @@ if TYPE_CHECKING:
42
43
  from sentry_sdk._types import Event, EventProcessor
43
44
 
44
45
 
46
+ REQUEST_PROPERTY_TO_ATTRIBUTE = {
47
+ "method": "http.request.method",
48
+ "path": "url.path",
49
+ "query": "url.query",
50
+ "protocol": "url.scheme",
51
+ }
52
+
53
+
45
54
  class TornadoIntegration(Integration):
46
55
  identifier = "tornado"
47
56
  origin = f"auto.http.{identifier}"
@@ -111,22 +120,19 @@ def _handle_request_impl(self):
111
120
  processor = _make_event_processor(weak_handler)
112
121
  scope.add_event_processor(processor)
113
122
 
114
- transaction = continue_trace(
115
- headers,
116
- op=OP.HTTP_SERVER,
117
- # Like with all other integrations, this is our
118
- # fallback transaction in case there is no route.
119
- # sentry_urldispatcher_resolve is responsible for
120
- # setting a transaction name later.
121
- name="generic Tornado request",
122
- source=TransactionSource.ROUTE,
123
- origin=TornadoIntegration.origin,
124
- )
125
-
126
- with sentry_sdk.start_transaction(
127
- transaction, custom_sampling_context={"tornado_request": self.request}
128
- ):
129
- yield
123
+ with sentry_sdk.continue_trace(headers):
124
+ with sentry_sdk.start_span(
125
+ op=OP.HTTP_SERVER,
126
+ # Like with all other integrations, this is our
127
+ # fallback transaction in case there is no route.
128
+ # sentry_urldispatcher_resolve is responsible for
129
+ # setting a transaction name later.
130
+ name="generic Tornado request",
131
+ source=TransactionSource.ROUTE,
132
+ origin=TornadoIntegration.origin,
133
+ attributes=_prepopulate_attributes(self.request),
134
+ ):
135
+ yield
130
136
 
131
137
 
132
138
  @ensure_integration_enabled(TornadoIntegration)
@@ -218,3 +224,36 @@ class TornadoRequestExtractor(RequestExtractor):
218
224
  def size_of_file(self, file):
219
225
  # type: (Any) -> int
220
226
  return len(file.body or ())
227
+
228
+
229
+ def _prepopulate_attributes(request):
230
+ # type: (HTTPServerRequest) -> dict[str, Any]
231
+ # https://www.tornadoweb.org/en/stable/httputil.html#tornado.httputil.HTTPServerRequest
232
+ attributes = {}
233
+
234
+ for prop, attr in REQUEST_PROPERTY_TO_ATTRIBUTE.items():
235
+ if getattr(request, prop, None) is not None:
236
+ attributes[attr] = getattr(request, prop)
237
+
238
+ if getattr(request, "version", None):
239
+ try:
240
+ proto, version = request.version.split("/")
241
+ attributes["network.protocol.name"] = proto
242
+ attributes["network.protocol.version"] = version
243
+ except ValueError:
244
+ attributes["network.protocol.name"] = request.version
245
+
246
+ if getattr(request, "host", None):
247
+ try:
248
+ address, port = request.host.split(":")
249
+ attributes["server.address"] = address
250
+ attributes["server.port"] = port
251
+ except ValueError:
252
+ attributes["server.address"] = request.host
253
+
254
+ with capture_internal_exceptions():
255
+ attributes["url.full"] = request.full_url()
256
+
257
+ attributes.update(_request_headers_to_span_attributes(request.headers))
258
+
259
+ return attributes
@@ -1,8 +1,9 @@
1
1
  import sentry_sdk
2
- from sentry_sdk.integrations import Integration
2
+ from sentry_sdk.integrations import _check_minimum_version, Integration
3
3
  from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware
4
4
  from sentry_sdk.utils import ensure_integration_enabled, event_from_exception
5
5
 
6
+ from trytond import __version__ as trytond_version # type: ignore
6
7
  from trytond.exceptions import TrytonException # type: ignore
7
8
  from trytond.wsgi import app # type: ignore
8
9
 
@@ -19,6 +20,8 @@ class TrytondWSGIIntegration(Integration):
19
20
 
20
21
  @staticmethod
21
22
  def setup_once(): # type: () -> None
23
+ _check_minimum_version(TrytondWSGIIntegration, trytond_version)
24
+
22
25
  app.wsgi_app = SentryWsgiMiddleware(
23
26
  app.wsgi_app,
24
27
  span_origin=TrytondWSGIIntegration.origin,
@@ -3,17 +3,15 @@ from functools import partial
3
3
 
4
4
  import sentry_sdk
5
5
  from sentry_sdk._werkzeug import get_host, _get_headers
6
- from sentry_sdk.api import continue_trace
7
6
  from sentry_sdk.consts import OP
8
7
  from sentry_sdk.scope import should_send_default_pii
9
8
  from sentry_sdk.integrations._wsgi_common import (
10
9
  DEFAULT_HTTP_METHODS_TO_CAPTURE,
11
10
  _filter_headers,
12
- nullcontext,
11
+ _request_headers_to_span_attributes,
13
12
  )
14
13
  from sentry_sdk.sessions import track_session
15
- from sentry_sdk.scope import use_isolation_scope
16
- from sentry_sdk.tracing import Transaction, TransactionSource
14
+ from sentry_sdk.tracing import Span, TransactionSource
17
15
  from sentry_sdk.utils import (
18
16
  ContextVar,
19
17
  capture_internal_exceptions,
@@ -48,6 +46,17 @@ if TYPE_CHECKING:
48
46
 
49
47
  _wsgi_middleware_applied = ContextVar("sentry_wsgi_middleware_applied")
50
48
 
49
+ DEFAULT_TRANSACTION_NAME = "generic WSGI request"
50
+
51
+ ENVIRON_TO_ATTRIBUTE = {
52
+ "PATH_INFO": "url.path",
53
+ "QUERY_STRING": "url.query",
54
+ "REQUEST_METHOD": "http.request.method",
55
+ "SERVER_NAME": "server.address",
56
+ "SERVER_PORT": "server.port",
57
+ "wsgi.url_scheme": "url.scheme",
58
+ }
59
+
51
60
 
52
61
  def wsgi_decoding_dance(s, charset="utf-8", errors="replace"):
53
62
  # type: (str, str, str) -> str
@@ -81,7 +90,7 @@ class SentryWsgiMiddleware:
81
90
  self,
82
91
  app, # type: Callable[[Dict[str, str], Callable[..., Any]], Any]
83
92
  use_x_forwarded_for=False, # type: bool
84
- span_origin="manual", # type: str
93
+ span_origin=None, # type: Optional[str]
85
94
  http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
86
95
  ):
87
96
  # type: (...) -> None
@@ -98,6 +107,10 @@ class SentryWsgiMiddleware:
98
107
  _wsgi_middleware_applied.set(True)
99
108
  try:
100
109
  with sentry_sdk.isolation_scope() as scope:
110
+ scope.set_transaction_name(
111
+ DEFAULT_TRANSACTION_NAME, source=TransactionSource.ROUTE
112
+ )
113
+
101
114
  with track_session(scope, session_mode="request"):
102
115
  with capture_internal_exceptions():
103
116
  scope.clear_breadcrumbs()
@@ -107,44 +120,48 @@ class SentryWsgiMiddleware:
107
120
  environ, self.use_x_forwarded_for
108
121
  )
109
122
  )
110
-
111
123
  method = environ.get("REQUEST_METHOD", "").upper()
112
- transaction = None
113
- if method in self.http_methods_to_capture:
114
- transaction = continue_trace(
115
- environ,
116
- op=OP.HTTP_SERVER,
117
- name="generic WSGI request",
118
- source=TransactionSource.ROUTE,
119
- origin=self.span_origin,
120
- )
121
-
122
- with (
123
- sentry_sdk.start_transaction(
124
- transaction,
125
- custom_sampling_context={"wsgi_environ": environ},
126
- )
127
- if transaction is not None
128
- else nullcontext()
129
- ):
130
- try:
131
- response = self.app(
132
- environ,
133
- partial(
134
- _sentry_start_response, start_response, transaction
124
+ should_trace = method in self.http_methods_to_capture
125
+ if should_trace:
126
+ with sentry_sdk.continue_trace(environ):
127
+ with sentry_sdk.start_span(
128
+ op=OP.HTTP_SERVER,
129
+ name=DEFAULT_TRANSACTION_NAME,
130
+ source=TransactionSource.ROUTE,
131
+ origin=self.span_origin,
132
+ attributes=_prepopulate_attributes(
133
+ environ, self.use_x_forwarded_for
135
134
  ),
136
- )
137
- except BaseException:
138
- reraise(*_capture_exception())
135
+ ) as span:
136
+ response = self._run_original_app(
137
+ environ, start_response, span
138
+ )
139
+ else:
140
+ response = self._run_original_app(environ, start_response, None)
141
+
139
142
  finally:
140
143
  _wsgi_middleware_applied.set(False)
141
144
 
142
145
  return _ScopedResponse(scope, response)
143
146
 
147
+ def _run_original_app(self, environ, start_response, span):
148
+ # type: (dict[str, str], StartResponse, Optional[Span]) -> Any
149
+ try:
150
+ return self.app(
151
+ environ,
152
+ partial(
153
+ _sentry_start_response,
154
+ start_response,
155
+ span,
156
+ ),
157
+ )
158
+ except BaseException:
159
+ reraise(*_capture_exception())
160
+
144
161
 
145
162
  def _sentry_start_response( # type: ignore
146
163
  old_start_response, # type: StartResponse
147
- transaction, # type: Optional[Transaction]
164
+ span, # type: Optional[Span]
148
165
  status, # type: str
149
166
  response_headers, # type: WsgiResponseHeaders
150
167
  exc_info=None, # type: Optional[WsgiExcInfo]
@@ -152,8 +169,8 @@ def _sentry_start_response( # type: ignore
152
169
  # type: (...) -> WsgiResponseIter
153
170
  with capture_internal_exceptions():
154
171
  status_int = int(status.split(" ", 1)[0])
155
- if transaction is not None:
156
- transaction.set_http_status(status_int)
172
+ if span is not None:
173
+ span.set_http_status(status_int)
157
174
 
158
175
  if exc_info is None:
159
176
  # The Django Rest Framework WSGI test client, and likely other
@@ -237,7 +254,7 @@ class _ScopedResponse:
237
254
  __slots__ = ("_response", "_scope")
238
255
 
239
256
  def __init__(self, scope, response):
240
- # type: (sentry_sdk.scope.Scope, Iterator[bytes]) -> None
257
+ # type: (sentry_sdk.Scope, Iterator[bytes]) -> None
241
258
  self._scope = scope
242
259
  self._response = response
243
260
 
@@ -246,7 +263,7 @@ class _ScopedResponse:
246
263
  iterator = iter(self._response)
247
264
 
248
265
  while True:
249
- with use_isolation_scope(self._scope):
266
+ with sentry_sdk.use_isolation_scope(self._scope):
250
267
  try:
251
268
  chunk = next(iterator)
252
269
  except StopIteration:
@@ -258,7 +275,7 @@ class _ScopedResponse:
258
275
 
259
276
  def close(self):
260
277
  # type: () -> None
261
- with use_isolation_scope(self._scope):
278
+ with sentry_sdk.use_isolation_scope(self._scope):
262
279
  try:
263
280
  self._response.close() # type: ignore
264
281
  except AttributeError:
@@ -308,3 +325,32 @@ def _make_wsgi_event_processor(environ, use_x_forwarded_for):
308
325
  return event
309
326
 
310
327
  return event_processor
328
+
329
+
330
+ def _prepopulate_attributes(wsgi_environ, use_x_forwarded_for=False):
331
+ # type: (dict[str, str], bool) -> dict[str, str]
332
+ """Extract span attributes from the WSGI environment."""
333
+ attributes = {}
334
+
335
+ for property, attr in ENVIRON_TO_ATTRIBUTE.items():
336
+ if wsgi_environ.get(property) is not None:
337
+ attributes[attr] = wsgi_environ[property]
338
+
339
+ if wsgi_environ.get("SERVER_PROTOCOL") is not None:
340
+ try:
341
+ proto, version = wsgi_environ["SERVER_PROTOCOL"].split("/")
342
+ attributes["network.protocol.name"] = proto
343
+ attributes["network.protocol.version"] = version
344
+ except Exception:
345
+ attributes["network.protocol.name"] = wsgi_environ["SERVER_PROTOCOL"]
346
+
347
+ with capture_internal_exceptions():
348
+ url = get_request_url(wsgi_environ, use_x_forwarded_for)
349
+ query = wsgi_environ.get("QUERY_STRING")
350
+ attributes["url.full"] = f"{url}?{query}"
351
+
352
+ attributes.update(
353
+ _request_headers_to_span_attributes(dict(_get_headers(wsgi_environ)))
354
+ )
355
+
356
+ return attributes
@@ -0,0 +1,9 @@
1
+ from sentry_sdk.opentelemetry.propagator import SentryPropagator
2
+ from sentry_sdk.opentelemetry.sampler import SentrySampler
3
+ from sentry_sdk.opentelemetry.span_processor import SentrySpanProcessor
4
+
5
+ __all__ = [
6
+ "SentryPropagator",
7
+ "SentrySampler",
8
+ "SentrySpanProcessor",
9
+ ]
@@ -0,0 +1,33 @@
1
+ from opentelemetry.context import create_key
2
+ from sentry_sdk.tracing_utils import Baggage
3
+
4
+
5
+ # propagation keys
6
+ SENTRY_TRACE_KEY = create_key("sentry-trace")
7
+ SENTRY_BAGGAGE_KEY = create_key("sentry-baggage")
8
+
9
+ # scope management keys
10
+ SENTRY_SCOPES_KEY = create_key("sentry_scopes")
11
+ SENTRY_FORK_ISOLATION_SCOPE_KEY = create_key("sentry_fork_isolation_scope")
12
+ SENTRY_USE_CURRENT_SCOPE_KEY = create_key("sentry_use_current_scope")
13
+ SENTRY_USE_ISOLATION_SCOPE_KEY = create_key("sentry_use_isolation_scope")
14
+
15
+ # trace state keys
16
+ TRACESTATE_SAMPLED_KEY = Baggage.SENTRY_PREFIX + "sampled"
17
+ TRACESTATE_SAMPLE_RATE_KEY = Baggage.SENTRY_PREFIX + "sample_rate"
18
+ TRACESTATE_SAMPLE_RAND_KEY = Baggage.SENTRY_PREFIX + "sample_rand"
19
+
20
+ # misc
21
+ OTEL_SENTRY_CONTEXT = "otel"
22
+ SPAN_ORIGIN = "auto.otel"
23
+
24
+
25
+ class SentrySpanAttribute:
26
+ DESCRIPTION = "sentry.description"
27
+ OP = "sentry.op"
28
+ ORIGIN = "sentry.origin"
29
+ TAG = "sentry.tag"
30
+ NAME = "sentry.name"
31
+ SOURCE = "sentry.source"
32
+ CONTEXT = "sentry.context"
33
+ CUSTOM_SAMPLED = "sentry.custom_sampled" # used for saving start_span(sampled=X)