sentry-sdk 0.7.5__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 -30
  2. sentry_sdk/_compat.py +74 -61
  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 +289 -0
  8. sentry_sdk/_types.py +338 -0
  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 +496 -80
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +1023 -103
  16. sentry_sdk/consts.py +1438 -66
  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 +15 -14
  22. sentry_sdk/envelope.py +369 -0
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +611 -280
  25. sentry_sdk/integrations/__init__.py +276 -49
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +180 -44
  28. sentry_sdk/integrations/aiohttp.py +291 -42
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +9 -8
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +341 -0
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +17 -10
  37. sentry_sdk/integrations/aws_lambda.py +377 -62
  38. sentry_sdk/integrations/beam.py +176 -0
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +221 -0
  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 +134 -0
  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 +48 -14
  49. sentry_sdk/integrations/django/__init__.py +584 -191
  50. sentry_sdk/integrations/django/asgi.py +245 -0
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +187 -0
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +79 -5
  55. sentry_sdk/integrations/django/transactions.py +49 -22
  56. sentry_sdk/integrations/django/views.py +96 -0
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +50 -13
  59. sentry_sdk/integrations/executing.py +67 -0
  60. sentry_sdk/integrations/falcon.py +272 -0
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +142 -88
  63. sentry_sdk/integrations/gcp.py +239 -0
  64. sentry_sdk/integrations/gnu_backtrace.py +99 -0
  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 +307 -96
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +14 -31
  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 +141 -0
  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 +112 -68
  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 +95 -37
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +294 -123
  145. sentry_sdk/integrations/serverless.py +48 -19
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/__init__.py +4 -0
  148. sentry_sdk/integrations/spark/spark_driver.py +316 -0
  149. sentry_sdk/integrations/spark/spark_worker.py +116 -0
  150. sentry_sdk/integrations/sqlalchemy.py +142 -0
  151. sentry_sdk/integrations/starlette.py +737 -0
  152. sentry_sdk/integrations/starlite.py +292 -0
  153. sentry_sdk/integrations/statsig.py +37 -0
  154. sentry_sdk/integrations/stdlib.py +235 -29
  155. sentry_sdk/integrations/strawberry.py +394 -0
  156. sentry_sdk/integrations/sys_exit.py +70 -0
  157. sentry_sdk/integrations/threading.py +158 -28
  158. sentry_sdk/integrations/tornado.py +84 -52
  159. sentry_sdk/integrations/trytond.py +50 -0
  160. sentry_sdk/integrations/typer.py +60 -0
  161. sentry_sdk/integrations/unleash.py +33 -0
  162. sentry_sdk/integrations/unraisablehook.py +53 -0
  163. sentry_sdk/integrations/wsgi.py +201 -119
  164. sentry_sdk/logger.py +96 -0
  165. sentry_sdk/metrics.py +81 -0
  166. sentry_sdk/monitor.py +120 -0
  167. sentry_sdk/profiler/__init__.py +49 -0
  168. sentry_sdk/profiler/continuous_profiler.py +730 -0
  169. sentry_sdk/profiler/transaction_profiler.py +839 -0
  170. sentry_sdk/profiler/utils.py +195 -0
  171. sentry_sdk/py.typed +0 -0
  172. sentry_sdk/scope.py +1713 -85
  173. sentry_sdk/scrubber.py +177 -0
  174. sentry_sdk/serializer.py +405 -0
  175. sentry_sdk/session.py +177 -0
  176. sentry_sdk/sessions.py +275 -0
  177. sentry_sdk/spotlight.py +242 -0
  178. sentry_sdk/tracing.py +1486 -0
  179. sentry_sdk/tracing_utils.py +1236 -0
  180. sentry_sdk/transport.py +806 -134
  181. sentry_sdk/types.py +52 -0
  182. sentry_sdk/utils.py +1625 -465
  183. sentry_sdk/worker.py +54 -25
  184. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  185. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  186. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  187. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  188. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  189. sentry_sdk/integrations/celery.py +0 -119
  190. sentry_sdk-0.7.5.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.7.5.dist-info/METADATA +0 -36
  192. sentry_sdk-0.7.5.dist-info/RECORD +0 -39
  193. {sentry_sdk-0.7.5.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/utils.py CHANGED
@@ -1,85 +1,282 @@
1
- import os
2
- import sys
1
+ import base64
2
+ import json
3
3
  import linecache
4
4
  import logging
5
+ import math
6
+ import os
7
+ import random
8
+ import re
9
+ import subprocess
10
+ import sys
11
+ import threading
12
+ import time
13
+ from collections import namedtuple
14
+ from datetime import datetime, timezone
15
+ from decimal import Decimal
16
+ from functools import partial, partialmethod, wraps
17
+ from numbers import Real
18
+ from urllib.parse import parse_qs, unquote, urlencode, urlsplit, urlunsplit
5
19
 
6
- from contextlib import contextmanager
7
- from datetime import datetime
8
-
9
- from sentry_sdk._compat import (
10
- urlparse,
11
- text_type,
12
- implements_str,
13
- string_types,
14
- number_types,
15
- int_types,
16
- PY2,
20
+ try:
21
+ # Python 3.11
22
+ from builtins import BaseExceptionGroup
23
+ except ImportError:
24
+ # Python 3.10 and below
25
+ BaseExceptionGroup = None # type: ignore
26
+
27
+ import sentry_sdk
28
+ from sentry_sdk._compat import PY37
29
+ from sentry_sdk.consts import (
30
+ DEFAULT_ADD_FULL_STACK,
31
+ DEFAULT_MAX_STACK_FRAMES,
32
+ DEFAULT_MAX_VALUE_LENGTH,
33
+ EndpointType,
17
34
  )
35
+ from sentry_sdk._types import Annotated, AnnotatedValue, SENSITIVE_DATA_SUBSTITUTE
36
+
37
+ from typing import TYPE_CHECKING
38
+
39
+ if TYPE_CHECKING:
40
+ from types import FrameType, TracebackType
41
+ from typing import (
42
+ Any,
43
+ Callable,
44
+ cast,
45
+ ContextManager,
46
+ Dict,
47
+ Iterator,
48
+ List,
49
+ NoReturn,
50
+ Optional,
51
+ overload,
52
+ ParamSpec,
53
+ Set,
54
+ Tuple,
55
+ Type,
56
+ TypeVar,
57
+ Union,
58
+ )
18
59
 
19
- if False:
20
- from typing import Any
21
- from typing import Dict
22
- from typing import Union
23
- from typing import Iterator
24
- from typing import Tuple
25
- from typing import Optional
26
- from typing import List
27
- from typing import Set
28
- from typing import Type
29
-
30
- from sentry_sdk.consts import ClientOptions
31
-
32
- ExcInfo = Tuple[
33
- Optional[Type[BaseException]], Optional[BaseException], Optional[Any]
34
- ]
60
+ from gevent.hub import Hub
35
61
 
36
- if PY2:
37
- # Importing ABCs from collections is deprecated, and will stop working in 3.8
38
- # https://github.com/python/cpython/blob/master/Lib/collections/__init__.py#L49
39
- from collections import Mapping, Sequence
40
- else:
41
- # New in 3.3
42
- # https://docs.python.org/3/library/collections.abc.html
43
- from collections.abc import Mapping, Sequence
62
+ from sentry_sdk._types import Event, ExcInfo, Log, Hint, Metric
44
63
 
45
- epoch = datetime(1970, 1, 1)
64
+ P = ParamSpec("P")
65
+ R = TypeVar("R")
46
66
 
47
67
 
68
+ epoch = datetime(1970, 1, 1)
69
+
48
70
  # The logger is created here but initialized in the debug support module
49
71
  logger = logging.getLogger("sentry_sdk.errors")
50
72
 
51
- CYCLE_MARKER = object()
73
+ _installed_modules = None
52
74
 
75
+ BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$")
53
76
 
54
- global_repr_processors = []
77
+ FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0"))
78
+ TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1"))
55
79
 
80
+ MAX_STACK_FRAMES = 2000
81
+ """Maximum number of stack frames to send to Sentry.
56
82
 
57
- def add_global_repr_processor(processor):
58
- global_repr_processors.append(processor)
83
+ If we have more than this number of stack frames, we will stop processing
84
+ the stacktrace to avoid getting stuck in a long-lasting loop. This value
85
+ exceeds the default sys.getrecursionlimit() of 1000, so users will only
86
+ be affected by this limit if they have a custom recursion limit.
87
+ """
59
88
 
60
89
 
61
- def _get_debug_hub():
62
- # This function is replaced by debug.py
63
- pass
90
+ def env_to_bool(value, *, strict=False):
91
+ # type: (Any, Optional[bool]) -> bool | None
92
+ """Casts an ENV variable value to boolean using the constants defined above.
93
+ In strict mode, it may return None if the value doesn't match any of the predefined values.
94
+ """
95
+ normalized = str(value).lower() if value is not None else None
64
96
 
97
+ if normalized in FALSY_ENV_VALUES:
98
+ return False
65
99
 
66
- @contextmanager
67
- def capture_internal_exceptions():
68
- # type: () -> Iterator
100
+ if normalized in TRUTHY_ENV_VALUES:
101
+ return True
102
+
103
+ return None if strict else bool(value)
104
+
105
+
106
+ def json_dumps(data):
107
+ # type: (Any) -> bytes
108
+ """Serialize data into a compact JSON representation encoded as UTF-8."""
109
+ return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8")
110
+
111
+
112
+ def get_git_revision():
113
+ # type: () -> Optional[str]
69
114
  try:
70
- yield
71
- except Exception:
72
- hub = _get_debug_hub()
73
- if hub is not None:
74
- hub._capture_internal_exception(sys.exc_info())
115
+ with open(os.path.devnull, "w+") as null:
116
+ # prevent command prompt windows from popping up on windows
117
+ startupinfo = None
118
+ if sys.platform == "win32" or sys.platform == "cygwin":
119
+ startupinfo = subprocess.STARTUPINFO()
120
+ startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
121
+
122
+ revision = (
123
+ subprocess.Popen(
124
+ ["git", "rev-parse", "HEAD"],
125
+ startupinfo=startupinfo,
126
+ stdout=subprocess.PIPE,
127
+ stderr=null,
128
+ stdin=null,
129
+ )
130
+ .communicate()[0]
131
+ .strip()
132
+ .decode("utf-8")
133
+ )
134
+ except (OSError, IOError, FileNotFoundError):
135
+ return None
136
+
137
+ return revision
138
+
139
+
140
+ def get_default_release():
141
+ # type: () -> Optional[str]
142
+ """Try to guess a default release."""
143
+ release = os.environ.get("SENTRY_RELEASE")
144
+ if release:
145
+ return release
146
+
147
+ release = get_git_revision()
148
+ if release:
149
+ return release
150
+
151
+ for var in (
152
+ "HEROKU_SLUG_COMMIT",
153
+ "SOURCE_VERSION",
154
+ "CODEBUILD_RESOLVED_SOURCE_VERSION",
155
+ "CIRCLE_SHA1",
156
+ "GAE_DEPLOYMENT_ID",
157
+ ):
158
+ release = os.environ.get(var)
159
+ if release:
160
+ return release
161
+ return None
162
+
163
+
164
+ def get_sdk_name(installed_integrations):
165
+ # type: (List[str]) -> str
166
+ """Return the SDK name including the name of the used web framework."""
167
+
168
+ # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier
169
+ # here because if django is not installed the integration is not accessible.
170
+ framework_integrations = [
171
+ "django",
172
+ "flask",
173
+ "fastapi",
174
+ "bottle",
175
+ "falcon",
176
+ "quart",
177
+ "sanic",
178
+ "starlette",
179
+ "litestar",
180
+ "starlite",
181
+ "chalice",
182
+ "serverless",
183
+ "pyramid",
184
+ "tornado",
185
+ "aiohttp",
186
+ "aws_lambda",
187
+ "gcp",
188
+ "beam",
189
+ "asgi",
190
+ "wsgi",
191
+ ]
192
+
193
+ for integration in framework_integrations:
194
+ if integration in installed_integrations:
195
+ return "sentry.python.{}".format(integration)
196
+
197
+ return "sentry.python"
198
+
199
+
200
+ class CaptureInternalException:
201
+ __slots__ = ()
202
+
203
+ def __enter__(self):
204
+ # type: () -> ContextManager[Any]
205
+ return self
206
+
207
+ def __exit__(self, ty, value, tb):
208
+ # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> bool
209
+ if ty is not None and value is not None:
210
+ capture_internal_exception((ty, value, tb))
211
+
212
+ return True
213
+
214
+
215
+ _CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException()
216
+
217
+
218
+ def capture_internal_exceptions():
219
+ # type: () -> ContextManager[Any]
220
+ return _CAPTURE_INTERNAL_EXCEPTION
221
+
222
+
223
+ def capture_internal_exception(exc_info):
224
+ # type: (ExcInfo) -> None
225
+ """
226
+ Capture an exception that is likely caused by a bug in the SDK
227
+ itself.
228
+
229
+ These exceptions do not end up in Sentry and are just logged instead.
230
+ """
231
+ if sentry_sdk.get_client().is_active():
232
+ logger.error("Internal error in sentry_sdk", exc_info=exc_info)
75
233
 
76
234
 
77
235
  def to_timestamp(value):
236
+ # type: (datetime) -> float
78
237
  return (value - epoch).total_seconds()
79
238
 
80
239
 
240
+ def format_timestamp(value):
241
+ # type: (datetime) -> str
242
+ """Formats a timestamp in RFC 3339 format.
243
+
244
+ Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC.
245
+ """
246
+ utctime = value.astimezone(timezone.utc)
247
+
248
+ # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for
249
+ # several years now), and isoformat is slightly different.
250
+ return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
251
+
252
+
253
+ ISO_TZ_SEPARATORS = frozenset(("+", "-"))
254
+
255
+
256
+ def datetime_from_isoformat(value):
257
+ # type: (str) -> datetime
258
+ try:
259
+ result = datetime.fromisoformat(value)
260
+ except (AttributeError, ValueError):
261
+ # py 3.6
262
+ timestamp_format = (
263
+ "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
264
+ )
265
+ if value.endswith("Z"):
266
+ value = value[:-1] + "+0000"
267
+
268
+ if value[-6] in ISO_TZ_SEPARATORS:
269
+ timestamp_format += "%z"
270
+ value = value[:-3] + value[-2:]
271
+ elif value[-5] in ISO_TZ_SEPARATORS:
272
+ timestamp_format += "%z"
273
+
274
+ result = datetime.strptime(value, timestamp_format)
275
+ return result.astimezone(timezone.utc)
276
+
277
+
81
278
  def event_hint_with_exc_info(exc_info=None):
82
- # type: (ExcInfo) -> Dict[str, Optional[ExcInfo]]
279
+ # type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]]
83
280
  """Creates a hint with the exc info filled in."""
84
281
  if exc_info is None:
85
282
  exc_info = sys.exc_info()
@@ -94,31 +291,40 @@ class BadDsn(ValueError):
94
291
  """Raised on invalid DSNs."""
95
292
 
96
293
 
97
- @implements_str
98
- class Dsn(object):
294
+ class Dsn:
99
295
  """Represents a DSN."""
100
296
 
101
297
  def __init__(self, value):
298
+ # type: (Union[Dsn, str]) -> None
102
299
  if isinstance(value, Dsn):
103
300
  self.__dict__ = dict(value.__dict__)
104
301
  return
105
- parts = urlparse.urlsplit(text_type(value))
106
- if parts.scheme not in (u"http", u"https"):
302
+ parts = urlsplit(str(value))
303
+
304
+ if parts.scheme not in ("http", "https"):
107
305
  raise BadDsn("Unsupported scheme %r" % parts.scheme)
108
306
  self.scheme = parts.scheme
307
+
308
+ if parts.hostname is None:
309
+ raise BadDsn("Missing hostname")
310
+
109
311
  self.host = parts.hostname
110
- self.port = parts.port
111
- if self.port is None:
112
- self.port = self.scheme == "https" and 443 or 80
113
- self.public_key = parts.username
114
- if not self.public_key:
312
+
313
+ if parts.port is None:
314
+ self.port = self.scheme == "https" and 443 or 80 # type: int
315
+ else:
316
+ self.port = parts.port
317
+
318
+ if not parts.username:
115
319
  raise BadDsn("Missing public key")
320
+
321
+ self.public_key = parts.username
116
322
  self.secret_key = parts.password
117
323
 
118
324
  path = parts.path.rsplit("/", 1)
119
325
 
120
326
  try:
121
- self.project_id = text_type(int(path.pop()))
327
+ self.project_id = str(int(path.pop()))
122
328
  except (ValueError, TypeError):
123
329
  raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:])
124
330
 
@@ -126,6 +332,7 @@ class Dsn(object):
126
332
 
127
333
  @property
128
334
  def netloc(self):
335
+ # type: () -> str
129
336
  """The netloc part of a DSN."""
130
337
  rv = self.host
131
338
  if (self.scheme, self.port) not in (("http", 80), ("https", 443)):
@@ -133,6 +340,7 @@ class Dsn(object):
133
340
  return rv
134
341
 
135
342
  def to_auth(self, client=None):
343
+ # type: (Optional[Any]) -> Auth
136
344
  """Returns the auth info object for this dsn."""
137
345
  return Auth(
138
346
  scheme=self.scheme,
@@ -145,6 +353,7 @@ class Dsn(object):
145
353
  )
146
354
 
147
355
  def __str__(self):
356
+ # type: () -> str
148
357
  return "%s://%s%s@%s%s%s" % (
149
358
  self.scheme,
150
359
  self.public_key,
@@ -155,7 +364,7 @@ class Dsn(object):
155
364
  )
156
365
 
157
366
 
158
- class Auth(object):
367
+ class Auth:
159
368
  """Helper object that represents the auth info."""
160
369
 
161
370
  def __init__(
@@ -169,6 +378,7 @@ class Auth(object):
169
378
  client=None,
170
379
  path="/",
171
380
  ):
381
+ # type: (str, str, str, str, Optional[str], int, Optional[Any], str) -> None
172
382
  self.scheme = scheme
173
383
  self.host = host
174
384
  self.path = path
@@ -178,35 +388,38 @@ class Auth(object):
178
388
  self.version = version
179
389
  self.client = client
180
390
 
181
- @property
182
- def store_api_url(self):
391
+ def get_api_url(
392
+ self,
393
+ type=EndpointType.ENVELOPE, # type: EndpointType
394
+ ):
395
+ # type: (...) -> str
183
396
  """Returns the API url for storing events."""
184
- return "%s://%s%sapi/%s/store/" % (
397
+ return "%s://%s%sapi/%s/%s/" % (
185
398
  self.scheme,
186
399
  self.host,
187
400
  self.path,
188
401
  self.project_id,
402
+ type.value,
189
403
  )
190
404
 
191
- def to_header(self, timestamp=None):
405
+ def to_header(self):
406
+ # type: () -> str
192
407
  """Returns the auth header a string."""
193
408
  rv = [("sentry_key", self.public_key), ("sentry_version", self.version)]
194
- if timestamp is not None:
195
- rv.append(("sentry_timestamp", str(to_timestamp(timestamp))))
196
409
  if self.client is not None:
197
410
  rv.append(("sentry_client", self.client))
198
411
  if self.secret_key is not None:
199
412
  rv.append(("sentry_secret", self.secret_key))
200
- return u"Sentry " + u", ".join("%s=%s" % (key, value) for key, value in rv)
413
+ return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv)
201
414
 
202
415
 
203
416
  def get_type_name(cls):
204
- # type: (Any) -> str
417
+ # type: (Optional[type]) -> Optional[str]
205
418
  return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None)
206
419
 
207
420
 
208
421
  def get_type_module(cls):
209
- # type: (Any) -> Optional[Any]
422
+ # type: (Optional[type]) -> Optional[str]
210
423
  mod = getattr(cls, "__module__", None)
211
424
  if mod not in (None, "builtins", "__builtins__"):
212
425
  return mod
@@ -214,10 +427,11 @@ def get_type_module(cls):
214
427
 
215
428
 
216
429
  def should_hide_frame(frame):
217
- # type: (Any) -> bool
430
+ # type: (FrameType) -> bool
218
431
  try:
219
432
  mod = frame.f_globals["__name__"]
220
- return mod.startswith("sentry_sdk.")
433
+ if mod.startswith("sentry_sdk."):
434
+ return True
221
435
  except (AttributeError, KeyError):
222
436
  pass
223
437
 
@@ -232,34 +446,27 @@ def should_hide_frame(frame):
232
446
 
233
447
 
234
448
  def iter_stacks(tb):
235
- # type: (Any) -> Iterator[Any]
236
- while tb is not None:
237
- if not should_hide_frame(tb.tb_frame):
238
- yield tb
239
- tb = tb.tb_next
240
-
241
-
242
- def slim_string(value, length=512):
243
- # type: (str, int) -> str
244
- if not value:
245
- return value
246
- if len(value) > length:
247
- return value[: length - 3] + "..."
248
- return value[:length]
449
+ # type: (Optional[TracebackType]) -> Iterator[TracebackType]
450
+ tb_ = tb # type: Optional[TracebackType]
451
+ while tb_ is not None:
452
+ if not should_hide_frame(tb_.tb_frame):
453
+ yield tb_
454
+ tb_ = tb_.tb_next
249
455
 
250
456
 
251
457
  def get_lines_from_file(
252
458
  filename, # type: str
253
459
  lineno, # type: int
254
- loader=None, # type: Any
255
- module=None, # type: str
460
+ max_length=None, # type: Optional[int]
461
+ loader=None, # type: Optional[Any]
462
+ module=None, # type: Optional[str]
256
463
  ):
257
- # type: (...) -> Tuple[List[str], Optional[str], List[str]]
464
+ # type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
258
465
  context_lines = 5
259
466
  source = None
260
467
  if loader is not None and hasattr(loader, "get_source"):
261
468
  try:
262
- source_str = loader.get_source(module)
469
+ source_str = loader.get_source(module) # type: Optional[str]
263
470
  except (ImportError, IOError):
264
471
  source_str = None
265
472
  if source_str is not None:
@@ -279,11 +486,12 @@ def get_lines_from_file(
279
486
 
280
487
  try:
281
488
  pre_context = [
282
- slim_string(line.strip("\r\n")) for line in source[lower_bound:lineno]
489
+ strip_string(line.strip("\r\n"), max_length=max_length)
490
+ for line in source[lower_bound:lineno]
283
491
  ]
284
- context_line = slim_string(source[lineno].strip("\r\n"))
492
+ context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length)
285
493
  post_context = [
286
- slim_string(line.strip("\r\n"))
494
+ strip_string(line.strip("\r\n"), max_length=max_length)
287
495
  for line in source[(lineno + 1) : upper_bound]
288
496
  ]
289
497
  return pre_context, context_line, post_context
@@ -292,10 +500,14 @@ def get_lines_from_file(
292
500
  return [], None, []
293
501
 
294
502
 
295
- def get_source_context(frame, tb_lineno):
296
- # type: (Any, int) -> Tuple[List[str], Optional[str], List[str]]
503
+ def get_source_context(
504
+ frame, # type: FrameType
505
+ tb_lineno, # type: Optional[int]
506
+ max_value_length=None, # type: Optional[int]
507
+ ):
508
+ # type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
297
509
  try:
298
- abs_path = frame.f_code.co_filename
510
+ abs_path = frame.f_code.co_filename # type: Optional[str]
299
511
  except Exception:
300
512
  abs_path = None
301
513
  try:
@@ -306,16 +518,20 @@ def get_source_context(frame, tb_lineno):
306
518
  loader = frame.f_globals["__loader__"]
307
519
  except Exception:
308
520
  loader = None
309
- lineno = tb_lineno - 1
310
- if lineno is not None and abs_path:
311
- return get_lines_from_file(abs_path, lineno, loader, module)
521
+
522
+ if tb_lineno is not None and abs_path:
523
+ lineno = tb_lineno - 1
524
+ return get_lines_from_file(
525
+ abs_path, lineno, max_value_length, loader=loader, module=module
526
+ )
527
+
312
528
  return [], None, []
313
529
 
314
530
 
315
531
  def safe_str(value):
316
532
  # type: (Any) -> str
317
533
  try:
318
- return text_type(value)
534
+ return str(value)
319
535
  except Exception:
320
536
  return safe_repr(value)
321
537
 
@@ -323,77 +539,16 @@ def safe_str(value):
323
539
  def safe_repr(value):
324
540
  # type: (Any) -> str
325
541
  try:
326
- rv = repr(value)
327
- if isinstance(rv, bytes):
328
- rv = rv.decode("utf-8", "replace")
329
-
330
- # At this point `rv` contains a bunch of literal escape codes, like
331
- # this (exaggerated example):
332
- #
333
- # u"\\x2f"
334
- #
335
- # But we want to show this string as:
336
- #
337
- # u"/"
338
- try:
339
- # unicode-escape does this job, but can only decode latin1. So we
340
- # attempt to encode in latin1.
341
- return rv.encode("latin1").decode("unicode-escape")
342
- except Exception:
343
- # Since usually strings aren't latin1 this can break. In those
344
- # cases we just give up.
345
- return rv
542
+ return repr(value)
346
543
  except Exception:
347
- # If e.g. the call to `repr` already fails
348
- return u"<broken repr>"
349
-
350
-
351
- def object_to_json(obj, remaining_depth=4, memo=None):
352
- if memo is None:
353
- memo = Memo()
354
- if memo.memoize(obj):
355
- return CYCLE_MARKER
356
-
357
- try:
358
- if remaining_depth > 0:
359
- hints = {"memo": memo, "remaining_depth": remaining_depth}
360
- for processor in global_repr_processors:
361
- with capture_internal_exceptions():
362
- result = processor(obj, hints)
363
- if result is not NotImplemented:
364
- return result
365
-
366
- if isinstance(obj, (list, tuple)):
367
- # It is not safe to iterate over another sequence types as this may raise errors or
368
- # bring undesired side-effects (e.g. Django querysets are executed during iteration)
369
- return [
370
- object_to_json(x, remaining_depth=remaining_depth - 1, memo=memo)
371
- for x in obj
372
- ]
373
-
374
- if isinstance(obj, Mapping):
375
- return {
376
- safe_str(k): object_to_json(
377
- v, remaining_depth=remaining_depth - 1, memo=memo
378
- )
379
- for k, v in obj.items()
380
- }
381
-
382
- return safe_repr(obj)
383
- finally:
384
- memo.unmemoize(obj)
385
-
386
-
387
- def extract_locals(frame):
388
- # type: (Any) -> Dict[str, Any]
389
- rv = {}
390
- for key, value in frame.f_locals.items():
391
- rv[str(key)] = object_to_json(value)
392
- return rv
544
+ return "<broken repr>"
393
545
 
394
546
 
395
547
  def filename_for_module(module, abs_path):
396
- # type: (str, str) -> str
548
+ # type: (Optional[str], Optional[str]) -> Optional[str]
549
+ if not abs_path or not module:
550
+ return abs_path
551
+
397
552
  try:
398
553
  if abs_path.endswith(".pyc"):
399
554
  abs_path = abs_path[:-1]
@@ -403,6 +558,9 @@ def filename_for_module(module, abs_path):
403
558
  return os.path.basename(abs_path)
404
559
 
405
560
  base_module_path = sys.modules[base_module].__file__
561
+ if not base_module_path:
562
+ return abs_path
563
+
406
564
  return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip(
407
565
  os.sep
408
566
  )
@@ -410,15 +568,22 @@ def filename_for_module(module, abs_path):
410
568
  return abs_path
411
569
 
412
570
 
413
- def serialize_frame(frame, tb_lineno=None, with_locals=True):
414
- # type: (Any, int, bool) -> Dict[str, Any]
571
+ def serialize_frame(
572
+ frame,
573
+ tb_lineno=None,
574
+ include_local_variables=True,
575
+ include_source_context=True,
576
+ max_value_length=None,
577
+ custom_repr=None,
578
+ ):
579
+ # type: (FrameType, Optional[int], bool, bool, Optional[int], Optional[Callable[..., Optional[str]]]) -> Dict[str, Any]
415
580
  f_code = getattr(frame, "f_code", None)
416
- if f_code:
417
- abs_path = frame.f_code.co_filename
418
- function = frame.f_code.co_name
419
- else:
581
+ if not f_code:
420
582
  abs_path = None
421
583
  function = None
584
+ else:
585
+ abs_path = frame.f_code.co_filename
586
+ function = frame.f_code.co_name
422
587
  try:
423
588
  module = frame.f_globals["__name__"]
424
589
  except Exception:
@@ -427,43 +592,54 @@ def serialize_frame(frame, tb_lineno=None, with_locals=True):
427
592
  if tb_lineno is None:
428
593
  tb_lineno = frame.f_lineno
429
594
 
430
- pre_context, context_line, post_context = get_source_context(frame, tb_lineno)
595
+ try:
596
+ os_abs_path = os.path.abspath(abs_path) if abs_path else None
597
+ except Exception:
598
+ os_abs_path = None
431
599
 
432
600
  rv = {
433
601
  "filename": filename_for_module(module, abs_path) or None,
434
- "abs_path": os.path.abspath(abs_path) if abs_path else None,
602
+ "abs_path": os_abs_path,
435
603
  "function": function or "<unknown>",
436
604
  "module": module,
437
605
  "lineno": tb_lineno,
438
- "pre_context": pre_context,
439
- "context_line": context_line,
440
- "post_context": post_context,
441
- }
442
- if with_locals:
443
- rv["vars"] = extract_locals(frame)
444
- return rv
606
+ } # type: Dict[str, Any]
445
607
 
608
+ if include_source_context:
609
+ rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context(
610
+ frame, tb_lineno, max_value_length
611
+ )
446
612
 
447
- def stacktrace_from_traceback(tb=None, with_locals=True):
448
- # type: (Any, bool) -> Dict[str, List[Dict[str, Any]]]
449
- return {
450
- "frames": [
451
- serialize_frame(
452
- tb.tb_frame, tb_lineno=tb.tb_lineno, with_locals=with_locals
453
- )
454
- for tb in iter_stacks(tb)
455
- ]
456
- }
613
+ if include_local_variables:
614
+ from sentry_sdk.serializer import serialize
615
+
616
+ rv["vars"] = serialize(
617
+ dict(frame.f_locals), is_vars=True, custom_repr=custom_repr
618
+ )
619
+
620
+ return rv
457
621
 
458
622
 
459
- def current_stacktrace(with_locals=True):
623
+ def current_stacktrace(
624
+ include_local_variables=True, # type: bool
625
+ include_source_context=True, # type: bool
626
+ max_value_length=None, # type: Optional[int]
627
+ ):
628
+ # type: (...) -> Dict[str, Any]
460
629
  __tracebackhide__ = True
461
630
  frames = []
462
631
 
463
- f = sys._getframe()
632
+ f = sys._getframe() # type: Optional[FrameType]
464
633
  while f is not None:
465
634
  if not should_hide_frame(f):
466
- frames.append(serialize_frame(f, with_locals=with_locals))
635
+ frames.append(
636
+ serialize_frame(
637
+ f,
638
+ include_local_variables=include_local_variables,
639
+ include_source_context=include_source_context,
640
+ max_value_length=max_value_length,
641
+ )
642
+ )
467
643
  f = f.f_back
468
644
 
469
645
  frames.reverse()
@@ -476,36 +652,126 @@ def get_errno(exc_value):
476
652
  return getattr(exc_value, "errno", None)
477
653
 
478
654
 
655
+ def get_error_message(exc_value):
656
+ # type: (Optional[BaseException]) -> str
657
+ message = safe_str(
658
+ getattr(exc_value, "message", "")
659
+ or getattr(exc_value, "detail", "")
660
+ or safe_str(exc_value)
661
+ ) # type: str
662
+
663
+ # __notes__ should be a list of strings when notes are added
664
+ # via add_note, but can be anything else if __notes__ is set
665
+ # directly. We only support strings in __notes__, since that
666
+ # is the correct use.
667
+ notes = getattr(exc_value, "__notes__", None) # type: object
668
+ if isinstance(notes, list) and len(notes) > 0:
669
+ message += "\n" + "\n".join(note for note in notes if isinstance(note, str))
670
+
671
+ return message
672
+
673
+
479
674
  def single_exception_from_error_tuple(
480
675
  exc_type, # type: Optional[type]
481
676
  exc_value, # type: Optional[BaseException]
482
- tb, # type: Optional[Any]
483
- client_options=None, # type: Optional[ClientOptions]
484
- mechanism=None, # type: Dict[str, Any]
677
+ tb, # type: Optional[TracebackType]
678
+ client_options=None, # type: Optional[Dict[str, Any]]
679
+ mechanism=None, # type: Optional[Dict[str, Any]]
680
+ exception_id=None, # type: Optional[int]
681
+ parent_id=None, # type: Optional[int]
682
+ source=None, # type: Optional[str]
683
+ full_stack=None, # type: Optional[list[dict[str, Any]]]
485
684
  ):
486
685
  # type: (...) -> Dict[str, Any]
686
+ """
687
+ Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry.
688
+
689
+ See the Exception Interface documentation for more details:
690
+ https://develop.sentry.dev/sdk/event-payloads/exception/
691
+ """
692
+ exception_value = {} # type: Dict[str, Any]
693
+ exception_value["mechanism"] = (
694
+ mechanism.copy() if mechanism else {"type": "generic", "handled": True}
695
+ )
696
+ if exception_id is not None:
697
+ exception_value["mechanism"]["exception_id"] = exception_id
698
+
487
699
  if exc_value is not None:
488
700
  errno = get_errno(exc_value)
489
701
  else:
490
702
  errno = None
491
703
 
492
704
  if errno is not None:
493
- mechanism = mechanism or {}
494
- mechanism_meta = mechanism.setdefault("meta", {})
495
- mechanism_meta.setdefault("errno", {"code": errno})
705
+ exception_value["mechanism"].setdefault("meta", {}).setdefault(
706
+ "errno", {}
707
+ ).setdefault("number", errno)
708
+
709
+ if source is not None:
710
+ exception_value["mechanism"]["source"] = source
711
+
712
+ is_root_exception = exception_id == 0
713
+ if not is_root_exception and parent_id is not None:
714
+ exception_value["mechanism"]["parent_id"] = parent_id
715
+ exception_value["mechanism"]["type"] = "chained"
716
+
717
+ if is_root_exception and "type" not in exception_value["mechanism"]:
718
+ exception_value["mechanism"]["type"] = "generic"
719
+
720
+ is_exception_group = BaseExceptionGroup is not None and isinstance(
721
+ exc_value, BaseExceptionGroup
722
+ )
723
+ if is_exception_group:
724
+ exception_value["mechanism"]["is_exception_group"] = True
725
+
726
+ exception_value["module"] = get_type_module(exc_type)
727
+ exception_value["type"] = get_type_name(exc_type)
728
+ exception_value["value"] = get_error_message(exc_value)
496
729
 
497
730
  if client_options is None:
498
- with_locals = True
731
+ include_local_variables = True
732
+ include_source_context = True
733
+ max_value_length = DEFAULT_MAX_VALUE_LENGTH # fallback
734
+ custom_repr = None
499
735
  else:
500
- with_locals = client_options["with_locals"]
501
-
502
- return {
503
- "module": get_type_module(exc_type),
504
- "type": get_type_name(exc_type),
505
- "value": safe_str(exc_value),
506
- "mechanism": mechanism,
507
- "stacktrace": stacktrace_from_traceback(tb, with_locals),
508
- }
736
+ include_local_variables = client_options["include_local_variables"]
737
+ include_source_context = client_options["include_source_context"]
738
+ max_value_length = client_options["max_value_length"]
739
+ custom_repr = client_options.get("custom_repr")
740
+
741
+ frames = [
742
+ serialize_frame(
743
+ tb.tb_frame,
744
+ tb_lineno=tb.tb_lineno,
745
+ include_local_variables=include_local_variables,
746
+ include_source_context=include_source_context,
747
+ max_value_length=max_value_length,
748
+ custom_repr=custom_repr,
749
+ )
750
+ # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on
751
+ # processing a super-long stacktrace.
752
+ for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1))
753
+ ] # type: List[Dict[str, Any]]
754
+
755
+ if len(frames) > MAX_STACK_FRAMES:
756
+ # If we have more frames than the limit, we remove the stacktrace completely.
757
+ # We don't trim the stacktrace here because we have not processed the whole
758
+ # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would
759
+ # intelligently trim by removing frames in the middle of the stacktrace, but
760
+ # since we don't have the whole stacktrace, we can't do that. Instead, we
761
+ # drop the entire stacktrace.
762
+ exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit(
763
+ value=None
764
+ )
765
+
766
+ elif frames:
767
+ if not full_stack:
768
+ new_frames = frames
769
+ else:
770
+ new_frames = merge_stack_frames(frames, full_stack, client_options)
771
+
772
+ exception_value["stacktrace"] = {"frames": new_frames}
773
+
774
+ return exception_value
509
775
 
510
776
 
511
777
  HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__")
@@ -532,7 +798,7 @@ if HAS_CHAINED_EXCEPTIONS:
532
798
  seen_exceptions.append(exc_value)
533
799
  seen_exception_ids.add(id(exc_value))
534
800
 
535
- if exc_value.__suppress_context__: # type: ignore
801
+ if exc_value.__suppress_context__:
536
802
  cause = exc_value.__cause__
537
803
  else:
538
804
  cause = exc_value.__context__
@@ -542,7 +808,6 @@ if HAS_CHAINED_EXCEPTIONS:
542
808
  exc_value = cause
543
809
  tb = getattr(cause, "__traceback__", None)
544
810
 
545
-
546
811
  else:
547
812
 
548
813
  def walk_exception_chain(exc_info):
@@ -550,70 +815,238 @@ else:
550
815
  yield exc_info
551
816
 
552
817
 
818
+ def exceptions_from_error(
819
+ exc_type, # type: Optional[type]
820
+ exc_value, # type: Optional[BaseException]
821
+ tb, # type: Optional[TracebackType]
822
+ client_options=None, # type: Optional[Dict[str, Any]]
823
+ mechanism=None, # type: Optional[Dict[str, Any]]
824
+ exception_id=0, # type: int
825
+ parent_id=0, # type: int
826
+ source=None, # type: Optional[str]
827
+ full_stack=None, # type: Optional[list[dict[str, Any]]]
828
+ ):
829
+ # type: (...) -> Tuple[int, List[Dict[str, Any]]]
830
+ """
831
+ Creates the list of exceptions.
832
+ This can include chained exceptions and exceptions from an ExceptionGroup.
833
+
834
+ See the Exception Interface documentation for more details:
835
+ https://develop.sentry.dev/sdk/event-payloads/exception/
836
+ """
837
+
838
+ parent = single_exception_from_error_tuple(
839
+ exc_type=exc_type,
840
+ exc_value=exc_value,
841
+ tb=tb,
842
+ client_options=client_options,
843
+ mechanism=mechanism,
844
+ exception_id=exception_id,
845
+ parent_id=parent_id,
846
+ source=source,
847
+ full_stack=full_stack,
848
+ )
849
+ exceptions = [parent]
850
+
851
+ parent_id = exception_id
852
+ exception_id += 1
853
+
854
+ should_supress_context = (
855
+ hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore
856
+ )
857
+ if should_supress_context:
858
+ # Add direct cause.
859
+ # The field `__cause__` is set when raised with the exception (using the `from` keyword).
860
+ exception_has_cause = (
861
+ exc_value
862
+ and hasattr(exc_value, "__cause__")
863
+ and exc_value.__cause__ is not None
864
+ )
865
+ if exception_has_cause:
866
+ cause = exc_value.__cause__ # type: ignore
867
+ (exception_id, child_exceptions) = exceptions_from_error(
868
+ exc_type=type(cause),
869
+ exc_value=cause,
870
+ tb=getattr(cause, "__traceback__", None),
871
+ client_options=client_options,
872
+ mechanism=mechanism,
873
+ exception_id=exception_id,
874
+ source="__cause__",
875
+ full_stack=full_stack,
876
+ )
877
+ exceptions.extend(child_exceptions)
878
+
879
+ else:
880
+ # Add indirect cause.
881
+ # The field `__context__` is assigned if another exception occurs while handling the exception.
882
+ exception_has_content = (
883
+ exc_value
884
+ and hasattr(exc_value, "__context__")
885
+ and exc_value.__context__ is not None
886
+ )
887
+ if exception_has_content:
888
+ context = exc_value.__context__ # type: ignore
889
+ (exception_id, child_exceptions) = exceptions_from_error(
890
+ exc_type=type(context),
891
+ exc_value=context,
892
+ tb=getattr(context, "__traceback__", None),
893
+ client_options=client_options,
894
+ mechanism=mechanism,
895
+ exception_id=exception_id,
896
+ source="__context__",
897
+ full_stack=full_stack,
898
+ )
899
+ exceptions.extend(child_exceptions)
900
+
901
+ # Add exceptions from an ExceptionGroup.
902
+ is_exception_group = exc_value and hasattr(exc_value, "exceptions")
903
+ if is_exception_group:
904
+ for idx, e in enumerate(exc_value.exceptions): # type: ignore
905
+ (exception_id, child_exceptions) = exceptions_from_error(
906
+ exc_type=type(e),
907
+ exc_value=e,
908
+ tb=getattr(e, "__traceback__", None),
909
+ client_options=client_options,
910
+ mechanism=mechanism,
911
+ exception_id=exception_id,
912
+ parent_id=parent_id,
913
+ source="exceptions[%s]" % idx,
914
+ full_stack=full_stack,
915
+ )
916
+ exceptions.extend(child_exceptions)
917
+
918
+ return (exception_id, exceptions)
919
+
920
+
553
921
  def exceptions_from_error_tuple(
554
922
  exc_info, # type: ExcInfo
555
- client_options=None, # type: Optional[ClientOptions]
556
- mechanism=None, # type: Dict[str, Any]
923
+ client_options=None, # type: Optional[Dict[str, Any]]
924
+ mechanism=None, # type: Optional[Dict[str, Any]]
925
+ full_stack=None, # type: Optional[list[dict[str, Any]]]
557
926
  ):
558
927
  # type: (...) -> List[Dict[str, Any]]
559
928
  exc_type, exc_value, tb = exc_info
560
- rv = []
561
- for exc_type, exc_value, tb in walk_exception_chain(exc_info):
562
- rv.append(
563
- single_exception_from_error_tuple(
564
- exc_type, exc_value, tb, client_options, mechanism
565
- )
929
+
930
+ is_exception_group = BaseExceptionGroup is not None and isinstance(
931
+ exc_value, BaseExceptionGroup
932
+ )
933
+
934
+ if is_exception_group:
935
+ (_, exceptions) = exceptions_from_error(
936
+ exc_type=exc_type,
937
+ exc_value=exc_value,
938
+ tb=tb,
939
+ client_options=client_options,
940
+ mechanism=mechanism,
941
+ exception_id=0,
942
+ parent_id=0,
943
+ full_stack=full_stack,
566
944
  )
567
- return rv
945
+
946
+ else:
947
+ exceptions = []
948
+ for exc_type, exc_value, tb in walk_exception_chain(exc_info):
949
+ exceptions.append(
950
+ single_exception_from_error_tuple(
951
+ exc_type=exc_type,
952
+ exc_value=exc_value,
953
+ tb=tb,
954
+ client_options=client_options,
955
+ mechanism=mechanism,
956
+ full_stack=full_stack,
957
+ )
958
+ )
959
+
960
+ exceptions.reverse()
961
+
962
+ return exceptions
568
963
 
569
964
 
570
965
  def to_string(value):
571
966
  # type: (str) -> str
572
967
  try:
573
- return text_type(value)
968
+ return str(value)
574
969
  except UnicodeDecodeError:
575
970
  return repr(value)[1:-1]
576
971
 
577
972
 
578
- def iter_event_frames(event):
579
- # type: (Dict[str, Any]) -> Iterator[Dict[str, Any]]
580
- stacktraces = []
973
+ def iter_event_stacktraces(event):
974
+ # type: (Event) -> Iterator[Annotated[Dict[str, Any]]]
581
975
  if "stacktrace" in event:
582
- stacktraces.append(event["stacktrace"])
976
+ yield event["stacktrace"]
977
+ if "threads" in event:
978
+ for thread in event["threads"].get("values") or ():
979
+ if "stacktrace" in thread:
980
+ yield thread["stacktrace"]
583
981
  if "exception" in event:
584
982
  for exception in event["exception"].get("values") or ():
585
- if "stacktrace" in exception:
586
- stacktraces.append(exception["stacktrace"])
587
- for stacktrace in stacktraces:
983
+ if isinstance(exception, dict) and "stacktrace" in exception:
984
+ yield exception["stacktrace"]
985
+
986
+
987
+ def iter_event_frames(event):
988
+ # type: (Event) -> Iterator[Dict[str, Any]]
989
+ for stacktrace in iter_event_stacktraces(event):
990
+ if isinstance(stacktrace, AnnotatedValue):
991
+ stacktrace = stacktrace.value or {}
992
+
588
993
  for frame in stacktrace.get("frames") or ():
589
994
  yield frame
590
995
 
591
996
 
592
- def handle_in_app(event, in_app_exclude=None, in_app_include=None):
593
- # type: (Dict[str, Any], List, List) -> Dict[str, Any]
594
- any_in_app = False
595
- for frame in iter_event_frames(event):
596
- in_app = frame.get("in_app")
597
- if in_app is not None:
598
- if in_app:
599
- any_in_app = True
997
+ def handle_in_app(event, in_app_exclude=None, in_app_include=None, project_root=None):
998
+ # type: (Event, Optional[List[str]], Optional[List[str]], Optional[str]) -> Event
999
+ for stacktrace in iter_event_stacktraces(event):
1000
+ if isinstance(stacktrace, AnnotatedValue):
1001
+ stacktrace = stacktrace.value or {}
1002
+
1003
+ set_in_app_in_frames(
1004
+ stacktrace.get("frames"),
1005
+ in_app_exclude=in_app_exclude,
1006
+ in_app_include=in_app_include,
1007
+ project_root=project_root,
1008
+ )
1009
+
1010
+ return event
1011
+
1012
+
1013
+ def set_in_app_in_frames(frames, in_app_exclude, in_app_include, project_root=None):
1014
+ # type: (Any, Optional[List[str]], Optional[List[str]], Optional[str]) -> Optional[Any]
1015
+ if not frames:
1016
+ return None
1017
+
1018
+ for frame in frames:
1019
+ # if frame has already been marked as in_app, skip it
1020
+ current_in_app = frame.get("in_app")
1021
+ if current_in_app is not None:
600
1022
  continue
601
1023
 
602
1024
  module = frame.get("module")
603
- if not module:
1025
+
1026
+ # check if module in frame is in the list of modules to include
1027
+ if _module_in_list(module, in_app_include):
1028
+ frame["in_app"] = True
604
1029
  continue
605
1030
 
606
- if _module_in_set(module, in_app_exclude):
1031
+ # check if module in frame is in the list of modules to exclude
1032
+ if _module_in_list(module, in_app_exclude):
607
1033
  frame["in_app"] = False
608
- if _module_in_set(module, in_app_include):
609
- frame["in_app"] = True
610
- any_in_app = True
1034
+ continue
1035
+
1036
+ # if frame has no abs_path, skip further checks
1037
+ abs_path = frame.get("abs_path")
1038
+ if abs_path is None:
1039
+ continue
611
1040
 
612
- if not any_in_app:
613
- for frame in iter_event_frames(event):
1041
+ if _is_external_source(abs_path):
1042
+ frame["in_app"] = False
1043
+ continue
1044
+
1045
+ if _is_in_project_root(abs_path, project_root):
614
1046
  frame["in_app"] = True
1047
+ continue
615
1048
 
616
- return event
1049
+ return frames
617
1050
 
618
1051
 
619
1052
  def exc_info_from_error(error):
@@ -633,25 +1066,81 @@ def exc_info_from_error(error):
633
1066
  exc_type = type(error)
634
1067
 
635
1068
  else:
636
- raise ValueError()
1069
+ raise ValueError("Expected Exception object to report, got %s!" % type(error))
1070
+
1071
+ exc_info = (exc_type, exc_value, tb)
1072
+
1073
+ if TYPE_CHECKING:
1074
+ # This cast is safe because exc_type and exc_value are either both
1075
+ # None or both not None.
1076
+ exc_info = cast(ExcInfo, exc_info)
1077
+
1078
+ return exc_info
1079
+
1080
+
1081
+ def merge_stack_frames(frames, full_stack, client_options):
1082
+ # type: (List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]]) -> List[Dict[str, Any]]
1083
+ """
1084
+ Add the missing frames from full_stack to frames and return the merged list.
1085
+ """
1086
+ frame_ids = {
1087
+ (
1088
+ frame["abs_path"],
1089
+ frame["context_line"],
1090
+ frame["lineno"],
1091
+ frame["function"],
1092
+ )
1093
+ for frame in frames
1094
+ }
1095
+
1096
+ new_frames = [
1097
+ stackframe
1098
+ for stackframe in full_stack
1099
+ if (
1100
+ stackframe["abs_path"],
1101
+ stackframe["context_line"],
1102
+ stackframe["lineno"],
1103
+ stackframe["function"],
1104
+ )
1105
+ not in frame_ids
1106
+ ]
1107
+ new_frames.extend(frames)
1108
+
1109
+ # Limit the number of frames
1110
+ max_stack_frames = (
1111
+ client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES)
1112
+ if client_options
1113
+ else None
1114
+ )
1115
+ if max_stack_frames is not None:
1116
+ new_frames = new_frames[len(new_frames) - max_stack_frames :]
637
1117
 
638
- return exc_type, exc_value, tb
1118
+ return new_frames
639
1119
 
640
1120
 
641
1121
  def event_from_exception(
642
1122
  exc_info, # type: Union[BaseException, ExcInfo]
643
- client_options=None, # type: Optional[ClientOptions]
644
- mechanism=None, # type: Dict[str, Any]
1123
+ client_options=None, # type: Optional[Dict[str, Any]]
1124
+ mechanism=None, # type: Optional[Dict[str, Any]]
645
1125
  ):
646
- # type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]]
1126
+ # type: (...) -> Tuple[Event, Dict[str, Any]]
647
1127
  exc_info = exc_info_from_error(exc_info)
648
1128
  hint = event_hint_with_exc_info(exc_info)
1129
+
1130
+ if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK):
1131
+ full_stack = current_stacktrace(
1132
+ include_local_variables=client_options["include_local_variables"],
1133
+ max_value_length=client_options["max_value_length"],
1134
+ )["frames"]
1135
+ else:
1136
+ full_stack = None
1137
+
649
1138
  return (
650
1139
  {
651
1140
  "level": "error",
652
1141
  "exception": {
653
1142
  "values": exceptions_from_error_tuple(
654
- exc_info, client_options, mechanism
1143
+ exc_info, client_options, mechanism, full_stack
655
1144
  )
656
1145
  },
657
1146
  },
@@ -659,260 +1148,931 @@ def event_from_exception(
659
1148
  )
660
1149
 
661
1150
 
662
- def _module_in_set(name, set):
663
- # type: (str, Optional[List]) -> bool
664
- if not set:
1151
+ def _module_in_list(name, items):
1152
+ # type: (Optional[str], Optional[List[str]]) -> bool
1153
+ if name is None:
1154
+ return False
1155
+
1156
+ if not items:
665
1157
  return False
666
- for item in set or ():
1158
+
1159
+ for item in items:
667
1160
  if item == name or name.startswith(item + "."):
668
1161
  return True
1162
+
669
1163
  return False
670
1164
 
671
1165
 
672
- class AnnotatedValue(object):
673
- def __init__(self, value, metadata):
674
- # type: (Optional[Any], Dict[str, Any]) -> None
675
- self.value = value
676
- self.metadata = metadata
677
-
678
-
679
- def flatten_metadata(obj):
680
- # type: (Dict[str, Any]) -> Dict[str, Any]
681
- def inner(obj):
682
- # type: (Any) -> Any
683
- if isinstance(obj, Mapping):
684
- dict_rv = {}
685
- meta = {}
686
- for k, v in obj.items():
687
- # if we actually have "" keys in our data, throw them away. It's
688
- # unclear how we would tell them apart from metadata
689
- if k == "":
690
- continue
691
-
692
- dict_rv[k], meta[k] = inner(v)
693
- if meta[k] is None:
694
- del meta[k]
695
- if dict_rv[k] is None:
696
- del dict_rv[k]
697
- return dict_rv, (meta or None)
698
- if isinstance(obj, Sequence) and not isinstance(obj, (text_type, bytes)):
699
- list_rv = []
700
- meta = {}
701
- for i, v in enumerate(obj):
702
- new_v, meta[str(i)] = inner(v)
703
- list_rv.append(new_v)
704
- if meta[str(i)] is None:
705
- del meta[str(i)]
706
- return list_rv, (meta or None)
707
- if isinstance(obj, AnnotatedValue):
708
- return obj.value, {"": obj.metadata}
709
- return obj, None
710
-
711
- obj, meta = inner(obj)
712
- if meta is not None:
713
- obj["_meta"] = meta
714
- return obj
715
-
716
-
717
- def strip_event_mut(event):
718
- # type: (Dict[str, Any]) -> None
719
- strip_stacktrace_mut(event.get("stacktrace", None))
720
- exception = event.get("exception", None)
721
- if exception:
722
- for exception in exception.get("values", None) or ():
723
- strip_stacktrace_mut(exception.get("stacktrace", None))
724
-
725
- strip_request_mut(event.get("request", None))
726
-
727
-
728
- def strip_stacktrace_mut(stacktrace):
729
- # type: (Optional[Dict[str, List[Dict[str, Any]]]]) -> None
730
- if not stacktrace:
731
- return
732
- for frame in stacktrace.get("frames", None) or ():
733
- strip_frame_mut(frame)
734
-
735
-
736
- def strip_request_mut(request):
737
- # type: (Dict[str, Any]) -> None
738
- if not request:
739
- return
740
- data = request.get("data", None)
741
- if not data:
742
- return
743
- request["data"] = strip_databag(data)
744
-
745
-
746
- def strip_frame_mut(frame):
747
- # type: (Dict[str, Any]) -> None
748
- if "vars" in frame:
749
- frame["vars"] = strip_databag(frame["vars"])
750
-
751
-
752
- class Memo(object):
753
- def __init__(self):
754
- self._inner = {}
755
-
756
- def memoize(self, obj):
757
- if id(obj) in self._inner:
758
- return True
759
- self._inner[id(obj)] = obj
1166
+ def _is_external_source(abs_path):
1167
+ # type: (Optional[str]) -> bool
1168
+ # check if frame is in 'site-packages' or 'dist-packages'
1169
+ if abs_path is None:
760
1170
  return False
761
1171
 
762
- def unmemoize(self, obj):
763
- self._inner.pop(id(obj), None)
764
-
765
-
766
- def convert_types(obj):
767
- # type: (Any) -> Any
768
- if obj is CYCLE_MARKER:
769
- return u"<cyclic>"
770
- if isinstance(obj, datetime):
771
- return obj.strftime("%Y-%m-%dT%H:%M:%SZ")
772
- if isinstance(obj, Mapping):
773
- return {k: convert_types(v) for k, v in obj.items()}
774
- if isinstance(obj, Sequence) and not isinstance(obj, (text_type, bytes)):
775
- return [convert_types(v) for v in obj]
776
- if not isinstance(obj, string_types + number_types):
777
- return safe_repr(obj)
778
- if isinstance(obj, bytes):
779
- return obj.decode("utf-8", "replace")
780
- return obj
781
-
782
-
783
- def strip_databag(obj, remaining_depth=20):
784
- # type: (Any, int) -> Any
785
- assert not isinstance(obj, bytes), "bytes should have been normalized before"
786
- if remaining_depth <= 0:
787
- return AnnotatedValue(None, {"rem": [["!limit", "x"]]})
788
- if isinstance(obj, text_type):
789
- return strip_string(obj)
790
- if isinstance(obj, Mapping):
791
- return {k: strip_databag(v, remaining_depth - 1) for k, v in obj.items()}
792
- if isinstance(obj, Sequence):
793
- return [strip_databag(v, remaining_depth - 1) for v in obj]
794
- return obj
795
-
796
-
797
- def strip_string(value, max_length=512):
798
- # type: (str, int) -> Union[AnnotatedValue, str]
799
- # TODO: read max_length from config
800
- if not value:
801
- return value
802
- length = len(value)
803
- if length > max_length:
804
- return AnnotatedValue(
805
- value=value[: max_length - 3] + u"...",
806
- metadata={
807
- "len": length,
808
- "rem": [["!limit", "x", max_length - 3, max_length]],
809
- },
810
- )
811
- return value
1172
+ external_source = (
1173
+ re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None
1174
+ )
1175
+ return external_source
1176
+
1177
+
1178
+ def _is_in_project_root(abs_path, project_root):
1179
+ # type: (Optional[str], Optional[str]) -> bool
1180
+ if abs_path is None or project_root is None:
1181
+ return False
1182
+
1183
+ # check if path is in the project root
1184
+ if abs_path.startswith(project_root):
1185
+ return True
812
1186
 
1187
+ return False
813
1188
 
814
- def format_and_strip(template, params, strip_string=strip_string):
815
- """Format a string containing %s for placeholders and call `strip_string`
816
- on each parameter. The string template itself does not have a maximum
817
- length.
818
1189
 
819
- TODO: handle other placeholders, not just %s
1190
+ def _truncate_by_bytes(string, max_bytes):
1191
+ # type: (str, int) -> str
820
1192
  """
821
- chunks = template.split(u"%s")
822
- if not chunks:
823
- raise ValueError("No formatting placeholders found")
1193
+ Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes.
1194
+ """
1195
+ truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore")
824
1196
 
825
- params = list(reversed(params))
826
- rv_remarks = []
827
- rv_original_length = 0
828
- rv_length = 0
829
- rv = []
1197
+ return truncated + "..."
830
1198
 
831
- def realign_remark(remark):
832
- return [
833
- (rv_length + x if isinstance(x, int_types) and i < 4 else x)
834
- for i, x in enumerate(remark)
835
- ]
836
1199
 
837
- for chunk in chunks[:-1]:
838
- rv.append(chunk)
839
- rv_length += len(chunk)
840
- rv_original_length += len(chunk)
841
- if not params:
842
- raise ValueError("Not enough params.")
843
- param = params.pop()
844
-
845
- stripped_param = strip_string(param)
846
- if isinstance(stripped_param, AnnotatedValue):
847
- rv_remarks.extend(
848
- realign_remark(remark) for remark in stripped_param.metadata["rem"]
849
- )
850
- stripped_param = stripped_param.value
1200
+ def _get_size_in_bytes(value):
1201
+ # type: (str) -> Optional[int]
1202
+ try:
1203
+ return len(value.encode("utf-8"))
1204
+ except (UnicodeEncodeError, UnicodeDecodeError):
1205
+ return None
1206
+
851
1207
 
852
- rv_original_length += len(param)
853
- rv_length += len(stripped_param)
854
- rv.append(stripped_param)
1208
+ def strip_string(value, max_length=None):
1209
+ # type: (str, Optional[int]) -> Union[AnnotatedValue, str]
1210
+ if not value:
1211
+ return value
855
1212
 
856
- rv.append(chunks[-1])
857
- rv_length += len(chunks[-1])
858
- rv_original_length += len(chunks[-1])
1213
+ if max_length is None:
1214
+ max_length = DEFAULT_MAX_VALUE_LENGTH
859
1215
 
860
- rv = u"".join(rv)
861
- assert len(rv) == rv_length
1216
+ byte_size = _get_size_in_bytes(value)
1217
+ text_size = len(value)
862
1218
 
863
- if not rv_remarks:
864
- return rv
1219
+ if byte_size is not None and byte_size > max_length:
1220
+ # truncate to max_length bytes, preserving code points
1221
+ truncated_value = _truncate_by_bytes(value, max_length)
1222
+ elif text_size is not None and text_size > max_length:
1223
+ # fallback to truncating by string length
1224
+ truncated_value = value[: max_length - 3] + "..."
1225
+ else:
1226
+ return value
865
1227
 
866
1228
  return AnnotatedValue(
867
- value=rv, metadata={"len": rv_original_length, "rem": rv_remarks}
1229
+ value=truncated_value,
1230
+ metadata={
1231
+ "len": byte_size or text_size,
1232
+ "rem": [["!limit", "x", max_length - 3, max_length]],
1233
+ },
868
1234
  )
869
1235
 
870
1236
 
871
- try:
872
- from contextvars import ContextVar # type: ignore
873
- except ImportError:
874
- from threading import local
1237
+ def parse_version(version):
1238
+ # type: (str) -> Optional[Tuple[int, ...]]
1239
+ """
1240
+ Parses a version string into a tuple of integers.
1241
+ This uses the parsing loging from PEP 440:
1242
+ https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
1243
+ """
1244
+ VERSION_PATTERN = r""" # noqa: N806
1245
+ v?
1246
+ (?:
1247
+ (?:(?P<epoch>[0-9]+)!)? # epoch
1248
+ (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
1249
+ (?P<pre> # pre-release
1250
+ [-_\.]?
1251
+ (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
1252
+ [-_\.]?
1253
+ (?P<pre_n>[0-9]+)?
1254
+ )?
1255
+ (?P<post> # post release
1256
+ (?:-(?P<post_n1>[0-9]+))
1257
+ |
1258
+ (?:
1259
+ [-_\.]?
1260
+ (?P<post_l>post|rev|r)
1261
+ [-_\.]?
1262
+ (?P<post_n2>[0-9]+)?
1263
+ )
1264
+ )?
1265
+ (?P<dev> # dev release
1266
+ [-_\.]?
1267
+ (?P<dev_l>dev)
1268
+ [-_\.]?
1269
+ (?P<dev_n>[0-9]+)?
1270
+ )?
1271
+ )
1272
+ (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
1273
+ """
1274
+
1275
+ pattern = re.compile(
1276
+ r"^\s*" + VERSION_PATTERN + r"\s*$",
1277
+ re.VERBOSE | re.IGNORECASE,
1278
+ )
1279
+
1280
+ try:
1281
+ release = pattern.match(version).groupdict()["release"] # type: ignore
1282
+ release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
1283
+ except (TypeError, ValueError, AttributeError):
1284
+ return None
1285
+
1286
+ return release_tuple
875
1287
 
876
- class ContextVar(object): # type: ignore
1288
+
1289
+ def _is_contextvars_broken():
1290
+ # type: () -> bool
1291
+ """
1292
+ Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
1293
+ """
1294
+ try:
1295
+ import gevent
1296
+ from gevent.monkey import is_object_patched
1297
+
1298
+ # Get the MAJOR and MINOR version numbers of Gevent
1299
+ version_tuple = tuple(
1300
+ [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
1301
+ )
1302
+ if is_object_patched("threading", "local"):
1303
+ # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
1304
+ # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
1305
+ # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
1306
+ # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
1307
+ # for contextvars, is able to patch both thread locals and contextvars, in
1308
+ # that case, check if contextvars are effectively patched.
1309
+ if (
1310
+ # Gevent 20.9.0+
1311
+ (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
1312
+ # Gevent 20.5.0+ or Python < 3.7
1313
+ or (is_object_patched("contextvars", "ContextVar"))
1314
+ ):
1315
+ return False
1316
+
1317
+ return True
1318
+ except ImportError:
1319
+ pass
1320
+
1321
+ try:
1322
+ import greenlet
1323
+ from eventlet.patcher import is_monkey_patched # type: ignore
1324
+
1325
+ greenlet_version = parse_version(greenlet.__version__)
1326
+
1327
+ if greenlet_version is None:
1328
+ logger.error(
1329
+ "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
1330
+ )
1331
+ return False
1332
+
1333
+ if is_monkey_patched("thread") and greenlet_version < (0, 5):
1334
+ return True
1335
+ except ImportError:
1336
+ pass
1337
+
1338
+ return False
1339
+
1340
+
1341
+ def _make_threadlocal_contextvars(local):
1342
+ # type: (type) -> type
1343
+ class ContextVar:
877
1344
  # Super-limited impl of ContextVar
878
1345
 
879
- def __init__(self, name):
1346
+ def __init__(self, name, default=None):
1347
+ # type: (str, Any) -> None
880
1348
  self._name = name
1349
+ self._default = default
881
1350
  self._local = local()
1351
+ self._original_local = local()
882
1352
 
883
- def get(self, default):
884
- return getattr(self._local, "value", default)
1353
+ def get(self, default=None):
1354
+ # type: (Any) -> Any
1355
+ return getattr(self._local, "value", default or self._default)
885
1356
 
886
1357
  def set(self, value):
887
- setattr(self._local, "value", value)
1358
+ # type: (Any) -> Any
1359
+ token = str(random.getrandbits(64))
1360
+ original_value = self.get()
1361
+ setattr(self._original_local, token, original_value)
1362
+ self._local.value = value
1363
+ return token
888
1364
 
1365
+ def reset(self, token):
1366
+ # type: (Any) -> None
1367
+ self._local.value = getattr(self._original_local, token)
1368
+ # delete the original value (this way it works in Python 3.6+)
1369
+ del self._original_local.__dict__[token]
889
1370
 
890
- def transaction_from_function(func):
891
- # Methods in Python 2
1371
+ return ContextVar
1372
+
1373
+
1374
+ def _get_contextvars():
1375
+ # type: () -> Tuple[bool, type]
1376
+ """
1377
+ Figure out the "right" contextvars installation to use. Returns a
1378
+ `contextvars.ContextVar`-like class with a limited API.
1379
+
1380
+ See https://docs.sentry.io/platforms/python/contextvars/ for more information.
1381
+ """
1382
+ if not _is_contextvars_broken():
1383
+ # aiocontextvars is a PyPI package that ensures that the contextvars
1384
+ # backport (also a PyPI package) works with asyncio under Python 3.6
1385
+ #
1386
+ # Import it if available.
1387
+ if sys.version_info < (3, 7):
1388
+ # `aiocontextvars` is absolutely required for functional
1389
+ # contextvars on Python 3.6.
1390
+ try:
1391
+ from aiocontextvars import ContextVar
1392
+
1393
+ return True, ContextVar
1394
+ except ImportError:
1395
+ pass
1396
+ else:
1397
+ # On Python 3.7 contextvars are functional.
1398
+ try:
1399
+ from contextvars import ContextVar
1400
+
1401
+ return True, ContextVar
1402
+ except ImportError:
1403
+ pass
1404
+
1405
+ # Fall back to basic thread-local usage.
1406
+
1407
+ from threading import local
1408
+
1409
+ return False, _make_threadlocal_contextvars(local)
1410
+
1411
+
1412
+ HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
1413
+
1414
+ CONTEXTVARS_ERROR_MESSAGE = """
1415
+
1416
+ With asyncio/ASGI applications, the Sentry SDK requires a functional
1417
+ installation of `contextvars` to avoid leaking scope/context data across
1418
+ requests.
1419
+
1420
+ Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
1421
+ """
1422
+
1423
+
1424
+ def qualname_from_function(func):
1425
+ # type: (Callable[..., Any]) -> Optional[str]
1426
+ """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
1427
+ func_qualname = None # type: Optional[str]
1428
+
1429
+ # Python 2
892
1430
  try:
893
1431
  return "%s.%s.%s" % (
894
- func.im_class.__module__,
895
- func.im_class.__name__,
1432
+ func.im_class.__module__, # type: ignore
1433
+ func.im_class.__name__, # type: ignore
896
1434
  func.__name__,
897
1435
  )
898
1436
  except Exception:
899
1437
  pass
900
1438
 
901
- func_qualname = (
902
- getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None
1439
+ prefix, suffix = "", ""
1440
+
1441
+ if isinstance(func, partial) and hasattr(func.func, "__name__"):
1442
+ prefix, suffix = "partial(<function ", ">)"
1443
+ func = func.func
1444
+ else:
1445
+ # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
1446
+ # https://github.com/python/cpython/pull/16600
1447
+ partial_method = getattr(func, "_partialmethod", None) or getattr(
1448
+ func, "__partialmethod__", None
1449
+ )
1450
+ if isinstance(partial_method, partialmethod):
1451
+ prefix, suffix = "partialmethod(<function ", ">)"
1452
+ func = partial_method.func
1453
+
1454
+ if hasattr(func, "__qualname__"):
1455
+ func_qualname = func.__qualname__
1456
+ elif hasattr(func, "__name__"): # Python 2.7 has no __qualname__
1457
+ func_qualname = func.__name__
1458
+
1459
+ # Python 3: methods, functions, classes
1460
+ if func_qualname is not None:
1461
+ if hasattr(func, "__module__") and isinstance(func.__module__, str):
1462
+ func_qualname = func.__module__ + "." + func_qualname
1463
+ func_qualname = prefix + func_qualname + suffix
1464
+
1465
+ return func_qualname
1466
+
1467
+
1468
+ def transaction_from_function(func):
1469
+ # type: (Callable[..., Any]) -> Optional[str]
1470
+ return qualname_from_function(func)
1471
+
1472
+
1473
+ disable_capture_event = ContextVar("disable_capture_event")
1474
+
1475
+
1476
+ class ServerlessTimeoutWarning(Exception): # noqa: N818
1477
+ """Raised when a serverless method is about to reach its timeout."""
1478
+
1479
+ pass
1480
+
1481
+
1482
+ class TimeoutThread(threading.Thread):
1483
+ """Creates a Thread which runs (sleeps) for a time duration equal to
1484
+ waiting_time and raises a custom ServerlessTimeout exception.
1485
+ """
1486
+
1487
+ def __init__(
1488
+ self, waiting_time, configured_timeout, isolation_scope=None, current_scope=None
1489
+ ):
1490
+ # type: (float, int, Optional[sentry_sdk.Scope], Optional[sentry_sdk.Scope]) -> None
1491
+ threading.Thread.__init__(self)
1492
+ self.waiting_time = waiting_time
1493
+ self.configured_timeout = configured_timeout
1494
+
1495
+ self.isolation_scope = isolation_scope
1496
+ self.current_scope = current_scope
1497
+
1498
+ self._stop_event = threading.Event()
1499
+
1500
+ def stop(self):
1501
+ # type: () -> None
1502
+ self._stop_event.set()
1503
+
1504
+ def _capture_exception(self):
1505
+ # type: () -> ExcInfo
1506
+ exc_info = sys.exc_info()
1507
+
1508
+ client = sentry_sdk.get_client()
1509
+ event, hint = event_from_exception(
1510
+ exc_info,
1511
+ client_options=client.options,
1512
+ mechanism={"type": "threading", "handled": False},
1513
+ )
1514
+ sentry_sdk.capture_event(event, hint=hint)
1515
+
1516
+ return exc_info
1517
+
1518
+ def run(self):
1519
+ # type: () -> None
1520
+
1521
+ self._stop_event.wait(self.waiting_time)
1522
+
1523
+ if self._stop_event.is_set():
1524
+ return
1525
+
1526
+ integer_configured_timeout = int(self.configured_timeout)
1527
+
1528
+ # Setting up the exact integer value of configured time(in seconds)
1529
+ if integer_configured_timeout < self.configured_timeout:
1530
+ integer_configured_timeout = integer_configured_timeout + 1
1531
+
1532
+ # Raising Exception after timeout duration is reached
1533
+ if self.isolation_scope is not None and self.current_scope is not None:
1534
+ with sentry_sdk.scope.use_isolation_scope(self.isolation_scope):
1535
+ with sentry_sdk.scope.use_scope(self.current_scope):
1536
+ try:
1537
+ raise ServerlessTimeoutWarning(
1538
+ "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
1539
+ integer_configured_timeout
1540
+ )
1541
+ )
1542
+ except Exception:
1543
+ reraise(*self._capture_exception())
1544
+
1545
+ raise ServerlessTimeoutWarning(
1546
+ "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
1547
+ integer_configured_timeout
1548
+ )
1549
+ )
1550
+
1551
+
1552
+ def to_base64(original):
1553
+ # type: (str) -> Optional[str]
1554
+ """
1555
+ Convert a string to base64, via UTF-8. Returns None on invalid input.
1556
+ """
1557
+ base64_string = None
1558
+
1559
+ try:
1560
+ utf8_bytes = original.encode("UTF-8")
1561
+ base64_bytes = base64.b64encode(utf8_bytes)
1562
+ base64_string = base64_bytes.decode("UTF-8")
1563
+ except Exception as err:
1564
+ logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
1565
+
1566
+ return base64_string
1567
+
1568
+
1569
+ def from_base64(base64_string):
1570
+ # type: (str) -> Optional[str]
1571
+ """
1572
+ Convert a string from base64, via UTF-8. Returns None on invalid input.
1573
+ """
1574
+ utf8_string = None
1575
+
1576
+ try:
1577
+ only_valid_chars = BASE64_ALPHABET.match(base64_string)
1578
+ assert only_valid_chars
1579
+
1580
+ base64_bytes = base64_string.encode("UTF-8")
1581
+ utf8_bytes = base64.b64decode(base64_bytes)
1582
+ utf8_string = utf8_bytes.decode("UTF-8")
1583
+ except Exception as err:
1584
+ logger.warning(
1585
+ "Unable to decode {b64} from base64:".format(b64=base64_string), err
1586
+ )
1587
+
1588
+ return utf8_string
1589
+
1590
+
1591
+ Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
1592
+
1593
+
1594
+ def sanitize_url(url, remove_authority=True, remove_query_values=True, split=False):
1595
+ # type: (str, bool, bool, bool) -> Union[str, Components]
1596
+ """
1597
+ Removes the authority and query parameter values from a given URL.
1598
+ """
1599
+ parsed_url = urlsplit(url)
1600
+ query_params = parse_qs(parsed_url.query, keep_blank_values=True)
1601
+
1602
+ # strip username:password (netloc can be usr:pwd@example.com)
1603
+ if remove_authority:
1604
+ netloc_parts = parsed_url.netloc.split("@")
1605
+ if len(netloc_parts) > 1:
1606
+ netloc = "%s:%s@%s" % (
1607
+ SENSITIVE_DATA_SUBSTITUTE,
1608
+ SENSITIVE_DATA_SUBSTITUTE,
1609
+ netloc_parts[-1],
1610
+ )
1611
+ else:
1612
+ netloc = parsed_url.netloc
1613
+ else:
1614
+ netloc = parsed_url.netloc
1615
+
1616
+ # strip values from query string
1617
+ if remove_query_values:
1618
+ query_string = unquote(
1619
+ urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
1620
+ )
1621
+ else:
1622
+ query_string = parsed_url.query
1623
+
1624
+ components = Components(
1625
+ scheme=parsed_url.scheme,
1626
+ netloc=netloc,
1627
+ query=query_string,
1628
+ path=parsed_url.path,
1629
+ fragment=parsed_url.fragment,
1630
+ )
1631
+
1632
+ if split:
1633
+ return components
1634
+ else:
1635
+ return urlunsplit(components)
1636
+
1637
+
1638
+ ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
1639
+
1640
+
1641
+ def parse_url(url, sanitize=True):
1642
+ # type: (str, bool) -> ParsedUrl
1643
+ """
1644
+ Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
1645
+ parameters will be sanitized to remove sensitive data. The autority (username and password)
1646
+ in the URL will always be removed.
1647
+ """
1648
+ parsed_url = sanitize_url(
1649
+ url, remove_authority=True, remove_query_values=sanitize, split=True
1650
+ )
1651
+
1652
+ base_url = urlunsplit(
1653
+ Components(
1654
+ scheme=parsed_url.scheme, # type: ignore
1655
+ netloc=parsed_url.netloc, # type: ignore
1656
+ query="",
1657
+ path=parsed_url.path, # type: ignore
1658
+ fragment="",
1659
+ )
903
1660
  )
904
1661
 
905
- if not func_qualname:
906
- # No idea what it is
1662
+ return ParsedUrl(
1663
+ url=base_url,
1664
+ query=parsed_url.query, # type: ignore
1665
+ fragment=parsed_url.fragment, # type: ignore
1666
+ )
1667
+
1668
+
1669
+ def is_valid_sample_rate(rate, source):
1670
+ # type: (Any, str) -> bool
1671
+ """
1672
+ Checks the given sample rate to make sure it is valid type and value (a
1673
+ boolean or a number between 0 and 1, inclusive).
1674
+ """
1675
+
1676
+ # both booleans and NaN are instances of Real, so a) checking for Real
1677
+ # checks for the possibility of a boolean also, and b) we have to check
1678
+ # separately for NaN and Decimal does not derive from Real so need to check that too
1679
+ if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
1680
+ logger.warning(
1681
+ "{source} Given sample rate is invalid. Sample rate must be a boolean or a number between 0 and 1. Got {rate} of type {type}.".format(
1682
+ source=source, rate=rate, type=type(rate)
1683
+ )
1684
+ )
1685
+ return False
1686
+
1687
+ # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
1688
+ rate = float(rate)
1689
+ if rate < 0 or rate > 1:
1690
+ logger.warning(
1691
+ "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
1692
+ source=source, rate=rate
1693
+ )
1694
+ )
1695
+ return False
1696
+
1697
+ return True
1698
+
1699
+
1700
+ def match_regex_list(item, regex_list=None, substring_matching=False):
1701
+ # type: (str, Optional[List[str]], bool) -> bool
1702
+ if regex_list is None:
1703
+ return False
1704
+
1705
+ for item_matcher in regex_list:
1706
+ if not substring_matching and item_matcher[-1] != "$":
1707
+ item_matcher += "$"
1708
+
1709
+ matched = re.search(item_matcher, item)
1710
+ if matched:
1711
+ return True
1712
+
1713
+ return False
1714
+
1715
+
1716
+ def is_sentry_url(client, url):
1717
+ # type: (sentry_sdk.client.BaseClient, str) -> bool
1718
+ """
1719
+ Determines whether the given URL matches the Sentry DSN.
1720
+ """
1721
+ return (
1722
+ client is not None
1723
+ and client.transport is not None
1724
+ and client.transport.parsed_dsn is not None
1725
+ and client.transport.parsed_dsn.netloc in url
1726
+ )
1727
+
1728
+
1729
+ def _generate_installed_modules():
1730
+ # type: () -> Iterator[Tuple[str, str]]
1731
+ try:
1732
+ from importlib import metadata
1733
+
1734
+ yielded = set()
1735
+ for dist in metadata.distributions():
1736
+ name = dist.metadata.get("Name", None) # type: ignore[attr-defined]
1737
+ # `metadata` values may be `None`, see:
1738
+ # https://github.com/python/cpython/issues/91216
1739
+ # and
1740
+ # https://github.com/python/importlib_metadata/issues/371
1741
+ if name is not None:
1742
+ normalized_name = _normalize_module_name(name)
1743
+ if dist.version is not None and normalized_name not in yielded:
1744
+ yield normalized_name, dist.version
1745
+ yielded.add(normalized_name)
1746
+
1747
+ except ImportError:
1748
+ # < py3.8
1749
+ try:
1750
+ import pkg_resources
1751
+ except ImportError:
1752
+ return
1753
+
1754
+ for info in pkg_resources.working_set:
1755
+ yield _normalize_module_name(info.key), info.version
1756
+
1757
+
1758
+ def _normalize_module_name(name):
1759
+ # type: (str) -> str
1760
+ return name.lower()
1761
+
1762
+
1763
+ def _replace_hyphens_dots_and_underscores_with_dashes(name):
1764
+ # type: (str) -> str
1765
+ # https://peps.python.org/pep-0503/#normalized-names
1766
+ return re.sub(r"[-_.]+", "-", name)
1767
+
1768
+
1769
+ def _get_installed_modules():
1770
+ # type: () -> Dict[str, str]
1771
+ global _installed_modules
1772
+ if _installed_modules is None:
1773
+ _installed_modules = dict(_generate_installed_modules())
1774
+ return _installed_modules
1775
+
1776
+
1777
+ def package_version(package):
1778
+ # type: (str) -> Optional[Tuple[int, ...]]
1779
+ normalized_package = _normalize_module_name(
1780
+ _replace_hyphens_dots_and_underscores_with_dashes(package)
1781
+ )
1782
+
1783
+ installed_packages = {
1784
+ _replace_hyphens_dots_and_underscores_with_dashes(module): v
1785
+ for module, v in _get_installed_modules().items()
1786
+ }
1787
+ version = installed_packages.get(normalized_package)
1788
+ if version is None:
907
1789
  return None
908
1790
 
909
- # Methods in Python 3
910
- # Functions
911
- # Classes
1791
+ return parse_version(version)
1792
+
1793
+
1794
+ def reraise(tp, value, tb=None):
1795
+ # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> NoReturn
1796
+ assert value is not None
1797
+ if value.__traceback__ is not tb:
1798
+ raise value.with_traceback(tb)
1799
+ raise value
1800
+
1801
+
1802
+ def _no_op(*_a, **_k):
1803
+ # type: (*Any, **Any) -> None
1804
+ """No-op function for ensure_integration_enabled."""
1805
+ pass
1806
+
1807
+
1808
+ if TYPE_CHECKING:
1809
+
1810
+ @overload
1811
+ def ensure_integration_enabled(
1812
+ integration, # type: type[sentry_sdk.integrations.Integration]
1813
+ original_function, # type: Callable[P, R]
1814
+ ):
1815
+ # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
1816
+ ...
1817
+
1818
+ @overload
1819
+ def ensure_integration_enabled(
1820
+ integration, # type: type[sentry_sdk.integrations.Integration]
1821
+ ):
1822
+ # type: (...) -> Callable[[Callable[P, None]], Callable[P, None]]
1823
+ ...
1824
+
1825
+
1826
+ def ensure_integration_enabled(
1827
+ integration, # type: type[sentry_sdk.integrations.Integration]
1828
+ original_function=_no_op, # type: Union[Callable[P, R], Callable[P, None]]
1829
+ ):
1830
+ # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
1831
+ """
1832
+ Ensures a given integration is enabled prior to calling a Sentry-patched function.
1833
+
1834
+ The function takes as its parameters the integration that must be enabled and the original
1835
+ function that the SDK is patching. The function returns a function that takes the
1836
+ decorated (Sentry-patched) function as its parameter, and returns a function that, when
1837
+ called, checks whether the given integration is enabled. If the integration is enabled, the
1838
+ function calls the decorated, Sentry-patched function. If the integration is not enabled,
1839
+ the original function is called.
1840
+
1841
+ The function also takes care of preserving the original function's signature and docstring.
1842
+
1843
+ Example usage:
1844
+
1845
+ ```python
1846
+ @ensure_integration_enabled(MyIntegration, my_function)
1847
+ def patch_my_function():
1848
+ with sentry_sdk.start_transaction(...):
1849
+ return my_function()
1850
+ ```
1851
+ """
1852
+ if TYPE_CHECKING:
1853
+ # Type hint to ensure the default function has the right typing. The overloads
1854
+ # ensure the default _no_op function is only used when R is None.
1855
+ original_function = cast(Callable[P, R], original_function)
1856
+
1857
+ def patcher(sentry_patched_function):
1858
+ # type: (Callable[P, R]) -> Callable[P, R]
1859
+ def runner(*args: "P.args", **kwargs: "P.kwargs"):
1860
+ # type: (...) -> R
1861
+ if sentry_sdk.get_client().get_integration(integration) is None:
1862
+ return original_function(*args, **kwargs)
1863
+
1864
+ return sentry_patched_function(*args, **kwargs)
1865
+
1866
+ if original_function is _no_op:
1867
+ return wraps(sentry_patched_function)(runner)
1868
+
1869
+ return wraps(original_function)(runner)
1870
+
1871
+ return patcher
1872
+
1873
+
1874
+ if PY37:
1875
+
1876
+ def nanosecond_time():
1877
+ # type: () -> int
1878
+ return time.perf_counter_ns()
1879
+
1880
+ else:
1881
+
1882
+ def nanosecond_time():
1883
+ # type: () -> int
1884
+ return int(time.perf_counter() * 1e9)
1885
+
1886
+
1887
+ def now():
1888
+ # type: () -> float
1889
+ return time.perf_counter()
1890
+
1891
+
1892
+ try:
1893
+ from gevent import get_hub as get_gevent_hub
1894
+ from gevent.monkey import is_module_patched
1895
+ except ImportError:
1896
+ # it's not great that the signatures are different, get_hub can't return None
1897
+ # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
1898
+ def get_gevent_hub(): # type: ignore[misc]
1899
+ # type: () -> Optional[Hub]
1900
+ return None
1901
+
1902
+ def is_module_patched(mod_name):
1903
+ # type: (str) -> bool
1904
+ # unable to import from gevent means no modules have been patched
1905
+ return False
1906
+
1907
+
1908
+ def is_gevent():
1909
+ # type: () -> bool
1910
+ return is_module_patched("threading") or is_module_patched("_thread")
1911
+
1912
+
1913
+ def get_current_thread_meta(thread=None):
1914
+ # type: (Optional[threading.Thread]) -> Tuple[Optional[int], Optional[str]]
1915
+ """
1916
+ Try to get the id of the current thread, with various fall backs.
1917
+ """
1918
+
1919
+ # if a thread is specified, that takes priority
1920
+ if thread is not None:
1921
+ try:
1922
+ thread_id = thread.ident
1923
+ thread_name = thread.name
1924
+ if thread_id is not None:
1925
+ return thread_id, thread_name
1926
+ except AttributeError:
1927
+ pass
1928
+
1929
+ # if the app is using gevent, we should look at the gevent hub first
1930
+ # as the id there differs from what the threading module reports
1931
+ if is_gevent():
1932
+ gevent_hub = get_gevent_hub()
1933
+ if gevent_hub is not None:
1934
+ try:
1935
+ # this is undocumented, so wrap it in try except to be safe
1936
+ return gevent_hub.thread_ident, None
1937
+ except AttributeError:
1938
+ pass
1939
+
1940
+ # use the current thread's id if possible
912
1941
  try:
913
- return "%s.%s" % (func.__module__, func_qualname)
914
- except Exception:
1942
+ thread = threading.current_thread()
1943
+ thread_id = thread.ident
1944
+ thread_name = thread.name
1945
+ if thread_id is not None:
1946
+ return thread_id, thread_name
1947
+ except AttributeError:
915
1948
  pass
916
1949
 
917
- # Possibly a lambda
918
- return func_qualname
1950
+ # if we can't get the current thread id, fall back to the main thread id
1951
+ try:
1952
+ thread = threading.main_thread()
1953
+ thread_id = thread.ident
1954
+ thread_name = thread.name
1955
+ if thread_id is not None:
1956
+ return thread_id, thread_name
1957
+ except AttributeError:
1958
+ pass
1959
+
1960
+ # we've tried everything, time to give up
1961
+ return None, None
1962
+
1963
+
1964
+ def should_be_treated_as_error(ty, value):
1965
+ # type: (Any, Any) -> bool
1966
+ if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
1967
+ # https://docs.python.org/3/library/exceptions.html#SystemExit
1968
+ return False
1969
+
1970
+ return True
1971
+
1972
+
1973
+ if TYPE_CHECKING:
1974
+ T = TypeVar("T")
1975
+
1976
+
1977
+ def try_convert(convert_func, value):
1978
+ # type: (Callable[[Any], T], Any) -> Optional[T]
1979
+ """
1980
+ Attempt to convert from an unknown type to a specific type, using the
1981
+ given function. Return None if the conversion fails, i.e. if the function
1982
+ raises an exception.
1983
+ """
1984
+ try:
1985
+ if isinstance(value, convert_func): # type: ignore
1986
+ return value
1987
+ except TypeError:
1988
+ pass
1989
+
1990
+ try:
1991
+ return convert_func(value)
1992
+ except Exception:
1993
+ return None
1994
+
1995
+
1996
+ def safe_serialize(data):
1997
+ # type: (Any) -> str
1998
+ """Safely serialize to a readable string."""
1999
+
2000
+ def serialize_item(item):
2001
+ # type: (Any) -> Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]
2002
+ if callable(item):
2003
+ try:
2004
+ module = getattr(item, "__module__", None)
2005
+ qualname = getattr(item, "__qualname__", None)
2006
+ name = getattr(item, "__name__", "anonymous")
2007
+
2008
+ if module and qualname:
2009
+ full_path = f"{module}.{qualname}"
2010
+ elif module and name:
2011
+ full_path = f"{module}.{name}"
2012
+ else:
2013
+ full_path = name
2014
+
2015
+ return f"<function {full_path}>"
2016
+ except Exception:
2017
+ return f"<callable {type(item).__name__}>"
2018
+ elif isinstance(item, dict):
2019
+ return {k: serialize_item(v) for k, v in item.items()}
2020
+ elif isinstance(item, (list, tuple)):
2021
+ return [serialize_item(x) for x in item]
2022
+ elif hasattr(item, "__dict__"):
2023
+ try:
2024
+ attrs = {
2025
+ k: serialize_item(v)
2026
+ for k, v in vars(item).items()
2027
+ if not k.startswith("_")
2028
+ }
2029
+ return f"<{type(item).__name__} {attrs}>"
2030
+ except Exception:
2031
+ return repr(item)
2032
+ else:
2033
+ return item
2034
+
2035
+ try:
2036
+ serialized = serialize_item(data)
2037
+ return json.dumps(serialized, default=str)
2038
+ except Exception:
2039
+ return str(data)
2040
+
2041
+
2042
+ def has_logs_enabled(options):
2043
+ # type: (Optional[dict[str, Any]]) -> bool
2044
+ if options is None:
2045
+ return False
2046
+
2047
+ return bool(
2048
+ options.get("enable_logs", False)
2049
+ or options["_experiments"].get("enable_logs", False)
2050
+ )
2051
+
2052
+
2053
+ def get_before_send_log(options):
2054
+ # type: (Optional[dict[str, Any]]) -> Optional[Callable[[Log, Hint], Optional[Log]]]
2055
+ if options is None:
2056
+ return None
2057
+
2058
+ return options.get("before_send_log") or options["_experiments"].get(
2059
+ "before_send_log"
2060
+ )
2061
+
2062
+
2063
+ def has_metrics_enabled(options):
2064
+ # type: (Optional[dict[str, Any]]) -> bool
2065
+ if options is None:
2066
+ return False
2067
+
2068
+ return bool(options.get("enable_metrics", True))
2069
+
2070
+
2071
+ def get_before_send_metric(options):
2072
+ # type: (Optional[dict[str, Any]]) -> Optional[Callable[[Metric, Hint], Optional[Metric]]]
2073
+ if options is None:
2074
+ return None
2075
+
2076
+ return options.get("before_send_metric") or options["_experiments"].get(
2077
+ "before_send_metric"
2078
+ )