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

1import copy 

2import sys 

3 

4from datetime import datetime 

5from contextlib import contextmanager 

6 

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) 

18 

19from sentry_sdk._types import MYPY 

20 

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 

34 

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 

44 

45 T = TypeVar("T") 

46 

47else: 

48 

49 def overload(x): 

50 # type: (T) -> T 

51 return x 

52 

53 

54_local = ContextVar("sentry_current_hub") 

55 

56 

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 

73 

74 

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"] 

81 

82 

83class _InitGuard(object): 

84 def __init__(self, client): 

85 # type: (Client) -> None 

86 self._client = client 

87 

88 def __enter__(self): 

89 # type: () -> _InitGuard 

90 return self 

91 

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() 

97 

98 

99def _init(*args, **kwargs): 

100 # type: (*Optional[str], **Any) -> ContextManager[Any] 

101 """Initializes the SDK and optionally integrations. 

102 

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 

109 

110 

111from sentry_sdk._types import MYPY 

112 

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. 

119 

120 class init(ClientConstructor, _InitGuard): # noqa: N801 

121 pass 

122 

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). 

127 

128 init = (lambda: _init)() 

129 

130 

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 

141 

142 @property 

143 def main(cls): 

144 # type: () -> Hub 

145 """Returns the main instance of the hub.""" 

146 return GLOBAL_HUB 

147 

148 

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] 

155 

156 def __enter__(self): 

157 # type: () -> Scope 

158 scope = self._layer[1] 

159 assert scope is not None 

160 return scope 

161 

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 ) 

177 

178 layer = self._hub._stack[self._original_len - 1] 

179 del self._hub._stack[self._original_len - 1 :] 

180 

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) 

195 

196 

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. 

201 

202 If the hub is used with a with statement it's temporarily activated. 

203 """ 

204 

205 _stack = None # type: List[Tuple[Optional[Client], Scope]] 

206 

207 # Mypy doesn't pick up on the metaclass. 

208 

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 

212 

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() 

228 

229 self._stack = [(client, scope)] 

230 self._last_event_id = None # type: Optional[str] 

231 self._old_hubs = [] # type: List[Hub] 

232 

233 def __enter__(self): 

234 # type: () -> Hub 

235 self._old_hubs.append(Hub.current) 

236 _local.set(self) 

237 return self 

238 

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) 

248 

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() 

258 

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. 

266 

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") 

276 

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 

282 

283 @property 

284 def client(self): 

285 # type: () -> Optional[Client] 

286 """Returns the current client on the hub.""" 

287 return self._stack[-1][0] 

288 

289 @property 

290 def scope(self): 

291 # type: () -> Scope 

292 """Returns the current scope on the hub.""" 

293 return self._stack[-1][1] 

294 

295 def last_event_id(self): 

296 # type: () -> Optional[str] 

297 """Returns the last event ID.""" 

298 return self._last_event_id 

299 

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]) 

307 

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 

326 

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`. 

337 

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 ) 

347 

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. 

356 

357 :param error: An exception to catch. If `None`, `sys.exc_info()` will be used. 

358 

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() 

368 

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()) 

374 

375 return None 

376 

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. 

384 

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) 

388 

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. 

398 

399 :param crumb: Dictionary with the data as the sentry v7/v8 protocol expects. 

400 

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 

408 

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 

413 

414 hint = dict(hint or ()) # type: Hint 

415 

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" 

420 

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 

425 

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) 

430 

431 max_breadcrumbs = client.options["max_breadcrumbs"] # type: int 

432 while len(scope._breadcrumbs) > max_breadcrumbs: 

433 scope._breadcrumbs.popleft() 

434 

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. 

446 

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) 

467 

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 

470 

471 kwargs.setdefault("hub", self) 

472 

473 span = self.scope.span 

474 if span is not None: 

475 return span.start_child(**kwargs) 

476 

477 return Span(**kwargs) 

478 

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. 

487 

488 Start an existing transaction if given, otherwise create and start a new 

489 transaction with kwargs. 

490 

491 This is the entry point to manual tracing instrumentation. 

492 

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. 

496 

497 Every child span must be finished before the transaction is finished, 

498 otherwise the unfinished spans are discarded. 

499 

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. 

503 

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", {}) 

508 

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) 

513 

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) 

522 

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) 

530 

531 return transaction 

532 

533 @overload 

534 def push_scope( # noqa: F811 

535 self, callback=None # type: Optional[None] 

536 ): 

537 # type: (...) -> ContextManager[Scope] 

538 pass 

539 

540 @overload 

541 def push_scope( # noqa: F811 

542 self, callback # type: Callable[[Scope], None] 

543 ): 

544 # type: (...) -> None 

545 pass 

546 

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. 

553 

554 :param callback: If provided, this method pushes a scope, calls 

555 `callback`, and pops the scope again. 

556 

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 

564 

565 client, scope = self._stack[-1] 

566 new_layer = (client, copy.copy(scope)) 

567 self._stack.append(new_layer) 

568 

569 return _ScopeManager(self) 

570 

571 def pop_scope_unsafe(self): 

572 # type: () -> Tuple[Optional[Client], Scope] 

573 """ 

574 Pops a scope layer from the stack. 

575 

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 

581 

582 @overload 

583 def configure_scope( # noqa: F811 

584 self, callback=None # type: Optional[None] 

585 ): 

586 # type: (...) -> ContextManager[Scope] 

587 pass 

588 

589 @overload 

590 def configure_scope( # noqa: F811 

591 self, callback # type: Callable[[Scope], None] 

592 ): 

593 # type: (...) -> None 

594 pass 

595 

596 def configure_scope( # noqa 

597 self, callback=None # type: Optional[Callable[[Scope], None]] 

598 ): # noqa 

599 # type: (...) -> Optional[ContextManager[Scope]] 

600 

601 """ 

602 Reconfigures the scope. 

603 

604 :param callback: If provided, call the callback with the current scope. 

605 

606 :returns: If no callback is provided, returns a context manager that returns the scope. 

607 """ 

608 

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) 

613 

614 return None 

615 

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() 

623 

624 return inner() 

625 

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 ) 

639 

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 

646 

647 if session is not None: 

648 session.close() 

649 if client is not None: 

650 client.capture_session(session) 

651 

652 def stop_auto_session_tracking(self): 

653 # type: (...) -> None 

654 """Stops automatic session tracking. 

655 

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 

662 

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 

671 

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) 

684 

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 

695 

696 client = self._stack[-1][0] 

697 

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 

701 

702 for header in span.iter_headers(): 

703 yield header 

704 

705 

706GLOBAL_HUB = Hub() 

707_local.set(GLOBAL_HUB)