334 lines
11 KiB
Python
334 lines
11 KiB
Python
import os
|
|
import uuid
|
|
import random
|
|
from datetime import datetime
|
|
import socket
|
|
|
|
from sentry_sdk._compat import string_types, text_type, iteritems
|
|
from sentry_sdk.utils import (
|
|
handle_in_app,
|
|
get_type_name,
|
|
capture_internal_exceptions,
|
|
current_stacktrace,
|
|
logger,
|
|
)
|
|
from sentry_sdk.serializer import Serializer
|
|
from sentry_sdk.transport import make_transport
|
|
from sentry_sdk.consts import DEFAULT_OPTIONS, SDK_INFO, ClientConstructor
|
|
from sentry_sdk.integrations import setup_integrations
|
|
from sentry_sdk.utils import ContextVar
|
|
|
|
from sentry_sdk._types import MYPY
|
|
|
|
if MYPY:
|
|
from typing import Any
|
|
from typing import Callable
|
|
from typing import Dict
|
|
from typing import Optional
|
|
|
|
from sentry_sdk.scope import Scope
|
|
from sentry_sdk._types import Event, Hint
|
|
|
|
|
|
_client_init_debug = ContextVar("client_init_debug")
|
|
_client_in_capture_event = ContextVar("client_in_capture_event")
|
|
|
|
|
|
def _get_options(*args, **kwargs):
|
|
# type: (*Optional[str], **Any) -> Dict[str, Any]
|
|
if args and (isinstance(args[0], (text_type, bytes, str)) or args[0] is None):
|
|
dsn = args[0] # type: Optional[str]
|
|
args = args[1:]
|
|
else:
|
|
dsn = None
|
|
|
|
rv = dict(DEFAULT_OPTIONS)
|
|
options = dict(*args, **kwargs) # type: ignore
|
|
if dsn is not None and options.get("dsn") is None:
|
|
options["dsn"] = dsn # type: ignore
|
|
|
|
for key, value in iteritems(options):
|
|
if key not in rv:
|
|
raise TypeError("Unknown option %r" % (key,))
|
|
rv[key] = value
|
|
|
|
if rv["dsn"] is None:
|
|
rv["dsn"] = os.environ.get("SENTRY_DSN")
|
|
|
|
if rv["release"] is None:
|
|
rv["release"] = os.environ.get("SENTRY_RELEASE")
|
|
|
|
if rv["environment"] is None:
|
|
rv["environment"] = os.environ.get("SENTRY_ENVIRONMENT")
|
|
|
|
if rv["server_name"] is None and hasattr(socket, "gethostname"):
|
|
rv["server_name"] = socket.gethostname()
|
|
|
|
return rv # type: ignore
|
|
|
|
|
|
class _Client(object):
|
|
"""The client is internally responsible for capturing the events and
|
|
forwarding them to sentry through the configured transport. It takes
|
|
the client options as keyword arguments and optionally the DSN as first
|
|
argument.
|
|
"""
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
# type: (*Any, **Any) -> None
|
|
self.options = get_options(*args, **kwargs) # type: Dict[str, Any]
|
|
self._init_impl()
|
|
|
|
def __getstate__(self):
|
|
# type: () -> Any
|
|
return {"options": self.options}
|
|
|
|
def __setstate__(self, state):
|
|
# type: (Any) -> None
|
|
self.options = state["options"]
|
|
self._init_impl()
|
|
|
|
def _init_impl(self):
|
|
# type: () -> None
|
|
old_debug = _client_init_debug.get(False)
|
|
try:
|
|
_client_init_debug.set(self.options["debug"])
|
|
self.transport = make_transport(self.options)
|
|
|
|
request_bodies = ("always", "never", "small", "medium")
|
|
if self.options["request_bodies"] not in request_bodies:
|
|
raise ValueError(
|
|
"Invalid value for request_bodies. Must be one of {}".format(
|
|
request_bodies
|
|
)
|
|
)
|
|
|
|
self.integrations = setup_integrations(
|
|
self.options["integrations"],
|
|
with_defaults=self.options["default_integrations"],
|
|
)
|
|
finally:
|
|
_client_init_debug.set(old_debug)
|
|
|
|
@property
|
|
def dsn(self):
|
|
# type: () -> Optional[str]
|
|
"""Returns the configured DSN as string."""
|
|
return self.options["dsn"]
|
|
|
|
def _prepare_event(
|
|
self,
|
|
event, # type: Event
|
|
hint, # type: Optional[Hint]
|
|
scope, # type: Optional[Scope]
|
|
):
|
|
# type: (...) -> Optional[Event]
|
|
if event.get("timestamp") is None:
|
|
event["timestamp"] = datetime.utcnow()
|
|
|
|
hint = dict(hint or ()) # type: Hint
|
|
|
|
if scope is not None:
|
|
event_ = scope.apply_to_event(event, hint)
|
|
if event_ is None:
|
|
return None
|
|
event = event_
|
|
|
|
if (
|
|
self.options["attach_stacktrace"]
|
|
and "exception" not in event
|
|
and "stacktrace" not in event
|
|
and "threads" not in event
|
|
):
|
|
with capture_internal_exceptions():
|
|
event["threads"] = {
|
|
"values": [
|
|
{
|
|
"stacktrace": current_stacktrace(
|
|
self.options["with_locals"]
|
|
),
|
|
"crashed": False,
|
|
"current": True,
|
|
}
|
|
]
|
|
}
|
|
|
|
for key in "release", "environment", "server_name", "dist":
|
|
if event.get(key) is None and self.options[key] is not None: # type: ignore
|
|
event[key] = text_type(self.options[key]).strip() # type: ignore
|
|
if event.get("sdk") is None:
|
|
sdk_info = dict(SDK_INFO)
|
|
sdk_info["integrations"] = sorted(self.integrations.keys())
|
|
event["sdk"] = sdk_info
|
|
|
|
if event.get("platform") is None:
|
|
event["platform"] = "python"
|
|
|
|
event = handle_in_app(
|
|
event, self.options["in_app_exclude"], self.options["in_app_include"]
|
|
)
|
|
|
|
# Postprocess the event here so that annotated types do
|
|
# generally not surface in before_send
|
|
if event is not None:
|
|
event = Serializer().serialize_event(event)
|
|
|
|
before_send = self.options["before_send"]
|
|
if before_send is not None:
|
|
new_event = None
|
|
with capture_internal_exceptions():
|
|
new_event = before_send(event, hint or {})
|
|
if new_event is None:
|
|
logger.info("before send dropped event (%s)", event)
|
|
event = new_event # type: ignore
|
|
|
|
return event
|
|
|
|
def _is_ignored_error(self, event, hint):
|
|
# type: (Event, Hint) -> bool
|
|
exc_info = hint.get("exc_info")
|
|
if exc_info is None:
|
|
return False
|
|
|
|
type_name = get_type_name(exc_info[0])
|
|
full_name = "%s.%s" % (exc_info[0].__module__, type_name)
|
|
|
|
for errcls in self.options["ignore_errors"]:
|
|
# String types are matched against the type name in the
|
|
# exception only
|
|
if isinstance(errcls, string_types):
|
|
if errcls == full_name or errcls == type_name:
|
|
return True
|
|
else:
|
|
if issubclass(exc_info[0], errcls): # type: ignore
|
|
return True
|
|
|
|
return False
|
|
|
|
def _should_capture(
|
|
self,
|
|
event, # type: Event
|
|
hint, # type: Hint
|
|
scope=None, # type: Optional[Scope]
|
|
):
|
|
# type: (...) -> bool
|
|
if scope is not None and not scope._should_capture:
|
|
return False
|
|
|
|
if (
|
|
self.options["sample_rate"] < 1.0
|
|
and random.random() >= self.options["sample_rate"]
|
|
):
|
|
return False
|
|
|
|
if self._is_ignored_error(event, hint):
|
|
return False
|
|
|
|
return True
|
|
|
|
def capture_event(
|
|
self,
|
|
event, # type: Event
|
|
hint=None, # type: Optional[Hint]
|
|
scope=None, # type: Optional[Scope]
|
|
):
|
|
# type: (...) -> Optional[str]
|
|
"""Captures an event.
|
|
|
|
:param event: A ready-made event that can be directly sent to Sentry.
|
|
|
|
: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.
|
|
|
|
: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.
|
|
"""
|
|
is_recursive = _client_in_capture_event.get(False)
|
|
if is_recursive:
|
|
return None
|
|
|
|
_client_in_capture_event.set(True)
|
|
|
|
try:
|
|
if self.transport is None:
|
|
return None
|
|
if hint is None:
|
|
hint = {}
|
|
event_id = event.get("event_id")
|
|
if event_id is None:
|
|
event["event_id"] = event_id = uuid.uuid4().hex
|
|
if not self._should_capture(event, hint, scope):
|
|
return None
|
|
event_opt = self._prepare_event(event, hint, scope)
|
|
if event_opt is None:
|
|
return None
|
|
self.transport.capture_event(event_opt)
|
|
return event_id
|
|
finally:
|
|
_client_in_capture_event.set(False)
|
|
|
|
def close(
|
|
self,
|
|
timeout=None, # type: Optional[float]
|
|
callback=None, # type: Optional[Callable[[int, float], None]]
|
|
):
|
|
# type: (...) -> None
|
|
"""
|
|
Close the client and shut down the transport. Arguments have the same
|
|
semantics as :py:meth:`Client.flush`.
|
|
"""
|
|
if self.transport is not None:
|
|
self.flush(timeout=timeout, callback=callback)
|
|
self.transport.kill()
|
|
self.transport = None
|
|
|
|
def flush(
|
|
self,
|
|
timeout=None, # type: Optional[float]
|
|
callback=None, # type: Optional[Callable[[int, float], None]]
|
|
):
|
|
# type: (...) -> None
|
|
"""
|
|
Wait for the current events to be sent.
|
|
|
|
:param timeout: Wait for at most `timeout` seconds. If no `timeout` is provided, the `shutdown_timeout` option value is used.
|
|
|
|
:param callback: Is invoked with the number of pending events and the configured timeout.
|
|
"""
|
|
if self.transport is not None:
|
|
if timeout is None:
|
|
timeout = self.options["shutdown_timeout"]
|
|
self.transport.flush(timeout=timeout, callback=callback)
|
|
|
|
def __enter__(self):
|
|
# type: () -> _Client
|
|
return self
|
|
|
|
def __exit__(self, exc_type, exc_value, tb):
|
|
# type: (Any, Any, Any) -> None
|
|
self.close()
|
|
|
|
|
|
from sentry_sdk._types import MYPY
|
|
|
|
if MYPY:
|
|
# Make mypy, PyCharm and other static analyzers think `get_options` is a
|
|
# type to have nicer autocompletion for params.
|
|
#
|
|
# Use `ClientConstructor` to define the argument types of `init` and
|
|
# `Dict[str, Any]` to tell static analyzers about the return type.
|
|
|
|
class get_options(ClientConstructor, Dict[str, Any]):
|
|
pass
|
|
|
|
class Client(ClientConstructor, _Client):
|
|
pass
|
|
|
|
|
|
else:
|
|
# Alias `get_options` for actual usage. Go through the lambda indirection
|
|
# to throw PyCharm off of the weakly typed signature (it would otherwise
|
|
# discover both the weakly typed signature of `_init` and our faked `init`
|
|
# type).
|
|
|
|
get_options = (lambda: _get_options)()
|
|
Client = (lambda: _Client)()
|