sentry-sdk 3.0.0a1__py2.py3-none-any.whl → 3.0.0a3__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 (157) hide show
  1. sentry_sdk/__init__.py +2 -0
  2. sentry_sdk/_compat.py +5 -12
  3. sentry_sdk/_init_implementation.py +7 -7
  4. sentry_sdk/_log_batcher.py +17 -29
  5. sentry_sdk/_lru_cache.py +7 -9
  6. sentry_sdk/_queue.py +2 -4
  7. sentry_sdk/_types.py +11 -18
  8. sentry_sdk/_werkzeug.py +5 -7
  9. sentry_sdk/ai/monitoring.py +44 -31
  10. sentry_sdk/ai/utils.py +3 -4
  11. sentry_sdk/api.py +75 -87
  12. sentry_sdk/attachments.py +10 -12
  13. sentry_sdk/client.py +137 -155
  14. sentry_sdk/consts.py +430 -174
  15. sentry_sdk/crons/api.py +16 -17
  16. sentry_sdk/crons/decorator.py +25 -27
  17. sentry_sdk/debug.py +4 -6
  18. sentry_sdk/envelope.py +46 -112
  19. sentry_sdk/feature_flags.py +9 -15
  20. sentry_sdk/integrations/__init__.py +24 -19
  21. sentry_sdk/integrations/_asgi_common.py +15 -18
  22. sentry_sdk/integrations/_wsgi_common.py +22 -33
  23. sentry_sdk/integrations/aiohttp.py +32 -30
  24. sentry_sdk/integrations/anthropic.py +42 -37
  25. sentry_sdk/integrations/argv.py +3 -4
  26. sentry_sdk/integrations/ariadne.py +16 -18
  27. sentry_sdk/integrations/arq.py +21 -29
  28. sentry_sdk/integrations/asgi.py +63 -37
  29. sentry_sdk/integrations/asyncio.py +14 -16
  30. sentry_sdk/integrations/atexit.py +6 -10
  31. sentry_sdk/integrations/aws_lambda.py +26 -36
  32. sentry_sdk/integrations/beam.py +10 -18
  33. sentry_sdk/integrations/boto3.py +18 -16
  34. sentry_sdk/integrations/bottle.py +25 -34
  35. sentry_sdk/integrations/celery/__init__.py +41 -61
  36. sentry_sdk/integrations/celery/beat.py +23 -27
  37. sentry_sdk/integrations/celery/utils.py +15 -17
  38. sentry_sdk/integrations/chalice.py +8 -10
  39. sentry_sdk/integrations/clickhouse_driver.py +21 -31
  40. sentry_sdk/integrations/cloud_resource_context.py +9 -16
  41. sentry_sdk/integrations/cohere.py +27 -33
  42. sentry_sdk/integrations/dedupe.py +5 -8
  43. sentry_sdk/integrations/django/__init__.py +57 -72
  44. sentry_sdk/integrations/django/asgi.py +26 -34
  45. sentry_sdk/integrations/django/caching.py +23 -19
  46. sentry_sdk/integrations/django/middleware.py +17 -20
  47. sentry_sdk/integrations/django/signals_handlers.py +11 -10
  48. sentry_sdk/integrations/django/templates.py +19 -16
  49. sentry_sdk/integrations/django/transactions.py +16 -11
  50. sentry_sdk/integrations/django/views.py +6 -10
  51. sentry_sdk/integrations/dramatiq.py +21 -21
  52. sentry_sdk/integrations/excepthook.py +10 -10
  53. sentry_sdk/integrations/executing.py +3 -4
  54. sentry_sdk/integrations/falcon.py +27 -42
  55. sentry_sdk/integrations/fastapi.py +13 -16
  56. sentry_sdk/integrations/flask.py +31 -38
  57. sentry_sdk/integrations/gcp.py +13 -16
  58. sentry_sdk/integrations/gnu_backtrace.py +4 -6
  59. sentry_sdk/integrations/gql.py +16 -17
  60. sentry_sdk/integrations/graphene.py +13 -12
  61. sentry_sdk/integrations/grpc/__init__.py +19 -1
  62. sentry_sdk/integrations/grpc/aio/server.py +15 -14
  63. sentry_sdk/integrations/grpc/client.py +19 -9
  64. sentry_sdk/integrations/grpc/consts.py +2 -0
  65. sentry_sdk/integrations/grpc/server.py +12 -8
  66. sentry_sdk/integrations/httpx.py +9 -12
  67. sentry_sdk/integrations/huey.py +13 -20
  68. sentry_sdk/integrations/huggingface_hub.py +18 -18
  69. sentry_sdk/integrations/langchain.py +203 -113
  70. sentry_sdk/integrations/launchdarkly.py +13 -10
  71. sentry_sdk/integrations/litestar.py +37 -35
  72. sentry_sdk/integrations/logging.py +52 -65
  73. sentry_sdk/integrations/loguru.py +127 -57
  74. sentry_sdk/integrations/modules.py +3 -4
  75. sentry_sdk/integrations/openai.py +100 -88
  76. sentry_sdk/integrations/openai_agents/__init__.py +49 -0
  77. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  78. sentry_sdk/integrations/openai_agents/patches/__init__.py +4 -0
  79. sentry_sdk/integrations/openai_agents/patches/agent_run.py +152 -0
  80. sentry_sdk/integrations/openai_agents/patches/models.py +52 -0
  81. sentry_sdk/integrations/openai_agents/patches/runner.py +42 -0
  82. sentry_sdk/integrations/openai_agents/patches/tools.py +84 -0
  83. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  84. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +20 -0
  85. sentry_sdk/integrations/openai_agents/spans/ai_client.py +46 -0
  86. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +47 -0
  87. sentry_sdk/integrations/openai_agents/spans/handoff.py +24 -0
  88. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +41 -0
  89. sentry_sdk/integrations/openai_agents/utils.py +201 -0
  90. sentry_sdk/integrations/openfeature.py +11 -6
  91. sentry_sdk/integrations/pure_eval.py +6 -10
  92. sentry_sdk/integrations/pymongo.py +13 -17
  93. sentry_sdk/integrations/pyramid.py +31 -36
  94. sentry_sdk/integrations/quart.py +23 -28
  95. sentry_sdk/integrations/ray.py +73 -64
  96. sentry_sdk/integrations/redis/__init__.py +7 -4
  97. sentry_sdk/integrations/redis/_async_common.py +25 -12
  98. sentry_sdk/integrations/redis/_sync_common.py +19 -13
  99. sentry_sdk/integrations/redis/modules/caches.py +17 -8
  100. sentry_sdk/integrations/redis/modules/queries.py +9 -8
  101. sentry_sdk/integrations/redis/rb.py +3 -2
  102. sentry_sdk/integrations/redis/redis.py +4 -4
  103. sentry_sdk/integrations/redis/redis_cluster.py +21 -13
  104. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +3 -2
  105. sentry_sdk/integrations/redis/utils.py +23 -24
  106. sentry_sdk/integrations/rq.py +13 -16
  107. sentry_sdk/integrations/rust_tracing.py +9 -6
  108. sentry_sdk/integrations/sanic.py +34 -46
  109. sentry_sdk/integrations/serverless.py +22 -27
  110. sentry_sdk/integrations/socket.py +27 -15
  111. sentry_sdk/integrations/spark/__init__.py +1 -0
  112. sentry_sdk/integrations/spark/spark_driver.py +45 -83
  113. sentry_sdk/integrations/spark/spark_worker.py +7 -11
  114. sentry_sdk/integrations/sqlalchemy.py +22 -19
  115. sentry_sdk/integrations/starlette.py +86 -90
  116. sentry_sdk/integrations/starlite.py +28 -34
  117. sentry_sdk/integrations/statsig.py +5 -4
  118. sentry_sdk/integrations/stdlib.py +28 -24
  119. sentry_sdk/integrations/strawberry.py +62 -49
  120. sentry_sdk/integrations/sys_exit.py +7 -11
  121. sentry_sdk/integrations/threading.py +12 -14
  122. sentry_sdk/integrations/tornado.py +28 -32
  123. sentry_sdk/integrations/trytond.py +4 -3
  124. sentry_sdk/integrations/typer.py +8 -6
  125. sentry_sdk/integrations/unleash.py +5 -4
  126. sentry_sdk/integrations/wsgi.py +47 -46
  127. sentry_sdk/logger.py +41 -10
  128. sentry_sdk/monitor.py +16 -28
  129. sentry_sdk/opentelemetry/consts.py +11 -4
  130. sentry_sdk/opentelemetry/contextvars_context.py +26 -16
  131. sentry_sdk/opentelemetry/propagator.py +38 -21
  132. sentry_sdk/opentelemetry/sampler.py +51 -34
  133. sentry_sdk/opentelemetry/scope.py +36 -37
  134. sentry_sdk/opentelemetry/span_processor.py +48 -58
  135. sentry_sdk/opentelemetry/tracing.py +58 -14
  136. sentry_sdk/opentelemetry/utils.py +186 -194
  137. sentry_sdk/profiler/continuous_profiler.py +108 -97
  138. sentry_sdk/profiler/transaction_profiler.py +70 -97
  139. sentry_sdk/profiler/utils.py +11 -15
  140. sentry_sdk/scope.py +251 -273
  141. sentry_sdk/scrubber.py +22 -26
  142. sentry_sdk/serializer.py +40 -54
  143. sentry_sdk/session.py +44 -61
  144. sentry_sdk/sessions.py +35 -49
  145. sentry_sdk/spotlight.py +15 -21
  146. sentry_sdk/tracing.py +121 -187
  147. sentry_sdk/tracing_utils.py +104 -122
  148. sentry_sdk/transport.py +131 -157
  149. sentry_sdk/utils.py +232 -309
  150. sentry_sdk/worker.py +16 -28
  151. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/METADATA +3 -3
  152. sentry_sdk-3.0.0a3.dist-info/RECORD +168 -0
  153. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/WHEEL +1 -1
  154. sentry_sdk-3.0.0a1.dist-info/RECORD +0 -154
  155. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/entry_points.txt +0 -0
  156. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/licenses/LICENSE +0 -0
  157. {sentry_sdk-3.0.0a1.dist-info → sentry_sdk-3.0.0a3.dist-info}/top_level.txt +0 -0
@@ -1,35 +1,79 @@
1
+ from __future__ import annotations
1
2
  from opentelemetry import trace
2
3
  from opentelemetry.propagate import set_global_textmap
4
+ from opentelemetry.sdk.resources import Resource
3
5
  from opentelemetry.sdk.trace import TracerProvider, Span, ReadableSpan
4
6
 
7
+ from sentry_sdk.consts import VERSION
5
8
  from sentry_sdk.opentelemetry import (
6
9
  SentryPropagator,
7
10
  SentrySampler,
8
11
  SentrySpanProcessor,
9
12
  )
13
+ from sentry_sdk.opentelemetry.consts import (
14
+ RESOURCE_SERVICE_NAME,
15
+ RESOURCE_SERVICE_NAMESPACE,
16
+ RESOURCE_SERVICE_VERSION,
17
+ )
18
+ from sentry_sdk.utils import logger
19
+
10
20
 
21
+ READABLE_SPAN_PATCHED = False
11
22
 
12
- def patch_readable_span():
13
- # type: () -> None
23
+
24
+ def patch_readable_span() -> None:
14
25
  """
15
26
  We need to pass through sentry specific metadata/objects from Span to ReadableSpan
16
27
  to work with them consistently in the SpanProcessor.
17
28
  """
18
- old_readable_span = Span._readable_span
29
+ global READABLE_SPAN_PATCHED
30
+ if not READABLE_SPAN_PATCHED:
31
+ old_readable_span = Span._readable_span
32
+
33
+ def sentry_patched_readable_span(self: Span) -> ReadableSpan:
34
+ readable_span = old_readable_span(self)
35
+ readable_span._sentry_meta = getattr(self, "_sentry_meta", {}) # type: ignore[attr-defined]
36
+ return readable_span
37
+
38
+ Span._readable_span = sentry_patched_readable_span # type: ignore[method-assign]
39
+ READABLE_SPAN_PATCHED = True
40
+
41
+
42
+ def setup_sentry_tracing() -> None:
43
+ # TracerProvider can only be set once. If we're the first ones setting it,
44
+ # there's no issue. If it already exists, we need to patch it.
45
+ from opentelemetry.trace import _TRACER_PROVIDER
19
46
 
20
- def sentry_patched_readable_span(self):
21
- # type: (Span) -> ReadableSpan
22
- readable_span = old_readable_span(self)
23
- readable_span._sentry_meta = getattr(self, "_sentry_meta", {}) # type: ignore[attr-defined]
24
- return readable_span
47
+ if _TRACER_PROVIDER is not None:
48
+ logger.debug("[Tracing] Detected an existing TracerProvider, patching")
49
+ tracer_provider = _TRACER_PROVIDER
50
+ tracer_provider.sampler = SentrySampler() # type: ignore[attr-defined]
25
51
 
26
- Span._readable_span = sentry_patched_readable_span # type: ignore[method-assign]
52
+ else:
53
+ logger.debug("[Tracing] No TracerProvider set, creating a new one")
54
+ tracer_provider = TracerProvider(
55
+ sampler=SentrySampler(),
56
+ resource=Resource.create(
57
+ {
58
+ RESOURCE_SERVICE_NAME: "sentry-python",
59
+ RESOURCE_SERVICE_VERSION: VERSION,
60
+ RESOURCE_SERVICE_NAMESPACE: "sentry",
61
+ }
62
+ ),
63
+ )
64
+ trace.set_tracer_provider(tracer_provider)
27
65
 
66
+ try:
67
+ existing_span_processors = (
68
+ tracer_provider._active_span_processor._span_processors # type: ignore[attr-defined]
69
+ )
70
+ except Exception:
71
+ existing_span_processors = []
28
72
 
29
- def setup_sentry_tracing():
30
- # type: () -> None
31
- provider = TracerProvider(sampler=SentrySampler())
32
- provider.add_span_processor(SentrySpanProcessor())
33
- trace.set_tracer_provider(provider)
73
+ for span_processor in existing_span_processors:
74
+ if isinstance(span_processor, SentrySpanProcessor):
75
+ break
76
+ else:
77
+ tracer_provider.add_span_processor(SentrySpanProcessor()) # type: ignore[attr-defined]
34
78
 
35
79
  set_global_textmap(SentryPropagator())
@@ -1,6 +1,7 @@
1
+ from __future__ import annotations
1
2
  import re
2
- from typing import cast
3
3
  from datetime import datetime, timezone
4
+ from dataclasses import dataclass
4
5
 
5
6
  from urllib3.util import parse_url as urlparse
6
7
  from urllib.parse import quote, unquote
@@ -30,8 +31,10 @@ from sentry_sdk.tracing_utils import Baggage, get_span_status_from_http_code
30
31
  from sentry_sdk._types import TYPE_CHECKING
31
32
 
32
33
  if TYPE_CHECKING:
33
- from typing import Any, Optional, Mapping, Sequence, Union
34
- from sentry_sdk._types import OtelExtractedSpanData
34
+ from typing import Any, Optional, Union, Type, TypeVar
35
+ from opentelemetry.util.types import Attributes
36
+
37
+ T = TypeVar("T")
35
38
 
36
39
 
37
40
  GRPC_ERROR_MAP = {
@@ -54,8 +57,7 @@ GRPC_ERROR_MAP = {
54
57
  }
55
58
 
56
59
 
57
- def is_sentry_span(span):
58
- # type: (ReadableSpan) -> bool
60
+ def is_sentry_span(span: ReadableSpan) -> bool:
59
61
  """
60
62
  Break infinite loop:
61
63
  HTTP requests to Sentry are caught by OTel and send again to Sentry.
@@ -65,10 +67,8 @@ def is_sentry_span(span):
65
67
  if not span.attributes:
66
68
  return False
67
69
 
68
- span_url = span.attributes.get(SpanAttributes.HTTP_URL, None)
69
- span_url = cast("Optional[str]", span_url)
70
-
71
- if not span_url:
70
+ span_url = get_typed_attribute(span.attributes, SpanAttributes.HTTP_URL, str)
71
+ if span_url is None:
72
72
  return False
73
73
 
74
74
  dsn_url = None
@@ -89,201 +89,184 @@ def is_sentry_span(span):
89
89
  return False
90
90
 
91
91
 
92
- def convert_from_otel_timestamp(time):
93
- # type: (int) -> datetime
92
+ def convert_from_otel_timestamp(time: int) -> datetime:
94
93
  """Convert an OTel nanosecond-level timestamp to a datetime."""
95
94
  return datetime.fromtimestamp(time / 1e9, timezone.utc)
96
95
 
97
96
 
98
- def convert_to_otel_timestamp(time):
99
- # type: (Union[datetime, float]) -> int
97
+ def convert_to_otel_timestamp(time: Union[datetime, float]) -> int:
100
98
  """Convert a datetime to an OTel timestamp (with nanosecond precision)."""
101
99
  if isinstance(time, datetime):
102
100
  return int(time.timestamp() * 1e9)
103
101
  return int(time * 1e9)
104
102
 
105
103
 
106
- def extract_transaction_name_source(span):
107
- # type: (ReadableSpan) -> tuple[Optional[str], Optional[str]]
104
+ def extract_transaction_name_source(
105
+ span: ReadableSpan,
106
+ ) -> tuple[Optional[str], Optional[str]]:
108
107
  if not span.attributes:
109
108
  return (None, None)
110
109
  return (
111
- cast("Optional[str]", span.attributes.get(SentrySpanAttribute.NAME)),
112
- cast("Optional[str]", span.attributes.get(SentrySpanAttribute.SOURCE)),
110
+ get_typed_attribute(span.attributes, SentrySpanAttribute.NAME, str),
111
+ get_typed_attribute(span.attributes, SentrySpanAttribute.SOURCE, str),
113
112
  )
114
113
 
115
114
 
116
- def extract_span_data(span):
117
- # type: (ReadableSpan) -> OtelExtractedSpanData
118
- op = span.name
119
- description = span.name
120
- status, http_status = extract_span_status(span)
121
- origin = None
122
- if span.attributes is None:
123
- return (op, description, status, http_status, origin)
115
+ @dataclass
116
+ class ExtractedSpanData:
117
+ description: str
118
+ op: Optional[str] = None
119
+ status: Optional[str] = None
120
+ http_status: Optional[int] = None
121
+ origin: Optional[str] = None
124
122
 
125
- attribute_op = cast("Optional[str]", span.attributes.get(SentrySpanAttribute.OP))
126
- op = attribute_op or op
127
- description = cast(
128
- "str", span.attributes.get(SentrySpanAttribute.DESCRIPTION) or description
123
+
124
+ def extract_span_data(span: ReadableSpan) -> ExtractedSpanData:
125
+ """
126
+ Try to populate sane values for op, description and statuses based on what we have.
127
+ The op and description mapping is fundamentally janky because otel only has a single `name`.
128
+
129
+ Priority is given first to attributes explicitly defined by us via the SDK.
130
+ Otherwise we try to infer sane values from other attributes.
131
+ """
132
+ op = get_typed_attribute(span.attributes, SentrySpanAttribute.OP, str) or infer_op(
133
+ span
129
134
  )
130
- origin = cast("Optional[str]", span.attributes.get(SentrySpanAttribute.ORIGIN))
131
-
132
- http_method = span.attributes.get(SpanAttributes.HTTP_METHOD)
133
- http_method = cast("Optional[str]", http_method)
134
- if http_method:
135
- return span_data_for_http_method(span)
136
-
137
- db_query = span.attributes.get(SpanAttributes.DB_SYSTEM)
138
- if db_query:
139
- return span_data_for_db_query(span)
140
-
141
- rpc_service = span.attributes.get(SpanAttributes.RPC_SERVICE)
142
- if rpc_service:
143
- return (
144
- attribute_op or "rpc",
145
- description,
146
- status,
147
- http_status,
148
- origin,
149
- )
150
135
 
151
- messaging_system = span.attributes.get(SpanAttributes.MESSAGING_SYSTEM)
152
- if messaging_system:
153
- return (
154
- attribute_op or "message",
155
- description,
156
- status,
157
- http_status,
158
- origin,
159
- )
136
+ description = (
137
+ get_typed_attribute(span.attributes, SentrySpanAttribute.DESCRIPTION, str)
138
+ or get_typed_attribute(span.attributes, SentrySpanAttribute.NAME, str)
139
+ or infer_description(span)
140
+ )
160
141
 
161
- faas_trigger = span.attributes.get(SpanAttributes.FAAS_TRIGGER)
162
- if faas_trigger:
163
- return (str(faas_trigger), description, status, http_status, origin)
142
+ origin = get_typed_attribute(span.attributes, SentrySpanAttribute.ORIGIN, str)
164
143
 
165
- return (op, description, status, http_status, origin)
144
+ (status, http_status) = extract_span_status(span)
166
145
 
146
+ return ExtractedSpanData(
147
+ description=description or span.name,
148
+ op=op,
149
+ status=status,
150
+ http_status=http_status,
151
+ origin=origin,
152
+ )
167
153
 
168
- def span_data_for_http_method(span):
169
- # type: (ReadableSpan) -> OtelExtractedSpanData
170
- span_attributes = span.attributes or {}
171
154
 
172
- op = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.OP))
173
- if op is None:
174
- op = "http"
155
+ def infer_op(span: ReadableSpan) -> Optional[str]:
156
+ """
157
+ Try to infer op for the various types of instrumentation.
158
+ """
159
+ if span.attributes is None:
160
+ return None
175
161
 
162
+ if SpanAttributes.HTTP_METHOD in span.attributes:
176
163
  if span.kind == SpanKind.SERVER:
177
- op += ".server"
164
+ return OP.HTTP_SERVER
178
165
  elif span.kind == SpanKind.CLIENT:
179
- op += ".client"
166
+ return OP.HTTP_CLIENT
167
+ else:
168
+ return OP.HTTP
169
+ elif SpanAttributes.DB_SYSTEM in span.attributes:
170
+ return OP.DB
171
+ elif SpanAttributes.RPC_SERVICE in span.attributes:
172
+ return OP.RPC
173
+ elif SpanAttributes.MESSAGING_SYSTEM in span.attributes:
174
+ return OP.MESSAGE
175
+ elif SpanAttributes.FAAS_TRIGGER in span.attributes:
176
+ return get_typed_attribute(span.attributes, SpanAttributes.FAAS_TRIGGER, str)
177
+ else:
178
+ return None
180
179
 
181
- http_method = span_attributes.get(SpanAttributes.HTTP_METHOD)
182
- route = span_attributes.get(SpanAttributes.HTTP_ROUTE)
183
- target = span_attributes.get(SpanAttributes.HTTP_TARGET)
184
- peer_name = span_attributes.get(SpanAttributes.NET_PEER_NAME)
185
180
 
186
- # TODO-neel-potel remove description completely
187
- description = span_attributes.get(
188
- SentrySpanAttribute.DESCRIPTION
189
- ) or span_attributes.get(SentrySpanAttribute.NAME)
190
- description = cast("Optional[str]", description)
191
- if description is None:
192
- description = f"{http_method}"
181
+ def infer_description(span: ReadableSpan) -> Optional[str]:
182
+ if span.attributes is None:
183
+ return None
184
+
185
+ if SpanAttributes.HTTP_METHOD in span.attributes:
186
+ http_method = get_typed_attribute(
187
+ span.attributes, SpanAttributes.HTTP_METHOD, str
188
+ )
189
+ route = get_typed_attribute(span.attributes, SpanAttributes.HTTP_ROUTE, str)
190
+ target = get_typed_attribute(span.attributes, SpanAttributes.HTTP_TARGET, str)
191
+ peer_name = get_typed_attribute(
192
+ span.attributes, SpanAttributes.NET_PEER_NAME, str
193
+ )
194
+ url = get_typed_attribute(span.attributes, SpanAttributes.HTTP_URL, str)
193
195
 
194
196
  if route:
195
- description = f"{http_method} {route}"
197
+ return f"{http_method} {route}"
196
198
  elif target:
197
- description = f"{http_method} {target}"
199
+ return f"{http_method} {target}"
198
200
  elif peer_name:
199
- description = f"{http_method} {peer_name}"
201
+ return f"{http_method} {peer_name}"
202
+ elif url:
203
+ parsed_url = urlparse(url)
204
+ url = "{}://{}{}".format(
205
+ parsed_url.scheme, parsed_url.netloc, parsed_url.path
206
+ )
207
+ return f"{http_method} {url}"
200
208
  else:
201
- url = span_attributes.get(SpanAttributes.HTTP_URL)
202
- url = cast("Optional[str]", url)
203
-
204
- if url:
205
- parsed_url = urlparse(url)
206
- url = "{}://{}{}".format(
207
- parsed_url.scheme, parsed_url.netloc, parsed_url.path
208
- )
209
- description = f"{http_method} {url}"
210
-
211
- status, http_status = extract_span_status(span)
212
-
213
- origin = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.ORIGIN))
214
-
215
- return (op, description, status, http_status, origin)
216
-
217
-
218
- def span_data_for_db_query(span):
219
- # type: (ReadableSpan) -> OtelExtractedSpanData
220
- span_attributes = span.attributes or {}
221
-
222
- op = cast("str", span_attributes.get(SentrySpanAttribute.OP, OP.DB))
223
-
224
- statement = span_attributes.get(SpanAttributes.DB_STATEMENT, None)
225
- statement = cast("Optional[str]", statement)
226
-
227
- description = statement or span.name
228
- origin = cast("Optional[str]", span_attributes.get(SentrySpanAttribute.ORIGIN))
229
-
230
- return (op, description, None, None, origin)
231
-
232
-
233
- def extract_span_status(span):
234
- # type: (ReadableSpan) -> tuple[Optional[str], Optional[int]]
235
- span_attributes = span.attributes or {}
236
- status = span.status or None
237
-
238
- if status:
239
- inferred_status, http_status = infer_status_from_attributes(span_attributes)
240
-
241
- if status.status_code == StatusCode.OK:
242
- return (SPANSTATUS.OK, http_status)
243
- elif status.status_code == StatusCode.ERROR:
244
- if status.description is None:
245
- if inferred_status:
246
- return (inferred_status, http_status)
247
-
248
- if http_status is not None:
249
- return (inferred_status, http_status)
250
-
251
- if (
252
- status.description is not None
253
- and status.description in GRPC_ERROR_MAP.values()
254
- ):
255
- return (status.description, None)
256
- else:
257
- return (SPANSTATUS.UNKNOWN_ERROR, None)
209
+ return http_method
210
+ elif SpanAttributes.DB_SYSTEM in span.attributes:
211
+ return get_typed_attribute(span.attributes, SpanAttributes.DB_STATEMENT, str)
212
+ else:
213
+ return None
258
214
 
259
- inferred_status, http_status = infer_status_from_attributes(span_attributes)
260
- if inferred_status:
261
- return (inferred_status, http_status)
262
215
 
263
- if status and status.status_code == StatusCode.UNSET:
264
- return (None, None)
216
+ def extract_span_status(span: ReadableSpan) -> tuple[Optional[str], Optional[int]]:
217
+ """
218
+ Extract a reasonable Sentry SPANSTATUS and a HTTP status code from the otel span.
219
+ OKs are simply OKs.
220
+ ERRORs first try to map to HTTP/GRPC statuses via attributes otherwise fallback
221
+ on the description if it is a valid status for Sentry.
222
+ In the final UNSET case, we try to infer HTTP/GRPC.
223
+ """
224
+ status = span.status
225
+ http_status = get_http_status_code(span.attributes)
226
+ final_status = None
227
+
228
+ if status.status_code == StatusCode.OK:
229
+ final_status = SPANSTATUS.OK
230
+ elif status.status_code == StatusCode.ERROR:
231
+ inferred_status = infer_status_from_attributes(span.attributes, http_status)
232
+
233
+ if inferred_status is not None:
234
+ final_status = inferred_status
235
+ elif (
236
+ status.description is not None
237
+ and status.description in GRPC_ERROR_MAP.values()
238
+ ):
239
+ final_status = status.description
240
+ else:
241
+ final_status = SPANSTATUS.UNKNOWN_ERROR
265
242
  else:
266
- return (SPANSTATUS.UNKNOWN_ERROR, None)
243
+ # UNSET case
244
+ final_status = infer_status_from_attributes(span.attributes, http_status)
267
245
 
246
+ return (final_status, http_status)
268
247
 
269
- def infer_status_from_attributes(span_attributes):
270
- # type: (Mapping[str, str | bool | int | float | Sequence[str] | Sequence[bool] | Sequence[int] | Sequence[float]]) -> tuple[Optional[str], Optional[int]]
271
- http_status = get_http_status_code(span_attributes)
248
+
249
+ def infer_status_from_attributes(
250
+ span_attributes: Attributes, http_status: Optional[int]
251
+ ) -> Optional[str]:
252
+ if span_attributes is None:
253
+ return None
272
254
 
273
255
  if http_status:
274
- return (get_span_status_from_http_code(http_status), http_status)
256
+ return get_span_status_from_http_code(http_status)
275
257
 
276
258
  grpc_status = span_attributes.get(SpanAttributes.RPC_GRPC_STATUS_CODE)
277
259
  if grpc_status:
278
- return (GRPC_ERROR_MAP.get(str(grpc_status), SPANSTATUS.UNKNOWN_ERROR), None)
260
+ return GRPC_ERROR_MAP.get(str(grpc_status), SPANSTATUS.UNKNOWN_ERROR)
279
261
 
280
- return (None, None)
262
+ return None
281
263
 
282
264
 
283
- def get_http_status_code(span_attributes):
284
- # type: (Mapping[str, str | bool | int | float | Sequence[str] | Sequence[bool] | Sequence[int] | Sequence[float]]) -> Optional[int]
265
+ def get_http_status_code(span_attributes: Attributes) -> Optional[int]:
285
266
  try:
286
- http_status = span_attributes.get(SpanAttributes.HTTP_RESPONSE_STATUS_CODE)
267
+ http_status = get_typed_attribute(
268
+ span_attributes, SpanAttributes.HTTP_RESPONSE_STATUS_CODE, int
269
+ )
287
270
  except AttributeError:
288
271
  # HTTP_RESPONSE_STATUS_CODE was added in 1.21, so if we're on an older
289
272
  # OTel version SpanAttributes.HTTP_RESPONSE_STATUS_CODE will throw an
@@ -292,19 +275,18 @@ def get_http_status_code(span_attributes):
292
275
 
293
276
  if http_status is None:
294
277
  # Fall back to the deprecated attribute
295
- http_status = span_attributes.get(SpanAttributes.HTTP_STATUS_CODE)
296
-
297
- http_status = cast("Optional[int]", http_status)
278
+ http_status = get_typed_attribute(
279
+ span_attributes, SpanAttributes.HTTP_STATUS_CODE, int
280
+ )
298
281
 
299
282
  return http_status
300
283
 
301
284
 
302
- def extract_span_attributes(span, namespace):
303
- # type: (ReadableSpan, str) -> dict[str, Any]
285
+ def extract_span_attributes(span: ReadableSpan, namespace: str) -> dict[str, Any]:
304
286
  """
305
287
  Extract Sentry-specific span attributes and make them look the way Sentry expects.
306
288
  """
307
- extracted_attrs = {} # type: dict[str, Any]
289
+ extracted_attrs: dict[str, Any] = {}
308
290
 
309
291
  for attr, value in (span.attributes or {}).items():
310
292
  if attr.startswith(namespace):
@@ -314,8 +296,9 @@ def extract_span_attributes(span, namespace):
314
296
  return extracted_attrs
315
297
 
316
298
 
317
- def get_trace_context(span, span_data=None):
318
- # type: (ReadableSpan, Optional[OtelExtractedSpanData]) -> dict[str, Any]
299
+ def get_trace_context(
300
+ span: ReadableSpan, span_data: Optional[ExtractedSpanData] = None
301
+ ) -> dict[str, Any]:
319
302
  if not span.context:
320
303
  return {}
321
304
 
@@ -326,32 +309,27 @@ def get_trace_context(span, span_data=None):
326
309
  if span_data is None:
327
310
  span_data = extract_span_data(span)
328
311
 
329
- (op, _, status, _, origin) = span_data
330
-
331
- trace_context = {
312
+ trace_context: dict[str, Any] = {
332
313
  "trace_id": trace_id,
333
314
  "span_id": span_id,
334
315
  "parent_span_id": parent_span_id,
335
- "op": op,
336
- "origin": origin or DEFAULT_SPAN_ORIGIN,
337
- } # type: dict[str, Any]
338
-
339
- if status:
340
- trace_context["status"] = status
316
+ "origin": span_data.origin or DEFAULT_SPAN_ORIGIN,
317
+ }
341
318
 
319
+ if span_data.op:
320
+ trace_context["op"] = span_data.op
321
+ if span_data.status:
322
+ trace_context["status"] = span_data.status
342
323
  if span.attributes:
343
324
  trace_context["data"] = dict(span.attributes)
344
325
 
345
326
  trace_state = get_trace_state(span)
346
327
  trace_context["dynamic_sampling_context"] = dsc_from_trace_state(trace_state)
347
328
 
348
- # TODO-neel-potel profiler thread_id, thread_name
349
-
350
329
  return trace_context
351
330
 
352
331
 
353
- def trace_state_from_baggage(baggage):
354
- # type: (Baggage) -> TraceState
332
+ def trace_state_from_baggage(baggage: Baggage) -> TraceState:
355
333
  items = []
356
334
  for k, v in baggage.sentry_items.items():
357
335
  key = Baggage.SENTRY_PREFIX + quote(k)
@@ -360,13 +338,11 @@ def trace_state_from_baggage(baggage):
360
338
  return TraceState(items)
361
339
 
362
340
 
363
- def baggage_from_trace_state(trace_state):
364
- # type: (TraceState) -> Baggage
341
+ def baggage_from_trace_state(trace_state: TraceState) -> Baggage:
365
342
  return Baggage(dsc_from_trace_state(trace_state))
366
343
 
367
344
 
368
- def serialize_trace_state(trace_state):
369
- # type: (TraceState) -> str
345
+ def serialize_trace_state(trace_state: TraceState) -> str:
370
346
  sentry_items = []
371
347
  for k, v in trace_state.items():
372
348
  if Baggage.SENTRY_PREFIX_REGEX.match(k):
@@ -374,8 +350,7 @@ def serialize_trace_state(trace_state):
374
350
  return ",".join(key + "=" + value for key, value in sentry_items)
375
351
 
376
352
 
377
- def dsc_from_trace_state(trace_state):
378
- # type: (TraceState) -> dict[str, str]
353
+ def dsc_from_trace_state(trace_state: TraceState) -> dict[str, str]:
379
354
  dsc = {}
380
355
  for k, v in trace_state.items():
381
356
  if Baggage.SENTRY_PREFIX_REGEX.match(k):
@@ -384,16 +359,14 @@ def dsc_from_trace_state(trace_state):
384
359
  return dsc
385
360
 
386
361
 
387
- def has_incoming_trace(trace_state):
388
- # type: (TraceState) -> bool
362
+ def has_incoming_trace(trace_state: TraceState) -> bool:
389
363
  """
390
364
  The existence of a sentry-trace_id in the baggage implies we continued an upstream trace.
391
365
  """
392
366
  return (Baggage.SENTRY_PREFIX + "trace_id") in trace_state
393
367
 
394
368
 
395
- def get_trace_state(span):
396
- # type: (Union[AbstractSpan, ReadableSpan]) -> TraceState
369
+ def get_trace_state(span: Union[AbstractSpan, ReadableSpan]) -> TraceState:
397
370
  """
398
371
  Get the existing trace_state with sentry items
399
372
  or populate it if we are the head SDK.
@@ -451,26 +424,45 @@ def get_trace_state(span):
451
424
  return trace_state
452
425
 
453
426
 
454
- def get_sentry_meta(span, key):
455
- # type: (Union[AbstractSpan, ReadableSpan], str) -> Any
427
+ def get_sentry_meta(span: Union[AbstractSpan, ReadableSpan], key: str) -> Any:
456
428
  sentry_meta = getattr(span, "_sentry_meta", None)
457
429
  return sentry_meta.get(key) if sentry_meta else None
458
430
 
459
431
 
460
- def set_sentry_meta(span, key, value):
461
- # type: (Union[AbstractSpan, ReadableSpan], str, Any) -> None
432
+ def set_sentry_meta(
433
+ span: Union[AbstractSpan, ReadableSpan], key: str, value: Any
434
+ ) -> None:
462
435
  sentry_meta = getattr(span, "_sentry_meta", {})
463
436
  sentry_meta[key] = value
464
437
  span._sentry_meta = sentry_meta # type: ignore[union-attr]
465
438
 
466
439
 
467
- def get_profile_context(span):
468
- # type: (ReadableSpan) -> Optional[dict[str, str]]
440
+ def delete_sentry_meta(span: Union[AbstractSpan, ReadableSpan]) -> None:
441
+ try:
442
+ del span._sentry_meta # type: ignore[union-attr]
443
+ except AttributeError:
444
+ pass
445
+
446
+
447
+ def get_profile_context(span: ReadableSpan) -> Optional[dict[str, str]]:
469
448
  if not span.attributes:
470
449
  return None
471
450
 
472
- profiler_id = cast("Optional[str]", span.attributes.get(SPANDATA.PROFILER_ID))
451
+ profiler_id = get_typed_attribute(span.attributes, SPANDATA.PROFILER_ID, str)
473
452
  if profiler_id is None:
474
453
  return None
475
454
 
476
455
  return {"profiler_id": profiler_id}
456
+
457
+
458
+ def get_typed_attribute(attributes: Attributes, key: str, type: Type[T]) -> Optional[T]:
459
+ """
460
+ helper method to coerce types of attribute values
461
+ """
462
+ if attributes is None:
463
+ return None
464
+ value = attributes.get(key)
465
+ if value is not None and isinstance(value, type):
466
+ return value
467
+ else:
468
+ return None