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
« 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
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
27from sentry_sdk._types import MYPY
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
35 from sentry_sdk.scope import Scope
36 from sentry_sdk._types import Event, Hint
37 from sentry_sdk.session import Session
40_client_init_debug = ContextVar("client_init_debug")
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
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")
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
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
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")
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()
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"
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()
76 return rv
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 """
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()
91 def __getstate__(self):
92 # type: () -> Any
93 return {"options": self.options}
95 def __setstate__(self, state):
96 # type: (Any) -> None
97 self.options = state["options"]
98 self._init_impl()
100 def _init_impl(self):
101 # type: () -> None
102 old_debug = _client_init_debug.get(False)
104 def _capture_envelope(envelope):
105 # type: (Envelope) -> None
106 if self.transport is not None:
107 self.transport.capture_envelope(envelope)
109 try:
110 _client_init_debug.set(self.options["debug"])
111 self.transport = make_transport(self.options)
113 self.session_flusher = SessionFlusher(capture_func=_capture_envelope)
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 )
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)
133 @property
134 def dsn(self):
135 # type: () -> Optional[str]
136 """Returns the configured DSN as string."""
137 return self.options["dsn"]
139 def _prepare_event(
140 self,
141 event, # type: Event
142 hint, # type: Hint
143 scope, # type: Optional[Scope]
144 ):
145 # type: (...) -> Optional[Event]
147 if event.get("timestamp") is None:
148 event["timestamp"] = datetime.utcnow()
150 if scope is not None:
151 is_transaction = event.get("type") == "transaction"
152 event_ = scope.apply_to_event(event, hint)
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
163 event = event_
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 }
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
192 if event.get("platform") is None:
193 event["platform"] = "python"
195 event = handle_in_app(
196 event, self.options["in_app_exclude"], self.options["in_app_include"]
197 )
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 )
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
222 return event
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
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)
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
244 return False
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
258 ignoring_prevents_recursion = scope is not None and not scope._should_capture
259 if ignoring_prevents_recursion:
260 return False
262 ignored_by_config_option = self._is_ignored_error(event, hint)
263 if ignored_by_config_option:
264 return False
266 return True
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")
282 return False
284 return True
286 def _update_session_from_event(
287 self,
288 session, # type: Session
289 event, # type: Event
290 ):
291 # type: (...) -> None
293 crashed = False
294 errored = False
295 user_agent = None
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
306 user = event.get("user")
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
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 )
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.
331 :param event: A ready-made event that can be directly sent to Sentry.
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.
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
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
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
352 event_opt = self._prepare_event(event, hint, scope)
353 if event_opt is None:
354 return None
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)
362 is_transaction = event_opt.get("type") == "transaction"
364 if not is_transaction and not self._should_sample_error(event):
365 return None
367 attachments = hint.get("attachments")
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 )
376 dynamic_sampling_context = (
377 event_opt.get("contexts", {})
378 .get("trace", {})
379 .pop("dynamic_sampling_context", {})
380 )
382 # Transactions or events with attachments should go to the /envelope/
383 # endpoint.
384 if is_transaction or attachments:
386 headers = {
387 "event_id": event_opt["event_id"],
388 "sent_at": format_timestamp(datetime.utcnow()),
389 }
391 if has_tracestate_enabled():
392 tracestate_data = raw_tracestate and reinflate_tracestate(
393 raw_tracestate.replace("sentry=", "")
394 )
396 if tracestate_data:
397 headers["trace"] = tracestate_data
398 elif dynamic_sampling_context:
399 headers["trace"] = dynamic_sampling_context
401 envelope = Envelope(headers=headers)
403 if is_transaction:
404 envelope.add_transaction(event_opt)
405 else:
406 envelope.add_event(event_opt)
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
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)
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
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.
450 :param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
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)
460 def __enter__(self):
461 # type: () -> _Client
462 return self
464 def __exit__(self, exc_type, exc_value, tb):
465 # type: (Any, Any, Any) -> None
466 self.close()
469from sentry_sdk._types import MYPY
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.
478 class get_options(ClientConstructor, Dict[str, Any]): # noqa: N801
479 pass
481 class Client(ClientConstructor, _Client):
482 pass
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).
490 get_options = (lambda: _get_options)()
491 Client = (lambda: _Client)()