25개 이상의 토픽을 선택하실 수 없습니다. Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

1987 lines
60 KiB

  1. import base64
  2. import json
  3. import linecache
  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
  19. try:
  20. # Python 3.11
  21. from builtins import BaseExceptionGroup
  22. except ImportError:
  23. # Python 3.10 and below
  24. BaseExceptionGroup = None # type: ignore
  25. import sentry_sdk
  26. from sentry_sdk._compat import PY37
  27. from sentry_sdk.consts import (
  28. DEFAULT_ADD_FULL_STACK,
  29. DEFAULT_MAX_STACK_FRAMES,
  30. DEFAULT_MAX_VALUE_LENGTH,
  31. EndpointType,
  32. )
  33. from sentry_sdk._types import Annotated, AnnotatedValue, SENSITIVE_DATA_SUBSTITUTE
  34. from typing import TYPE_CHECKING
  35. if TYPE_CHECKING:
  36. from types import FrameType, TracebackType
  37. from typing import (
  38. Any,
  39. Callable,
  40. cast,
  41. ContextManager,
  42. Dict,
  43. Iterator,
  44. List,
  45. NoReturn,
  46. Optional,
  47. overload,
  48. ParamSpec,
  49. Set,
  50. Tuple,
  51. Type,
  52. TypeVar,
  53. Union,
  54. )
  55. from gevent.hub import Hub
  56. from sentry_sdk._types import Event, ExcInfo
  57. P = ParamSpec("P")
  58. R = TypeVar("R")
  59. epoch = datetime(1970, 1, 1)
  60. # The logger is created here but initialized in the debug support module
  61. logger = logging.getLogger("sentry_sdk.errors")
  62. _installed_modules = None
  63. BASE64_ALPHABET = re.compile(r"^[a-zA-Z0-9/+=]*$")
  64. FALSY_ENV_VALUES = frozenset(("false", "f", "n", "no", "off", "0"))
  65. TRUTHY_ENV_VALUES = frozenset(("true", "t", "y", "yes", "on", "1"))
  66. MAX_STACK_FRAMES = 2000
  67. """Maximum number of stack frames to send to Sentry.
  68. If we have more than this number of stack frames, we will stop processing
  69. the stacktrace to avoid getting stuck in a long-lasting loop. This value
  70. exceeds the default sys.getrecursionlimit() of 1000, so users will only
  71. be affected by this limit if they have a custom recursion limit.
  72. """
  73. def env_to_bool(value, *, strict=False):
  74. # type: (Any, Optional[bool]) -> bool | None
  75. """Casts an ENV variable value to boolean using the constants defined above.
  76. In strict mode, it may return None if the value doesn't match any of the predefined values.
  77. """
  78. normalized = str(value).lower() if value is not None else None
  79. if normalized in FALSY_ENV_VALUES:
  80. return False
  81. if normalized in TRUTHY_ENV_VALUES:
  82. return True
  83. return None if strict else bool(value)
  84. def json_dumps(data):
  85. # type: (Any) -> bytes
  86. """Serialize data into a compact JSON representation encoded as UTF-8."""
  87. return json.dumps(data, allow_nan=False, separators=(",", ":")).encode("utf-8")
  88. def get_git_revision():
  89. # type: () -> Optional[str]
  90. try:
  91. with open(os.path.devnull, "w+") as null:
  92. # prevent command prompt windows from popping up on windows
  93. startupinfo = None
  94. if sys.platform == "win32" or sys.platform == "cygwin":
  95. startupinfo = subprocess.STARTUPINFO()
  96. startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
  97. revision = (
  98. subprocess.Popen(
  99. ["git", "rev-parse", "HEAD"],
  100. startupinfo=startupinfo,
  101. stdout=subprocess.PIPE,
  102. stderr=null,
  103. stdin=null,
  104. )
  105. .communicate()[0]
  106. .strip()
  107. .decode("utf-8")
  108. )
  109. except (OSError, IOError, FileNotFoundError):
  110. return None
  111. return revision
  112. def get_default_release():
  113. # type: () -> Optional[str]
  114. """Try to guess a default release."""
  115. release = os.environ.get("SENTRY_RELEASE")
  116. if release:
  117. return release
  118. release = get_git_revision()
  119. if release:
  120. return release
  121. for var in (
  122. "HEROKU_SLUG_COMMIT",
  123. "SOURCE_VERSION",
  124. "CODEBUILD_RESOLVED_SOURCE_VERSION",
  125. "CIRCLE_SHA1",
  126. "GAE_DEPLOYMENT_ID",
  127. ):
  128. release = os.environ.get(var)
  129. if release:
  130. return release
  131. return None
  132. def get_sdk_name(installed_integrations):
  133. # type: (List[str]) -> str
  134. """Return the SDK name including the name of the used web framework."""
  135. # Note: I can not use for example sentry_sdk.integrations.django.DjangoIntegration.identifier
  136. # here because if django is not installed the integration is not accessible.
  137. framework_integrations = [
  138. "django",
  139. "flask",
  140. "fastapi",
  141. "bottle",
  142. "falcon",
  143. "quart",
  144. "sanic",
  145. "starlette",
  146. "litestar",
  147. "starlite",
  148. "chalice",
  149. "serverless",
  150. "pyramid",
  151. "tornado",
  152. "aiohttp",
  153. "aws_lambda",
  154. "gcp",
  155. "beam",
  156. "asgi",
  157. "wsgi",
  158. ]
  159. for integration in framework_integrations:
  160. if integration in installed_integrations:
  161. return "sentry.python.{}".format(integration)
  162. return "sentry.python"
  163. class CaptureInternalException:
  164. __slots__ = ()
  165. def __enter__(self):
  166. # type: () -> ContextManager[Any]
  167. return self
  168. def __exit__(self, ty, value, tb):
  169. # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> bool
  170. if ty is not None and value is not None:
  171. capture_internal_exception((ty, value, tb))
  172. return True
  173. _CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException()
  174. def capture_internal_exceptions():
  175. # type: () -> ContextManager[Any]
  176. return _CAPTURE_INTERNAL_EXCEPTION
  177. def capture_internal_exception(exc_info):
  178. # type: (ExcInfo) -> None
  179. """
  180. Capture an exception that is likely caused by a bug in the SDK
  181. itself.
  182. These exceptions do not end up in Sentry and are just logged instead.
  183. """
  184. if sentry_sdk.get_client().is_active():
  185. logger.error("Internal error in sentry_sdk", exc_info=exc_info)
  186. def to_timestamp(value):
  187. # type: (datetime) -> float
  188. return (value - epoch).total_seconds()
  189. def format_timestamp(value):
  190. # type: (datetime) -> str
  191. """Formats a timestamp in RFC 3339 format.
  192. Any datetime objects with a non-UTC timezone are converted to UTC, so that all timestamps are formatted in UTC.
  193. """
  194. utctime = value.astimezone(timezone.utc)
  195. # We use this custom formatting rather than isoformat for backwards compatibility (we have used this format for
  196. # several years now), and isoformat is slightly different.
  197. return utctime.strftime("%Y-%m-%dT%H:%M:%S.%fZ")
  198. ISO_TZ_SEPARATORS = frozenset(("+", "-"))
  199. def datetime_from_isoformat(value):
  200. # type: (str) -> datetime
  201. try:
  202. result = datetime.fromisoformat(value)
  203. except (AttributeError, ValueError):
  204. # py 3.6
  205. timestamp_format = (
  206. "%Y-%m-%dT%H:%M:%S.%f" if "." in value else "%Y-%m-%dT%H:%M:%S"
  207. )
  208. if value.endswith("Z"):
  209. value = value[:-1] + "+0000"
  210. if value[-6] in ISO_TZ_SEPARATORS:
  211. timestamp_format += "%z"
  212. value = value[:-3] + value[-2:]
  213. elif value[-5] in ISO_TZ_SEPARATORS:
  214. timestamp_format += "%z"
  215. result = datetime.strptime(value, timestamp_format)
  216. return result.astimezone(timezone.utc)
  217. def event_hint_with_exc_info(exc_info=None):
  218. # type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]]
  219. """Creates a hint with the exc info filled in."""
  220. if exc_info is None:
  221. exc_info = sys.exc_info()
  222. else:
  223. exc_info = exc_info_from_error(exc_info)
  224. if exc_info[0] is None:
  225. exc_info = None
  226. return {"exc_info": exc_info}
  227. class BadDsn(ValueError):
  228. """Raised on invalid DSNs."""
  229. class Dsn:
  230. """Represents a DSN."""
  231. def __init__(self, value):
  232. # type: (Union[Dsn, str]) -> None
  233. if isinstance(value, Dsn):
  234. self.__dict__ = dict(value.__dict__)
  235. return
  236. parts = urlsplit(str(value))
  237. if parts.scheme not in ("http", "https"):
  238. raise BadDsn("Unsupported scheme %r" % parts.scheme)
  239. self.scheme = parts.scheme
  240. if parts.hostname is None:
  241. raise BadDsn("Missing hostname")
  242. self.host = parts.hostname
  243. if parts.port is None:
  244. self.port = self.scheme == "https" and 443 or 80 # type: int
  245. else:
  246. self.port = parts.port
  247. if not parts.username:
  248. raise BadDsn("Missing public key")
  249. self.public_key = parts.username
  250. self.secret_key = parts.password
  251. path = parts.path.rsplit("/", 1)
  252. try:
  253. self.project_id = str(int(path.pop()))
  254. except (ValueError, TypeError):
  255. raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:])
  256. self.path = "/".join(path) + "/"
  257. @property
  258. def netloc(self):
  259. # type: () -> str
  260. """The netloc part of a DSN."""
  261. rv = self.host
  262. if (self.scheme, self.port) not in (("http", 80), ("https", 443)):
  263. rv = "%s:%s" % (rv, self.port)
  264. return rv
  265. def to_auth(self, client=None):
  266. # type: (Optional[Any]) -> Auth
  267. """Returns the auth info object for this dsn."""
  268. return Auth(
  269. scheme=self.scheme,
  270. host=self.netloc,
  271. path=self.path,
  272. project_id=self.project_id,
  273. public_key=self.public_key,
  274. secret_key=self.secret_key,
  275. client=client,
  276. )
  277. def __str__(self):
  278. # type: () -> str
  279. return "%s://%s%s@%s%s%s" % (
  280. self.scheme,
  281. self.public_key,
  282. self.secret_key and "@" + self.secret_key or "",
  283. self.netloc,
  284. self.path,
  285. self.project_id,
  286. )
  287. class Auth:
  288. """Helper object that represents the auth info."""
  289. def __init__(
  290. self,
  291. scheme,
  292. host,
  293. project_id,
  294. public_key,
  295. secret_key=None,
  296. version=7,
  297. client=None,
  298. path="/",
  299. ):
  300. # type: (str, str, str, str, Optional[str], int, Optional[Any], str) -> None
  301. self.scheme = scheme
  302. self.host = host
  303. self.path = path
  304. self.project_id = project_id
  305. self.public_key = public_key
  306. self.secret_key = secret_key
  307. self.version = version
  308. self.client = client
  309. def get_api_url(
  310. self, type=EndpointType.ENVELOPE # type: EndpointType
  311. ):
  312. # type: (...) -> str
  313. """Returns the API url for storing events."""
  314. return "%s://%s%sapi/%s/%s/" % (
  315. self.scheme,
  316. self.host,
  317. self.path,
  318. self.project_id,
  319. type.value,
  320. )
  321. def to_header(self):
  322. # type: () -> str
  323. """Returns the auth header a string."""
  324. rv = [("sentry_key", self.public_key), ("sentry_version", self.version)]
  325. if self.client is not None:
  326. rv.append(("sentry_client", self.client))
  327. if self.secret_key is not None:
  328. rv.append(("sentry_secret", self.secret_key))
  329. return "Sentry " + ", ".join("%s=%s" % (key, value) for key, value in rv)
  330. def get_type_name(cls):
  331. # type: (Optional[type]) -> Optional[str]
  332. return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None)
  333. def get_type_module(cls):
  334. # type: (Optional[type]) -> Optional[str]
  335. mod = getattr(cls, "__module__", None)
  336. if mod not in (None, "builtins", "__builtins__"):
  337. return mod
  338. return None
  339. def should_hide_frame(frame):
  340. # type: (FrameType) -> bool
  341. try:
  342. mod = frame.f_globals["__name__"]
  343. if mod.startswith("sentry_sdk."):
  344. return True
  345. except (AttributeError, KeyError):
  346. pass
  347. for flag_name in "__traceback_hide__", "__tracebackhide__":
  348. try:
  349. if frame.f_locals[flag_name]:
  350. return True
  351. except Exception:
  352. pass
  353. return False
  354. def iter_stacks(tb):
  355. # type: (Optional[TracebackType]) -> Iterator[TracebackType]
  356. tb_ = tb # type: Optional[TracebackType]
  357. while tb_ is not None:
  358. if not should_hide_frame(tb_.tb_frame):
  359. yield tb_
  360. tb_ = tb_.tb_next
  361. def get_lines_from_file(
  362. filename, # type: str
  363. lineno, # type: int
  364. max_length=None, # type: Optional[int]
  365. loader=None, # type: Optional[Any]
  366. module=None, # type: Optional[str]
  367. ):
  368. # type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
  369. context_lines = 5
  370. source = None
  371. if loader is not None and hasattr(loader, "get_source"):
  372. try:
  373. source_str = loader.get_source(module) # type: Optional[str]
  374. except (ImportError, IOError):
  375. source_str = None
  376. if source_str is not None:
  377. source = source_str.splitlines()
  378. if source is None:
  379. try:
  380. source = linecache.getlines(filename)
  381. except (OSError, IOError):
  382. return [], None, []
  383. if not source:
  384. return [], None, []
  385. lower_bound = max(0, lineno - context_lines)
  386. upper_bound = min(lineno + 1 + context_lines, len(source))
  387. try:
  388. pre_context = [
  389. strip_string(line.strip("\r\n"), max_length=max_length)
  390. for line in source[lower_bound:lineno]
  391. ]
  392. context_line = strip_string(source[lineno].strip("\r\n"), max_length=max_length)
  393. post_context = [
  394. strip_string(line.strip("\r\n"), max_length=max_length)
  395. for line in source[(lineno + 1) : upper_bound]
  396. ]
  397. return pre_context, context_line, post_context
  398. except IndexError:
  399. # the file may have changed since it was loaded into memory
  400. return [], None, []
  401. def get_source_context(
  402. frame, # type: FrameType
  403. tb_lineno, # type: Optional[int]
  404. max_value_length=None, # type: Optional[int]
  405. ):
  406. # type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]]
  407. try:
  408. abs_path = frame.f_code.co_filename # type: Optional[str]
  409. except Exception:
  410. abs_path = None
  411. try:
  412. module = frame.f_globals["__name__"]
  413. except Exception:
  414. return [], None, []
  415. try:
  416. loader = frame.f_globals["__loader__"]
  417. except Exception:
  418. loader = None
  419. if tb_lineno is not None and abs_path:
  420. lineno = tb_lineno - 1
  421. return get_lines_from_file(
  422. abs_path, lineno, max_value_length, loader=loader, module=module
  423. )
  424. return [], None, []
  425. def safe_str(value):
  426. # type: (Any) -> str
  427. try:
  428. return str(value)
  429. except Exception:
  430. return safe_repr(value)
  431. def safe_repr(value):
  432. # type: (Any) -> str
  433. try:
  434. return repr(value)
  435. except Exception:
  436. return "<broken repr>"
  437. def filename_for_module(module, abs_path):
  438. # type: (Optional[str], Optional[str]) -> Optional[str]
  439. if not abs_path or not module:
  440. return abs_path
  441. try:
  442. if abs_path.endswith(".pyc"):
  443. abs_path = abs_path[:-1]
  444. base_module = module.split(".", 1)[0]
  445. if base_module == module:
  446. return os.path.basename(abs_path)
  447. base_module_path = sys.modules[base_module].__file__
  448. if not base_module_path:
  449. return abs_path
  450. return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip(
  451. os.sep
  452. )
  453. except Exception:
  454. return abs_path
  455. def serialize_frame(
  456. frame,
  457. tb_lineno=None,
  458. include_local_variables=True,
  459. include_source_context=True,
  460. max_value_length=None,
  461. custom_repr=None,
  462. ):
  463. # type: (FrameType, Optional[int], bool, bool, Optional[int], Optional[Callable[..., Optional[str]]]) -> Dict[str, Any]
  464. f_code = getattr(frame, "f_code", None)
  465. if not f_code:
  466. abs_path = None
  467. function = None
  468. else:
  469. abs_path = frame.f_code.co_filename
  470. function = frame.f_code.co_name
  471. try:
  472. module = frame.f_globals["__name__"]
  473. except Exception:
  474. module = None
  475. if tb_lineno is None:
  476. tb_lineno = frame.f_lineno
  477. try:
  478. os_abs_path = os.path.abspath(abs_path) if abs_path else None
  479. except Exception:
  480. os_abs_path = None
  481. rv = {
  482. "filename": filename_for_module(module, abs_path) or None,
  483. "abs_path": os_abs_path,
  484. "function": function or "<unknown>",
  485. "module": module,
  486. "lineno": tb_lineno,
  487. } # type: Dict[str, Any]
  488. if include_source_context:
  489. rv["pre_context"], rv["context_line"], rv["post_context"] = get_source_context(
  490. frame, tb_lineno, max_value_length
  491. )
  492. if include_local_variables:
  493. from sentry_sdk.serializer import serialize
  494. rv["vars"] = serialize(
  495. dict(frame.f_locals), is_vars=True, custom_repr=custom_repr
  496. )
  497. return rv
  498. def current_stacktrace(
  499. include_local_variables=True, # type: bool
  500. include_source_context=True, # type: bool
  501. max_value_length=None, # type: Optional[int]
  502. ):
  503. # type: (...) -> Dict[str, Any]
  504. __tracebackhide__ = True
  505. frames = []
  506. f = sys._getframe() # type: Optional[FrameType]
  507. while f is not None:
  508. if not should_hide_frame(f):
  509. frames.append(
  510. serialize_frame(
  511. f,
  512. include_local_variables=include_local_variables,
  513. include_source_context=include_source_context,
  514. max_value_length=max_value_length,
  515. )
  516. )
  517. f = f.f_back
  518. frames.reverse()
  519. return {"frames": frames}
  520. def get_errno(exc_value):
  521. # type: (BaseException) -> Optional[Any]
  522. return getattr(exc_value, "errno", None)
  523. def get_error_message(exc_value):
  524. # type: (Optional[BaseException]) -> str
  525. message = (
  526. getattr(exc_value, "message", "")
  527. or getattr(exc_value, "detail", "")
  528. or safe_str(exc_value)
  529. ) # type: str
  530. # __notes__ should be a list of strings when notes are added
  531. # via add_note, but can be anything else if __notes__ is set
  532. # directly. We only support strings in __notes__, since that
  533. # is the correct use.
  534. notes = getattr(exc_value, "__notes__", None) # type: object
  535. if isinstance(notes, list) and len(notes) > 0:
  536. message += "\n" + "\n".join(note for note in notes if isinstance(note, str))
  537. return message
  538. def single_exception_from_error_tuple(
  539. exc_type, # type: Optional[type]
  540. exc_value, # type: Optional[BaseException]
  541. tb, # type: Optional[TracebackType]
  542. client_options=None, # type: Optional[Dict[str, Any]]
  543. mechanism=None, # type: Optional[Dict[str, Any]]
  544. exception_id=None, # type: Optional[int]
  545. parent_id=None, # type: Optional[int]
  546. source=None, # type: Optional[str]
  547. full_stack=None, # type: Optional[list[dict[str, Any]]]
  548. ):
  549. # type: (...) -> Dict[str, Any]
  550. """
  551. Creates a dict that goes into the events `exception.values` list and is ingestible by Sentry.
  552. See the Exception Interface documentation for more details:
  553. https://develop.sentry.dev/sdk/event-payloads/exception/
  554. """
  555. exception_value = {} # type: Dict[str, Any]
  556. exception_value["mechanism"] = (
  557. mechanism.copy() if mechanism else {"type": "generic", "handled": True}
  558. )
  559. if exception_id is not None:
  560. exception_value["mechanism"]["exception_id"] = exception_id
  561. if exc_value is not None:
  562. errno = get_errno(exc_value)
  563. else:
  564. errno = None
  565. if errno is not None:
  566. exception_value["mechanism"].setdefault("meta", {}).setdefault(
  567. "errno", {}
  568. ).setdefault("number", errno)
  569. if source is not None:
  570. exception_value["mechanism"]["source"] = source
  571. is_root_exception = exception_id == 0
  572. if not is_root_exception and parent_id is not None:
  573. exception_value["mechanism"]["parent_id"] = parent_id
  574. exception_value["mechanism"]["type"] = "chained"
  575. if is_root_exception and "type" not in exception_value["mechanism"]:
  576. exception_value["mechanism"]["type"] = "generic"
  577. is_exception_group = BaseExceptionGroup is not None and isinstance(
  578. exc_value, BaseExceptionGroup
  579. )
  580. if is_exception_group:
  581. exception_value["mechanism"]["is_exception_group"] = True
  582. exception_value["module"] = get_type_module(exc_type)
  583. exception_value["type"] = get_type_name(exc_type)
  584. exception_value["value"] = get_error_message(exc_value)
  585. if client_options is None:
  586. include_local_variables = True
  587. include_source_context = True
  588. max_value_length = DEFAULT_MAX_VALUE_LENGTH # fallback
  589. custom_repr = None
  590. else:
  591. include_local_variables = client_options["include_local_variables"]
  592. include_source_context = client_options["include_source_context"]
  593. max_value_length = client_options["max_value_length"]
  594. custom_repr = client_options.get("custom_repr")
  595. frames = [
  596. serialize_frame(
  597. tb.tb_frame,
  598. tb_lineno=tb.tb_lineno,
  599. include_local_variables=include_local_variables,
  600. include_source_context=include_source_context,
  601. max_value_length=max_value_length,
  602. custom_repr=custom_repr,
  603. )
  604. # Process at most MAX_STACK_FRAMES + 1 frames, to avoid hanging on
  605. # processing a super-long stacktrace.
  606. for tb, _ in zip(iter_stacks(tb), range(MAX_STACK_FRAMES + 1))
  607. ] # type: List[Dict[str, Any]]
  608. if len(frames) > MAX_STACK_FRAMES:
  609. # If we have more frames than the limit, we remove the stacktrace completely.
  610. # We don't trim the stacktrace here because we have not processed the whole
  611. # thing (see above, we stop at MAX_STACK_FRAMES + 1). Normally, Relay would
  612. # intelligently trim by removing frames in the middle of the stacktrace, but
  613. # since we don't have the whole stacktrace, we can't do that. Instead, we
  614. # drop the entire stacktrace.
  615. exception_value["stacktrace"] = AnnotatedValue.removed_because_over_size_limit(
  616. value=None
  617. )
  618. elif frames:
  619. if not full_stack:
  620. new_frames = frames
  621. else:
  622. new_frames = merge_stack_frames(frames, full_stack, client_options)
  623. exception_value["stacktrace"] = {"frames": new_frames}
  624. return exception_value
  625. HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__")
  626. if HAS_CHAINED_EXCEPTIONS:
  627. def walk_exception_chain(exc_info):
  628. # type: (ExcInfo) -> Iterator[ExcInfo]
  629. exc_type, exc_value, tb = exc_info
  630. seen_exceptions = []
  631. seen_exception_ids = set() # type: Set[int]
  632. while (
  633. exc_type is not None
  634. and exc_value is not None
  635. and id(exc_value) not in seen_exception_ids
  636. ):
  637. yield exc_type, exc_value, tb
  638. # Avoid hashing random types we don't know anything
  639. # about. Use the list to keep a ref so that the `id` is
  640. # not used for another object.
  641. seen_exceptions.append(exc_value)
  642. seen_exception_ids.add(id(exc_value))
  643. if exc_value.__suppress_context__:
  644. cause = exc_value.__cause__
  645. else:
  646. cause = exc_value.__context__
  647. if cause is None:
  648. break
  649. exc_type = type(cause)
  650. exc_value = cause
  651. tb = getattr(cause, "__traceback__", None)
  652. else:
  653. def walk_exception_chain(exc_info):
  654. # type: (ExcInfo) -> Iterator[ExcInfo]
  655. yield exc_info
  656. def exceptions_from_error(
  657. exc_type, # type: Optional[type]
  658. exc_value, # type: Optional[BaseException]
  659. tb, # type: Optional[TracebackType]
  660. client_options=None, # type: Optional[Dict[str, Any]]
  661. mechanism=None, # type: Optional[Dict[str, Any]]
  662. exception_id=0, # type: int
  663. parent_id=0, # type: int
  664. source=None, # type: Optional[str]
  665. full_stack=None, # type: Optional[list[dict[str, Any]]]
  666. ):
  667. # type: (...) -> Tuple[int, List[Dict[str, Any]]]
  668. """
  669. Creates the list of exceptions.
  670. This can include chained exceptions and exceptions from an ExceptionGroup.
  671. See the Exception Interface documentation for more details:
  672. https://develop.sentry.dev/sdk/event-payloads/exception/
  673. """
  674. parent = single_exception_from_error_tuple(
  675. exc_type=exc_type,
  676. exc_value=exc_value,
  677. tb=tb,
  678. client_options=client_options,
  679. mechanism=mechanism,
  680. exception_id=exception_id,
  681. parent_id=parent_id,
  682. source=source,
  683. full_stack=full_stack,
  684. )
  685. exceptions = [parent]
  686. parent_id = exception_id
  687. exception_id += 1
  688. should_supress_context = hasattr(exc_value, "__suppress_context__") and exc_value.__suppress_context__ # type: ignore
  689. if should_supress_context:
  690. # Add direct cause.
  691. # The field `__cause__` is set when raised with the exception (using the `from` keyword).
  692. exception_has_cause = (
  693. exc_value
  694. and hasattr(exc_value, "__cause__")
  695. and exc_value.__cause__ is not None
  696. )
  697. if exception_has_cause:
  698. cause = exc_value.__cause__ # type: ignore
  699. (exception_id, child_exceptions) = exceptions_from_error(
  700. exc_type=type(cause),
  701. exc_value=cause,
  702. tb=getattr(cause, "__traceback__", None),
  703. client_options=client_options,
  704. mechanism=mechanism,
  705. exception_id=exception_id,
  706. source="__cause__",
  707. full_stack=full_stack,
  708. )
  709. exceptions.extend(child_exceptions)
  710. else:
  711. # Add indirect cause.
  712. # The field `__context__` is assigned if another exception occurs while handling the exception.
  713. exception_has_content = (
  714. exc_value
  715. and hasattr(exc_value, "__context__")
  716. and exc_value.__context__ is not None
  717. )
  718. if exception_has_content:
  719. context = exc_value.__context__ # type: ignore
  720. (exception_id, child_exceptions) = exceptions_from_error(
  721. exc_type=type(context),
  722. exc_value=context,
  723. tb=getattr(context, "__traceback__", None),
  724. client_options=client_options,
  725. mechanism=mechanism,
  726. exception_id=exception_id,
  727. source="__context__",
  728. full_stack=full_stack,
  729. )
  730. exceptions.extend(child_exceptions)
  731. # Add exceptions from an ExceptionGroup.
  732. is_exception_group = exc_value and hasattr(exc_value, "exceptions")
  733. if is_exception_group:
  734. for idx, e in enumerate(exc_value.exceptions): # type: ignore
  735. (exception_id, child_exceptions) = exceptions_from_error(
  736. exc_type=type(e),
  737. exc_value=e,
  738. tb=getattr(e, "__traceback__", None),
  739. client_options=client_options,
  740. mechanism=mechanism,
  741. exception_id=exception_id,
  742. parent_id=parent_id,
  743. source="exceptions[%s]" % idx,
  744. full_stack=full_stack,
  745. )
  746. exceptions.extend(child_exceptions)
  747. return (exception_id, exceptions)
  748. def exceptions_from_error_tuple(
  749. exc_info, # type: ExcInfo
  750. client_options=None, # type: Optional[Dict[str, Any]]
  751. mechanism=None, # type: Optional[Dict[str, Any]]
  752. full_stack=None, # type: Optional[list[dict[str, Any]]]
  753. ):
  754. # type: (...) -> List[Dict[str, Any]]
  755. exc_type, exc_value, tb = exc_info
  756. is_exception_group = BaseExceptionGroup is not None and isinstance(
  757. exc_value, BaseExceptionGroup
  758. )
  759. if is_exception_group:
  760. (_, exceptions) = exceptions_from_error(
  761. exc_type=exc_type,
  762. exc_value=exc_value,
  763. tb=tb,
  764. client_options=client_options,
  765. mechanism=mechanism,
  766. exception_id=0,
  767. parent_id=0,
  768. full_stack=full_stack,
  769. )
  770. else:
  771. exceptions = []
  772. for exc_type, exc_value, tb in walk_exception_chain(exc_info):
  773. exceptions.append(
  774. single_exception_from_error_tuple(
  775. exc_type=exc_type,
  776. exc_value=exc_value,
  777. tb=tb,
  778. client_options=client_options,
  779. mechanism=mechanism,
  780. full_stack=full_stack,
  781. )
  782. )
  783. exceptions.reverse()
  784. return exceptions
  785. def to_string(value):
  786. # type: (str) -> str
  787. try:
  788. return str(value)
  789. except UnicodeDecodeError:
  790. return repr(value)[1:-1]
  791. def iter_event_stacktraces(event):
  792. # type: (Event) -> Iterator[Annotated[Dict[str, Any]]]
  793. if "stacktrace" in event:
  794. yield event["stacktrace"]
  795. if "threads" in event:
  796. for thread in event["threads"].get("values") or ():
  797. if "stacktrace" in thread:
  798. yield thread["stacktrace"]
  799. if "exception" in event:
  800. for exception in event["exception"].get("values") or ():
  801. if isinstance(exception, dict) and "stacktrace" in exception:
  802. yield exception["stacktrace"]
  803. def iter_event_frames(event):
  804. # type: (Event) -> Iterator[Dict[str, Any]]
  805. for stacktrace in iter_event_stacktraces(event):
  806. if isinstance(stacktrace, AnnotatedValue):
  807. stacktrace = stacktrace.value or {}
  808. for frame in stacktrace.get("frames") or ():
  809. yield frame
  810. def handle_in_app(event, in_app_exclude=None, in_app_include=None, project_root=None):
  811. # type: (Event, Optional[List[str]], Optional[List[str]], Optional[str]) -> Event
  812. for stacktrace in iter_event_stacktraces(event):
  813. if isinstance(stacktrace, AnnotatedValue):
  814. stacktrace = stacktrace.value or {}
  815. set_in_app_in_frames(
  816. stacktrace.get("frames"),
  817. in_app_exclude=in_app_exclude,
  818. in_app_include=in_app_include,
  819. project_root=project_root,
  820. )
  821. return event
  822. def set_in_app_in_frames(frames, in_app_exclude, in_app_include, project_root=None):
  823. # type: (Any, Optional[List[str]], Optional[List[str]], Optional[str]) -> Optional[Any]
  824. if not frames:
  825. return None
  826. for frame in frames:
  827. # if frame has already been marked as in_app, skip it
  828. current_in_app = frame.get("in_app")
  829. if current_in_app is not None:
  830. continue
  831. module = frame.get("module")
  832. # check if module in frame is in the list of modules to include
  833. if _module_in_list(module, in_app_include):
  834. frame["in_app"] = True
  835. continue
  836. # check if module in frame is in the list of modules to exclude
  837. if _module_in_list(module, in_app_exclude):
  838. frame["in_app"] = False
  839. continue
  840. # if frame has no abs_path, skip further checks
  841. abs_path = frame.get("abs_path")
  842. if abs_path is None:
  843. continue
  844. if _is_external_source(abs_path):
  845. frame["in_app"] = False
  846. continue
  847. if _is_in_project_root(abs_path, project_root):
  848. frame["in_app"] = True
  849. continue
  850. return frames
  851. def exc_info_from_error(error):
  852. # type: (Union[BaseException, ExcInfo]) -> ExcInfo
  853. if isinstance(error, tuple) and len(error) == 3:
  854. exc_type, exc_value, tb = error
  855. elif isinstance(error, BaseException):
  856. tb = getattr(error, "__traceback__", None)
  857. if tb is not None:
  858. exc_type = type(error)
  859. exc_value = error
  860. else:
  861. exc_type, exc_value, tb = sys.exc_info()
  862. if exc_value is not error:
  863. tb = None
  864. exc_value = error
  865. exc_type = type(error)
  866. else:
  867. raise ValueError("Expected Exception object to report, got %s!" % type(error))
  868. exc_info = (exc_type, exc_value, tb)
  869. if TYPE_CHECKING:
  870. # This cast is safe because exc_type and exc_value are either both
  871. # None or both not None.
  872. exc_info = cast(ExcInfo, exc_info)
  873. return exc_info
  874. def merge_stack_frames(frames, full_stack, client_options):
  875. # type: (List[Dict[str, Any]], List[Dict[str, Any]], Optional[Dict[str, Any]]) -> List[Dict[str, Any]]
  876. """
  877. Add the missing frames from full_stack to frames and return the merged list.
  878. """
  879. frame_ids = {
  880. (
  881. frame["abs_path"],
  882. frame["context_line"],
  883. frame["lineno"],
  884. frame["function"],
  885. )
  886. for frame in frames
  887. }
  888. new_frames = [
  889. stackframe
  890. for stackframe in full_stack
  891. if (
  892. stackframe["abs_path"],
  893. stackframe["context_line"],
  894. stackframe["lineno"],
  895. stackframe["function"],
  896. )
  897. not in frame_ids
  898. ]
  899. new_frames.extend(frames)
  900. # Limit the number of frames
  901. max_stack_frames = (
  902. client_options.get("max_stack_frames", DEFAULT_MAX_STACK_FRAMES)
  903. if client_options
  904. else None
  905. )
  906. if max_stack_frames is not None:
  907. new_frames = new_frames[len(new_frames) - max_stack_frames :]
  908. return new_frames
  909. def event_from_exception(
  910. exc_info, # type: Union[BaseException, ExcInfo]
  911. client_options=None, # type: Optional[Dict[str, Any]]
  912. mechanism=None, # type: Optional[Dict[str, Any]]
  913. ):
  914. # type: (...) -> Tuple[Event, Dict[str, Any]]
  915. exc_info = exc_info_from_error(exc_info)
  916. hint = event_hint_with_exc_info(exc_info)
  917. if client_options and client_options.get("add_full_stack", DEFAULT_ADD_FULL_STACK):
  918. full_stack = current_stacktrace(
  919. include_local_variables=client_options["include_local_variables"],
  920. max_value_length=client_options["max_value_length"],
  921. )["frames"]
  922. else:
  923. full_stack = None
  924. return (
  925. {
  926. "level": "error",
  927. "exception": {
  928. "values": exceptions_from_error_tuple(
  929. exc_info, client_options, mechanism, full_stack
  930. )
  931. },
  932. },
  933. hint,
  934. )
  935. def _module_in_list(name, items):
  936. # type: (Optional[str], Optional[List[str]]) -> bool
  937. if name is None:
  938. return False
  939. if not items:
  940. return False
  941. for item in items:
  942. if item == name or name.startswith(item + "."):
  943. return True
  944. return False
  945. def _is_external_source(abs_path):
  946. # type: (Optional[str]) -> bool
  947. # check if frame is in 'site-packages' or 'dist-packages'
  948. if abs_path is None:
  949. return False
  950. external_source = (
  951. re.search(r"[\\/](?:dist|site)-packages[\\/]", abs_path) is not None
  952. )
  953. return external_source
  954. def _is_in_project_root(abs_path, project_root):
  955. # type: (Optional[str], Optional[str]) -> bool
  956. if abs_path is None or project_root is None:
  957. return False
  958. # check if path is in the project root
  959. if abs_path.startswith(project_root):
  960. return True
  961. return False
  962. def _truncate_by_bytes(string, max_bytes):
  963. # type: (str, int) -> str
  964. """
  965. Truncate a UTF-8-encodable string to the last full codepoint so that it fits in max_bytes.
  966. """
  967. truncated = string.encode("utf-8")[: max_bytes - 3].decode("utf-8", errors="ignore")
  968. return truncated + "..."
  969. def _get_size_in_bytes(value):
  970. # type: (str) -> Optional[int]
  971. try:
  972. return len(value.encode("utf-8"))
  973. except (UnicodeEncodeError, UnicodeDecodeError):
  974. return None
  975. def strip_string(value, max_length=None):
  976. # type: (str, Optional[int]) -> Union[AnnotatedValue, str]
  977. if not value:
  978. return value
  979. if max_length is None:
  980. max_length = DEFAULT_MAX_VALUE_LENGTH
  981. byte_size = _get_size_in_bytes(value)
  982. text_size = len(value)
  983. if byte_size is not None and byte_size > max_length:
  984. # truncate to max_length bytes, preserving code points
  985. truncated_value = _truncate_by_bytes(value, max_length)
  986. elif text_size is not None and text_size > max_length:
  987. # fallback to truncating by string length
  988. truncated_value = value[: max_length - 3] + "..."
  989. else:
  990. return value
  991. return AnnotatedValue(
  992. value=truncated_value,
  993. metadata={
  994. "len": byte_size or text_size,
  995. "rem": [["!limit", "x", max_length - 3, max_length]],
  996. },
  997. )
  998. def parse_version(version):
  999. # type: (str) -> Optional[Tuple[int, ...]]
  1000. """
  1001. Parses a version string into a tuple of integers.
  1002. This uses the parsing loging from PEP 440:
  1003. https://peps.python.org/pep-0440/#appendix-b-parsing-version-strings-with-regular-expressions
  1004. """
  1005. VERSION_PATTERN = r""" # noqa: N806
  1006. v?
  1007. (?:
  1008. (?:(?P<epoch>[0-9]+)!)? # epoch
  1009. (?P<release>[0-9]+(?:\.[0-9]+)*) # release segment
  1010. (?P<pre> # pre-release
  1011. [-_\.]?
  1012. (?P<pre_l>(a|b|c|rc|alpha|beta|pre|preview))
  1013. [-_\.]?
  1014. (?P<pre_n>[0-9]+)?
  1015. )?
  1016. (?P<post> # post release
  1017. (?:-(?P<post_n1>[0-9]+))
  1018. |
  1019. (?:
  1020. [-_\.]?
  1021. (?P<post_l>post|rev|r)
  1022. [-_\.]?
  1023. (?P<post_n2>[0-9]+)?
  1024. )
  1025. )?
  1026. (?P<dev> # dev release
  1027. [-_\.]?
  1028. (?P<dev_l>dev)
  1029. [-_\.]?
  1030. (?P<dev_n>[0-9]+)?
  1031. )?
  1032. )
  1033. (?:\+(?P<local>[a-z0-9]+(?:[-_\.][a-z0-9]+)*))? # local version
  1034. """
  1035. pattern = re.compile(
  1036. r"^\s*" + VERSION_PATTERN + r"\s*$",
  1037. re.VERBOSE | re.IGNORECASE,
  1038. )
  1039. try:
  1040. release = pattern.match(version).groupdict()["release"] # type: ignore
  1041. release_tuple = tuple(map(int, release.split(".")[:3])) # type: Tuple[int, ...]
  1042. except (TypeError, ValueError, AttributeError):
  1043. return None
  1044. return release_tuple
  1045. def _is_contextvars_broken():
  1046. # type: () -> bool
  1047. """
  1048. Returns whether gevent/eventlet have patched the stdlib in a way where thread locals are now more "correct" than contextvars.
  1049. """
  1050. try:
  1051. import gevent
  1052. from gevent.monkey import is_object_patched
  1053. # Get the MAJOR and MINOR version numbers of Gevent
  1054. version_tuple = tuple(
  1055. [int(part) for part in re.split(r"a|b|rc|\.", gevent.__version__)[:2]]
  1056. )
  1057. if is_object_patched("threading", "local"):
  1058. # Gevent 20.9.0 depends on Greenlet 0.4.17 which natively handles switching
  1059. # context vars when greenlets are switched, so, Gevent 20.9.0+ is all fine.
  1060. # Ref: https://github.com/gevent/gevent/blob/83c9e2ae5b0834b8f84233760aabe82c3ba065b4/src/gevent/monkey.py#L604-L609
  1061. # Gevent 20.5, that doesn't depend on Greenlet 0.4.17 with native support
  1062. # for contextvars, is able to patch both thread locals and contextvars, in
  1063. # that case, check if contextvars are effectively patched.
  1064. if (
  1065. # Gevent 20.9.0+
  1066. (sys.version_info >= (3, 7) and version_tuple >= (20, 9))
  1067. # Gevent 20.5.0+ or Python < 3.7
  1068. or (is_object_patched("contextvars", "ContextVar"))
  1069. ):
  1070. return False
  1071. return True
  1072. except ImportError:
  1073. pass
  1074. try:
  1075. import greenlet
  1076. from eventlet.patcher import is_monkey_patched # type: ignore
  1077. greenlet_version = parse_version(greenlet.__version__)
  1078. if greenlet_version is None:
  1079. logger.error(
  1080. "Internal error in Sentry SDK: Could not parse Greenlet version from greenlet.__version__."
  1081. )
  1082. return False
  1083. if is_monkey_patched("thread") and greenlet_version < (0, 5):
  1084. return True
  1085. except ImportError:
  1086. pass
  1087. return False
  1088. def _make_threadlocal_contextvars(local):
  1089. # type: (type) -> type
  1090. class ContextVar:
  1091. # Super-limited impl of ContextVar
  1092. def __init__(self, name, default=None):
  1093. # type: (str, Any) -> None
  1094. self._name = name
  1095. self._default = default
  1096. self._local = local()
  1097. self._original_local = local()
  1098. def get(self, default=None):
  1099. # type: (Any) -> Any
  1100. return getattr(self._local, "value", default or self._default)
  1101. def set(self, value):
  1102. # type: (Any) -> Any
  1103. token = str(random.getrandbits(64))
  1104. original_value = self.get()
  1105. setattr(self._original_local, token, original_value)
  1106. self._local.value = value
  1107. return token
  1108. def reset(self, token):
  1109. # type: (Any) -> None
  1110. self._local.value = getattr(self._original_local, token)
  1111. # delete the original value (this way it works in Python 3.6+)
  1112. del self._original_local.__dict__[token]
  1113. return ContextVar
  1114. def _get_contextvars():
  1115. # type: () -> Tuple[bool, type]
  1116. """
  1117. Figure out the "right" contextvars installation to use. Returns a
  1118. `contextvars.ContextVar`-like class with a limited API.
  1119. See https://docs.sentry.io/platforms/python/contextvars/ for more information.
  1120. """
  1121. if not _is_contextvars_broken():
  1122. # aiocontextvars is a PyPI package that ensures that the contextvars
  1123. # backport (also a PyPI package) works with asyncio under Python 3.6
  1124. #
  1125. # Import it if available.
  1126. if sys.version_info < (3, 7):
  1127. # `aiocontextvars` is absolutely required for functional
  1128. # contextvars on Python 3.6.
  1129. try:
  1130. from aiocontextvars import ContextVar
  1131. return True, ContextVar
  1132. except ImportError:
  1133. pass
  1134. else:
  1135. # On Python 3.7 contextvars are functional.
  1136. try:
  1137. from contextvars import ContextVar
  1138. return True, ContextVar
  1139. except ImportError:
  1140. pass
  1141. # Fall back to basic thread-local usage.
  1142. from threading import local
  1143. return False, _make_threadlocal_contextvars(local)
  1144. HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars()
  1145. CONTEXTVARS_ERROR_MESSAGE = """
  1146. With asyncio/ASGI applications, the Sentry SDK requires a functional
  1147. installation of `contextvars` to avoid leaking scope/context data across
  1148. requests.
  1149. Please refer to https://docs.sentry.io/platforms/python/contextvars/ for more information.
  1150. """
  1151. def qualname_from_function(func):
  1152. # type: (Callable[..., Any]) -> Optional[str]
  1153. """Return the qualified name of func. Works with regular function, lambda, partial and partialmethod."""
  1154. func_qualname = None # type: Optional[str]
  1155. # Python 2
  1156. try:
  1157. return "%s.%s.%s" % (
  1158. func.im_class.__module__, # type: ignore
  1159. func.im_class.__name__, # type: ignore
  1160. func.__name__,
  1161. )
  1162. except Exception:
  1163. pass
  1164. prefix, suffix = "", ""
  1165. if isinstance(func, partial) and hasattr(func.func, "__name__"):
  1166. prefix, suffix = "partial(<function ", ">)"
  1167. func = func.func
  1168. else:
  1169. # The _partialmethod attribute of methods wrapped with partialmethod() was renamed to __partialmethod__ in CPython 3.13:
  1170. # https://github.com/python/cpython/pull/16600
  1171. partial_method = getattr(func, "_partialmethod", None) or getattr(
  1172. func, "__partialmethod__", None
  1173. )
  1174. if isinstance(partial_method, partialmethod):
  1175. prefix, suffix = "partialmethod(<function ", ">)"
  1176. func = partial_method.func
  1177. if hasattr(func, "__qualname__"):
  1178. func_qualname = func.__qualname__
  1179. elif hasattr(func, "__name__"): # Python 2.7 has no __qualname__
  1180. func_qualname = func.__name__
  1181. # Python 3: methods, functions, classes
  1182. if func_qualname is not None:
  1183. if hasattr(func, "__module__") and isinstance(func.__module__, str):
  1184. func_qualname = func.__module__ + "." + func_qualname
  1185. func_qualname = prefix + func_qualname + suffix
  1186. return func_qualname
  1187. def transaction_from_function(func):
  1188. # type: (Callable[..., Any]) -> Optional[str]
  1189. return qualname_from_function(func)
  1190. disable_capture_event = ContextVar("disable_capture_event")
  1191. class ServerlessTimeoutWarning(Exception): # noqa: N818
  1192. """Raised when a serverless method is about to reach its timeout."""
  1193. pass
  1194. class TimeoutThread(threading.Thread):
  1195. """Creates a Thread which runs (sleeps) for a time duration equal to
  1196. waiting_time and raises a custom ServerlessTimeout exception.
  1197. """
  1198. def __init__(self, waiting_time, configured_timeout):
  1199. # type: (float, int) -> None
  1200. threading.Thread.__init__(self)
  1201. self.waiting_time = waiting_time
  1202. self.configured_timeout = configured_timeout
  1203. self._stop_event = threading.Event()
  1204. def stop(self):
  1205. # type: () -> None
  1206. self._stop_event.set()
  1207. def run(self):
  1208. # type: () -> None
  1209. self._stop_event.wait(self.waiting_time)
  1210. if self._stop_event.is_set():
  1211. return
  1212. integer_configured_timeout = int(self.configured_timeout)
  1213. # Setting up the exact integer value of configured time(in seconds)
  1214. if integer_configured_timeout < self.configured_timeout:
  1215. integer_configured_timeout = integer_configured_timeout + 1
  1216. # Raising Exception after timeout duration is reached
  1217. raise ServerlessTimeoutWarning(
  1218. "WARNING : Function is expected to get timed out. Configured timeout duration = {} seconds.".format(
  1219. integer_configured_timeout
  1220. )
  1221. )
  1222. def to_base64(original):
  1223. # type: (str) -> Optional[str]
  1224. """
  1225. Convert a string to base64, via UTF-8. Returns None on invalid input.
  1226. """
  1227. base64_string = None
  1228. try:
  1229. utf8_bytes = original.encode("UTF-8")
  1230. base64_bytes = base64.b64encode(utf8_bytes)
  1231. base64_string = base64_bytes.decode("UTF-8")
  1232. except Exception as err:
  1233. logger.warning("Unable to encode {orig} to base64:".format(orig=original), err)
  1234. return base64_string
  1235. def from_base64(base64_string):
  1236. # type: (str) -> Optional[str]
  1237. """
  1238. Convert a string from base64, via UTF-8. Returns None on invalid input.
  1239. """
  1240. utf8_string = None
  1241. try:
  1242. only_valid_chars = BASE64_ALPHABET.match(base64_string)
  1243. assert only_valid_chars
  1244. base64_bytes = base64_string.encode("UTF-8")
  1245. utf8_bytes = base64.b64decode(base64_bytes)
  1246. utf8_string = utf8_bytes.decode("UTF-8")
  1247. except Exception as err:
  1248. logger.warning(
  1249. "Unable to decode {b64} from base64:".format(b64=base64_string), err
  1250. )
  1251. return utf8_string
  1252. Components = namedtuple("Components", ["scheme", "netloc", "path", "query", "fragment"])
  1253. def sanitize_url(url, remove_authority=True, remove_query_values=True, split=False):
  1254. # type: (str, bool, bool, bool) -> Union[str, Components]
  1255. """
  1256. Removes the authority and query parameter values from a given URL.
  1257. """
  1258. parsed_url = urlsplit(url)
  1259. query_params = parse_qs(parsed_url.query, keep_blank_values=True)
  1260. # strip username:password (netloc can be usr:pwd@example.com)
  1261. if remove_authority:
  1262. netloc_parts = parsed_url.netloc.split("@")
  1263. if len(netloc_parts) > 1:
  1264. netloc = "%s:%s@%s" % (
  1265. SENSITIVE_DATA_SUBSTITUTE,
  1266. SENSITIVE_DATA_SUBSTITUTE,
  1267. netloc_parts[-1],
  1268. )
  1269. else:
  1270. netloc = parsed_url.netloc
  1271. else:
  1272. netloc = parsed_url.netloc
  1273. # strip values from query string
  1274. if remove_query_values:
  1275. query_string = unquote(
  1276. urlencode({key: SENSITIVE_DATA_SUBSTITUTE for key in query_params})
  1277. )
  1278. else:
  1279. query_string = parsed_url.query
  1280. components = Components(
  1281. scheme=parsed_url.scheme,
  1282. netloc=netloc,
  1283. query=query_string,
  1284. path=parsed_url.path,
  1285. fragment=parsed_url.fragment,
  1286. )
  1287. if split:
  1288. return components
  1289. else:
  1290. return urlunsplit(components)
  1291. ParsedUrl = namedtuple("ParsedUrl", ["url", "query", "fragment"])
  1292. def parse_url(url, sanitize=True):
  1293. # type: (str, bool) -> ParsedUrl
  1294. """
  1295. Splits a URL into a url (including path), query and fragment. If sanitize is True, the query
  1296. parameters will be sanitized to remove sensitive data. The autority (username and password)
  1297. in the URL will always be removed.
  1298. """
  1299. parsed_url = sanitize_url(
  1300. url, remove_authority=True, remove_query_values=sanitize, split=True
  1301. )
  1302. base_url = urlunsplit(
  1303. Components(
  1304. scheme=parsed_url.scheme, # type: ignore
  1305. netloc=parsed_url.netloc, # type: ignore
  1306. query="",
  1307. path=parsed_url.path, # type: ignore
  1308. fragment="",
  1309. )
  1310. )
  1311. return ParsedUrl(
  1312. url=base_url,
  1313. query=parsed_url.query, # type: ignore
  1314. fragment=parsed_url.fragment, # type: ignore
  1315. )
  1316. def is_valid_sample_rate(rate, source):
  1317. # type: (Any, str) -> bool
  1318. """
  1319. Checks the given sample rate to make sure it is valid type and value (a
  1320. boolean or a number between 0 and 1, inclusive).
  1321. """
  1322. # both booleans and NaN are instances of Real, so a) checking for Real
  1323. # checks for the possibility of a boolean also, and b) we have to check
  1324. # separately for NaN and Decimal does not derive from Real so need to check that too
  1325. if not isinstance(rate, (Real, Decimal)) or math.isnan(rate):
  1326. logger.warning(
  1327. "{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(
  1328. source=source, rate=rate, type=type(rate)
  1329. )
  1330. )
  1331. return False
  1332. # in case rate is a boolean, it will get cast to 1 if it's True and 0 if it's False
  1333. rate = float(rate)
  1334. if rate < 0 or rate > 1:
  1335. logger.warning(
  1336. "{source} Given sample rate is invalid. Sample rate must be between 0 and 1. Got {rate}.".format(
  1337. source=source, rate=rate
  1338. )
  1339. )
  1340. return False
  1341. return True
  1342. def match_regex_list(item, regex_list=None, substring_matching=False):
  1343. # type: (str, Optional[List[str]], bool) -> bool
  1344. if regex_list is None:
  1345. return False
  1346. for item_matcher in regex_list:
  1347. if not substring_matching and item_matcher[-1] != "$":
  1348. item_matcher += "$"
  1349. matched = re.search(item_matcher, item)
  1350. if matched:
  1351. return True
  1352. return False
  1353. def is_sentry_url(client, url):
  1354. # type: (sentry_sdk.client.BaseClient, str) -> bool
  1355. """
  1356. Determines whether the given URL matches the Sentry DSN.
  1357. """
  1358. return (
  1359. client is not None
  1360. and client.transport is not None
  1361. and client.transport.parsed_dsn is not None
  1362. and client.transport.parsed_dsn.netloc in url
  1363. )
  1364. def _generate_installed_modules():
  1365. # type: () -> Iterator[Tuple[str, str]]
  1366. try:
  1367. from importlib import metadata
  1368. yielded = set()
  1369. for dist in metadata.distributions():
  1370. name = dist.metadata.get("Name", None) # type: ignore[attr-defined]
  1371. # `metadata` values may be `None`, see:
  1372. # https://github.com/python/cpython/issues/91216
  1373. # and
  1374. # https://github.com/python/importlib_metadata/issues/371
  1375. if name is not None:
  1376. normalized_name = _normalize_module_name(name)
  1377. if dist.version is not None and normalized_name not in yielded:
  1378. yield normalized_name, dist.version
  1379. yielded.add(normalized_name)
  1380. except ImportError:
  1381. # < py3.8
  1382. try:
  1383. import pkg_resources
  1384. except ImportError:
  1385. return
  1386. for info in pkg_resources.working_set:
  1387. yield _normalize_module_name(info.key), info.version
  1388. def _normalize_module_name(name):
  1389. # type: (str) -> str
  1390. return name.lower()
  1391. def _get_installed_modules():
  1392. # type: () -> Dict[str, str]
  1393. global _installed_modules
  1394. if _installed_modules is None:
  1395. _installed_modules = dict(_generate_installed_modules())
  1396. return _installed_modules
  1397. def package_version(package):
  1398. # type: (str) -> Optional[Tuple[int, ...]]
  1399. installed_packages = _get_installed_modules()
  1400. version = installed_packages.get(package)
  1401. if version is None:
  1402. return None
  1403. return parse_version(version)
  1404. def reraise(tp, value, tb=None):
  1405. # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[Any]) -> NoReturn
  1406. assert value is not None
  1407. if value.__traceback__ is not tb:
  1408. raise value.with_traceback(tb)
  1409. raise value
  1410. def _no_op(*_a, **_k):
  1411. # type: (*Any, **Any) -> None
  1412. """No-op function for ensure_integration_enabled."""
  1413. pass
  1414. if TYPE_CHECKING:
  1415. @overload
  1416. def ensure_integration_enabled(
  1417. integration, # type: type[sentry_sdk.integrations.Integration]
  1418. original_function, # type: Callable[P, R]
  1419. ):
  1420. # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
  1421. ...
  1422. @overload
  1423. def ensure_integration_enabled(
  1424. integration, # type: type[sentry_sdk.integrations.Integration]
  1425. ):
  1426. # type: (...) -> Callable[[Callable[P, None]], Callable[P, None]]
  1427. ...
  1428. def ensure_integration_enabled(
  1429. integration, # type: type[sentry_sdk.integrations.Integration]
  1430. original_function=_no_op, # type: Union[Callable[P, R], Callable[P, None]]
  1431. ):
  1432. # type: (...) -> Callable[[Callable[P, R]], Callable[P, R]]
  1433. """
  1434. Ensures a given integration is enabled prior to calling a Sentry-patched function.
  1435. The function takes as its parameters the integration that must be enabled and the original
  1436. function that the SDK is patching. The function returns a function that takes the
  1437. decorated (Sentry-patched) function as its parameter, and returns a function that, when
  1438. called, checks whether the given integration is enabled. If the integration is enabled, the
  1439. function calls the decorated, Sentry-patched function. If the integration is not enabled,
  1440. the original function is called.
  1441. The function also takes care of preserving the original function's signature and docstring.
  1442. Example usage:
  1443. ```python
  1444. @ensure_integration_enabled(MyIntegration, my_function)
  1445. def patch_my_function():
  1446. with sentry_sdk.start_transaction(...):
  1447. return my_function()
  1448. ```
  1449. """
  1450. if TYPE_CHECKING:
  1451. # Type hint to ensure the default function has the right typing. The overloads
  1452. # ensure the default _no_op function is only used when R is None.
  1453. original_function = cast(Callable[P, R], original_function)
  1454. def patcher(sentry_patched_function):
  1455. # type: (Callable[P, R]) -> Callable[P, R]
  1456. def runner(*args: "P.args", **kwargs: "P.kwargs"):
  1457. # type: (...) -> R
  1458. if sentry_sdk.get_client().get_integration(integration) is None:
  1459. return original_function(*args, **kwargs)
  1460. return sentry_patched_function(*args, **kwargs)
  1461. if original_function is _no_op:
  1462. return wraps(sentry_patched_function)(runner)
  1463. return wraps(original_function)(runner)
  1464. return patcher
  1465. if PY37:
  1466. def nanosecond_time():
  1467. # type: () -> int
  1468. return time.perf_counter_ns()
  1469. else:
  1470. def nanosecond_time():
  1471. # type: () -> int
  1472. return int(time.perf_counter() * 1e9)
  1473. def now():
  1474. # type: () -> float
  1475. return time.perf_counter()
  1476. try:
  1477. from gevent import get_hub as get_gevent_hub
  1478. from gevent.monkey import is_module_patched
  1479. except ImportError:
  1480. # it's not great that the signatures are different, get_hub can't return None
  1481. # consider adding an if TYPE_CHECKING to change the signature to Optional[Hub]
  1482. def get_gevent_hub(): # type: ignore[misc]
  1483. # type: () -> Optional[Hub]
  1484. return None
  1485. def is_module_patched(mod_name):
  1486. # type: (str) -> bool
  1487. # unable to import from gevent means no modules have been patched
  1488. return False
  1489. def is_gevent():
  1490. # type: () -> bool
  1491. return is_module_patched("threading") or is_module_patched("_thread")
  1492. def get_current_thread_meta(thread=None):
  1493. # type: (Optional[threading.Thread]) -> Tuple[Optional[int], Optional[str]]
  1494. """
  1495. Try to get the id of the current thread, with various fall backs.
  1496. """
  1497. # if a thread is specified, that takes priority
  1498. if thread is not None:
  1499. try:
  1500. thread_id = thread.ident
  1501. thread_name = thread.name
  1502. if thread_id is not None:
  1503. return thread_id, thread_name
  1504. except AttributeError:
  1505. pass
  1506. # if the app is using gevent, we should look at the gevent hub first
  1507. # as the id there differs from what the threading module reports
  1508. if is_gevent():
  1509. gevent_hub = get_gevent_hub()
  1510. if gevent_hub is not None:
  1511. try:
  1512. # this is undocumented, so wrap it in try except to be safe
  1513. return gevent_hub.thread_ident, None
  1514. except AttributeError:
  1515. pass
  1516. # use the current thread's id if possible
  1517. try:
  1518. thread = threading.current_thread()
  1519. thread_id = thread.ident
  1520. thread_name = thread.name
  1521. if thread_id is not None:
  1522. return thread_id, thread_name
  1523. except AttributeError:
  1524. pass
  1525. # if we can't get the current thread id, fall back to the main thread id
  1526. try:
  1527. thread = threading.main_thread()
  1528. thread_id = thread.ident
  1529. thread_name = thread.name
  1530. if thread_id is not None:
  1531. return thread_id, thread_name
  1532. except AttributeError:
  1533. pass
  1534. # we've tried everything, time to give up
  1535. return None, None
  1536. def should_be_treated_as_error(ty, value):
  1537. # type: (Any, Any) -> bool
  1538. if ty == SystemExit and hasattr(value, "code") and value.code in (0, None):
  1539. # https://docs.python.org/3/library/exceptions.html#SystemExit
  1540. return False
  1541. return True
  1542. if TYPE_CHECKING:
  1543. T = TypeVar("T")
  1544. def try_convert(convert_func, value):
  1545. # type: (Callable[[Any], T], Any) -> Optional[T]
  1546. """
  1547. Attempt to convert from an unknown type to a specific type, using the
  1548. given function. Return None if the conversion fails, i.e. if the function
  1549. raises an exception.
  1550. """
  1551. try:
  1552. return convert_func(value)
  1553. except Exception:
  1554. return None
  1555. def safe_serialize(data):
  1556. # type: (Any) -> str
  1557. """Safely serialize to a readable string."""
  1558. def serialize_item(item):
  1559. # type: (Any) -> Union[str, dict[Any, Any], list[Any], tuple[Any, ...]]
  1560. if callable(item):
  1561. try:
  1562. module = getattr(item, "__module__", None)
  1563. qualname = getattr(item, "__qualname__", None)
  1564. name = getattr(item, "__name__", "anonymous")
  1565. if module and qualname:
  1566. full_path = f"{module}.{qualname}"
  1567. elif module and name:
  1568. full_path = f"{module}.{name}"
  1569. else:
  1570. full_path = name
  1571. return f"<function {full_path}>"
  1572. except Exception:
  1573. return f"<callable {type(item).__name__}>"
  1574. elif isinstance(item, dict):
  1575. return {k: serialize_item(v) for k, v in item.items()}
  1576. elif isinstance(item, (list, tuple)):
  1577. return [serialize_item(x) for x in item]
  1578. elif hasattr(item, "__dict__"):
  1579. try:
  1580. attrs = {
  1581. k: serialize_item(v)
  1582. for k, v in vars(item).items()
  1583. if not k.startswith("_")
  1584. }
  1585. return f"<{type(item).__name__} {attrs}>"
  1586. except Exception:
  1587. return repr(item)
  1588. else:
  1589. return item
  1590. try:
  1591. serialized = serialize_item(data)
  1592. return json.dumps(serialized, default=str)
  1593. except Exception:
  1594. return str(data)