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