1import os 2import sys 3import linecache 4import logging 5 6from datetime import datetime 7 8import sentry_sdk 9from sentry_sdk._compat import urlparse, text_type, implements_str, PY2 10 11from sentry_sdk._types import MYPY 12 13if MYPY: 14 from types import FrameType 15 from types import TracebackType 16 from typing import Any 17 from typing import Callable 18 from typing import Dict 19 from typing import ContextManager 20 from typing import Iterator 21 from typing import List 22 from typing import Optional 23 from typing import Set 24 from typing import Tuple 25 from typing import Union 26 from typing import Type 27 28 from sentry_sdk._types import ExcInfo 29 30epoch = datetime(1970, 1, 1) 31 32 33# The logger is created here but initialized in the debug support module 34logger = logging.getLogger("sentry_sdk.errors") 35 36MAX_STRING_LENGTH = 512 37MAX_FORMAT_PARAM_LENGTH = 128 38 39 40def _get_debug_hub(): 41 # type: () -> Optional[sentry_sdk.Hub] 42 # This function is replaced by debug.py 43 pass 44 45 46class CaptureInternalException(object): 47 __slots__ = () 48 49 def __enter__(self): 50 # type: () -> ContextManager[Any] 51 return self 52 53 def __exit__(self, ty, value, tb): 54 # type: (Optional[Type[BaseException]], Optional[BaseException], Optional[TracebackType]) -> bool 55 if ty is not None and value is not None: 56 capture_internal_exception((ty, value, tb)) 57 58 return True 59 60 61_CAPTURE_INTERNAL_EXCEPTION = CaptureInternalException() 62 63 64def capture_internal_exceptions(): 65 # type: () -> ContextManager[Any] 66 return _CAPTURE_INTERNAL_EXCEPTION 67 68 69def capture_internal_exception(exc_info): 70 # type: (ExcInfo) -> None 71 hub = _get_debug_hub() 72 if hub is not None: 73 hub._capture_internal_exception(exc_info) 74 75 76def to_timestamp(value): 77 # type: (datetime) -> float 78 return (value - epoch).total_seconds() 79 80 81def format_timestamp(value): 82 # type: (datetime) -> str 83 return value.strftime("%Y-%m-%dT%H:%M:%S.%fZ") 84 85 86def event_hint_with_exc_info(exc_info=None): 87 # type: (Optional[ExcInfo]) -> Dict[str, Optional[ExcInfo]] 88 """Creates a hint with the exc info filled in.""" 89 if exc_info is None: 90 exc_info = sys.exc_info() 91 else: 92 exc_info = exc_info_from_error(exc_info) 93 if exc_info[0] is None: 94 exc_info = None 95 return {"exc_info": exc_info} 96 97 98class BadDsn(ValueError): 99 """Raised on invalid DSNs.""" 100 101 102@implements_str 103class Dsn(object): 104 """Represents a DSN.""" 105 106 def __init__(self, value): 107 # type: (Union[Dsn, str]) -> None 108 if isinstance(value, Dsn): 109 self.__dict__ = dict(value.__dict__) 110 return 111 parts = urlparse.urlsplit(text_type(value)) 112 113 if parts.scheme not in (u"http", u"https"): 114 raise BadDsn("Unsupported scheme %r" % parts.scheme) 115 self.scheme = parts.scheme 116 117 if parts.hostname is None: 118 raise BadDsn("Missing hostname") 119 120 self.host = parts.hostname 121 122 if parts.port is None: 123 self.port = self.scheme == "https" and 443 or 80 124 else: 125 self.port = parts.port 126 127 if not parts.username: 128 raise BadDsn("Missing public key") 129 130 self.public_key = parts.username 131 self.secret_key = parts.password 132 133 path = parts.path.rsplit("/", 1) 134 135 try: 136 self.project_id = text_type(int(path.pop())) 137 except (ValueError, TypeError): 138 raise BadDsn("Invalid project in DSN (%r)" % (parts.path or "")[1:]) 139 140 self.path = "/".join(path) + "/" 141 142 @property 143 def netloc(self): 144 # type: () -> str 145 """The netloc part of a DSN.""" 146 rv = self.host 147 if (self.scheme, self.port) not in (("http", 80), ("https", 443)): 148 rv = "%s:%s" % (rv, self.port) 149 return rv 150 151 def to_auth(self, client=None): 152 # type: (Optional[Any]) -> Auth 153 """Returns the auth info object for this dsn.""" 154 return Auth( 155 scheme=self.scheme, 156 host=self.netloc, 157 path=self.path, 158 project_id=self.project_id, 159 public_key=self.public_key, 160 secret_key=self.secret_key, 161 client=client, 162 ) 163 164 def __str__(self): 165 # type: () -> str 166 return "%s://%s%s@%s%s%s" % ( 167 self.scheme, 168 self.public_key, 169 self.secret_key and "@" + self.secret_key or "", 170 self.netloc, 171 self.path, 172 self.project_id, 173 ) 174 175 176class Auth(object): 177 """Helper object that represents the auth info.""" 178 179 def __init__( 180 self, 181 scheme, 182 host, 183 project_id, 184 public_key, 185 secret_key=None, 186 version=7, 187 client=None, 188 path="/", 189 ): 190 # type: (str, str, str, str, Optional[str], int, Optional[Any], str) -> None 191 self.scheme = scheme 192 self.host = host 193 self.path = path 194 self.project_id = project_id 195 self.public_key = public_key 196 self.secret_key = secret_key 197 self.version = version 198 self.client = client 199 200 @property 201 def store_api_url(self): 202 # type: () -> str 203 """Returns the API url for storing events.""" 204 return "%s://%s%sapi/%s/store/" % ( 205 self.scheme, 206 self.host, 207 self.path, 208 self.project_id, 209 ) 210 211 def to_header(self, timestamp=None): 212 # type: (Optional[datetime]) -> str 213 """Returns the auth header a string.""" 214 rv = [("sentry_key", self.public_key), ("sentry_version", self.version)] 215 if timestamp is not None: 216 rv.append(("sentry_timestamp", str(to_timestamp(timestamp)))) 217 if self.client is not None: 218 rv.append(("sentry_client", self.client)) 219 if self.secret_key is not None: 220 rv.append(("sentry_secret", self.secret_key)) 221 return u"Sentry " + u", ".join("%s=%s" % (key, value) for key, value in rv) 222 223 224class AnnotatedValue(object): 225 __slots__ = ("value", "metadata") 226 227 def __init__(self, value, metadata): 228 # type: (Optional[Any], Dict[str, Any]) -> None 229 self.value = value 230 self.metadata = metadata 231 232 233if MYPY: 234 from typing import TypeVar 235 236 T = TypeVar("T") 237 Annotated = Union[AnnotatedValue, T] 238 239 240def get_type_name(cls): 241 # type: (Optional[type]) -> Optional[str] 242 return getattr(cls, "__qualname__", None) or getattr(cls, "__name__", None) 243 244 245def get_type_module(cls): 246 # type: (Optional[type]) -> Optional[str] 247 mod = getattr(cls, "__module__", None) 248 if mod not in (None, "builtins", "__builtins__"): 249 return mod 250 return None 251 252 253def should_hide_frame(frame): 254 # type: (FrameType) -> bool 255 try: 256 mod = frame.f_globals["__name__"] 257 if mod.startswith("sentry_sdk."): 258 return True 259 except (AttributeError, KeyError): 260 pass 261 262 for flag_name in "__traceback_hide__", "__tracebackhide__": 263 try: 264 if frame.f_locals[flag_name]: 265 return True 266 except Exception: 267 pass 268 269 return False 270 271 272def iter_stacks(tb): 273 # type: (Optional[TracebackType]) -> Iterator[TracebackType] 274 tb_ = tb # type: Optional[TracebackType] 275 while tb_ is not None: 276 if not should_hide_frame(tb_.tb_frame): 277 yield tb_ 278 tb_ = tb_.tb_next 279 280 281def get_lines_from_file( 282 filename, # type: str 283 lineno, # type: int 284 loader=None, # type: Optional[Any] 285 module=None, # type: Optional[str] 286): 287 # type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]] 288 context_lines = 5 289 source = None 290 if loader is not None and hasattr(loader, "get_source"): 291 try: 292 source_str = loader.get_source(module) # type: Optional[str] 293 except (ImportError, IOError): 294 source_str = None 295 if source_str is not None: 296 source = source_str.splitlines() 297 298 if source is None: 299 try: 300 source = linecache.getlines(filename) 301 except (OSError, IOError): 302 return [], None, [] 303 304 if not source: 305 return [], None, [] 306 307 lower_bound = max(0, lineno - context_lines) 308 upper_bound = min(lineno + 1 + context_lines, len(source)) 309 310 try: 311 pre_context = [ 312 strip_string(line.strip("\r\n")) for line in source[lower_bound:lineno] 313 ] 314 context_line = strip_string(source[lineno].strip("\r\n")) 315 post_context = [ 316 strip_string(line.strip("\r\n")) 317 for line in source[(lineno + 1) : upper_bound] 318 ] 319 return pre_context, context_line, post_context 320 except IndexError: 321 # the file may have changed since it was loaded into memory 322 return [], None, [] 323 324 325def get_source_context( 326 frame, # type: FrameType 327 tb_lineno, # type: int 328): 329 # type: (...) -> Tuple[List[Annotated[str]], Optional[Annotated[str]], List[Annotated[str]]] 330 try: 331 abs_path = frame.f_code.co_filename # type: Optional[str] 332 except Exception: 333 abs_path = None 334 try: 335 module = frame.f_globals["__name__"] 336 except Exception: 337 return [], None, [] 338 try: 339 loader = frame.f_globals["__loader__"] 340 except Exception: 341 loader = None 342 lineno = tb_lineno - 1 343 if lineno is not None and abs_path: 344 return get_lines_from_file(abs_path, lineno, loader, module) 345 return [], None, [] 346 347 348def safe_str(value): 349 # type: (Any) -> str 350 try: 351 return text_type(value) 352 except Exception: 353 return safe_repr(value) 354 355 356if PY2: 357 358 def safe_repr(value): 359 # type: (Any) -> str 360 try: 361 rv = repr(value).decode("utf-8", "replace") 362 363 # At this point `rv` contains a bunch of literal escape codes, like 364 # this (exaggerated example): 365 # 366 # u"\\x2f" 367 # 368 # But we want to show this string as: 369 # 370 # u"/" 371 try: 372 # unicode-escape does this job, but can only decode latin1. So we 373 # attempt to encode in latin1. 374 return rv.encode("latin1").decode("unicode-escape") 375 except Exception: 376 # Since usually strings aren't latin1 this can break. In those 377 # cases we just give up. 378 return rv 379 except Exception: 380 # If e.g. the call to `repr` already fails 381 return u"<broken repr>" 382 383 384else: 385 386 def safe_repr(value): 387 # type: (Any) -> str 388 try: 389 return repr(value) 390 except Exception: 391 return "<broken repr>" 392 393 394def filename_for_module(module, abs_path): 395 # type: (Optional[str], Optional[str]) -> Optional[str] 396 if not abs_path or not module: 397 return abs_path 398 399 try: 400 if abs_path.endswith(".pyc"): 401 abs_path = abs_path[:-1] 402 403 base_module = module.split(".", 1)[0] 404 if base_module == module: 405 return os.path.basename(abs_path) 406 407 base_module_path = sys.modules[base_module].__file__ 408 return abs_path.split(base_module_path.rsplit(os.sep, 2)[0], 1)[-1].lstrip( 409 os.sep 410 ) 411 except Exception: 412 return abs_path 413 414 415def serialize_frame(frame, tb_lineno=None, with_locals=True): 416 # type: (FrameType, Optional[int], bool) -> Dict[str, Any] 417 f_code = getattr(frame, "f_code", None) 418 if not f_code: 419 abs_path = None 420 function = None 421 else: 422 abs_path = frame.f_code.co_filename 423 function = frame.f_code.co_name 424 try: 425 module = frame.f_globals["__name__"] 426 except Exception: 427 module = None 428 429 if tb_lineno is None: 430 tb_lineno = frame.f_lineno 431 432 pre_context, context_line, post_context = get_source_context(frame, tb_lineno) 433 434 rv = { 435 "filename": filename_for_module(module, abs_path) or None, 436 "abs_path": os.path.abspath(abs_path) if abs_path else None, 437 "function": function or "<unknown>", 438 "module": module, 439 "lineno": tb_lineno, 440 "pre_context": pre_context, 441 "context_line": context_line, 442 "post_context": post_context, 443 } # type: Dict[str, Any] 444 if with_locals: 445 rv["vars"] = frame.f_locals 446 447 return rv 448 449 450def stacktrace_from_traceback(tb=None, with_locals=True): 451 # type: (Optional[TracebackType], bool) -> Dict[str, List[Dict[str, Any]]] 452 return { 453 "frames": [ 454 serialize_frame( 455 tb.tb_frame, tb_lineno=tb.tb_lineno, with_locals=with_locals 456 ) 457 for tb in iter_stacks(tb) 458 ] 459 } 460 461 462def current_stacktrace(with_locals=True): 463 # type: (bool) -> Any 464 __tracebackhide__ = True 465 frames = [] 466 467 f = sys._getframe() # type: Optional[FrameType] 468 while f is not None: 469 if not should_hide_frame(f): 470 frames.append(serialize_frame(f, with_locals=with_locals)) 471 f = f.f_back 472 473 frames.reverse() 474 475 return {"frames": frames} 476 477 478def get_errno(exc_value): 479 # type: (BaseException) -> Optional[Any] 480 return getattr(exc_value, "errno", None) 481 482 483def single_exception_from_error_tuple( 484 exc_type, # type: Optional[type] 485 exc_value, # type: Optional[BaseException] 486 tb, # type: Optional[TracebackType] 487 client_options=None, # type: Optional[Dict[str, Any]] 488 mechanism=None, # type: Optional[Dict[str, Any]] 489): 490 # type: (...) -> Dict[str, Any] 491 if exc_value is not None: 492 errno = get_errno(exc_value) 493 else: 494 errno = None 495 496 if errno is not None: 497 mechanism = mechanism or {} 498 mechanism.setdefault("meta", {}).setdefault("errno", {}).setdefault( 499 "number", errno 500 ) 501 502 if client_options is None: 503 with_locals = True 504 else: 505 with_locals = client_options["with_locals"] 506 507 return { 508 "module": get_type_module(exc_type), 509 "type": get_type_name(exc_type), 510 "value": safe_str(exc_value), 511 "mechanism": mechanism, 512 "stacktrace": stacktrace_from_traceback(tb, with_locals), 513 } 514 515 516HAS_CHAINED_EXCEPTIONS = hasattr(Exception, "__suppress_context__") 517 518if HAS_CHAINED_EXCEPTIONS: 519 520 def walk_exception_chain(exc_info): 521 # type: (ExcInfo) -> Iterator[ExcInfo] 522 exc_type, exc_value, tb = exc_info 523 524 seen_exceptions = [] 525 seen_exception_ids = set() # type: Set[int] 526 527 while ( 528 exc_type is not None 529 and exc_value is not None 530 and id(exc_value) not in seen_exception_ids 531 ): 532 yield exc_type, exc_value, tb 533 534 # Avoid hashing random types we don't know anything 535 # about. Use the list to keep a ref so that the `id` is 536 # not used for another object. 537 seen_exceptions.append(exc_value) 538 seen_exception_ids.add(id(exc_value)) 539 540 if exc_value.__suppress_context__: 541 cause = exc_value.__cause__ 542 else: 543 cause = exc_value.__context__ 544 if cause is None: 545 break 546 exc_type = type(cause) 547 exc_value = cause 548 tb = getattr(cause, "__traceback__", None) 549 550 551else: 552 553 def walk_exception_chain(exc_info): 554 # type: (ExcInfo) -> Iterator[ExcInfo] 555 yield exc_info 556 557 558def exceptions_from_error_tuple( 559 exc_info, # type: ExcInfo 560 client_options=None, # type: Optional[Dict[str, Any]] 561 mechanism=None, # type: Optional[Dict[str, Any]] 562): 563 # type: (...) -> List[Dict[str, Any]] 564 exc_type, exc_value, tb = exc_info 565 rv = [] 566 for exc_type, exc_value, tb in walk_exception_chain(exc_info): 567 rv.append( 568 single_exception_from_error_tuple( 569 exc_type, exc_value, tb, client_options, mechanism 570 ) 571 ) 572 573 rv.reverse() 574 575 return rv 576 577 578def to_string(value): 579 # type: (str) -> str 580 try: 581 return text_type(value) 582 except UnicodeDecodeError: 583 return repr(value)[1:-1] 584 585 586def iter_event_stacktraces(event): 587 # type: (Dict[str, Any]) -> Iterator[Dict[str, Any]] 588 if "stacktrace" in event: 589 yield event["stacktrace"] 590 if "threads" in event: 591 for thread in event["threads"].get("values") or (): 592 if "stacktrace" in thread: 593 yield thread["stacktrace"] 594 if "exception" in event: 595 for exception in event["exception"].get("values") or (): 596 if "stacktrace" in exception: 597 yield exception["stacktrace"] 598 599 600def iter_event_frames(event): 601 # type: (Dict[str, Any]) -> Iterator[Dict[str, Any]] 602 for stacktrace in iter_event_stacktraces(event): 603 for frame in stacktrace.get("frames") or (): 604 yield frame 605 606 607def handle_in_app(event, in_app_exclude=None, in_app_include=None): 608 # type: (Dict[str, Any], Optional[List[str]], Optional[List[str]]) -> Dict[str, Any] 609 for stacktrace in iter_event_stacktraces(event): 610 handle_in_app_impl( 611 stacktrace.get("frames"), 612 in_app_exclude=in_app_exclude, 613 in_app_include=in_app_include, 614 ) 615 616 return event 617 618 619def handle_in_app_impl(frames, in_app_exclude, in_app_include): 620 # type: (Any, Optional[List[str]], Optional[List[str]]) -> Optional[Any] 621 if not frames: 622 return None 623 624 any_in_app = False 625 for frame in frames: 626 in_app = frame.get("in_app") 627 if in_app is not None: 628 if in_app: 629 any_in_app = True 630 continue 631 632 module = frame.get("module") 633 if not module: 634 continue 635 elif _module_in_set(module, in_app_include): 636 frame["in_app"] = True 637 any_in_app = True 638 elif _module_in_set(module, in_app_exclude): 639 frame["in_app"] = False 640 641 if not any_in_app: 642 for frame in frames: 643 if frame.get("in_app") is None: 644 frame["in_app"] = True 645 646 return frames 647 648 649def exc_info_from_error(error): 650 # type: (Union[BaseException, ExcInfo]) -> ExcInfo 651 if isinstance(error, tuple) and len(error) == 3: 652 exc_type, exc_value, tb = error 653 elif isinstance(error, BaseException): 654 tb = getattr(error, "__traceback__", None) 655 if tb is not None: 656 exc_type = type(error) 657 exc_value = error 658 else: 659 exc_type, exc_value, tb = sys.exc_info() 660 if exc_value is not error: 661 tb = None 662 exc_value = error 663 exc_type = type(error) 664 665 else: 666 raise ValueError("Expected Exception object to report, got %s!" % type(error)) 667 668 return exc_type, exc_value, tb 669 670 671def event_from_exception( 672 exc_info, # type: Union[BaseException, ExcInfo] 673 client_options=None, # type: Optional[Dict[str, Any]] 674 mechanism=None, # type: Optional[Dict[str, Any]] 675): 676 # type: (...) -> Tuple[Dict[str, Any], Dict[str, Any]] 677 exc_info = exc_info_from_error(exc_info) 678 hint = event_hint_with_exc_info(exc_info) 679 return ( 680 { 681 "level": "error", 682 "exception": { 683 "values": exceptions_from_error_tuple( 684 exc_info, client_options, mechanism 685 ) 686 }, 687 }, 688 hint, 689 ) 690 691 692def _module_in_set(name, set): 693 # type: (str, Optional[List[str]]) -> bool 694 if not set: 695 return False 696 for item in set or (): 697 if item == name or name.startswith(item + "."): 698 return True 699 return False 700 701 702def strip_string(value, max_length=None): 703 # type: (str, Optional[int]) -> Union[AnnotatedValue, str] 704 # TODO: read max_length from config 705 if not value: 706 return value 707 708 if max_length is None: 709 # This is intentionally not just the default such that one can patch `MAX_STRING_LENGTH` and affect `strip_string`. 710 max_length = MAX_STRING_LENGTH 711 712 length = len(value) 713 714 if length > max_length: 715 return AnnotatedValue( 716 value=value[: max_length - 3] + u"...", 717 metadata={ 718 "len": length, 719 "rem": [["!limit", "x", max_length - 3, max_length]], 720 }, 721 ) 722 return value 723 724 725def _is_threading_local_monkey_patched(): 726 # type: () -> bool 727 try: 728 from gevent.monkey import is_object_patched # type: ignore 729 730 if is_object_patched("threading", "local"): 731 return True 732 except ImportError: 733 pass 734 735 try: 736 from eventlet.patcher import is_monkey_patched # type: ignore 737 738 if is_monkey_patched("thread"): 739 return True 740 except ImportError: 741 pass 742 743 return False 744 745 746def _get_contextvars(): 747 # type: () -> Tuple[bool, type] 748 """ 749 Try to import contextvars and use it if it's deemed safe. We should not use 750 contextvars if gevent or eventlet have patched thread locals, as 751 contextvars are unaffected by that patch. 752 753 https://github.com/gevent/gevent/issues/1407 754 """ 755 if not _is_threading_local_monkey_patched(): 756 # aiocontextvars is a PyPI package that ensures that the contextvars 757 # backport (also a PyPI package) works with asyncio under Python 3.6 758 # 759 # Import it if available. 760 if not PY2 and sys.version_info < (3, 7): 761 try: 762 from aiocontextvars import ContextVar # noqa 763 764 return True, ContextVar 765 except ImportError: 766 pass 767 768 try: 769 from contextvars import ContextVar 770 771 return True, ContextVar 772 except ImportError: 773 pass 774 775 from threading import local 776 777 class ContextVar(object): 778 # Super-limited impl of ContextVar 779 780 def __init__(self, name): 781 # type: (str) -> None 782 self._name = name 783 self._local = local() 784 785 def get(self, default): 786 # type: (Any) -> Any 787 return getattr(self._local, "value", default) 788 789 def set(self, value): 790 # type: (Any) -> None 791 self._local.value = value 792 793 return False, ContextVar 794 795 796HAS_REAL_CONTEXTVARS, ContextVar = _get_contextvars() 797 798 799def transaction_from_function(func): 800 # type: (Callable[..., Any]) -> Optional[str] 801 # Methods in Python 2 802 try: 803 return "%s.%s.%s" % ( 804 func.im_class.__module__, # type: ignore 805 func.im_class.__name__, # type: ignore 806 func.__name__, 807 ) 808 except Exception: 809 pass 810 811 func_qualname = ( 812 getattr(func, "__qualname__", None) or getattr(func, "__name__", None) or None 813 ) # type: Optional[str] 814 815 if not func_qualname: 816 # No idea what it is 817 return None 818 819 # Methods in Python 3 820 # Functions 821 # Classes 822 try: 823 return "%s.%s" % (func.__module__, func_qualname) 824 except Exception: 825 pass 826 827 # Possibly a lambda 828 return func_qualname 829 830 831disable_capture_event = ContextVar("disable_capture_event") 832