sentry-sdk 0.18.0__py2.py3-none-any.whl → 2.46.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (193) hide show
  1. sentry_sdk/__init__.py +48 -6
  2. sentry_sdk/_compat.py +64 -56
  3. sentry_sdk/_init_implementation.py +84 -0
  4. sentry_sdk/_log_batcher.py +172 -0
  5. sentry_sdk/_lru_cache.py +47 -0
  6. sentry_sdk/_metrics_batcher.py +167 -0
  7. sentry_sdk/_queue.py +81 -19
  8. sentry_sdk/_types.py +311 -11
  9. sentry_sdk/_werkzeug.py +98 -0
  10. sentry_sdk/ai/__init__.py +7 -0
  11. sentry_sdk/ai/monitoring.py +137 -0
  12. sentry_sdk/ai/utils.py +144 -0
  13. sentry_sdk/api.py +409 -67
  14. sentry_sdk/attachments.py +75 -0
  15. sentry_sdk/client.py +849 -103
  16. sentry_sdk/consts.py +1389 -34
  17. sentry_sdk/crons/__init__.py +10 -0
  18. sentry_sdk/crons/api.py +62 -0
  19. sentry_sdk/crons/consts.py +4 -0
  20. sentry_sdk/crons/decorator.py +135 -0
  21. sentry_sdk/debug.py +12 -15
  22. sentry_sdk/envelope.py +112 -61
  23. sentry_sdk/feature_flags.py +71 -0
  24. sentry_sdk/hub.py +442 -386
  25. sentry_sdk/integrations/__init__.py +228 -58
  26. sentry_sdk/integrations/_asgi_common.py +108 -0
  27. sentry_sdk/integrations/_wsgi_common.py +131 -40
  28. sentry_sdk/integrations/aiohttp.py +221 -72
  29. sentry_sdk/integrations/anthropic.py +439 -0
  30. sentry_sdk/integrations/argv.py +4 -6
  31. sentry_sdk/integrations/ariadne.py +161 -0
  32. sentry_sdk/integrations/arq.py +247 -0
  33. sentry_sdk/integrations/asgi.py +237 -135
  34. sentry_sdk/integrations/asyncio.py +144 -0
  35. sentry_sdk/integrations/asyncpg.py +208 -0
  36. sentry_sdk/integrations/atexit.py +13 -18
  37. sentry_sdk/integrations/aws_lambda.py +233 -80
  38. sentry_sdk/integrations/beam.py +27 -35
  39. sentry_sdk/integrations/boto3.py +137 -0
  40. sentry_sdk/integrations/bottle.py +91 -69
  41. sentry_sdk/integrations/celery/__init__.py +529 -0
  42. sentry_sdk/integrations/celery/beat.py +293 -0
  43. sentry_sdk/integrations/celery/utils.py +43 -0
  44. sentry_sdk/integrations/chalice.py +35 -28
  45. sentry_sdk/integrations/clickhouse_driver.py +177 -0
  46. sentry_sdk/integrations/cloud_resource_context.py +280 -0
  47. sentry_sdk/integrations/cohere.py +274 -0
  48. sentry_sdk/integrations/dedupe.py +32 -8
  49. sentry_sdk/integrations/django/__init__.py +343 -89
  50. sentry_sdk/integrations/django/asgi.py +201 -22
  51. sentry_sdk/integrations/django/caching.py +204 -0
  52. sentry_sdk/integrations/django/middleware.py +80 -32
  53. sentry_sdk/integrations/django/signals_handlers.py +91 -0
  54. sentry_sdk/integrations/django/templates.py +69 -2
  55. sentry_sdk/integrations/django/transactions.py +39 -14
  56. sentry_sdk/integrations/django/views.py +69 -16
  57. sentry_sdk/integrations/dramatiq.py +226 -0
  58. sentry_sdk/integrations/excepthook.py +19 -13
  59. sentry_sdk/integrations/executing.py +5 -6
  60. sentry_sdk/integrations/falcon.py +128 -65
  61. sentry_sdk/integrations/fastapi.py +141 -0
  62. sentry_sdk/integrations/flask.py +114 -75
  63. sentry_sdk/integrations/gcp.py +67 -36
  64. sentry_sdk/integrations/gnu_backtrace.py +14 -22
  65. sentry_sdk/integrations/google_genai/__init__.py +301 -0
  66. sentry_sdk/integrations/google_genai/consts.py +16 -0
  67. sentry_sdk/integrations/google_genai/streaming.py +155 -0
  68. sentry_sdk/integrations/google_genai/utils.py +576 -0
  69. sentry_sdk/integrations/gql.py +162 -0
  70. sentry_sdk/integrations/graphene.py +151 -0
  71. sentry_sdk/integrations/grpc/__init__.py +168 -0
  72. sentry_sdk/integrations/grpc/aio/__init__.py +7 -0
  73. sentry_sdk/integrations/grpc/aio/client.py +95 -0
  74. sentry_sdk/integrations/grpc/aio/server.py +100 -0
  75. sentry_sdk/integrations/grpc/client.py +91 -0
  76. sentry_sdk/integrations/grpc/consts.py +1 -0
  77. sentry_sdk/integrations/grpc/server.py +66 -0
  78. sentry_sdk/integrations/httpx.py +178 -0
  79. sentry_sdk/integrations/huey.py +174 -0
  80. sentry_sdk/integrations/huggingface_hub.py +378 -0
  81. sentry_sdk/integrations/langchain.py +1132 -0
  82. sentry_sdk/integrations/langgraph.py +337 -0
  83. sentry_sdk/integrations/launchdarkly.py +61 -0
  84. sentry_sdk/integrations/litellm.py +287 -0
  85. sentry_sdk/integrations/litestar.py +315 -0
  86. sentry_sdk/integrations/logging.py +261 -85
  87. sentry_sdk/integrations/loguru.py +213 -0
  88. sentry_sdk/integrations/mcp.py +566 -0
  89. sentry_sdk/integrations/modules.py +6 -33
  90. sentry_sdk/integrations/openai.py +725 -0
  91. sentry_sdk/integrations/openai_agents/__init__.py +61 -0
  92. sentry_sdk/integrations/openai_agents/consts.py +1 -0
  93. sentry_sdk/integrations/openai_agents/patches/__init__.py +5 -0
  94. sentry_sdk/integrations/openai_agents/patches/agent_run.py +140 -0
  95. sentry_sdk/integrations/openai_agents/patches/error_tracing.py +77 -0
  96. sentry_sdk/integrations/openai_agents/patches/models.py +50 -0
  97. sentry_sdk/integrations/openai_agents/patches/runner.py +45 -0
  98. sentry_sdk/integrations/openai_agents/patches/tools.py +77 -0
  99. sentry_sdk/integrations/openai_agents/spans/__init__.py +5 -0
  100. sentry_sdk/integrations/openai_agents/spans/agent_workflow.py +21 -0
  101. sentry_sdk/integrations/openai_agents/spans/ai_client.py +42 -0
  102. sentry_sdk/integrations/openai_agents/spans/execute_tool.py +48 -0
  103. sentry_sdk/integrations/openai_agents/spans/handoff.py +19 -0
  104. sentry_sdk/integrations/openai_agents/spans/invoke_agent.py +86 -0
  105. sentry_sdk/integrations/openai_agents/utils.py +199 -0
  106. sentry_sdk/integrations/openfeature.py +35 -0
  107. sentry_sdk/integrations/opentelemetry/__init__.py +7 -0
  108. sentry_sdk/integrations/opentelemetry/consts.py +5 -0
  109. sentry_sdk/integrations/opentelemetry/integration.py +58 -0
  110. sentry_sdk/integrations/opentelemetry/propagator.py +117 -0
  111. sentry_sdk/integrations/opentelemetry/span_processor.py +391 -0
  112. sentry_sdk/integrations/otlp.py +82 -0
  113. sentry_sdk/integrations/pure_eval.py +20 -11
  114. sentry_sdk/integrations/pydantic_ai/__init__.py +47 -0
  115. sentry_sdk/integrations/pydantic_ai/consts.py +1 -0
  116. sentry_sdk/integrations/pydantic_ai/patches/__init__.py +4 -0
  117. sentry_sdk/integrations/pydantic_ai/patches/agent_run.py +215 -0
  118. sentry_sdk/integrations/pydantic_ai/patches/graph_nodes.py +110 -0
  119. sentry_sdk/integrations/pydantic_ai/patches/model_request.py +40 -0
  120. sentry_sdk/integrations/pydantic_ai/patches/tools.py +98 -0
  121. sentry_sdk/integrations/pydantic_ai/spans/__init__.py +3 -0
  122. sentry_sdk/integrations/pydantic_ai/spans/ai_client.py +246 -0
  123. sentry_sdk/integrations/pydantic_ai/spans/execute_tool.py +49 -0
  124. sentry_sdk/integrations/pydantic_ai/spans/invoke_agent.py +112 -0
  125. sentry_sdk/integrations/pydantic_ai/utils.py +223 -0
  126. sentry_sdk/integrations/pymongo.py +214 -0
  127. sentry_sdk/integrations/pyramid.py +71 -60
  128. sentry_sdk/integrations/quart.py +237 -0
  129. sentry_sdk/integrations/ray.py +165 -0
  130. sentry_sdk/integrations/redis/__init__.py +48 -0
  131. sentry_sdk/integrations/redis/_async_common.py +116 -0
  132. sentry_sdk/integrations/redis/_sync_common.py +119 -0
  133. sentry_sdk/integrations/redis/consts.py +19 -0
  134. sentry_sdk/integrations/redis/modules/__init__.py +0 -0
  135. sentry_sdk/integrations/redis/modules/caches.py +118 -0
  136. sentry_sdk/integrations/redis/modules/queries.py +65 -0
  137. sentry_sdk/integrations/redis/rb.py +32 -0
  138. sentry_sdk/integrations/redis/redis.py +69 -0
  139. sentry_sdk/integrations/redis/redis_cluster.py +107 -0
  140. sentry_sdk/integrations/redis/redis_py_cluster_legacy.py +50 -0
  141. sentry_sdk/integrations/redis/utils.py +148 -0
  142. sentry_sdk/integrations/rq.py +62 -52
  143. sentry_sdk/integrations/rust_tracing.py +284 -0
  144. sentry_sdk/integrations/sanic.py +248 -114
  145. sentry_sdk/integrations/serverless.py +13 -22
  146. sentry_sdk/integrations/socket.py +96 -0
  147. sentry_sdk/integrations/spark/spark_driver.py +115 -62
  148. sentry_sdk/integrations/spark/spark_worker.py +42 -50
  149. sentry_sdk/integrations/sqlalchemy.py +82 -37
  150. sentry_sdk/integrations/starlette.py +737 -0
  151. sentry_sdk/integrations/starlite.py +292 -0
  152. sentry_sdk/integrations/statsig.py +37 -0
  153. sentry_sdk/integrations/stdlib.py +100 -58
  154. sentry_sdk/integrations/strawberry.py +394 -0
  155. sentry_sdk/integrations/sys_exit.py +70 -0
  156. sentry_sdk/integrations/threading.py +142 -38
  157. sentry_sdk/integrations/tornado.py +68 -53
  158. sentry_sdk/integrations/trytond.py +15 -20
  159. sentry_sdk/integrations/typer.py +60 -0
  160. sentry_sdk/integrations/unleash.py +33 -0
  161. sentry_sdk/integrations/unraisablehook.py +53 -0
  162. sentry_sdk/integrations/wsgi.py +126 -125
  163. sentry_sdk/logger.py +96 -0
  164. sentry_sdk/metrics.py +81 -0
  165. sentry_sdk/monitor.py +120 -0
  166. sentry_sdk/profiler/__init__.py +49 -0
  167. sentry_sdk/profiler/continuous_profiler.py +730 -0
  168. sentry_sdk/profiler/transaction_profiler.py +839 -0
  169. sentry_sdk/profiler/utils.py +195 -0
  170. sentry_sdk/scope.py +1542 -112
  171. sentry_sdk/scrubber.py +177 -0
  172. sentry_sdk/serializer.py +152 -210
  173. sentry_sdk/session.py +177 -0
  174. sentry_sdk/sessions.py +202 -179
  175. sentry_sdk/spotlight.py +242 -0
  176. sentry_sdk/tracing.py +1202 -294
  177. sentry_sdk/tracing_utils.py +1236 -0
  178. sentry_sdk/transport.py +693 -189
  179. sentry_sdk/types.py +52 -0
  180. sentry_sdk/utils.py +1395 -228
  181. sentry_sdk/worker.py +30 -17
  182. sentry_sdk-2.46.0.dist-info/METADATA +268 -0
  183. sentry_sdk-2.46.0.dist-info/RECORD +189 -0
  184. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/WHEEL +1 -1
  185. sentry_sdk-2.46.0.dist-info/entry_points.txt +2 -0
  186. sentry_sdk-2.46.0.dist-info/licenses/LICENSE +21 -0
  187. sentry_sdk/_functools.py +0 -66
  188. sentry_sdk/integrations/celery.py +0 -275
  189. sentry_sdk/integrations/redis.py +0 -103
  190. sentry_sdk-0.18.0.dist-info/LICENSE +0 -9
  191. sentry_sdk-0.18.0.dist-info/METADATA +0 -66
  192. sentry_sdk-0.18.0.dist-info/RECORD +0 -65
  193. {sentry_sdk-0.18.0.dist-info → sentry_sdk-2.46.0.dist-info}/top_level.txt +0 -0
sentry_sdk/utils.py CHANGED
@@ -1,44 +1,106 @@
1
+ import base64
1
2
  import json
2
3
  import linecache
3
4
  import logging
5
+ import math
4
6
  import os
7
+ import random
8
+ import re
9
+ import subprocess
5
10
  import sys
6
- import time
7
11
  import threading
8
-
9
- from datetime import datetime
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
19
+
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
10
26
 
11
27
  import sentry_sdk
12
- from sentry_sdk._compat import urlparse, text_type, implements_str, PY2
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,
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
+ )
13
59
 
14
- from sentry_sdk._types import MYPY
60
+ from gevent.hub import Hub
15
61
 
16
- if MYPY:
17
- from types import FrameType
18
- from types import TracebackType
19
- from typing import Any
20
- from typing import Callable
21
- from typing import Dict
22
- from typing import ContextManager
23
- from typing import Iterator
24
- from typing import List
25
- from typing import Optional
26
- from typing import Set
27
- from typing import Tuple
28
- from typing import Union
29
- from typing import Type
62
+ from sentry_sdk._types import Event, ExcInfo, Log, Hint, Metric
30
63
 
31
- from sentry_sdk._types import ExcInfo, EndpointType
64
+ P = ParamSpec("P")
65
+ R = TypeVar("R")
32
66
 
33
67
 
34
68
  epoch = datetime(1970, 1, 1)
35
69
 
36
-
37
70
  # The logger is created here but initialized in the debug support module
38
71
  logger = logging.getLogger("sentry_sdk.errors")
39
72
 
40
- MAX_STRING_LENGTH = 512
41
- MAX_FORMAT_PARAM_LENGTH = 128
73
+ _installed_modules = None
74
+
75
+ BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$")
76
+
77
+ FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0"))
78
+ TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1"))
79
+
80
+ MAX_STACK_FRAMES = 2000
81
+ """Maximum number of stack frames to send to Sentry.
82
+
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
+ """
88
+
89
+
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
96
+
97
+ if normalized in FALSY_ENV_VALUES:
98
+ return False
99
+
100
+ if normalized in TRUTHY_ENV_VALUES:
101
+ return True
102
+
103
+ return None if strict else bool(value)
42
104
 
43
105
 
44
106
  def json_dumps(data):
@@ -47,13 +109,95 @@ def json_dumps(data):
47
109
  return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8")
48
110
 
49
111
 
50
- def _get_debug_hub():
51
- # type: () -> Optional[sentry_sdk.Hub]
52
- # This function is replaced by debug.py
53
- pass
112
+ def get_git_revision():
113
+ # type: () -> Optional[str]
114
+ try:
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
54
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
55
162
 
56
- class CaptureInternalException(object):
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:
57
201
  __slots__ = ()
58
202
 
59
203
  def __enter__(self):
@@ -78,9 +222,14 @@ def capture_internal_exceptions():
78
222
 
79
223
  def capture_internal_exception(exc_info):
80
224
  # type: (ExcInfo) -> None
81
- hub = _get_debug_hub()
82
- if hub is not None:
83
- hub._capture_internal_exception(exc_info)
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)
84
233
 
85
234
 
86
235
  def to_timestamp(value):
@@ -90,7 +239,40 @@ def to_timestamp(value):
90
239
 
91
240
  def format_timestamp(value):
92
241
  # type: (datetime) -> str
93
- return value.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
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)
94
276
 
95
277
 
96
278
  def event_hint_with_exc_info(exc_info=None):
@@ -109,8 +291,7 @@ class BadDsn(ValueError):
109
291
  """Raised on invalid DSNs."""
110
292
 
111
293
 
112
- @implements_str
113
- class Dsn(object):
294
+ class Dsn:
114
295
  """Represents a DSN."""
115
296
 
116
297
  def __init__(self, value):
@@ -118,9 +299,9 @@ class Dsn(object):
118
299
  if isinstance(value, Dsn):
119
300
  self.__dict__ = dict(value.__dict__)
120
301
  return
121
- parts = urlparse.urlsplit(text_type(value))
302
+ parts = urlsplit(str(value))
122
303
 
123
- if parts.scheme not in (u"http", u"https"):
304
+ if parts.scheme not in ("http", "https"):
124
305
  raise BadDsn("Unsupported scheme %r" % parts.scheme)
125
306
  self.scheme = parts.scheme
126
307
 
@@ -130,7 +311,7 @@ class Dsn(object):
130
311
  self.host = parts.hostname
131
312
 
132
313
  if parts.port is None:
133
- self.port = self.scheme == "https" and 443 or 80
314
+ self.port = self.scheme == "https" and 443 or 80 # type: int
134
315
  else:
135
316
  self.port = parts.port
136
317
 
@@ -143,7 +324,7 @@ class Dsn(object):
143
324
  path = parts.path.rsplit("/", 1)
144
325
 
145
326
  try:
146
- self.project_id = text_type(int(path.pop()))
327
+ self.project_id = str(int(path.pop()))
147
328
  except (ValueError, TypeError):
148
329
  raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:])
149
330
 
@@ -183,7 +364,7 @@ class Dsn(object):
183
364
  )
184
365
 
185
366
 
186
- class Auth(object):
367
+ class Auth:
187
368
  """Helper object that represents the auth info."""
188
369
 
189
370
  def __init__(
@@ -207,17 +388,9 @@ class Auth(object):
207
388
  self.version = version
208
389
  self.client = client
209
390
 
210
- @property
211
- def store_api_url(self):
212
- # type: () -> str
213
- """Returns the API url for storing events.
214
-
215
- Deprecated: use get_api_url instead.
216
- """
217
- return self.get_api_url(type="store")
218
-
219
391
  def get_api_url(
220
- self, type="store" # type: EndpointType
392
+ self,
393
+ type=EndpointType.ENVELOPE, # type: EndpointType
221
394
  ):
222
395
  # type: (...) -> str
223
396
  """Returns the API url for storing events."""
@@ -226,36 +399,18 @@ class Auth(object):
226
399
  self.host,
227
400
  self.path,
228
401
  self.project_id,
229
- type,
402
+ type.value,
230
403
  )
231
404
 
232
- def to_header(self, timestamp=None):
233
- # type: (Optional[datetime]) -> str
405
+ def to_header(self):
406
+ # type: () -> str
234
407
  """Returns the auth header a string."""
235
408
  rv = [("sentry_key", self.public_key), ("sentry_version", self.version)]
236
- if timestamp is not None:
237
- rv.append(("sentry_timestamp", str(to_timestamp(timestamp))))
238
409
  if self.client is not None:
239
410
  rv.append(("sentry_client", self.client))
240
411
  if self.secret_key is not None:
241
412
  rv.append(("sentry_secret", self.secret_key))
242
- return u"Sentry " + u", ".join("%s=%s" % (key, value) for key, value in rv)
243
-
244
-
245
- class AnnotatedValue(object):
246
- __slots__ = ("value", "metadata")
247
-
248
- def __init__(self, value, metadata):
249
- # type: (Optional[Any], Dict[str, Any]) -> None
250
- self.value = value
251
- self.metadata = metadata
252
-
253
-
254
- if MYPY:
255
- from typing import TypeVar
256
-
257
- T = TypeVar("T")
258
- Annotated = Union[AnnotatedValue, T]
413
+ return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv)
259
414
 
260
415
 
261
416
  def get_type_name(cls):
@@ -302,6 +457,7 @@ def iter_stacks(tb):
302
457
  def get_lines_from_file(
303
458
  filename, # type: str
304
459
  lineno, # type: int
460
+ max_length=None, # type: Optional[int]
305
461
  loader=None, # type: Optional[Any]
306
462
  module=None, # type: Optional[str]
307
463
  ):
@@ -330,11 +486,12 @@ def get_lines_from_file(
330
486
 
331
487
  try:
332
488
  pre_context = [
333
- strip_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]
334
491
  ]
335
- context_line = strip_string(source[lineno].strip("\r\n"))
492
+ context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length)
336
493
  post_context = [
337
- strip_string(line.strip("\r\n"))
494
+ strip_string(line.strip("\r\n"), max_length=max_length)
338
495
  for line in source[(lineno + 1) : upper_bound]
339
496
  ]
340
497
  return pre_context, context_line, post_context
@@ -345,7 +502,8 @@ def get_lines_from_file(
345
502
 
346
503
  def get_source_context(
347
504
  frame, # type: FrameType
348
- tb_lineno, # type: int
505
+ tb_lineno, # type: Optional[int]
506
+ max_value_length=None, # type: Optional[int]
349
507
  ):
350
508
  # type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
351
509
  try:
@@ -360,56 +518,30 @@ def get_source_context(
360
518
  loader = frame.f_globals["__loader__"]
361
519
  except Exception:
362
520
  loader = None
363
- lineno = tb_lineno - 1
364
- if lineno is not None and abs_path:
365
- 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
+
366
528
  return [], None, []
367
529
 
368
530
 
369
531
  def safe_str(value):
370
532
  # type: (Any) -> str
371
533
  try:
372
- return text_type(value)
534
+ return str(value)
373
535
  except Exception:
374
536
  return safe_repr(value)
375
537
 
376
538
 
377
- if PY2:
378
-
379
- def safe_repr(value):
380
- # type: (Any) -> str
381
- try:
382
- rv = repr(value).decode("utf-8", "replace")
383
-
384
- # At this point `rv` contains a bunch of literal escape codes, like
385
- # this (exaggerated example):
386
- #
387
- # u"\\x2f"
388
- #
389
- # But we want to show this string as:
390
- #
391
- # u"/"
392
- try:
393
- # unicode-escape does this job, but can only decode latin1. So we
394
- # attempt to encode in latin1.
395
- return rv.encode("latin1").decode("unicode-escape")
396
- except Exception:
397
- # Since usually strings aren't latin1 this can break. In those
398
- # cases we just give up.
399
- return rv
400
- except Exception:
401
- # If e.g. the call to `repr` already fails
402
- return u"<broken repr>"
403
-
404
-
405
- else:
406
-
407
- def safe_repr(value):
408
- # type: (Any) -> str
409
- try:
410
- return repr(value)
411
- except Exception:
412
- return "<broken repr>"
539
+ def safe_repr(value):
540
+ # type: (Any) -> str
541
+ try:
542
+ return repr(value)
543
+ except Exception:
544
+ return "<broken repr>"
413
545
 
414
546
 
415
547
  def filename_for_module(module, abs_path):
@@ -426,6 +558,9 @@ def filename_for_module(module, abs_path):
426
558
  return os.path.basename(abs_path)
427
559
 
428
560
  base_module_path = sys.modules[base_module].__file__
561
+ if not base_module_path:
562
+ return abs_path
563
+
429
564
  return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip(
430
565
  os.sep
431
566
  )
@@ -433,8 +568,15 @@ def filename_for_module(module, abs_path):
433
568
  return abs_path
434
569
 
435
570
 
436
- def serialize_frame(frame, tb_lineno=None, with_locals=True):
437
- # type: (FrameType, Optional[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]
438
580
  f_code = getattr(frame, "f_code", None)
439
581
  if not f_code:
440
582
  abs_path = None
@@ -450,33 +592,54 @@ def serialize_frame(frame, tb_lineno=None, with_locals=True):
450
592
  if tb_lineno is None:
451
593
  tb_lineno = frame.f_lineno
452
594
 
453
- 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
454
599
 
455
600
  rv = {
456
601
  "filename": filename_for_module(module, abs_path) or None,
457
- "abs_path": os.path.abspath(abs_path) if abs_path else None,
602
+ "abs_path": os_abs_path,
458
603
  "function": function or "<unknown>",
459
604
  "module": module,
460
605
  "lineno": tb_lineno,
461
- "pre_context": pre_context,
462
- "context_line": context_line,
463
- "post_context": post_context,
464
606
  } # type: Dict[str, Any]
465
- if with_locals:
466
- rv["vars"] = frame.f_locals
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
+ )
612
+
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
+ )
467
619
 
468
620
  return rv
469
621
 
470
622
 
471
- def current_stacktrace(with_locals=True):
472
- # type: (bool) -> Any
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]
473
629
  __tracebackhide__ = True
474
630
  frames = []
475
631
 
476
632
  f = sys._getframe() # type: Optional[FrameType]
477
633
  while f is not None:
478
634
  if not should_hide_frame(f):
479
- 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
+ )
480
643
  f = f.f_back
481
644
 
482
645
  frames.reverse()
@@ -489,46 +652,126 @@ def get_errno(exc_value):
489
652
  return getattr(exc_value, "errno", None)
490
653
 
491
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
+
492
674
  def single_exception_from_error_tuple(
493
675
  exc_type, # type: Optional[type]
494
676
  exc_value, # type: Optional[BaseException]
495
677
  tb, # type: Optional[TracebackType]
496
678
  client_options=None, # type: Optional[Dict[str, Any]]
497
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]]]
498
684
  ):
499
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
+
500
699
  if exc_value is not None:
501
700
  errno = get_errno(exc_value)
502
701
  else:
503
702
  errno = None
504
703
 
505
704
  if errno is not None:
506
- mechanism = mechanism or {"type": "generic"}
507
- mechanism.setdefault("meta", {}).setdefault("errno", {}).setdefault(
508
- "number", errno
509
- )
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)
510
729
 
511
730
  if client_options is None:
512
- 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
513
735
  else:
514
- with_locals = client_options["with_locals"]
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")
515
740
 
516
741
  frames = [
517
- serialize_frame(tb.tb_frame, tb_lineno=tb.tb_lineno, with_locals=with_locals)
518
- for tb in iter_stacks(tb)
519
- ]
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
+ )
520
765
 
521
- rv = {
522
- "module": get_type_module(exc_type),
523
- "type": get_type_name(exc_type),
524
- "value": safe_str(exc_value),
525
- "mechanism": mechanism,
526
- }
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)
527
771
 
528
- if frames:
529
- rv["stacktrace"] = {"frames": frames}
772
+ exception_value["stacktrace"] = {"frames": new_frames}
530
773
 
531
- return rv
774
+ return exception_value
532
775
 
533
776
 
534
777
  HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__")
@@ -565,7 +808,6 @@ if HAS_CHAINED_EXCEPTIONS:
565
808
  exc_value = cause
566
809
  tb = getattr(cause, "__traceback__", None)
567
810
 
568
-
569
811
  else:
570
812
 
571
813
  def walk_exception_chain(exc_info):
@@ -573,36 +815,163 @@ else:
573
815
  yield exc_info
574
816
 
575
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
+
576
921
  def exceptions_from_error_tuple(
577
922
  exc_info, # type: ExcInfo
578
923
  client_options=None, # type: Optional[Dict[str, Any]]
579
924
  mechanism=None, # type: Optional[Dict[str, Any]]
925
+ full_stack=None, # type: Optional[list[dict[str, Any]]]
580
926
  ):
581
927
  # type: (...) -> List[Dict[str, Any]]
582
928
  exc_type, exc_value, tb = exc_info
583
- rv = []
584
- for exc_type, exc_value, tb in walk_exception_chain(exc_info):
585
- rv.append(
586
- single_exception_from_error_tuple(
587
- exc_type, exc_value, tb, client_options, mechanism
588
- )
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,
589
944
  )
590
945
 
591
- rv.reverse()
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
+ )
592
959
 
593
- return rv
960
+ exceptions.reverse()
961
+
962
+ return exceptions
594
963
 
595
964
 
596
965
  def to_string(value):
597
966
  # type: (str) -> str
598
967
  try:
599
- return text_type(value)
968
+ return str(value)
600
969
  except UnicodeDecodeError:
601
970
  return repr(value)[1:-1]
602
971
 
603
972
 
604
973
  def iter_event_stacktraces(event):
605
- # type: (Dict[str, Any]) -> Iterator[Dict[str, Any]]
974
+ # type: (Event) -> Iterator[Annotated[Dict[str, Any]]]
606
975
  if "stacktrace" in event:
607
976
  yield event["stacktrace"]
608
977
  if "threads" in event:
@@ -611,55 +980,71 @@ def iter_event_stacktraces(event):
611
980
  yield thread["stacktrace"]
612
981
  if "exception" in event:
613
982
  for exception in event["exception"].get("values") or ():
614
- if "stacktrace" in exception:
983
+ if isinstance(exception, dict) and "stacktrace" in exception:
615
984
  yield exception["stacktrace"]
616
985
 
617
986
 
618
987
  def iter_event_frames(event):
619
- # type: (Dict[str, Any]) -> Iterator[Dict[str, Any]]
988
+ # type: (Event) -> Iterator[Dict[str, Any]]
620
989
  for stacktrace in iter_event_stacktraces(event):
990
+ if isinstance(stacktrace, AnnotatedValue):
991
+ stacktrace = stacktrace.value or {}
992
+
621
993
  for frame in stacktrace.get("frames") or ():
622
994
  yield frame
623
995
 
624
996
 
625
- def handle_in_app(event, in_app_exclude=None, in_app_include=None):
626
- # type: (Dict[str, Any], Optional[List[str]], Optional[List[str]]) -> Dict[str, Any]
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
627
999
  for stacktrace in iter_event_stacktraces(event):
628
- handle_in_app_impl(
1000
+ if isinstance(stacktrace, AnnotatedValue):
1001
+ stacktrace = stacktrace.value or {}
1002
+
1003
+ set_in_app_in_frames(
629
1004
  stacktrace.get("frames"),
630
1005
  in_app_exclude=in_app_exclude,
631
1006
  in_app_include=in_app_include,
1007
+ project_root=project_root,
632
1008
  )
633
1009
 
634
1010
  return event
635
1011
 
636
1012
 
637
- def handle_in_app_impl(frames, in_app_exclude, in_app_include):
638
- # type: (Any, Optional[List[str]], Optional[List[str]]) -> Optional[Any]
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]
639
1015
  if not frames:
640
1016
  return None
641
1017
 
642
- any_in_app = False
643
1018
  for frame in frames:
644
- in_app = frame.get("in_app")
645
- if in_app is not None:
646
- if in_app:
647
- any_in_app = True
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:
648
1022
  continue
649
1023
 
650
1024
  module = frame.get("module")
651
- if not module:
652
- continue
653
- elif _module_in_set(module, in_app_include):
1025
+
1026
+ # check if module in frame is in the list of modules to include
1027
+ if _module_in_list(module, in_app_include):
654
1028
  frame["in_app"] = True
655
- any_in_app = True
656
- elif _module_in_set(module, in_app_exclude):
1029
+ continue
1030
+
1031
+ # check if module in frame is in the list of modules to exclude
1032
+ if _module_in_list(module, in_app_exclude):
1033
+ frame["in_app"] = False
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
1040
+
1041
+ if _is_external_source(abs_path):
657
1042
  frame["in_app"] = False
1043
+ continue
658
1044
 
659
- if not any_in_app:
660
- for frame in frames:
661
- if frame.get("in_app") is None:
662
- frame["in_app"] = True
1045
+ if _is_in_project_root(abs_path, project_root):
1046
+ frame["in_app"] = True
1047
+ continue
663
1048
 
664
1049
  return frames
665
1050
 
@@ -683,7 +1068,54 @@ def exc_info_from_error(error):
683
1068
  else:
684
1069
  raise ValueError("Expected Exception object to report, got %s!" % type(error))
685
1070
 
686
- return exc_type, exc_value, tb
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 :]
1117
+
1118
+ return new_frames
687
1119
 
688
1120
 
689
1121
  def event_from_exception(
@@ -691,15 +1123,24 @@ def event_from_exception(
691
1123
  client_options=None, # type: Optional[Dict[str, Any]]
692
1124
  mechanism=None, # type: Optional[Dict[str, Any]]
693
1125
  ):
694
- # type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]]
1126
+ # type: (...) -> Tuple[Event, Dict[str, Any]]
695
1127
  exc_info = exc_info_from_error(exc_info)
696
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
+
697
1138
  return (
698
1139
  {
699
1140
  "level": "error",
700
1141
  "exception": {
701
1142
  "values": exceptions_from_error_tuple(
702
- exc_info, client_options, mechanism
1143
+ exc_info, client_options, mechanism, full_stack
703
1144
  )
704
1145
  },
705
1146
  },
@@ -707,37 +1148,142 @@ def event_from_exception(
707
1148
  )
708
1149
 
709
1150
 
710
- def _module_in_set(name, set):
711
- # type: (str, Optional[List[str]]) -> bool
712
- 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:
713
1157
  return False
714
- for item in set or ():
1158
+
1159
+ for item in items:
715
1160
  if item == name or name.startswith(item + "."):
716
1161
  return True
1162
+
717
1163
  return False
718
1164
 
719
1165
 
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:
1170
+ return False
1171
+
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
1186
+
1187
+ return False
1188
+
1189
+
1190
+ def _truncate_by_bytes(string, max_bytes):
1191
+ # type: (str, int) -> str
1192
+ """
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")
1196
+
1197
+ return truncated + "..."
1198
+
1199
+
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
+
1207
+
720
1208
  def strip_string(value, max_length=None):
721
1209
  # type: (str, Optional[int]) -> Union[AnnotatedValue, str]
722
- # TODO: read max_length from config
723
1210
  if not value:
724
1211
  return value
725
1212
 
726
1213
  if max_length is None:
727
- # This is intentionally not just the default such that one can patch `MAX_STRING_LENGTH` and affect `strip_string`.
728
- max_length = MAX_STRING_LENGTH
1214
+ max_length = DEFAULT_MAX_VALUE_LENGTH
729
1215
 
730
- length = len(value)
1216
+ byte_size = _get_size_in_bytes(value)
1217
+ text_size = len(value)
731
1218
 
732
- if length > max_length:
733
- return AnnotatedValue(
734
- value=value[: max_length - 3] + u"...",
735
- metadata={
736
- "len": length,
737
- "rem": [["!limit", "x", max_length - 3, max_length]],
738
- },
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
1227
+
1228
+ return AnnotatedValue(
1229
+ value=truncated_value,
1230
+ metadata={
1231
+ "len": byte_size or text_size,
1232
+ "rem": [["!limit", "x", max_length - 3, max_length]],
1233
+ },
1234
+ )
1235
+
1236
+
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
+ )?
739
1271
  )
740
- return value
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
741
1287
 
742
1288
 
743
1289
  def _is_contextvars_broken():
@@ -746,12 +1292,26 @@ def _is_contextvars_broken():
746
1292
  Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
747
1293
  """
748
1294
  try:
749
- from gevent.monkey import is_object_patched # type: ignore
1295
+ import gevent
1296
+ from gevent.monkey import is_object_patched
750
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
+ )
751
1302
  if is_object_patched("threading", "local"):
752
- # Gevent 20.5 is able to patch both thread locals and contextvars,
753
- # in that case all is good.
754
- if is_object_patched("contextvars", "ContextVar"):
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
+ ):
755
1315
  return False
756
1316
 
757
1317
  return True
@@ -759,9 +1319,18 @@ def _is_contextvars_broken():
759
1319
  pass
760
1320
 
761
1321
  try:
1322
+ import greenlet
762
1323
  from eventlet.patcher import is_monkey_patched # type: ignore
763
1324
 
764
- if is_monkey_patched("thread"):
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):
765
1334
  return True
766
1335
  except ImportError:
767
1336
  pass
@@ -771,21 +1340,33 @@ def _is_contextvars_broken():
771
1340
 
772
1341
  def _make_threadlocal_contextvars(local):
773
1342
  # type: (type) -> type
774
- class ContextVar(object):
1343
+ class ContextVar:
775
1344
  # Super-limited impl of ContextVar
776
1345
 
777
- def __init__(self, name):
778
- # type: (str) -> None
1346
+ def __init__(self, name, default=None):
1347
+ # type: (str, Any) -> None
779
1348
  self._name = name
1349
+ self._default = default
780
1350
  self._local = local()
1351
+ self._original_local = local()
781
1352
 
782
- def get(self, default):
1353
+ def get(self, default=None):
783
1354
  # type: (Any) -> Any
784
- return getattr(self._local, "value", default)
1355
+ return getattr(self._local, "value", default or self._default)
785
1356
 
786
1357
  def set(self, value):
787
- # type: (Any) -> None
1358
+ # type: (Any) -> Any
1359
+ token = str(random.getrandbits(64))
1360
+ original_value = self.get()
1361
+ setattr(self._original_local, token, original_value)
788
1362
  self._local.value = value
1363
+ return token
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]
789
1370
 
790
1371
  return ContextVar
791
1372
 
@@ -807,7 +1388,7 @@ def _get_contextvars():
807
1388
  # `aiocontextvars` is absolutely required for functional
808
1389
  # contextvars on Python 3.6.
809
1390
  try:
810
- from aiocontextvars import ContextVar # noqa
1391
+ from aiocontextvars import ContextVar
811
1392
 
812
1393
  return True, ContextVar
813
1394
  except ImportError:
@@ -840,9 +1421,12 @@ Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more in
840
1421
  """
841
1422
 
842
1423
 
843
- def transaction_from_function(func):
1424
+ def qualname_from_function(func):
844
1425
  # type: (Callable[..., Any]) -> Optional[str]
845
- # Methods in Python 2
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
846
1430
  try:
847
1431
  return "%s.%s.%s" % (
848
1432
  func.im_class.__module__, # type: ignore
@@ -852,30 +1436,44 @@ def transaction_from_function(func):
852
1436
  except Exception:
853
1437
  pass
854
1438
 
855
- func_qualname = (
856
- getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None
857
- ) # type: Optional[str]
1439
+ prefix, suffix = "", ""
858
1440
 
859
- if not func_qualname:
860
- # No idea what it is
861
- return None
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
862
1453
 
863
- # Methods in Python 3
864
- # Functions
865
- # Classes
866
- try:
867
- return "%s.%s" % (func.__module__, func_qualname)
868
- except Exception:
869
- pass
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
870
1464
 
871
- # Possibly a lambda
872
1465
  return func_qualname
873
1466
 
874
1467
 
1468
+ def transaction_from_function(func):
1469
+ # type: (Callable[..., Any]) -> Optional[str]
1470
+ return qualname_from_function(func)
1471
+
1472
+
875
1473
  disable_capture_event = ContextVar("disable_capture_event")
876
1474
 
877
1475
 
878
- class ServerlessTimeoutWarning(Exception):
1476
+ class ServerlessTimeoutWarning(Exception): # noqa: N818
879
1477
  """Raised when a serverless method is about to reach its timeout."""
880
1478
 
881
1479
  pass
@@ -886,16 +1484,44 @@ class TimeoutThread(threading.Thread):
886
1484
  waiting_time and raises a custom ServerlessTimeout exception.
887
1485
  """
888
1486
 
889
- def __init__(self, waiting_time, configured_timeout):
890
- # type: (float, int) -> None
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
891
1491
  threading.Thread.__init__(self)
892
1492
  self.waiting_time = waiting_time
893
1493
  self.configured_timeout = configured_timeout
894
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
+
895
1518
  def run(self):
896
1519
  # type: () -> None
897
1520
 
898
- time.sleep(self.waiting_time)
1521
+ self._stop_event.wait(self.waiting_time)
1522
+
1523
+ if self._stop_event.is_set():
1524
+ return
899
1525
 
900
1526
  integer_configured_timeout = int(self.configured_timeout)
901
1527
 
@@ -904,8 +1530,549 @@ class TimeoutThread(threading.Thread):
904
1530
  integer_configured_timeout = integer_configured_timeout + 1
905
1531
 
906
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
+
907
1545
  raise ServerlessTimeoutWarning(
908
1546
  "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
909
1547
  integer_configured_timeout
910
1548
  )
911
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
+ )
1660
+ )
1661
+
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:
1789
+ return None
1790
+
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
1941
+ try:
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:
1948
+ pass
1949
+
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
+ )