1import copy
2import random
3import sys
4
5from datetime import datetime
6from contextlib import contextmanager
7
8from sentry_sdk._compat import with_metaclass
9from sentry_sdk.scope import Scope
10from sentry_sdk.client import Client
11from sentry_sdk.tracing import Span
12from sentry_sdk.sessions import Session
13from sentry_sdk.utils import (
14    exc_info_from_error,
15    event_from_exception,
16    logger,
17    ContextVar,
18)
19
20from sentry_sdk._types import MYPY
21
22if MYPY:
23    from typing import Union
24    from typing import Any
25    from typing import Optional
26    from typing import Tuple
27    from typing import Dict
28    from typing import List
29    from typing import Callable
30    from typing import Generator
31    from typing import Type
32    from typing import TypeVar
33    from typing import overload
34    from typing import ContextManager
35
36    from sentry_sdk.integrations import Integration
37    from sentry_sdk._types import (
38        Event,
39        Hint,
40        Breadcrumb,
41        BreadcrumbHint,
42        ExcInfo,
43    )
44    from sentry_sdk.consts import ClientConstructor
45
46    T = TypeVar("T")
47
48else:
49
50    def overload(x):
51        # type: (T) -> T
52        return x
53
54
55_local = ContextVar("sentry_current_hub")
56
57
58def _update_scope(base, scope_change, scope_kwargs):
59    # type: (Scope, Optional[Any], Dict[str, Any]) -> Scope
60    if scope_change and scope_kwargs:
61        raise TypeError("cannot provide scope and kwargs")
62    if scope_change is not None:
63        final_scope = copy.copy(base)
64        if callable(scope_change):
65            scope_change(final_scope)
66        else:
67            final_scope.update_from_scope(scope_change)
68    elif scope_kwargs:
69        final_scope = copy.copy(base)
70        final_scope.update_from_kwargs(scope_kwargs)
71    else:
72        final_scope = base
73    return final_scope
74
75
76def _should_send_default_pii():
77    # type: () -> bool
78    client = Hub.current.client
79    if not client:
80        return False
81    return client.options["send_default_pii"]
82
83
84class _InitGuard(object):
85    def __init__(self, client):
86        # type: (Client) -> None
87        self._client = client
88
89    def __enter__(self):
90        # type: () -> _InitGuard
91        return self
92
93    def __exit__(self, exc_type, exc_value, tb):
94        # type: (Any, Any, Any) -> None
95        c = self._client
96        if c is not None:
97            c.close()
98
99
100def _init(*args, **kwargs):
101    # type: (*Optional[str], **Any) -> ContextManager[Any]
102    """Initializes the SDK and optionally integrations.
103
104    This takes the same arguments as the client constructor.
105    """
106    client = Client(*args, **kwargs)  # type: ignore
107    Hub.current.bind_client(client)
108    rv = _InitGuard(client)
109    return rv
110
111
112from sentry_sdk._types import MYPY
113
114if MYPY:
115    # Make mypy, PyCharm and other static analyzers think `init` is a type to
116    # have nicer autocompletion for params.
117    #
118    # Use `ClientConstructor` to define the argument types of `init` and
119    # `ContextManager[Any]` to tell static analyzers about the return type.
120
121    class init(ClientConstructor, ContextManager[Any]):  # noqa: N801
122        pass
123
124
125else:
126    # Alias `init` for actual usage. Go through the lambda indirection to throw
127    # PyCharm off of the weakly typed signature (it would otherwise discover
128    # both the weakly typed signature of `_init` and our faked `init` type).
129
130    init = (lambda: _init)()
131
132
133class HubMeta(type):
134    @property
135    def current(cls):
136        # type: () -> Hub
137        """Returns the current instance of the hub."""
138        rv = _local.get(None)
139        if rv is None:
140            rv = Hub(GLOBAL_HUB)
141            _local.set(rv)
142        return rv
143
144    @property
145    def main(cls):
146        # type: () -> Hub
147        """Returns the main instance of the hub."""
148        return GLOBAL_HUB
149
150
151class _ScopeManager(object):
152    def __init__(self, hub):
153        # type: (Hub) -> None
154        self._hub = hub
155        self._original_len = len(hub._stack)
156        self._layer = hub._stack[-1]
157
158    def __enter__(self):
159        # type: () -> Scope
160        scope = self._layer[1]
161        assert scope is not None
162        return scope
163
164    def __exit__(self, exc_type, exc_value, tb):
165        # type: (Any, Any, Any) -> None
166        current_len = len(self._hub._stack)
167        if current_len < self._original_len:
168            logger.error(
169                "Scope popped too soon. Popped %s scopes too many.",
170                self._original_len - current_len,
171            )
172            return
173        elif current_len > self._original_len:
174            logger.warning(
175                "Leaked %s scopes: %s",
176                current_len - self._original_len,
177                self._hub._stack[self._original_len :],
178            )
179
180        layer = self._hub._stack[self._original_len - 1]
181        del self._hub._stack[self._original_len - 1 :]
182
183        if layer[1] != self._layer[1]:
184            logger.error(
185                "Wrong scope found. Meant to pop %s, but popped %s.",
186                layer[1],
187                self._layer[1],
188            )
189        elif layer[0] != self._layer[0]:
190            warning = (
191                "init() called inside of pushed scope. This might be entirely "
192                "legitimate but usually occurs when initializing the SDK inside "
193                "a request handler or task/job function. Try to initialize the "
194                "SDK as early as possible instead."
195            )
196            logger.warning(warning)
197
198
199class Hub(with_metaclass(HubMeta)):  # type: ignore
200    """The hub wraps the concurrency management of the SDK.  Each thread has
201    its own hub but the hub might transfer with the flow of execution if
202    context vars are available.
203
204    If the hub is used with a with statement it's temporarily activated.
205    """
206
207    _stack = None  # type: List[Tuple[Optional[Client], Scope]]
208
209    # Mypy doesn't pick up on the metaclass.
210
211    if MYPY:
212        current = None  # type: Hub
213        main = None  # type: Hub
214
215    def __init__(
216        self,
217        client_or_hub=None,  # type: Optional[Union[Hub, Client]]
218        scope=None,  # type: Optional[Any]
219    ):
220        # type: (...) -> None
221        if isinstance(client_or_hub, Hub):
222            hub = client_or_hub
223            client, other_scope = hub._stack[-1]
224            if scope is None:
225                scope = copy.copy(other_scope)
226        else:
227            client = client_or_hub
228        if scope is None:
229            scope = Scope()
230
231        self._stack = [(client, scope)]
232        self._last_event_id = None  # type: Optional[str]
233        self._old_hubs = []  # type: List[Hub]
234
235    def __enter__(self):
236        # type: () -> Hub
237        self._old_hubs.append(Hub.current)
238        _local.set(self)
239        return self
240
241    def __exit__(
242        self,
243        exc_type,  # type: Optional[type]
244        exc_value,  # type: Optional[BaseException]
245        tb,  # type: Optional[Any]
246    ):
247        # type: (...) -> None
248        old = self._old_hubs.pop()
249        _local.set(old)
250
251    def run(
252        self, callback  # type: Callable[[], T]
253    ):
254        # type: (...) -> T
255        """Runs a callback in the context of the hub.  Alternatively the
256        with statement can be used on the hub directly.
257        """
258        with self:
259            return callback()
260
261    def get_integration(
262        self, name_or_class  # type: Union[str, Type[Integration]]
263    ):
264        # type: (...) -> Any
265        """Returns the integration for this hub by name or class.  If there
266        is no client bound or the client does not have that integration
267        then `None` is returned.
268
269        If the return value is not `None` the hub is guaranteed to have a
270        client attached.
271        """
272        if isinstance(name_or_class, str):
273            integration_name = name_or_class
274        elif name_or_class.identifier is not None:
275            integration_name = name_or_class.identifier
276        else:
277            raise ValueError("Integration has no name")
278
279        client = self._stack[-1][0]
280        if client is not None:
281            rv = client.integrations.get(integration_name)
282            if rv is not None:
283                return rv
284
285    @property
286    def client(self):
287        # type: () -> Optional[Client]
288        """Returns the current client on the hub."""
289        return self._stack[-1][0]
290
291    @property
292    def scope(self):
293        # type: () -> Scope
294        """Returns the current scope on the hub."""
295        return self._stack[-1][1]
296
297    def last_event_id(self):
298        # type: () -> Optional[str]
299        """Returns the last event ID."""
300        return self._last_event_id
301
302    def bind_client(
303        self, new  # type: Optional[Client]
304    ):
305        # type: (...) -> None
306        """Binds a new client to the hub."""
307        top = self._stack[-1]
308        self._stack[-1] = (new, top[1])
309
310    def capture_event(
311        self,
312        event,  # type: Event
313        hint=None,  # type: Optional[Hint]
314        scope=None,  # type: Optional[Any]
315        **scope_args  # type: Dict[str, Any]
316    ):
317        # type: (...) -> Optional[str]
318        """Captures an event. Alias of :py:meth:`sentry_sdk.Client.capture_event`.
319        """
320        client, top_scope = self._stack[-1]
321        scope = _update_scope(top_scope, scope, scope_args)
322        if client is not None:
323            rv = client.capture_event(event, hint, scope)
324            if rv is not None:
325                self._last_event_id = rv
326            return rv
327        return None
328
329    def capture_message(
330        self,
331        message,  # type: str
332        level=None,  # type: Optional[str]
333        scope=None,  # type: Optional[Any]
334        **scope_args  # type: Dict[str, Any]
335    ):
336        # type: (...) -> Optional[str]
337        """Captures a message.  The message is just a string.  If no level
338        is provided the default level is `info`.
339
340        :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
341        """
342        if self.client is None:
343            return None
344        if level is None:
345            level = "info"
346        return self.capture_event(
347            {"message": message, "level": level}, scope=scope, **scope_args
348        )
349
350    def capture_exception(
351        self,
352        error=None,  # type: Optional[Union[BaseException, ExcInfo]]
353        scope=None,  # type: Optional[Any]
354        **scope_args  # type: Dict[str, Any]
355    ):
356        # type: (...) -> Optional[str]
357        """Captures an exception.
358
359        :param error: An exception to catch. If `None`, `sys.exc_info()` will be used.
360
361        :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
362        """
363        client = self.client
364        if client is None:
365            return None
366        if error is not None:
367            exc_info = exc_info_from_error(error)
368        else:
369            exc_info = sys.exc_info()
370
371        event, hint = event_from_exception(exc_info, client_options=client.options)
372        try:
373            return self.capture_event(event, hint=hint, scope=scope, **scope_args)
374        except Exception:
375            self._capture_internal_exception(sys.exc_info())
376
377        return None
378
379    def _capture_internal_exception(
380        self, exc_info  # type: Any
381    ):
382        # type: (...) -> Any
383        """
384        Capture an exception that is likely caused by a bug in the SDK
385        itself.
386
387        These exceptions do not end up in Sentry and are just logged instead.
388        """
389        logger.error("Internal error in sentry_sdk", exc_info=exc_info)
390
391    def add_breadcrumb(
392        self,
393        crumb=None,  # type: Optional[Breadcrumb]
394        hint=None,  # type: Optional[BreadcrumbHint]
395        **kwargs  # type: Any
396    ):
397        # type: (...) -> None
398        """
399        Adds a breadcrumb.
400
401        :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects.
402
403        :param hint: An optional value that can be used by `before_breadcrumb`
404            to customize the breadcrumbs that are emitted.
405        """
406        client, scope = self._stack[-1]
407        if client is None:
408            logger.info("Dropped breadcrumb because no client bound")
409            return
410
411        crumb = dict(crumb or ())  # type: Breadcrumb
412        crumb.update(kwargs)
413        if not crumb:
414            return
415
416        hint = dict(hint or ())  # type: Hint
417
418        if crumb.get("timestamp") is None:
419            crumb["timestamp"] = datetime.utcnow()
420        if crumb.get("type") is None:
421            crumb["type"] = "default"
422
423        if client.options["before_breadcrumb"] is not None:
424            new_crumb = client.options["before_breadcrumb"](crumb, hint)
425        else:
426            new_crumb = crumb
427
428        if new_crumb is not None:
429            scope._breadcrumbs.append(new_crumb)
430        else:
431            logger.info("before breadcrumb dropped breadcrumb (%s)", crumb)
432
433        max_breadcrumbs = client.options["max_breadcrumbs"]  # type: int
434        while len(scope._breadcrumbs) > max_breadcrumbs:
435            scope._breadcrumbs.popleft()
436
437    def start_span(
438        self,
439        span=None,  # type: Optional[Span]
440        **kwargs  # type: Any
441    ):
442        # type: (...) -> Span
443        """
444        Create a new span whose parent span is the currently active
445        span, if any. The return value is the span object that can
446        be used as a context manager to start and stop timing.
447
448        Note that you will not see any span that is not contained
449        within a transaction. Create a transaction with
450        ``start_span(transaction="my transaction")`` if an
451        integration doesn't already do this for you.
452        """
453
454        client, scope = self._stack[-1]
455
456        kwargs.setdefault("hub", self)
457
458        if span is None:
459            span = scope.span
460            if span is not None:
461                span = span.new_span(**kwargs)
462            else:
463                span = Span(**kwargs)
464
465        if span.sampled is None and span.transaction is not None:
466            sample_rate = client and client.options["traces_sample_rate"] or 0
467            span.sampled = random.random() < sample_rate
468
469        if span.sampled:
470            max_spans = (
471                client and client.options["_experiments"].get("max_spans") or 1000
472            )
473            span.init_finished_spans(maxlen=max_spans)
474
475        return span
476
477    @overload  # noqa
478    def push_scope(
479        self, callback=None  # type: Optional[None]
480    ):
481        # type: (...) -> ContextManager[Scope]
482        pass
483
484    @overload  # noqa
485    def push_scope(
486        self, callback  # type: Callable[[Scope], None]
487    ):
488        # type: (...) -> None
489        pass
490
491    def push_scope(  # noqa
492        self, callback=None  # type: Optional[Callable[[Scope], None]]
493    ):
494        # type: (...) -> Optional[ContextManager[Scope]]
495        """
496        Pushes a new layer on the scope stack.
497
498        :param callback: If provided, this method pushes a scope, calls
499            `callback`, and pops the scope again.
500
501        :returns: If no `callback` is provided, a context manager that should
502            be used to pop the scope again.
503        """
504        if callback is not None:
505            with self.push_scope() as scope:
506                callback(scope)
507            return None
508
509        client, scope = self._stack[-1]
510        new_layer = (client, copy.copy(scope))
511        self._stack.append(new_layer)
512
513        return _ScopeManager(self)
514
515    def pop_scope_unsafe(self):
516        # type: () -> Tuple[Optional[Client], Scope]
517        """
518        Pops a scope layer from the stack.
519
520        Try to use the context manager :py:meth:`push_scope` instead.
521        """
522        rv = self._stack.pop()
523        assert self._stack, "stack must have at least one layer"
524        return rv
525
526    @overload  # noqa
527    def configure_scope(
528        self, callback=None  # type: Optional[None]
529    ):
530        # type: (...) -> ContextManager[Scope]
531        pass
532
533    @overload  # noqa
534    def configure_scope(
535        self, callback  # type: Callable[[Scope], None]
536    ):
537        # type: (...) -> None
538        pass
539
540    def configure_scope(  # noqa
541        self, callback=None  # type: Optional[Callable[[Scope], None]]
542    ):  # noqa
543        # type: (...) -> Optional[ContextManager[Scope]]
544
545        """
546        Reconfigures the scope.
547
548        :param callback: If provided, call the callback with the current scope.
549
550        :returns: If no callback is provided, returns a context manager that returns the scope.
551        """
552
553        client, scope = self._stack[-1]
554        if callback is not None:
555            if client is not None:
556                callback(scope)
557
558            return None
559
560        @contextmanager
561        def inner():
562            # type: () -> Generator[Scope, None, None]
563            if client is not None:
564                yield scope
565            else:
566                yield Scope()
567
568        return inner()
569
570    def start_session(self):
571        # type: (...) -> None
572        """Starts a new session."""
573        self.end_session()
574        client, scope = self._stack[-1]
575        scope._session = Session(
576            release=client.options["release"] if client else None,
577            environment=client.options["environment"] if client else None,
578            user=scope._user,
579        )
580
581    def end_session(self):
582        # type: (...) -> None
583        """Ends the current session if there is one."""
584        client, scope = self._stack[-1]
585        session = scope._session
586        if session is not None:
587            session.close()
588            if client is not None:
589                client.capture_session(session)
590        self._stack[-1][1]._session = None
591
592    def stop_auto_session_tracking(self):
593        # type: (...) -> None
594        """Stops automatic session tracking.
595
596        This temporarily session tracking for the current scope when called.
597        To resume session tracking call `resume_auto_session_tracking`.
598        """
599        self.end_session()
600        client, scope = self._stack[-1]
601        scope._force_auto_session_tracking = False
602
603    def resume_auto_session_tracking(self):
604        # type: (...) -> None
605        """Resumes automatic session tracking for the current scope if
606        disabled earlier.  This requires that generally automatic session
607        tracking is enabled.
608        """
609        client, scope = self._stack[-1]
610        scope._force_auto_session_tracking = None
611
612    def flush(
613        self,
614        timeout=None,  # type: Optional[float]
615        callback=None,  # type: Optional[Callable[[int, float], None]]
616    ):
617        # type: (...) -> None
618        """
619        Alias for :py:meth:`sentry_sdk.Client.flush`
620        """
621        client, scope = self._stack[-1]
622        if client is not None:
623            return client.flush(timeout=timeout, callback=callback)
624
625    def iter_trace_propagation_headers(self):
626        # type: () -> Generator[Tuple[str, str], None, None]
627        # TODO: Document
628        client, scope = self._stack[-1]
629        span = scope.span
630
631        if span is None:
632            return
633
634        propagate_traces = client and client.options["propagate_traces"]
635        if not propagate_traces:
636            return
637
638        if client and client.options["traceparent_v2"]:
639            traceparent = span.to_traceparent()
640        else:
641            traceparent = span.to_legacy_traceparent()
642
643        yield "sentry-trace", traceparent
644
645
646GLOBAL_HUB = Hub()
647_local.set(GLOBAL_HUB)
648