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
@@ -3,8 +3,14 @@ import weakref
3
3
  from functools import wraps
4
4
 
5
5
  import sentry_sdk
6
- from sentry_sdk.api import continue_trace
7
- from sentry_sdk.consts import OP, SPANSTATUS, SPANDATA
6
+ from sentry_sdk.consts import (
7
+ OP,
8
+ SPANSTATUS,
9
+ SPANDATA,
10
+ BAGGAGE_HEADER_NAME,
11
+ SOURCE_FOR_STYLE,
12
+ TransactionSource,
13
+ )
8
14
  from sentry_sdk.integrations import (
9
15
  _DEFAULT_FAILED_REQUEST_STATUS_CODES,
10
16
  _check_minimum_version,
@@ -15,22 +21,20 @@ from sentry_sdk.integrations.logging import ignore_logger
15
21
  from sentry_sdk.sessions import track_session
16
22
  from sentry_sdk.integrations._wsgi_common import (
17
23
  _filter_headers,
24
+ _request_headers_to_span_attributes,
18
25
  request_body_within_bounds,
19
26
  )
20
- from sentry_sdk.tracing import (
21
- BAGGAGE_HEADER_NAME,
22
- SOURCE_FOR_STYLE,
23
- TransactionSource,
24
- )
25
27
  from sentry_sdk.tracing_utils import should_propagate_trace
26
28
  from sentry_sdk.utils import (
27
29
  capture_internal_exceptions,
28
30
  ensure_integration_enabled,
29
31
  event_from_exception,
32
+ http_client_status_to_breadcrumb_level,
30
33
  logger,
31
34
  parse_url,
32
35
  parse_version,
33
36
  reraise,
37
+ set_thread_info_from_span,
34
38
  transaction_from_function,
35
39
  HAS_REAL_CONTEXTVARS,
36
40
  CONTEXTVARS_ERROR_MESSAGE,
@@ -67,6 +71,13 @@ if TYPE_CHECKING:
67
71
 
68
72
  TRANSACTION_STYLE_VALUES = ("handler_name", "method_and_path_pattern")
69
73
 
74
+ REQUEST_PROPERTY_TO_ATTRIBUTE = {
75
+ "query_string": "url.query",
76
+ "method": "http.request.method",
77
+ "scheme": "url.scheme",
78
+ "path": "url.path",
79
+ }
80
+
70
81
 
71
82
  class AioHttpIntegration(Integration):
72
83
  identifier = "aiohttp"
@@ -123,51 +134,38 @@ class AioHttpIntegration(Integration):
123
134
  scope.add_event_processor(_make_request_processor(weak_request))
124
135
 
125
136
  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
137
+ with sentry_sdk.continue_trace(headers):
138
+ with sentry_sdk.start_span(
139
+ op=OP.HTTP_SERVER,
140
+ # If this transaction name makes it to the UI, AIOHTTP's
141
+ # URL resolver did not find a route or died trying.
142
+ name="generic AIOHTTP request",
143
+ source=TransactionSource.ROUTE,
144
+ origin=AioHttpIntegration.origin,
145
+ attributes=_prepopulate_attributes(request),
146
+ ) as span:
147
+ try:
148
+ response = await old_handle(self, request)
149
+ except HTTPException as e:
150
+ span.set_http_status(e.status_code)
151
+
152
+ if (
153
+ e.status_code
154
+ in integration._failed_request_status_codes
155
+ ):
156
+ _capture_exception()
157
+
158
+ raise
159
+ except (asyncio.CancelledError, ConnectionResetError):
160
+ span.set_status(SPANSTATUS.CANCELLED)
161
+ raise
162
+ except Exception:
163
+ # This will probably map to a 500 but seems like we
164
+ # have no way to tell. Do not set span status.
165
+ reraise(*_capture_exception())
166
+
167
+ span.set_http_status(response.status)
168
+ return response
171
169
 
172
170
  Application._handle = sentry_app_handle
173
171
 
@@ -238,12 +236,21 @@ def create_trace_config():
238
236
  name="%s %s"
239
237
  % (method, parsed_url.url if parsed_url else SENSITIVE_DATA_SUBSTITUTE),
240
238
  origin=AioHttpIntegration.origin,
239
+ only_if_parent=True,
241
240
  )
242
- span.set_data(SPANDATA.HTTP_METHOD, method)
241
+
242
+ data = {
243
+ SPANDATA.HTTP_METHOD: method,
244
+ }
245
+ set_thread_info_from_span(data, span)
246
+
243
247
  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)
248
+ data["url"] = parsed_url.url
249
+ data[SPANDATA.HTTP_QUERY] = parsed_url.query
250
+ data[SPANDATA.HTTP_FRAGMENT] = parsed_url.fragment
251
+
252
+ for key, value in data.items():
253
+ span.set_attribute(key, value)
247
254
 
248
255
  client = sentry_sdk.get_client()
249
256
 
@@ -268,15 +275,28 @@ def create_trace_config():
268
275
  params.headers[key] = value
269
276
 
270
277
  trace_config_ctx.span = span
278
+ trace_config_ctx.span_data = data
271
279
 
272
280
  async def on_request_end(session, trace_config_ctx, params):
273
281
  # type: (ClientSession, SimpleNamespace, TraceRequestEndParams) -> None
274
282
  if trace_config_ctx.span is None:
275
283
  return
276
284
 
285
+ span_data = trace_config_ctx.span_data or {}
286
+ status_code = int(params.response.status)
287
+ span_data[SPANDATA.HTTP_STATUS_CODE] = status_code
288
+ span_data["reason"] = params.response.reason
289
+
290
+ sentry_sdk.add_breadcrumb(
291
+ type="http",
292
+ category="httplib",
293
+ data=span_data,
294
+ level=http_client_status_to_breadcrumb_level(status_code),
295
+ )
296
+
277
297
  span = trace_config_ctx.span
278
298
  span.set_http_status(int(params.response.status))
279
- span.set_data("reason", params.response.reason)
299
+ span.set_attribute("reason", params.response.reason)
280
300
  span.finish()
281
301
 
282
302
  trace_config = TraceConfig()
@@ -355,3 +375,30 @@ def get_aiohttp_request_data(request):
355
375
 
356
376
  # request has no body
357
377
  return None
378
+
379
+
380
+ def _prepopulate_attributes(request):
381
+ # type: (Request) -> dict[str, Any]
382
+ """Construct initial span attributes that can be used in traces sampler."""
383
+ attributes = {}
384
+
385
+ for prop, attr in REQUEST_PROPERTY_TO_ATTRIBUTE.items():
386
+ if getattr(request, prop, None) is not None:
387
+ attributes[attr] = getattr(request, prop)
388
+
389
+ if getattr(request, "host", None) is not None:
390
+ try:
391
+ host, port = request.host.split(":")
392
+ attributes["server.address"] = host
393
+ attributes["server.port"] = port
394
+ except ValueError:
395
+ attributes["server.address"] = request.host
396
+
397
+ with capture_internal_exceptions():
398
+ url = f"{request.scheme}://{request.host}{request.path}" # noqa: E231
399
+ if request.query_string:
400
+ attributes["url.full"] = f"{url}?{request.query_string}"
401
+
402
+ attributes.update(_request_headers_to_span_attributes(dict(request.headers)))
403
+
404
+ return attributes
@@ -121,13 +121,13 @@ def _add_ai_data_to_span(
121
121
  with capture_internal_exceptions():
122
122
  if should_send_default_pii() and integration.include_prompts:
123
123
  complete_message = "".join(content_blocks)
124
- span.set_data(
124
+ span.set_attribute(
125
125
  SPANDATA.AI_RESPONSES,
126
126
  [{"type": "text", "text": complete_message}],
127
127
  )
128
128
  total_tokens = input_tokens + output_tokens
129
129
  record_token_usage(span, input_tokens, output_tokens, total_tokens)
130
- span.set_data(SPANDATA.AI_STREAMING, True)
130
+ span.set_attribute(SPANDATA.AI_STREAMING, True)
131
131
 
132
132
 
133
133
  def _sentry_patched_create_common(f, *args, **kwargs):
@@ -148,6 +148,7 @@ def _sentry_patched_create_common(f, *args, **kwargs):
148
148
  op=OP.ANTHROPIC_MESSAGES_CREATE,
149
149
  description="Anthropic messages create",
150
150
  origin=AnthropicIntegration.origin,
151
+ only_if_parent=True,
151
152
  )
152
153
  span.__enter__()
153
154
 
@@ -158,15 +159,17 @@ def _sentry_patched_create_common(f, *args, **kwargs):
158
159
  model = kwargs.get("model")
159
160
 
160
161
  with capture_internal_exceptions():
161
- span.set_data(SPANDATA.AI_MODEL_ID, model)
162
- span.set_data(SPANDATA.AI_STREAMING, False)
162
+ span.set_attribute(SPANDATA.AI_MODEL_ID, model)
163
+ span.set_attribute(SPANDATA.AI_STREAMING, False)
163
164
 
164
165
  if should_send_default_pii() and integration.include_prompts:
165
- span.set_data(SPANDATA.AI_INPUT_MESSAGES, messages)
166
+ span.set_attribute(SPANDATA.AI_INPUT_MESSAGES, messages)
166
167
 
167
168
  if hasattr(result, "content"):
168
169
  if should_send_default_pii() and integration.include_prompts:
169
- span.set_data(SPANDATA.AI_RESPONSES, _get_responses(result.content))
170
+ span.set_attribute(
171
+ SPANDATA.AI_RESPONSES, _get_responses(result.content)
172
+ )
170
173
  _calculate_token_usage(result, span)
171
174
  span.__exit__(None, None, None)
172
175
 
@@ -214,7 +217,7 @@ def _sentry_patched_create_common(f, *args, **kwargs):
214
217
  result._iterator = new_iterator()
215
218
 
216
219
  else:
217
- span.set_data("unknown_response", True)
220
+ span.set_attribute("unknown_response", True)
218
221
  span.__exit__(None, None, None)
219
222
 
220
223
  return result
@@ -5,7 +5,7 @@ from sentry_sdk.consts import OP, SPANSTATUS
5
5
  from sentry_sdk.integrations import _check_minimum_version, DidNotEnable, Integration
6
6
  from sentry_sdk.integrations.logging import ignore_logger
7
7
  from sentry_sdk.scope import should_send_default_pii
8
- from sentry_sdk.tracing import Transaction, TransactionSource
8
+ from sentry_sdk.tracing import TransactionSource
9
9
  from sentry_sdk.utils import (
10
10
  capture_internal_exceptions,
11
11
  ensure_integration_enabled,
@@ -37,6 +37,8 @@ if TYPE_CHECKING:
37
37
 
38
38
  ARQ_CONTROL_FLOW_EXCEPTIONS = (JobExecutionFailed, Retry, RetryJob)
39
39
 
40
+ DEFAULT_TRANSACTION_NAME = "unknown arq task"
41
+
40
42
 
41
43
  class ArqIntegration(Integration):
42
44
  identifier = "arq"
@@ -76,7 +78,10 @@ def patch_enqueue_job():
76
78
  return await old_enqueue_job(self, function, *args, **kwargs)
77
79
 
78
80
  with sentry_sdk.start_span(
79
- op=OP.QUEUE_SUBMIT_ARQ, name=function, origin=ArqIntegration.origin
81
+ op=OP.QUEUE_SUBMIT_ARQ,
82
+ name=function,
83
+ origin=ArqIntegration.origin,
84
+ only_if_parent=True,
80
85
  ):
81
86
  return await old_enqueue_job(self, function, *args, **kwargs)
82
87
 
@@ -96,18 +101,24 @@ def patch_run_job():
96
101
 
97
102
  with sentry_sdk.isolation_scope() as scope:
98
103
  scope._name = "arq"
104
+ scope.set_transaction_name(
105
+ DEFAULT_TRANSACTION_NAME,
106
+ source=TransactionSource.TASK,
107
+ )
99
108
  scope.clear_breadcrumbs()
100
109
 
101
- transaction = Transaction(
102
- name="unknown arq task",
103
- status="ok",
110
+ with sentry_sdk.start_span(
104
111
  op=OP.QUEUE_TASK_ARQ,
112
+ name=DEFAULT_TRANSACTION_NAME,
105
113
  source=TransactionSource.TASK,
106
114
  origin=ArqIntegration.origin,
107
- )
115
+ ) as span:
116
+ return_value = await old_run_job(self, job_id, score)
117
+
118
+ if span.status is None:
119
+ span.set_status(SPANSTATUS.OK)
108
120
 
109
- with sentry_sdk.start_transaction(transaction):
110
- return await old_run_job(self, job_id, score)
121
+ return return_value
111
122
 
112
123
  Worker.run_job = _sentry_run_job
113
124
 
@@ -116,12 +127,12 @@ def _capture_exception(exc_info):
116
127
  # type: (ExcInfo) -> None
117
128
  scope = sentry_sdk.get_current_scope()
118
129
 
119
- if scope.transaction is not None:
130
+ if scope.root_span is not None:
120
131
  if exc_info[0] in ARQ_CONTROL_FLOW_EXCEPTIONS:
121
- scope.transaction.set_status(SPANSTATUS.ABORTED)
132
+ scope.root_span.set_status(SPANSTATUS.ABORTED)
122
133
  return
123
134
 
124
- scope.transaction.set_status(SPANSTATUS.INTERNAL_ERROR)
135
+ scope.root_span.set_status(SPANSTATUS.INTERNAL_ERROR)
125
136
 
126
137
  event, hint = event_from_exception(
127
138
  exc_info,
@@ -138,8 +149,8 @@ def _make_event_processor(ctx, *args, **kwargs):
138
149
 
139
150
  with capture_internal_exceptions():
140
151
  scope = sentry_sdk.get_current_scope()
141
- if scope.transaction is not None:
142
- scope.transaction.name = ctx["job_name"]
152
+ if scope.root_span is not None:
153
+ scope.root_span.name = ctx["job_name"]
143
154
  event["transaction"] = ctx["job_name"]
144
155
 
145
156
  tags = event.setdefault("tags", {})
@@ -10,25 +10,22 @@ from copy import deepcopy
10
10
  from functools import partial
11
11
 
12
12
  import sentry_sdk
13
- from sentry_sdk.api import continue_trace
14
- from sentry_sdk.consts import OP
13
+ from sentry_sdk.consts import OP, SOURCE_FOR_STYLE, TransactionSource
15
14
 
16
15
  from sentry_sdk.integrations._asgi_common import (
17
16
  _get_headers,
17
+ _get_query,
18
18
  _get_request_data,
19
19
  _get_url,
20
20
  )
21
21
  from sentry_sdk.integrations._wsgi_common import (
22
22
  DEFAULT_HTTP_METHODS_TO_CAPTURE,
23
- nullcontext,
23
+ _request_headers_to_span_attributes,
24
24
  )
25
25
  from sentry_sdk.sessions import track_session
26
- from sentry_sdk.tracing import (
27
- SOURCE_FOR_STYLE,
28
- TransactionSource,
29
- )
30
26
  from sentry_sdk.utils import (
31
27
  ContextVar,
28
+ capture_internal_exceptions,
32
29
  event_from_exception,
33
30
  HAS_REAL_CONTEXTVARS,
34
31
  CONTEXTVARS_ERROR_MESSAGE,
@@ -36,7 +33,6 @@ from sentry_sdk.utils import (
36
33
  transaction_from_function,
37
34
  _get_installed_modules,
38
35
  )
39
- from sentry_sdk.tracing import Transaction
40
36
 
41
37
  from typing import TYPE_CHECKING
42
38
 
@@ -56,6 +52,14 @@ _DEFAULT_TRANSACTION_NAME = "generic ASGI request"
56
52
 
57
53
  TRANSACTION_STYLE_VALUES = ("endpoint", "url")
58
54
 
55
+ ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE = {
56
+ "http_version": "network.protocol.version",
57
+ "method": "http.request.method",
58
+ "path": "url.path",
59
+ "scheme": "url.scheme",
60
+ "type": "network.protocol.name",
61
+ }
62
+
59
63
 
60
64
  def _capture_exception(exc, mechanism_type="asgi"):
61
65
  # type: (Any, str) -> None
@@ -100,7 +104,7 @@ class SentryAsgiMiddleware:
100
104
  unsafe_context_data=False, # type: bool
101
105
  transaction_style="endpoint", # type: str
102
106
  mechanism_type="asgi", # type: str
103
- span_origin="manual", # type: str
107
+ span_origin=None, # type: Optional[str]
104
108
  http_methods_to_capture=DEFAULT_HTTP_METHODS_TO_CAPTURE, # type: Tuple[str, ...]
105
109
  ):
106
110
  # type: (...) -> None
@@ -157,24 +161,40 @@ class SentryAsgiMiddleware:
157
161
  # type: (Any, Any, Any) -> Any
158
162
  return await self._run_app(scope, receive, send, asgi_version=3)
159
163
 
164
+ async def _run_original_app(self, scope, receive, send, asgi_version):
165
+ # type: (Any, Any, Any, Any, int) -> Any
166
+ try:
167
+ if asgi_version == 2:
168
+ return await self.app(scope)(receive, send)
169
+ else:
170
+ return await self.app(scope, receive, send)
171
+
172
+ except Exception as exc:
173
+ _capture_exception(exc, mechanism_type=self.mechanism_type)
174
+ raise exc from None
175
+
160
176
  async def _run_app(self, scope, receive, send, asgi_version):
161
177
  # type: (Any, Any, Any, Any, int) -> Any
162
178
  is_recursive_asgi_middleware = _asgi_middleware_applied.get(False)
163
179
  is_lifespan = scope["type"] == "lifespan"
164
180
  if is_recursive_asgi_middleware or is_lifespan:
165
- try:
166
- if asgi_version == 2:
167
- return await self.app(scope)(receive, send)
168
- else:
169
- return await self.app(scope, receive, send)
170
-
171
- except Exception as exc:
172
- _capture_exception(exc, mechanism_type=self.mechanism_type)
173
- raise exc from None
181
+ return await self._run_original_app(scope, receive, send, asgi_version)
174
182
 
175
183
  _asgi_middleware_applied.set(True)
176
184
  try:
177
185
  with sentry_sdk.isolation_scope() as sentry_scope:
186
+ (
187
+ transaction_name,
188
+ transaction_source,
189
+ ) = self._get_transaction_name_and_source(
190
+ self.transaction_style,
191
+ scope,
192
+ )
193
+ sentry_scope.set_transaction_name(
194
+ transaction_name,
195
+ source=transaction_source,
196
+ )
197
+
178
198
  with track_session(sentry_scope, session_mode="request"):
179
199
  sentry_scope.clear_breadcrumbs()
180
200
  sentry_scope._name = "asgi"
@@ -182,82 +202,47 @@ class SentryAsgiMiddleware:
182
202
  sentry_scope.add_event_processor(processor)
183
203
 
184
204
  ty = scope["type"]
185
- (
186
- transaction_name,
187
- transaction_source,
188
- ) = self._get_transaction_name_and_source(
189
- self.transaction_style,
190
- scope,
191
- )
192
205
 
193
206
  method = scope.get("method", "").upper()
194
- transaction = None
195
- if ty in ("http", "websocket"):
196
- if ty == "websocket" or method in self.http_methods_to_capture:
197
- transaction = continue_trace(
198
- _get_headers(scope),
199
- op="{}.server".format(ty),
200
- name=transaction_name,
201
- source=transaction_source,
202
- origin=self.span_origin,
203
- )
204
- logger.debug(
205
- "[ASGI] Created transaction (continuing trace): %s",
206
- transaction,
207
- )
208
- else:
209
- transaction = Transaction(
210
- op=OP.HTTP_SERVER,
207
+ should_trace = ty == "websocket" or (
208
+ ty == "http" and method in self.http_methods_to_capture
209
+ )
210
+ if not should_trace:
211
+ return await self._run_original_app(
212
+ scope, receive, send, asgi_version
213
+ )
214
+
215
+ with sentry_sdk.continue_trace(_get_headers(scope)):
216
+ with sentry_sdk.start_span(
217
+ op=(
218
+ OP.WEBSOCKET_SERVER
219
+ if ty == "websocket"
220
+ else OP.HTTP_SERVER
221
+ ),
211
222
  name=transaction_name,
212
223
  source=transaction_source,
213
224
  origin=self.span_origin,
214
- )
215
- logger.debug(
216
- "[ASGI] Created transaction (new): %s", transaction
217
- )
218
-
219
- if transaction:
220
- transaction.set_tag("asgi.type", ty)
221
- logger.debug(
222
- "[ASGI] Set transaction name and source on transaction: '%s' / '%s'",
223
- transaction.name,
224
- transaction.source,
225
- )
226
-
227
- with (
228
- sentry_sdk.start_transaction(
229
- transaction,
230
- custom_sampling_context={"asgi_scope": scope},
231
- )
232
- if transaction is not None
233
- else nullcontext()
234
- ):
235
- logger.debug("[ASGI] Started transaction: %s", transaction)
236
- try:
225
+ attributes=_prepopulate_attributes(scope),
226
+ ) as span:
227
+ if span is not None:
228
+ logger.debug("[ASGI] Started transaction: %s", span)
229
+ span.set_tag("asgi.type", ty)
237
230
 
238
231
  async def _sentry_wrapped_send(event):
239
232
  # type: (Dict[str, Any]) -> Any
240
- if transaction is not None:
241
- is_http_response = (
242
- event.get("type") == "http.response.start"
243
- and "status" in event
244
- )
245
- if is_http_response:
246
- transaction.set_http_status(event["status"])
233
+ is_http_response = (
234
+ event.get("type") == "http.response.start"
235
+ and span is not None
236
+ and "status" in event
237
+ )
238
+ if is_http_response:
239
+ span.set_http_status(event["status"])
247
240
 
248
241
  return await send(event)
249
242
 
250
- if asgi_version == 2:
251
- return await self.app(scope)(
252
- receive, _sentry_wrapped_send
253
- )
254
- else:
255
- return await self.app(
256
- scope, receive, _sentry_wrapped_send
257
- )
258
- except Exception as exc:
259
- _capture_exception(exc, mechanism_type=self.mechanism_type)
260
- raise exc from None
243
+ return await self._run_original_app(
244
+ scope, receive, _sentry_wrapped_send, asgi_version
245
+ )
261
246
  finally:
262
247
  _asgi_middleware_applied.set(False)
263
248
 
@@ -336,3 +321,37 @@ class SentryAsgiMiddleware:
336
321
  return name, source
337
322
 
338
323
  return name, source
324
+
325
+
326
+ def _prepopulate_attributes(scope):
327
+ # type: (Any) -> dict[str, Any]
328
+ """Unpack ASGI scope into serializable OTel attributes."""
329
+ scope = scope or {}
330
+
331
+ attributes = {}
332
+ for attr, key in ASGI_SCOPE_PROPERTY_TO_ATTRIBUTE.items():
333
+ if scope.get(attr):
334
+ attributes[key] = scope[attr]
335
+
336
+ for attr in ("client", "server"):
337
+ if scope.get(attr):
338
+ try:
339
+ host, port = scope[attr]
340
+ attributes[f"{attr}.address"] = host
341
+ if port is not None:
342
+ attributes[f"{attr}.port"] = port
343
+ except Exception:
344
+ pass
345
+
346
+ with capture_internal_exceptions():
347
+ full_url = _get_url(scope)
348
+ query = _get_query(scope)
349
+ if query:
350
+ attributes["url.query"] = query
351
+ full_url = f"{full_url}?{query}"
352
+
353
+ attributes["url.full"] = full_url
354
+
355
+ attributes.update(_request_headers_to_span_attributes(_get_headers(scope)))
356
+
357
+ return attributes
@@ -48,6 +48,7 @@ def patch_asyncio():
48
48
  op=OP.FUNCTION,
49
49
  name=get_name(coro),
50
50
  origin=AsyncioIntegration.origin,
51
+ only_if_parent=True,
51
52
  ):
52
53
  try:
53
54
  result = await coro