From 500e070ea199bf9f34f944a96ad0f7c70495c501 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Fri, 7 Sep 2018 12:36:34 +0200 Subject: [PATCH] feat: Added basic API docs (#57) --- Makefile | 5 ++++ README.md | 2 +- sentry_sdk/__init__.py | 27 +++++++++++++++++++ sentry_sdk/api.py | 40 ++++++++++++++--------------- sentry_sdk/hub.py | 31 +++++++++++++++++----- sentry_sdk/integrations/__init__.py | 39 ++++++++++++++++++---------- sentry_sdk/integrations/_wsgi.py | 13 +++++----- sentry_sdk/integrations/atexit.py | 5 ++++ sentry_sdk/integrations/celery.py | 8 +++--- sentry_sdk/integrations/dedupe.py | 6 ++--- sentry_sdk/integrations/flask.py | 6 ++--- sentry_sdk/integrations/logging.py | 16 +++++++----- tests/conftest.py | 8 +++--- tests/test_client.py | 6 ++--- 14 files changed, 142 insertions(+), 70 deletions(-) diff --git a/Makefile b/Makefile index 30aa6c822d..5d1dec52e2 100644 --- a/Makefile +++ b/Makefile @@ -24,3 +24,8 @@ tox-test: lint: @tox -e linters .PHONY: lint + +apidocs: + @pip install pdoc pygments + @pdoc --overwrite --html --html-dir build/apidocs sentry_sdk +.PHONY: apidocs diff --git a/README.md b/README.md index 6fb1e750e9..fe9f3aee5b 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ After initialization, you can capture exceptions like this: You can create a scope to attach data to all events happening inside of it: - with sentry_sdk.get_current_hub().push_scope(): + with sentry_sdk.Hub.current.push_scope(): with sentry_sdk.configure_scope() as scope: scope.transaction = "my_view_name" scope.set_tag("key", "value") diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index baccfa7e43..4b34d2e633 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -1,6 +1,33 @@ +""" +The Sentry SDK is the new-style SDK for [sentry.io](https://sentry.io/). It implements +the unified API that all modern SDKs follow for Python 2.7 and 3.5 or later. + +The user documentation can be found on [docs.sentry.io](https://docs.sentry.io/). + +## Quickstart + +The only thing to get going is to call `sentry_sdk.init()`. When not passed any +arguments the default options are used and the DSN is picked up from the `SENTRY_DSN` +environment variable. Otherwise the DSN can be passed with the `dsn` keyword +or first argument. + + import sentry_sdk + sentry_sdk.init() + +This initializes the default integrations which will automatically pick up any +uncaught exceptions. Additionally you can report arbitrary other exceptions: + + try: + my_failing_function() + except Exception as e: + sentry_sdk.capture_exception(e) +""" from .api import * # noqa from .api import __all__ # noqa +# modules we consider public +__all__.append("integrations") + # Initialize the debug support after everything is loaded from .debug import init_debug_support diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 46a3cdc662..ed0b665fae 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -1,13 +1,15 @@ +import inspect from contextlib import contextmanager from .hub import Hub from .scope import Scope from .utils import EventHint +from .transport import Transport, HttpTransport from .client import Client, get_options from .integrations import setup_integrations -__all__ = ["Hub", "Scope", "Client", "EventHint"] +__all__ = ["Hub", "Scope", "Client", "EventHint", "Transport", "HttpTransport"] def public(f): @@ -15,6 +17,14 @@ def public(f): return f +def hubmethod(f): + f.__doc__ = "%s\n\n%s" % ( + "Alias for `Hub.%s`" % f.__name__, + inspect.getdoc(getattr(Hub, f.__name__)), + ) + return public(f) + + class _InitGuard(object): def __init__(self, client): self._client = client @@ -32,7 +42,9 @@ def _init_on_hub(hub, args, kwargs): options = get_options(*args, **kwargs) client = Client(options) hub.bind_client(client) - setup_integrations(options) + setup_integrations( + options["integrations"] or [], with_defaults=options["default_integrations"] + ) return _InitGuard(client) @@ -52,41 +64,36 @@ def _init_on_current(*args, **kwargs): return _init_on_hub(Hub.current, args, kwargs) -@public +@hubmethod def capture_event(event, hint=None): - """Alias for `Hub.current.capture_event`""" hub = Hub.current if hub is not None: return hub.capture_event(event, hint) -@public +@hubmethod def capture_message(message, level=None): - """Alias for `Hub.current.capture_message`""" hub = Hub.current if hub is not None: return hub.capture_message(message, level) -@public +@hubmethod def capture_exception(error=None): - """Alias for `Hub.current.capture_exception`""" hub = Hub.current if hub is not None: return hub.capture_exception(error) -@public +@hubmethod def add_breadcrumb(*args, **kwargs): - """Alias for `Hub.current.add_breadcrumb`""" hub = Hub.current if hub is not None: return hub.add_breadcrumb(*args, **kwargs) -@public +@hubmethod def configure_scope(callback=None): - """Alias for `Hub.current.configure_scope`""" hub = Hub.current if hub is not None: return hub.configure_scope(callback) @@ -99,15 +106,8 @@ def inner(): return inner() -@public -def get_current_hub(): - """Alias for `Hub.current`""" - return Hub.current - - -@public +@hubmethod def last_event_id(): - """Alias for `Hub.last_event_id`""" hub = Hub.current if hub is not None: return hub.last_event_id() diff --git a/sentry_sdk/hub.py b/sentry_sdk/hub.py index 6b4b13f0c2..a5a664e65b 100644 --- a/sentry_sdk/hub.py +++ b/sentry_sdk/hub.py @@ -18,7 +18,7 @@ def _internal_exceptions(): except Exception: hub = Hub.current if hub: - hub.capture_internal_exception(sys.exc_info()) + hub._capture_internal_exception(sys.exc_info()) def _get_client_options(): @@ -124,7 +124,13 @@ def bind_client(self, new): self._stack[-1] = (new, top[1]) def capture_event(self, event, hint=None): - """Captures an event.""" + """Captures an event. The return value is the ID of the event. + + The event is a dictionary following the Sentry v7/v8 protocol + specification. Optionally an `EventHint` object can be passed that + is used by processors to extract additional information from it. + Typically the event hint object would contain exception information. + """ client, scope = self._stack[-1] if client is not None: rv = client.capture_event(event, hint, scope) @@ -133,7 +139,9 @@ def capture_event(self, event, hint=None): return rv def capture_message(self, message, level=None): - """Captures a message.""" + """Captures a message. The message is just a string. If no level + is provided the default level is `info`. + """ if self.client is None: return if level is None: @@ -141,7 +149,12 @@ def capture_message(self, message, level=None): return self.capture_event({"message": message, "level": level}) def capture_exception(self, error=None): - """Captures an exception.""" + """Captures an exception. + + The argument passed can be `None` in which case the last exception + will be reported, otherwise an exception object or an `exc_info` + tuple. + """ client = self.client if client is None: return @@ -156,15 +169,19 @@ def capture_exception(self, error=None): try: return self.capture_event(event, hint=hint) except Exception: - self.capture_internal_exception(sys.exc_info()) + self._capture_internal_exception(sys.exc_info()) - def capture_internal_exception(self, exc_info): + def _capture_internal_exception(self, exc_info): """Capture an exception that is likely caused by a bug in the SDK itself.""" logger.debug("Internal error in sentry_sdk", exc_info=exc_info) def add_breadcrumb(self, crumb=None, hint=None, **kwargs): - """Adds a breadcrumb.""" + """Adds a breadcrumb. The breadcrumbs are a dictionary with the + data as the sentry v7/v8 protocol expects. `hint` is an optional + value that can be used by `before_breadcrumb` to customize the + breadcrumbs that are emitted. + """ client, scope = self._stack[-1] if client is None: logger.info("Dropped breadcrumb because no client bound") diff --git a/sentry_sdk/integrations/__init__.py b/sentry_sdk/integrations/__init__.py index bb72875f3a..1c6c076aee 100644 --- a/sentry_sdk/integrations/__init__.py +++ b/sentry_sdk/integrations/__init__.py @@ -1,3 +1,4 @@ +"""This package""" from threading import Lock from ..utils import logger @@ -7,35 +8,47 @@ _installer_lock = Lock() -def _get_default_integrations(): +def get_default_integrations(): + """Returns an iterator of default integration instances.""" from .logging import LoggingIntegration from .excepthook import ExcepthookIntegration from .dedupe import DedupeIntegration from .atexit import AtexitIntegration - yield LoggingIntegration - yield ExcepthookIntegration - yield DedupeIntegration - yield AtexitIntegration + yield LoggingIntegration() + yield ExcepthookIntegration() + yield DedupeIntegration() + yield AtexitIntegration() -def setup_integrations(options): - integrations = list(options.get("integrations", None) or ()) - default_integrations = options.get("default_integrations") or False - - if default_integrations: - for cls in _get_default_integrations(): - if not any(isinstance(x, cls) for x in integrations): - integrations.append(cls()) +def setup_integrations(integrations, with_defaults=True): + """Given a list of integration instances this installs them all. When + `with_defaults` is set to `True` then all default integrations are added + unless they were already provided before. + """ + integrations = list(integrations) + if with_defaults: + for instance in get_default_integrations(): + if not any(isinstance(x, type(instance)) for x in integrations): + integrations.append(instance) for integration in integrations: integration() class Integration(object): + """Baseclass for all integrations.""" + identifier = None + """A unique identifying string for the integration. Integrations must + set this as a class attribute. + """ def install(self): + """An integration must implement all its code here. When the + `setup_integrations` function runs it will invoke this unless the + integration was already activated elsewhere. + """ raise NotImplementedError() def __call__(self): diff --git a/sentry_sdk/integrations/_wsgi.py b/sentry_sdk/integrations/_wsgi.py index 91173a4ec5..46fdacf0cd 100644 --- a/sentry_sdk/integrations/_wsgi.py +++ b/sentry_sdk/integrations/_wsgi.py @@ -1,8 +1,9 @@ import json import sys -import sentry_sdk +from sentry_sdk import capture_exception from sentry_sdk.hub import ( + Hub, _internal_exceptions, _should_send_default_pii, _get_client_options, @@ -150,7 +151,7 @@ def get_client_ip(environ): def run_wsgi_app(app, environ, start_response): - hub = sentry_sdk.get_current_hub() + hub = Hub.current hub.push_scope() with _internal_exceptions(): with hub.configure_scope() as scope: @@ -160,7 +161,7 @@ def run_wsgi_app(app, environ, start_response): rv = app(environ, start_response) except Exception: einfo = sys.exc_info() - sentry_sdk.capture_exception(einfo) + capture_exception(einfo) hub.pop_scope_unsafe() reraise(*einfo) @@ -180,7 +181,7 @@ def __iter__(self): self._response = iter(self._response) except Exception: einfo = sys.exc_info() - sentry_sdk.capture_exception(einfo) + capture_exception(einfo) reraise(*einfo) return self @@ -191,7 +192,7 @@ def __next__(self): raise except Exception: einfo = sys.exc_info() - sentry_sdk.capture_exception(einfo) + capture_exception(einfo) reraise(*einfo) def close(self): @@ -201,7 +202,7 @@ def close(self): self._response.close() except Exception: einfo = sys.exc_info() - sentry_sdk.capture_exception(einfo) + capture_exception(einfo) reraise(*einfo) diff --git a/sentry_sdk/integrations/atexit.py b/sentry_sdk/integrations/atexit.py index 8f73dc197a..323c7018dc 100644 --- a/sentry_sdk/integrations/atexit.py +++ b/sentry_sdk/integrations/atexit.py @@ -10,6 +10,11 @@ def default_shutdown_callback(pending, timeout): + """This is the default shutdown callback that is set on the options. + It prints out a message to stderr that informs the user that some events + are still pending and the process is waiting for them to flush out. + """ + def echo(msg): sys.stderr.write(msg + "\n") diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index d5706e6c04..44a96a7b34 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -3,7 +3,7 @@ from celery.signals import task_failure, task_prerun, task_postrun from celery.exceptions import SoftTimeLimitExceeded -from sentry_sdk import get_current_hub +from sentry_sdk import Hub from sentry_sdk.hub import _internal_exceptions from . import Integration @@ -24,7 +24,7 @@ def _process_failure_signal(self, sender, task_id, einfo, **kw): if hasattr(sender, "throws") and isinstance(einfo.exception, sender.throws): return - hub = get_current_hub() + hub = Hub.current if isinstance(einfo.exception, SoftTimeLimitExceeded): with hub.push_scope(): with hub.configure_scope() as scope: @@ -40,10 +40,10 @@ def _process_failure_signal(self, sender, task_id, einfo, **kw): def _handle_task_prerun(self, sender, task, **kw): with _internal_exceptions(): - hub = get_current_hub() + hub = Hub.current hub.push_scope() with hub.configure_scope() as scope: scope.transaction = task.name def _handle_task_postrun(self, sender, task_id, task, **kw): - get_current_hub().pop_scope_unsafe() + Hub.current.pop_scope_unsafe() diff --git a/sentry_sdk/integrations/dedupe.py b/sentry_sdk/integrations/dedupe.py index e1afd47e8f..8ce70eb6b8 100644 --- a/sentry_sdk/integrations/dedupe.py +++ b/sentry_sdk/integrations/dedupe.py @@ -4,7 +4,7 @@ from . import Integration -last_seen = ContextVar("last-seen") +_last_seen = ContextVar("last-seen") class DedupeIntegration(Integration): @@ -16,7 +16,7 @@ def install(self): @scope.add_error_processor def processor(event, exc_info): exc = exc_info[1] - if last_seen.get(None) is exc: + if _last_seen.get(None) is exc: return - last_seen.set(exc) + _last_seen.set(exc) return event diff --git a/sentry_sdk/integrations/flask.py b/sentry_sdk/integrations/flask.py index df73742525..7b02c91692 100644 --- a/sentry_sdk/integrations/flask.py +++ b/sentry_sdk/integrations/flask.py @@ -2,7 +2,7 @@ import weakref -from sentry_sdk import capture_exception, get_current_hub, configure_scope +from sentry_sdk import Hub, capture_exception, configure_scope from sentry_sdk.hub import _internal_exceptions, _should_send_default_pii from ._wsgi import RequestExtractor, run_wsgi_app from . import Integration @@ -43,14 +43,14 @@ def sentry_patched_wsgi_app(self, environ, start_response): def _push_appctx(*args, **kwargs): # always want to push scope regardless of whether WSGI app might already # have (not the case for CLI for example) - hub = get_current_hub() + hub = Hub.current hub.push_scope() with hub.configure_scope() as scope: scope.add_event_processor(event_processor) def _pop_appctx(*args, **kwargs): - get_current_hub().pop_scope_unsafe() + Hub.current.pop_scope_unsafe() def _request_started(sender, **kwargs): diff --git a/sentry_sdk/integrations/logging.py b/sentry_sdk/integrations/logging.py index 4c2d6b83c9..3d4e29fef3 100644 --- a/sentry_sdk/integrations/logging.py +++ b/sentry_sdk/integrations/logging.py @@ -4,18 +4,22 @@ import logging import datetime -from sentry_sdk import get_current_hub, capture_event, add_breadcrumb +from sentry_sdk import capture_event, add_breadcrumb from sentry_sdk.utils import to_string, event_from_exception -from sentry_sdk.hub import _internal_exceptions +from sentry_sdk.hub import Hub, _internal_exceptions from . import Integration -IGNORED_LOGGERS = set(["sentry_sdk.errors"]) +_IGNORED_LOGGERS = set(["sentry_sdk.errors"]) def ignore_logger(name): - IGNORED_LOGGERS.add(name) + """This disables the breadcrumb integration for a logger of a specific + name. This primary use is for some integrations to disable breadcrumbs + of this integration. + """ + _IGNORED_LOGGERS.add(name) class LoggingIntegration(Integration): @@ -50,7 +54,7 @@ def emit(self, record): return self._emit(record) def can_record(self, record): - return record.name not in IGNORED_LOGGERS + return record.name not in _IGNORED_LOGGERS def _breadcrumb_from_record(self, record): return { @@ -66,7 +70,7 @@ def _emit(self, record): return if self._should_create_event(record): - hub = get_current_hub() + hub = Hub.current if hub.client is None: return diff --git a/tests/conftest.py b/tests/conftest.py index c3cf483770..7cab13ec4a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -19,13 +19,13 @@ def reraise_internal_exceptions(request, monkeypatch): if "tests_internal_exceptions" in request.keywords: return - def capture_internal_exception(exc_info): + def _capture_internal_exception(exc_info): reraise(*exc_info) monkeypatch.setattr( - sentry_sdk.get_current_hub(), - "capture_internal_exception", - capture_internal_exception, + sentry_sdk.Hub.current, + "_capture_internal_exception", + _capture_internal_exception, ) diff --git a/tests/test_client.py b/tests/test_client.py index 232131488d..d09b9aaf4f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -47,7 +47,7 @@ def raise_it(exc_info): reraise(*exc_info) hub = Hub(Client(ignore_errors=[ZeroDivisionError], transport=_TestTransport())) - hub.capture_internal_exception = raise_it + hub._capture_internal_exception = raise_it def e(exc): try: @@ -159,7 +159,7 @@ def test_transport_works(httpserver, request, capsys): def test_client_debug_option_enabled(sentry_init, caplog): sentry_init(debug=True) - Hub.current.capture_internal_exception((ValueError, ValueError("OK"), None)) + Hub.current._capture_internal_exception((ValueError, ValueError("OK"), None)) assert "OK" in caplog.text @@ -169,5 +169,5 @@ def test_client_debug_option_disabled(with_client, sentry_init, caplog): if with_client: sentry_init() - Hub.current.capture_internal_exception((ValueError, ValueError("OK"), None)) + Hub.current._capture_internal_exception((ValueError, ValueError("OK"), None)) assert "OK" not in caplog.text