Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/sentry_sdk/client.py: 24%

234 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1import os 

2import uuid 

3import random 

4from datetime import datetime 

5import socket 

6 

7from sentry_sdk._compat import string_types, text_type, iteritems 

8from sentry_sdk.utils import ( 

9 capture_internal_exceptions, 

10 current_stacktrace, 

11 disable_capture_event, 

12 format_timestamp, 

13 get_type_name, 

14 get_default_release, 

15 handle_in_app, 

16 logger, 

17) 

18from sentry_sdk.serializer import serialize 

19from sentry_sdk.transport import make_transport 

20from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO, ClientConstructor 

21from sentry_sdk.integrations import setup_integrations 

22from sentry_sdk.utils import ContextVar 

23from sentry_sdk.sessions import SessionFlusher 

24from sentry_sdk.envelope import Envelope 

25from sentry_sdk.tracing_utils import has_tracestate_enabled, reinflate_tracestate 

26 

27from sentry_sdk._types import MYPY 

28 

29if MYPY: 29 ↛ 30line 29 didn't jump to line 30, because the condition on line 29 was never true

30 from typing import Any 

31 from typing import Callable 

32 from typing import Dict 

33 from typing import Optional 

34 

35 from sentry_sdk.scope import Scope 

36 from sentry_sdk._types import Event, Hint 

37 from sentry_sdk.session import Session 

38 

39 

40_client_init_debug = ContextVar("client_init_debug") 

41 

42 

43def _get_options(*args, **kwargs): 

44 # type: (*Optional[str], **Any) -> Dict[str, Any] 

45 if args and (isinstance(args[0], (text_type, bytes, str)) or args[0] is None): 45 ↛ 46line 45 didn't jump to line 46, because the condition on line 45 was never true

46 dsn = args[0] # type: Optional[str] 

47 args = args[1:] 

48 else: 

49 dsn = None 

50 

51 if len(args) > 1: 51 ↛ 52line 51 didn't jump to line 52, because the condition on line 51 was never true

52 raise TypeError("Only single positional argument is expected") 

53 

54 rv = dict(DEFAULT_OPTIONS) 

55 options = dict(*args, **kwargs) 

56 if dsn is not None and options.get("dsn") is None: 56 ↛ 57line 56 didn't jump to line 57, because the condition on line 56 was never true

57 options["dsn"] = dsn 

58 

59 for key, value in iteritems(options): 

60 if key not in rv: 60 ↛ 61line 60 didn't jump to line 61, because the condition on line 60 was never true

61 raise TypeError("Unknown option %r" % (key,)) 

62 rv[key] = value 

63 

64 if rv["dsn"] is None: 64 ↛ 65line 64 didn't jump to line 65, because the condition on line 64 was never true

65 rv["dsn"] = os.environ.get("SENTRY_DSN") 

66 

67 if rv["release"] is None: 67 ↛ 70line 67 didn't jump to line 70, because the condition on line 67 was never false

68 rv["release"] = get_default_release() 

69 

70 if rv["environment"] is None: 70 ↛ 71line 70 didn't jump to line 71, because the condition on line 70 was never true

71 rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT") or "production" 

72 

73 if rv["server_name"] is None and hasattr(socket, "gethostname"): 73 ↛ 76line 73 didn't jump to line 76, because the condition on line 73 was never false

74 rv["server_name"] = socket.gethostname() 

75 

76 return rv 

77 

78 

79class _Client(object): 

80 """The client is internally responsible for capturing the events and 

81 forwarding them to sentry through the configured transport. It takes 

82 the client options as keyword arguments and optionally the DSN as first 

83 argument. 

84 """ 

85 

86 def __init__(self, *args, **kwargs): 

87 # type: (*Any, **Any) -> None 

88 self.options = get_options(*args, **kwargs) # type: Dict[str, Any] 

89 self._init_impl() 

90 

91 def __getstate__(self): 

92 # type: () -> Any 

93 return {"options": self.options} 

94 

95 def __setstate__(self, state): 

96 # type: (Any) -> None 

97 self.options = state["options"] 

98 self._init_impl() 

99 

100 def _init_impl(self): 

101 # type: () -> None 

102 old_debug = _client_init_debug.get(False) 

103 

104 def _capture_envelope(envelope): 

105 # type: (Envelope) -> None 

106 if self.transport is not None: 

107 self.transport.capture_envelope(envelope) 

108 

109 try: 

110 _client_init_debug.set(self.options["debug"]) 

111 self.transport = make_transport(self.options) 

112 

113 self.session_flusher = SessionFlusher(capture_func=_capture_envelope) 

114 

115 request_bodies = ("always", "never", "small", "medium") 

116 if self.options["request_bodies"] not in request_bodies: 116 ↛ 117line 116 didn't jump to line 117, because the condition on line 116 was never true

117 raise ValueError( 

118 "Invalid value for request_bodies. Must be one of {}".format( 

119 request_bodies 

120 ) 

121 ) 

122 

123 self.integrations = setup_integrations( 

124 self.options["integrations"], 

125 with_defaults=self.options["default_integrations"], 

126 with_auto_enabling_integrations=self.options[ 

127 "auto_enabling_integrations" 

128 ], 

129 ) 

130 finally: 

131 _client_init_debug.set(old_debug) 

132 

133 @property 

134 def dsn(self): 

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

136 """Returns the configured DSN as string.""" 

137 return self.options["dsn"] 

138 

139 def _prepare_event( 

140 self, 

141 event, # type: Event 

142 hint, # type: Hint 

143 scope, # type: Optional[Scope] 

144 ): 

145 # type: (...) -> Optional[Event] 

146 

147 if event.get("timestamp") is None: 

148 event["timestamp"] = datetime.utcnow() 

149 

150 if scope is not None: 

151 is_transaction = event.get("type") == "transaction" 

152 event_ = scope.apply_to_event(event, hint) 

153 

154 # one of the event/error processors returned None 

155 if event_ is None: 

156 if self.transport: 

157 self.transport.record_lost_event( 

158 "event_processor", 

159 data_category=("transaction" if is_transaction else "error"), 

160 ) 

161 return None 

162 

163 event = event_ 

164 

165 if ( 

166 self.options["attach_stacktrace"] 

167 and "exception" not in event 

168 and "stacktrace" not in event 

169 and "threads" not in event 

170 ): 

171 with capture_internal_exceptions(): 

172 event["threads"] = { 

173 "values": [ 

174 { 

175 "stacktrace": current_stacktrace( 

176 self.options["with_locals"] 

177 ), 

178 "crashed": False, 

179 "current": True, 

180 } 

181 ] 

182 } 

183 

184 for key in "release", "environment", "server_name", "dist": 

185 if event.get(key) is None and self.options[key] is not None: 

186 event[key] = text_type(self.options[key]).strip() 

187 if event.get("sdk") is None: 

188 sdk_info = dict(SDK_INFO) 

189 sdk_info["integrations"] = sorted(self.integrations.keys()) 

190 event["sdk"] = sdk_info 

191 

192 if event.get("platform") is None: 

193 event["platform"] = "python" 

194 

195 event = handle_in_app( 

196 event, self.options["in_app_exclude"], self.options["in_app_include"] 

197 ) 

198 

199 # Postprocess the event here so that annotated types do 

200 # generally not surface in before_send 

201 if event is not None: 

202 event = serialize( 

203 event, 

204 smart_transaction_trimming=self.options["_experiments"].get( 

205 "smart_transaction_trimming" 

206 ), 

207 ) 

208 

209 before_send = self.options["before_send"] 

210 if before_send is not None and event.get("type") != "transaction": 

211 new_event = None 

212 with capture_internal_exceptions(): 

213 new_event = before_send(event, hint or {}) 

214 if new_event is None: 

215 logger.info("before send dropped event (%s)", event) 

216 if self.transport: 

217 self.transport.record_lost_event( 

218 "before_send", data_category="error" 

219 ) 

220 event = new_event # type: ignore 

221 

222 return event 

223 

224 def _is_ignored_error(self, event, hint): 

225 # type: (Event, Hint) -> bool 

226 exc_info = hint.get("exc_info") 

227 if exc_info is None: 

228 return False 

229 

230 error = exc_info[0] 

231 error_type_name = get_type_name(exc_info[0]) 

232 error_full_name = "%s.%s" % (exc_info[0].__module__, error_type_name) 

233 

234 for ignored_error in self.options["ignore_errors"]: 

235 # String types are matched against the type name in the 

236 # exception only 

237 if isinstance(ignored_error, string_types): 

238 if ignored_error == error_full_name or ignored_error == error_type_name: 

239 return True 

240 else: 

241 if issubclass(error, ignored_error): 

242 return True 

243 

244 return False 

245 

246 def _should_capture( 

247 self, 

248 event, # type: Event 

249 hint, # type: Hint 

250 scope=None, # type: Optional[Scope] 

251 ): 

252 # type: (...) -> bool 

253 # Transactions are sampled independent of error events. 

254 is_transaction = event.get("type") == "transaction" 

255 if is_transaction: 

256 return True 

257 

258 ignoring_prevents_recursion = scope is not None and not scope._should_capture 

259 if ignoring_prevents_recursion: 

260 return False 

261 

262 ignored_by_config_option = self._is_ignored_error(event, hint) 

263 if ignored_by_config_option: 

264 return False 

265 

266 return True 

267 

268 def _should_sample_error( 

269 self, 

270 event, # type: Event 

271 ): 

272 # type: (...) -> bool 

273 not_in_sample_rate = ( 

274 self.options["sample_rate"] < 1.0 

275 and random.random() >= self.options["sample_rate"] 

276 ) 

277 if not_in_sample_rate: 

278 # because we will not sample this event, record a "lost event". 

279 if self.transport: 

280 self.transport.record_lost_event("sample_rate", data_category="error") 

281 

282 return False 

283 

284 return True 

285 

286 def _update_session_from_event( 

287 self, 

288 session, # type: Session 

289 event, # type: Event 

290 ): 

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

292 

293 crashed = False 

294 errored = False 

295 user_agent = None 

296 

297 exceptions = (event.get("exception") or {}).get("values") 

298 if exceptions: 

299 errored = True 

300 for error in exceptions: 

301 mechanism = error.get("mechanism") 

302 if mechanism and mechanism.get("handled") is False: 

303 crashed = True 

304 break 

305 

306 user = event.get("user") 

307 

308 if session.user_agent is None: 

309 headers = (event.get("request") or {}).get("headers") 

310 for (k, v) in iteritems(headers or {}): 

311 if k.lower() == "user-agent": 

312 user_agent = v 

313 break 

314 

315 session.update( 

316 status="crashed" if crashed else None, 

317 user=user, 

318 user_agent=user_agent, 

319 errors=session.errors + (errored or crashed), 

320 ) 

321 

322 def capture_event( 

323 self, 

324 event, # type: Event 

325 hint=None, # type: Optional[Hint] 

326 scope=None, # type: Optional[Scope] 

327 ): 

328 # type: (...) -> Optional[str] 

329 """Captures an event. 

330 

331 :param event: A ready-made event that can be directly sent to Sentry. 

332 

333 :param hint: Contains metadata about the event that can be read from `before_send`, such as the original exception object or a HTTP request object. 

334 

335 :returns: An event ID. May be `None` if there is no DSN set or of if the SDK decided to discard the event for other reasons. In such situations setting `debug=True` on `init()` may help. 

336 """ 

337 if disable_capture_event.get(False): 

338 return None 

339 

340 if self.transport is None: 

341 return None 

342 if hint is None: 

343 hint = {} 

344 event_id = event.get("event_id") 

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

346 

347 if event_id is None: 

348 event["event_id"] = event_id = uuid.uuid4().hex 

349 if not self._should_capture(event, hint, scope): 

350 return None 

351 

352 event_opt = self._prepare_event(event, hint, scope) 

353 if event_opt is None: 

354 return None 

355 

356 # whenever we capture an event we also check if the session needs 

357 # to be updated based on that information. 

358 session = scope._session if scope else None 

359 if session: 

360 self._update_session_from_event(session, event) 

361 

362 is_transaction = event_opt.get("type") == "transaction" 

363 

364 if not is_transaction and not self._should_sample_error(event): 

365 return None 

366 

367 attachments = hint.get("attachments") 

368 

369 # this is outside of the `if` immediately below because even if we don't 

370 # use the value, we want to make sure we remove it before the event is 

371 # sent 

372 raw_tracestate = ( 

373 event_opt.get("contexts", {}).get("trace", {}).pop("tracestate", "") 

374 ) 

375 

376 dynamic_sampling_context = ( 

377 event_opt.get("contexts", {}) 

378 .get("trace", {}) 

379 .pop("dynamic_sampling_context", {}) 

380 ) 

381 

382 # Transactions or events with attachments should go to the /envelope/ 

383 # endpoint. 

384 if is_transaction or attachments: 

385 

386 headers = { 

387 "event_id": event_opt["event_id"], 

388 "sent_at": format_timestamp(datetime.utcnow()), 

389 } 

390 

391 if has_tracestate_enabled(): 

392 tracestate_data = raw_tracestate and reinflate_tracestate( 

393 raw_tracestate.replace("sentry=", "") 

394 ) 

395 

396 if tracestate_data: 

397 headers["trace"] = tracestate_data 

398 elif dynamic_sampling_context: 

399 headers["trace"] = dynamic_sampling_context 

400 

401 envelope = Envelope(headers=headers) 

402 

403 if is_transaction: 

404 envelope.add_transaction(event_opt) 

405 else: 

406 envelope.add_event(event_opt) 

407 

408 for attachment in attachments or (): 

409 envelope.add_item(attachment.to_envelope_item()) 

410 self.transport.capture_envelope(envelope) 

411 else: 

412 # All other events go to the /store/ endpoint. 

413 self.transport.capture_event(event_opt) 

414 return event_id 

415 

416 def capture_session( 

417 self, session # type: Session 

418 ): 

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

420 if not session.release: 

421 logger.info("Discarded session update because of missing release") 

422 else: 

423 self.session_flusher.add_session(session) 

424 

425 def close( 

426 self, 

427 timeout=None, # type: Optional[float] 

428 callback=None, # type: Optional[Callable[[int, float], None]] 

429 ): 

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

431 """ 

432 Close the client and shut down the transport. Arguments have the same 

433 semantics as :py:meth:`Client.flush`. 

434 """ 

435 if self.transport is not None: 

436 self.flush(timeout=timeout, callback=callback) 

437 self.session_flusher.kill() 

438 self.transport.kill() 

439 self.transport = None 

440 

441 def flush( 

442 self, 

443 timeout=None, # type: Optional[float] 

444 callback=None, # type: Optional[Callable[[int, float], None]] 

445 ): 

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

447 """ 

448 Wait for the current events to be sent. 

449 

450 :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used. 

451 

452 :param callback: Is invoked with the number of pending events and the configured timeout. 

453 """ 

454 if self.transport is not None: 

455 if timeout is None: 

456 timeout = self.options["shutdown_timeout"] 

457 self.session_flusher.flush() 

458 self.transport.flush(timeout=timeout, callback=callback) 

459 

460 def __enter__(self): 

461 # type: () -> _Client 

462 return self 

463 

464 def __exit__(self, exc_type, exc_value, tb): 

465 # type: (Any, Any, Any) -> None 

466 self.close() 

467 

468 

469from sentry_sdk._types import MYPY 

470 

471if MYPY: 471 ↛ 478line 471 didn't jump to line 478, because the condition on line 471 was never true

472 # Make mypy, PyCharm and other static analyzers think `get_options` is a 

473 # type to have nicer autocompletion for params. 

474 # 

475 # Use `ClientConstructor` to define the argument types of `init` and 

476 # `Dict[str, Any]` to tell static analyzers about the return type. 

477 

478 class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801 

479 pass 

480 

481 class Client(ClientConstructor, _Client): 

482 pass 

483 

484else: 

485 # Alias `get_options` for actual usage. Go through the lambda indirection 

486 # to throw PyCharm off of the weakly typed signature (it would otherwise 

487 # discover both the weakly typed signature of `_init` and our faked `init` 

488 # type). 

489 

490 get_options = (lambda: _get_options)() 

491 Client = (lambda: _Client)()