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