Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/hub.py: 42%
306 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import copy
2import sys
4from datetime import datetime
5from contextlib import contextmanager
7from sentry_sdk._compat import with_metaclass
8from sentry_sdk.scope import Scope
9from sentry_sdk.client import Client
10from sentry_sdk.tracing import Span, Transaction
11from sentry_sdk.session import Session
12from sentry_sdk.utils import (
13 exc_info_from_error,
14 event_from_exception,
15 logger,
16 ContextVar,
17)
19from sentry_sdk._types import MYPY
21if MYPY: 21 ↛ 22line 21 didn't jump to line 22, because the condition on line 21 was never true
22 from typing import Union
23 from typing import Any
24 from typing import Optional
25 from typing import Tuple
26 from typing import Dict
27 from typing import List
28 from typing import Callable
29 from typing import Generator
30 from typing import Type
31 from typing import TypeVar
32 from typing import overload
33 from typing import ContextManager
35 from sentry_sdk.integrations import Integration
36 from sentry_sdk._types import (
37 Event,
38 Hint,
39 Breadcrumb,
40 BreadcrumbHint,
41 ExcInfo,
42 )
43 from sentry_sdk.consts import ClientConstructor
45 T = TypeVar("T")
47else:
49 def overload(x):
50 # type: (T) -> T
51 return x
54_local = ContextVar("sentry_current_hub")
57def _update_scope(base, scope_change, scope_kwargs):
58 # type: (Scope, Optional[Any], Dict[str, Any]) -> Scope
59 if scope_change and scope_kwargs:
60 raise TypeError("cannot provide scope and kwargs")
61 if scope_change is not None:
62 final_scope = copy.copy(base)
63 if callable(scope_change):
64 scope_change(final_scope)
65 else:
66 final_scope.update_from_scope(scope_change)
67 elif scope_kwargs:
68 final_scope = copy.copy(base)
69 final_scope.update_from_kwargs(**scope_kwargs)
70 else:
71 final_scope = base
72 return final_scope
75def _should_send_default_pii():
76 # type: () -> bool
77 client = Hub.current.client
78 if not client:
79 return False
80 return client.options["send_default_pii"]
83class _InitGuard(object):
84 def __init__(self, client):
85 # type: (Client) -> None
86 self._client = client
88 def __enter__(self):
89 # type: () -> _InitGuard
90 return self
92 def __exit__(self, exc_type, exc_value, tb):
93 # type: (Any, Any, Any) -> None
94 c = self._client
95 if c is not None:
96 c.close()
99def _init(*args, **kwargs):
100 # type: (*Optional[str], **Any) -> ContextManager[Any]
101 """Initializes the SDK and optionally integrations.
103 This takes the same arguments as the client constructor.
104 """
105 client = Client(*args, **kwargs) # type: ignore
106 Hub.current.bind_client(client)
107 rv = _InitGuard(client)
108 return rv
111from sentry_sdk._types import MYPY
113if MYPY: 113 ↛ 120line 113 didn't jump to line 120, because the condition on line 113 was never true
114 # Make mypy, PyCharm and other static analyzers think `init` is a type to
115 # have nicer autocompletion for params.
116 #
117 # Use `ClientConstructor` to define the argument types of `init` and
118 # `ContextManager[Any]` to tell static analyzers about the return type.
120 class init(ClientConstructor, _InitGuard): # noqa: N801
121 pass
123else:
124 # Alias `init` for actual usage. Go through the lambda indirection to throw
125 # PyCharm off of the weakly typed signature (it would otherwise discover
126 # both the weakly typed signature of `_init` and our faked `init` type).
128 init = (lambda: _init)()
131class HubMeta(type):
132 @property
133 def current(cls):
134 # type: () -> Hub
135 """Returns the current instance of the hub."""
136 rv = _local.get(None)
137 if rv is None: 137 ↛ 138line 137 didn't jump to line 138, because the condition on line 137 was never true
138 rv = Hub(GLOBAL_HUB)
139 _local.set(rv)
140 return rv
142 @property
143 def main(cls):
144 # type: () -> Hub
145 """Returns the main instance of the hub."""
146 return GLOBAL_HUB
149class _ScopeManager(object):
150 def __init__(self, hub):
151 # type: (Hub) -> None
152 self._hub = hub
153 self._original_len = len(hub._stack)
154 self._layer = hub._stack[-1]
156 def __enter__(self):
157 # type: () -> Scope
158 scope = self._layer[1]
159 assert scope is not None
160 return scope
162 def __exit__(self, exc_type, exc_value, tb):
163 # type: (Any, Any, Any) -> None
164 current_len = len(self._hub._stack)
165 if current_len < self._original_len:
166 logger.error(
167 "Scope popped too soon. Popped %s scopes too many.",
168 self._original_len - current_len,
169 )
170 return
171 elif current_len > self._original_len:
172 logger.warning(
173 "Leaked %s scopes: %s",
174 current_len - self._original_len,
175 self._hub._stack[self._original_len :],
176 )
178 layer = self._hub._stack[self._original_len - 1]
179 del self._hub._stack[self._original_len - 1 :]
181 if layer[1] != self._layer[1]:
182 logger.error(
183 "Wrong scope found. Meant to pop %s, but popped %s.",
184 layer[1],
185 self._layer[1],
186 )
187 elif layer[0] != self._layer[0]:
188 warning = (
189 "init() called inside of pushed scope. This might be entirely "
190 "legitimate but usually occurs when initializing the SDK inside "
191 "a request handler or task/job function. Try to initialize the "
192 "SDK as early as possible instead."
193 )
194 logger.warning(warning)
197class Hub(with_metaclass(HubMeta)): # type: ignore
198 """The hub wraps the concurrency management of the SDK. Each thread has
199 its own hub but the hub might transfer with the flow of execution if
200 context vars are available.
202 If the hub is used with a with statement it's temporarily activated.
203 """
205 _stack = None # type: List[Tuple[Optional[Client], Scope]]
207 # Mypy doesn't pick up on the metaclass.
209 if MYPY: 209 ↛ 210line 209 didn't jump to line 210, because the condition on line 209 was never true
210 current = None # type: Hub
211 main = None # type: Hub
213 def __init__(
214 self,
215 client_or_hub=None, # type: Optional[Union[Hub, Client]]
216 scope=None, # type: Optional[Any]
217 ):
218 # type: (...) -> None
219 if isinstance(client_or_hub, Hub): 219 ↛ 220line 219 didn't jump to line 220, because the condition on line 219 was never true
220 hub = client_or_hub
221 client, other_scope = hub._stack[-1]
222 if scope is None:
223 scope = copy.copy(other_scope)
224 else:
225 client = client_or_hub
226 if scope is None: 226 ↛ 229line 226 didn't jump to line 229, because the condition on line 226 was never false
227 scope = Scope()
229 self._stack = [(client, scope)]
230 self._last_event_id = None # type: Optional[str]
231 self._old_hubs = [] # type: List[Hub]
233 def __enter__(self):
234 # type: () -> Hub
235 self._old_hubs.append(Hub.current)
236 _local.set(self)
237 return self
239 def __exit__(
240 self,
241 exc_type, # type: Optional[type]
242 exc_value, # type: Optional[BaseException]
243 tb, # type: Optional[Any]
244 ):
245 # type: (...) -> None
246 old = self._old_hubs.pop()
247 _local.set(old)
249 def run(
250 self, callback # type: Callable[[], T]
251 ):
252 # type: (...) -> T
253 """Runs a callback in the context of the hub. Alternatively the
254 with statement can be used on the hub directly.
255 """
256 with self:
257 return callback()
259 def get_integration(
260 self, name_or_class # type: Union[str, Type[Integration]]
261 ):
262 # type: (...) -> Any
263 """Returns the integration for this hub by name or class. If there
264 is no client bound or the client does not have that integration
265 then `None` is returned.
267 If the return value is not `None` the hub is guaranteed to have a
268 client attached.
269 """
270 if isinstance(name_or_class, str): 270 ↛ 271line 270 didn't jump to line 271, because the condition on line 270 was never true
271 integration_name = name_or_class
272 elif name_or_class.identifier is not None: 272 ↛ 275line 272 didn't jump to line 275, because the condition on line 272 was never false
273 integration_name = name_or_class.identifier
274 else:
275 raise ValueError("Integration has no name")
277 client = self.client
278 if client is not None: 278 ↛ exitline 278 didn't return from function 'get_integration', because the condition on line 278 was never false
279 rv = client.integrations.get(integration_name)
280 if rv is not None: 280 ↛ exitline 280 didn't return from function 'get_integration', because the condition on line 280 was never false
281 return rv
283 @property
284 def client(self):
285 # type: () -> Optional[Client]
286 """Returns the current client on the hub."""
287 return self._stack[-1][0]
289 @property
290 def scope(self):
291 # type: () -> Scope
292 """Returns the current scope on the hub."""
293 return self._stack[-1][1]
295 def last_event_id(self):
296 # type: () -> Optional[str]
297 """Returns the last event ID."""
298 return self._last_event_id
300 def bind_client(
301 self, new # type: Optional[Client]
302 ):
303 # type: (...) -> None
304 """Binds a new client to the hub."""
305 top = self._stack[-1]
306 self._stack[-1] = (new, top[1])
308 def capture_event(
309 self,
310 event, # type: Event
311 hint=None, # type: Optional[Hint]
312 scope=None, # type: Optional[Any]
313 **scope_args # type: Any
314 ):
315 # type: (...) -> Optional[str]
316 """Captures an event. Alias of :py:meth:`sentry_sdk.Client.capture_event`."""
317 client, top_scope = self._stack[-1]
318 scope = _update_scope(top_scope, scope, scope_args)
319 if client is not None:
320 is_transaction = event.get("type") == "transaction"
321 rv = client.capture_event(event, hint, scope)
322 if rv is not None and not is_transaction:
323 self._last_event_id = rv
324 return rv
325 return None
327 def capture_message(
328 self,
329 message, # type: str
330 level=None, # type: Optional[str]
331 scope=None, # type: Optional[Any]
332 **scope_args # type: Any
333 ):
334 # type: (...) -> Optional[str]
335 """Captures a message. The message is just a string. If no level
336 is provided the default level is `info`.
338 :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
339 """
340 if self.client is None:
341 return None
342 if level is None:
343 level = "info"
344 return self.capture_event(
345 {"message": message, "level": level}, scope=scope, **scope_args
346 )
348 def capture_exception(
349 self,
350 error=None, # type: Optional[Union[BaseException, ExcInfo]]
351 scope=None, # type: Optional[Any]
352 **scope_args # type: Any
353 ):
354 # type: (...) -> Optional[str]
355 """Captures an exception.
357 :param error: An exception to catch. If `None`, `sys.exc_info()` will be used.
359 :returns: An `event_id` if the SDK decided to send the event (see :py:meth:`sentry_sdk.Client.capture_event`).
360 """
361 client = self.client
362 if client is None:
363 return None
364 if error is not None:
365 exc_info = exc_info_from_error(error)
366 else:
367 exc_info = sys.exc_info()
369 event, hint = event_from_exception(exc_info, client_options=client.options)
370 try:
371 return self.capture_event(event, hint=hint, scope=scope, **scope_args)
372 except Exception:
373 self._capture_internal_exception(sys.exc_info())
375 return None
377 def _capture_internal_exception(
378 self, exc_info # type: Any
379 ):
380 # type: (...) -> Any
381 """
382 Capture an exception that is likely caused by a bug in the SDK
383 itself.
385 These exceptions do not end up in Sentry and are just logged instead.
386 """
387 logger.error("Internal error in sentry_sdk", exc_info=exc_info)
389 def add_breadcrumb(
390 self,
391 crumb=None, # type: Optional[Breadcrumb]
392 hint=None, # type: Optional[BreadcrumbHint]
393 **kwargs # type: Any
394 ):
395 # type: (...) -> None
396 """
397 Adds a breadcrumb.
399 :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects.
401 :param hint: An optional value that can be used by `before_breadcrumb`
402 to customize the breadcrumbs that are emitted.
403 """
404 client, scope = self._stack[-1]
405 if client is None: 405 ↛ 406line 405 didn't jump to line 406, because the condition on line 405 was never true
406 logger.info("Dropped breadcrumb because no client bound")
407 return
409 crumb = dict(crumb or ()) # type: Breadcrumb
410 crumb.update(kwargs)
411 if not crumb: 411 ↛ 412line 411 didn't jump to line 412, because the condition on line 411 was never true
412 return
414 hint = dict(hint or ()) # type: Hint
416 if crumb.get("timestamp") is None: 416 ↛ 418line 416 didn't jump to line 418, because the condition on line 416 was never false
417 crumb["timestamp"] = datetime.utcnow()
418 if crumb.get("type") is None:
419 crumb["type"] = "default"
421 if client.options["before_breadcrumb"] is not None: 421 ↛ 422line 421 didn't jump to line 422, because the condition on line 421 was never true
422 new_crumb = client.options["before_breadcrumb"](crumb, hint)
423 else:
424 new_crumb = crumb
426 if new_crumb is not None: 426 ↛ 429line 426 didn't jump to line 429, because the condition on line 426 was never false
427 scope._breadcrumbs.append(new_crumb)
428 else:
429 logger.info("before breadcrumb dropped breadcrumb (%s)", crumb)
431 max_breadcrumbs = client.options["max_breadcrumbs"] # type: int
432 while len(scope._breadcrumbs) > max_breadcrumbs:
433 scope._breadcrumbs.popleft()
435 def start_span(
436 self,
437 span=None, # type: Optional[Span]
438 **kwargs # type: Any
439 ):
440 # type: (...) -> Span
441 """
442 Create and start timing a new span whose parent is the currently active
443 span or transaction, if any. The return value is a span instance,
444 typically used as a context manager to start and stop timing in a `with`
445 block.
447 Only spans contained in a transaction are sent to Sentry. Most
448 integrations start a transaction at the appropriate time, for example
449 for every incoming HTTP request. Use `start_transaction` to start a new
450 transaction when one is not already in progress.
451 """
452 # TODO: consider removing this in a future release.
453 # This is for backwards compatibility with releases before
454 # start_transaction existed, to allow for a smoother transition.
455 if isinstance(span, Transaction) or "transaction" in kwargs: 455 ↛ 456line 455 didn't jump to line 456
456 deprecation_msg = (
457 "Deprecated: use start_transaction to start transactions and "
458 "Transaction.start_child to start spans."
459 )
460 if isinstance(span, Transaction):
461 logger.warning(deprecation_msg)
462 return self.start_transaction(span)
463 if "transaction" in kwargs:
464 logger.warning(deprecation_msg)
465 name = kwargs.pop("transaction")
466 return self.start_transaction(name=name, **kwargs)
468 if span is not None: 468 ↛ 469line 468 didn't jump to line 469, because the condition on line 468 was never true
469 return span
471 kwargs.setdefault("hub", self)
473 span = self.scope.span
474 if span is not None:
475 return span.start_child(**kwargs)
477 return Span(**kwargs)
479 def start_transaction(
480 self,
481 transaction=None, # type: Optional[Transaction]
482 **kwargs # type: Any
483 ):
484 # type: (...) -> Transaction
485 """
486 Start and return a transaction.
488 Start an existing transaction if given, otherwise create and start a new
489 transaction with kwargs.
491 This is the entry point to manual tracing instrumentation.
493 A tree structure can be built by adding child spans to the transaction,
494 and child spans to other spans. To start a new child span within the
495 transaction or any span, call the respective `.start_child()` method.
497 Every child span must be finished before the transaction is finished,
498 otherwise the unfinished spans are discarded.
500 When used as context managers, spans and transactions are automatically
501 finished at the end of the `with` block. If not using context managers,
502 call the `.finish()` method.
504 When the transaction is finished, it will be sent to Sentry with all its
505 finished child spans.
506 """
507 custom_sampling_context = kwargs.pop("custom_sampling_context", {})
509 # if we haven't been given a transaction, make one
510 if transaction is None:
511 kwargs.setdefault("hub", self)
512 transaction = Transaction(**kwargs)
514 # use traces_sample_rate, traces_sampler, and/or inheritance to make a
515 # sampling decision
516 sampling_context = {
517 "transaction_context": transaction.to_json(),
518 "parent_sampled": transaction.parent_sampled,
519 }
520 sampling_context.update(custom_sampling_context)
521 transaction._set_initial_sampling_decision(sampling_context=sampling_context)
523 # we don't bother to keep spans if we already know we're not going to
524 # send the transaction
525 if transaction.sampled:
526 max_spans = (
527 self.client and self.client.options["_experiments"].get("max_spans")
528 ) or 1000
529 transaction.init_span_recorder(maxlen=max_spans)
531 return transaction
533 @overload
534 def push_scope( # noqa: F811
535 self, callback=None # type: Optional[None]
536 ):
537 # type: (...) -> ContextManager[Scope]
538 pass
540 @overload
541 def push_scope( # noqa: F811
542 self, callback # type: Callable[[Scope], None]
543 ):
544 # type: (...) -> None
545 pass
547 def push_scope( # noqa
548 self, callback=None # type: Optional[Callable[[Scope], None]]
549 ):
550 # type: (...) -> Optional[ContextManager[Scope]]
551 """
552 Pushes a new layer on the scope stack.
554 :param callback: If provided, this method pushes a scope, calls
555 `callback`, and pops the scope again.
557 :returns: If no `callback` is provided, a context manager that should
558 be used to pop the scope again.
559 """
560 if callback is not None:
561 with self.push_scope() as scope:
562 callback(scope)
563 return None
565 client, scope = self._stack[-1]
566 new_layer = (client, copy.copy(scope))
567 self._stack.append(new_layer)
569 return _ScopeManager(self)
571 def pop_scope_unsafe(self):
572 # type: () -> Tuple[Optional[Client], Scope]
573 """
574 Pops a scope layer from the stack.
576 Try to use the context manager :py:meth:`push_scope` instead.
577 """
578 rv = self._stack.pop()
579 assert self._stack, "stack must have at least one layer"
580 return rv
582 @overload
583 def configure_scope( # noqa: F811
584 self, callback=None # type: Optional[None]
585 ):
586 # type: (...) -> ContextManager[Scope]
587 pass
589 @overload
590 def configure_scope( # noqa: F811
591 self, callback # type: Callable[[Scope], None]
592 ):
593 # type: (...) -> None
594 pass
596 def configure_scope( # noqa
597 self, callback=None # type: Optional[Callable[[Scope], None]]
598 ): # noqa
599 # type: (...) -> Optional[ContextManager[Scope]]
601 """
602 Reconfigures the scope.
604 :param callback: If provided, call the callback with the current scope.
606 :returns: If no callback is provided, returns a context manager that returns the scope.
607 """
609 client, scope = self._stack[-1]
610 if callback is not None: 610 ↛ 611line 610 didn't jump to line 611, because the condition on line 610 was never true
611 if client is not None:
612 callback(scope)
614 return None
616 @contextmanager
617 def inner():
618 # type: () -> Generator[Scope, None, None]
619 if client is not None: 619 ↛ 622line 619 didn't jump to line 622, because the condition on line 619 was never false
620 yield scope
621 else:
622 yield Scope()
624 return inner()
626 def start_session(
627 self, session_mode="application" # type: str
628 ):
629 # type: (...) -> None
630 """Starts a new session."""
631 self.end_session()
632 client, scope = self._stack[-1]
633 scope._session = Session(
634 release=client.options["release"] if client else None,
635 environment=client.options["environment"] if client else None,
636 user=scope._user,
637 session_mode=session_mode,
638 )
640 def end_session(self):
641 # type: (...) -> None
642 """Ends the current session if there is one."""
643 client, scope = self._stack[-1]
644 session = scope._session
645 self.scope._session = None
647 if session is not None:
648 session.close()
649 if client is not None:
650 client.capture_session(session)
652 def stop_auto_session_tracking(self):
653 # type: (...) -> None
654 """Stops automatic session tracking.
656 This temporarily session tracking for the current scope when called.
657 To resume session tracking call `resume_auto_session_tracking`.
658 """
659 self.end_session()
660 client, scope = self._stack[-1]
661 scope._force_auto_session_tracking = False
663 def resume_auto_session_tracking(self):
664 # type: (...) -> None
665 """Resumes automatic session tracking for the current scope if
666 disabled earlier. This requires that generally automatic session
667 tracking is enabled.
668 """
669 client, scope = self._stack[-1]
670 scope._force_auto_session_tracking = None
672 def flush(
673 self,
674 timeout=None, # type: Optional[float]
675 callback=None, # type: Optional[Callable[[int, float], None]]
676 ):
677 # type: (...) -> None
678 """
679 Alias for :py:meth:`sentry_sdk.Client.flush`
680 """
681 client, scope = self._stack[-1]
682 if client is not None:
683 return client.flush(timeout=timeout, callback=callback)
685 def iter_trace_propagation_headers(self, span=None):
686 # type: (Optional[Span]) -> Generator[Tuple[str, str], None, None]
687 """
688 Return HTTP headers which allow propagation of trace data. Data taken
689 from the span representing the request, if available, or the current
690 span on the scope if not.
691 """
692 span = span or self.scope.span
693 if not span: 693 ↛ 694line 693 didn't jump to line 694, because the condition on line 693 was never true
694 return
696 client = self._stack[-1][0]
698 propagate_traces = client and client.options["propagate_traces"]
699 if not propagate_traces: 699 ↛ 700line 699 didn't jump to line 700, because the condition on line 699 was never true
700 return
702 for header in span.iter_headers():
703 yield header
706GLOBAL_HUB = Hub()
707_local.set(GLOBAL_HUB)