1from copy import copy
2from collections import deque
3from functools import wraps
4from itertools import chain
5
6from sentry_sdk.utils import logger, capture_internal_exceptions
7from sentry_sdk._types import MYPY
8
9if MYPY:
10    from typing import Any
11    from typing import Dict
12    from typing import Optional
13    from typing import Deque
14    from typing import List
15    from typing import Callable
16    from typing import TypeVar
17
18    from sentry_sdk._types import (
19        Breadcrumb,
20        Event,
21        EventProcessor,
22        ErrorProcessor,
23        ExcInfo,
24        Hint,
25        Type,
26    )
27
28    from sentry_sdk.tracing import Span
29    from sentry_sdk.sessions import Session
30
31    F = TypeVar("F", bound=Callable[..., Any])
32    T = TypeVar("T")
33
34
35global_event_processors = []  # type: List[EventProcessor]
36
37
38def add_global_event_processor(processor):
39    # type: (EventProcessor) -> None
40    global_event_processors.append(processor)
41
42
43def _attr_setter(fn):
44    # type: (Any) -> Any
45    return property(fset=fn, doc=fn.__doc__)
46
47
48def _disable_capture(fn):
49    # type: (F) -> F
50    @wraps(fn)
51    def wrapper(self, *args, **kwargs):
52        # type: (Any, *Dict[str, Any], **Any) -> Any
53        if not self._should_capture:
54            return
55        try:
56            self._should_capture = False
57            return fn(self, *args, **kwargs)
58        finally:
59            self._should_capture = True
60
61    return wrapper  # type: ignore
62
63
64class Scope(object):
65    """The scope holds extra information that should be sent with all
66    events that belong to it.
67    """
68
69    # NOTE: Even though it should not happen, the scope needs to not crash when
70    # accessed by multiple threads. It's fine if it's full of races, but those
71    # races should never make the user application crash.
72    #
73    # The same needs to hold for any accesses of the scope the SDK makes.
74
75    __slots__ = (
76        "_level",
77        "_name",
78        "_fingerprint",
79        "_transaction",
80        "_user",
81        "_tags",
82        "_contexts",
83        "_extras",
84        "_breadcrumbs",
85        "_event_processors",
86        "_error_processors",
87        "_should_capture",
88        "_span",
89        "_session",
90        "_force_auto_session_tracking",
91    )
92
93    def __init__(self):
94        # type: () -> None
95        self._event_processors = []  # type: List[EventProcessor]
96        self._error_processors = []  # type: List[ErrorProcessor]
97
98        self._name = None  # type: Optional[str]
99        self.clear()
100
101    def clear(self):
102        # type: () -> None
103        """Clears the entire scope."""
104        self._level = None  # type: Optional[str]
105        self._fingerprint = None  # type: Optional[List[str]]
106        self._transaction = None  # type: Optional[str]
107        self._user = None  # type: Optional[Dict[str, Any]]
108
109        self._tags = {}  # type: Dict[str, Any]
110        self._contexts = {}  # type: Dict[str, Dict[str, Any]]
111        self._extras = {}  # type: Dict[str, Any]
112
113        self.clear_breadcrumbs()
114        self._should_capture = True
115
116        self._span = None  # type: Optional[Span]
117        self._session = None  # type: Optional[Session]
118        self._force_auto_session_tracking = None  # type: Optional[bool]
119
120    @_attr_setter
121    def level(self, value):
122        # type: (Optional[str]) -> None
123        """When set this overrides the level. Deprecated in favor of set_level."""
124        self._level = value
125
126    def set_level(self, value):
127        # type: (Optional[str]) -> None
128        """Sets the level for the scope."""
129        self._level = value
130
131    @_attr_setter
132    def fingerprint(self, value):
133        # type: (Optional[List[str]]) -> None
134        """When set this overrides the default fingerprint."""
135        self._fingerprint = value
136
137    @_attr_setter
138    def transaction(self, value):
139        # type: (Optional[str]) -> None
140        """When set this forces a specific transaction name to be set."""
141        self._transaction = value
142        span = self._span
143        if span:
144            span.transaction = value
145
146    @_attr_setter
147    def user(self, value):
148        # type: (Dict[str, Any]) -> None
149        """When set a specific user is bound to the scope. Deprecated in favor of set_user."""
150        self.set_user(value)
151
152    def set_user(self, value):
153        # type: (Dict[str, Any]) -> None
154        """Sets a user for the scope."""
155        self._user = value
156        if self._session is not None:
157            self._session.update(user=value)
158
159    @property
160    def span(self):
161        # type: () -> Optional[Span]
162        """Get/set current tracing span."""
163        return self._span
164
165    @span.setter
166    def span(self, span):
167        # type: (Optional[Span]) -> None
168        self._span = span
169        if span is not None:
170            span_transaction = span.transaction
171            if span_transaction:
172                self._transaction = span_transaction
173
174    def set_tag(
175        self,
176        key,  # type: str
177        value,  # type: Any
178    ):
179        # type: (...) -> None
180        """Sets a tag for a key to a specific value."""
181        self._tags[key] = value
182
183    def remove_tag(
184        self, key  # type: str
185    ):
186        # type: (...) -> None
187        """Removes a specific tag."""
188        self._tags.pop(key, None)
189
190    def set_context(
191        self,
192        key,  # type: str
193        value,  # type: Any
194    ):
195        # type: (...) -> None
196        """Binds a context at a certain key to a specific value."""
197        self._contexts[key] = value
198
199    def remove_context(
200        self, key  # type: str
201    ):
202        # type: (...) -> None
203        """Removes a context."""
204        self._contexts.pop(key, None)
205
206    def set_extra(
207        self,
208        key,  # type: str
209        value,  # type: Any
210    ):
211        # type: (...) -> None
212        """Sets an extra key to a specific value."""
213        self._extras[key] = value
214
215    def remove_extra(
216        self, key  # type: str
217    ):
218        # type: (...) -> None
219        """Removes a specific extra key."""
220        self._extras.pop(key, None)
221
222    def clear_breadcrumbs(self):
223        # type: () -> None
224        """Clears breadcrumb buffer."""
225        self._breadcrumbs = deque()  # type: Deque[Breadcrumb]
226
227    def add_event_processor(
228        self, func  # type: EventProcessor
229    ):
230        # type: (...) -> None
231        """Register a scope local event processor on the scope.
232
233        :param func: This function behaves like `before_send.`
234        """
235        if len(self._event_processors) > 20:
236            logger.warning(
237                "Too many event processors on scope! Clearing list to free up some memory: %r",
238                self._event_processors,
239            )
240            del self._event_processors[:]
241
242        self._event_processors.append(func)
243
244    def add_error_processor(
245        self,
246        func,  # type: ErrorProcessor
247        cls=None,  # type: Optional[Type[BaseException]]
248    ):
249        # type: (...) -> None
250        """Register a scope local error processor on the scope.
251
252        :param func: A callback that works similar to an event processor but is invoked with the original exception info triple as second argument.
253
254        :param cls: Optionally, only process exceptions of this type.
255        """
256        if cls is not None:
257            cls_ = cls  # For mypy.
258            real_func = func
259
260            def func(event, exc_info):
261                # type: (Event, ExcInfo) -> Optional[Event]
262                try:
263                    is_inst = isinstance(exc_info[1], cls_)
264                except Exception:
265                    is_inst = False
266                if is_inst:
267                    return real_func(event, exc_info)
268                return event
269
270        self._error_processors.append(func)
271
272    @_disable_capture
273    def apply_to_event(
274        self,
275        event,  # type: Event
276        hint,  # type: Hint
277    ):
278        # type: (...) -> Optional[Event]
279        """Applies the information contained on the scope to the given event."""
280
281        def _drop(event, cause, ty):
282            # type: (Dict[str, Any], Any, str) -> Optional[Any]
283            logger.info("%s (%s) dropped event (%s)", ty, cause, event)
284            return None
285
286        if self._level is not None:
287            event["level"] = self._level
288
289        if event.get("type") != "transaction":
290            event.setdefault("breadcrumbs", []).extend(self._breadcrumbs)
291
292        if event.get("user") is None and self._user is not None:
293            event["user"] = self._user
294
295        if event.get("transaction") is None and self._transaction is not None:
296            event["transaction"] = self._transaction
297
298        if event.get("fingerprint") is None and self._fingerprint is not None:
299            event["fingerprint"] = self._fingerprint
300
301        if self._extras:
302            event.setdefault("extra", {}).update(self._extras)
303
304        if self._tags:
305            event.setdefault("tags", {}).update(self._tags)
306
307        if self._contexts:
308            event.setdefault("contexts", {}).update(self._contexts)
309
310        if self._span is not None:
311            contexts = event.setdefault("contexts", {})
312            if not contexts.get("trace"):
313                contexts["trace"] = self._span.get_trace_context()
314
315        exc_info = hint.get("exc_info")
316        if exc_info is not None:
317            for error_processor in self._error_processors:
318                new_event = error_processor(event, exc_info)
319                if new_event is None:
320                    return _drop(event, error_processor, "error processor")
321                event = new_event
322
323        for event_processor in chain(global_event_processors, self._event_processors):
324            new_event = event
325            with capture_internal_exceptions():
326                new_event = event_processor(event, hint)
327            if new_event is None:
328                return _drop(event, event_processor, "event processor")
329            event = new_event
330
331        return event
332
333    def update_from_scope(self, scope):
334        # type: (Scope) -> None
335        if scope._level is not None:
336            self._level = scope._level
337        if scope._fingerprint is not None:
338            self._fingerprint = scope._fingerprint
339        if scope._transaction is not None:
340            self._transaction = scope._transaction
341        if scope._user is not None:
342            self._user = scope._user
343        if scope._tags:
344            self._tags.update(scope._tags)
345        if scope._contexts:
346            self._contexts.update(scope._contexts)
347        if scope._extras:
348            self._extras.update(scope._extras)
349        if scope._breadcrumbs:
350            self._breadcrumbs.extend(scope._breadcrumbs)
351        if scope._span:
352            self._span = scope._span
353
354    def update_from_kwargs(
355        self,
356        user=None,  # type: Optional[Any]
357        level=None,  # type: Optional[str]
358        extras=None,  # type: Optional[Dict[str, Any]]
359        contexts=None,  # type: Optional[Dict[str, Any]]
360        tags=None,  # type: Optional[Dict[str, str]]
361        fingerprint=None,  # type: Optional[List[str]]
362    ):
363        # type: (...) -> None
364        if level is not None:
365            self._level = level
366        if user is not None:
367            self._user = user
368        if extras is not None:
369            self._extras.update(extras)
370        if contexts is not None:
371            self._contexts.update(contexts)
372        if tags is not None:
373            self._tags.update(tags)
374        if fingerprint is not None:
375            self._fingerprint = fingerprint
376
377    def __copy__(self):
378        # type: () -> Scope
379        rv = object.__new__(self.__class__)  # type: Scope
380
381        rv._level = self._level
382        rv._name = self._name
383        rv._fingerprint = self._fingerprint
384        rv._transaction = self._transaction
385        rv._user = self._user
386
387        rv._tags = dict(self._tags)
388        rv._contexts = dict(self._contexts)
389        rv._extras = dict(self._extras)
390
391        rv._breadcrumbs = copy(self._breadcrumbs)
392        rv._event_processors = list(self._event_processors)
393        rv._error_processors = list(self._error_processors)
394
395        rv._should_capture = self._should_capture
396        rv._span = self._span
397        rv._session = self._session
398        rv._force_auto_session_tracking = self._force_auto_session_tracking
399
400        return rv
401
402    def __repr__(self):
403        # type: () -> str
404        return "<%s id=%s name=%s>" % (
405            self.__class__.__name__,
406            hex(id(self)),
407            self._name,
408        )
409