Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: SDK 2.0 Support #31

Merged
merged 21 commits into from
Apr 4, 2024
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -84,14 +84,14 @@ Advanced Options
================

``pytest-sentry`` supports marking your tests to use a different DSN, client or
hub per-test. You can use this to provide custom options to the ``Client``
scope per-test. You can use this to provide custom options to the ``Client``
object from the `Sentry SDK for Python
<https://github.com/getsentry/sentry-python>`_::

import random
import pytest

from sentry_sdk import Hub
from sentry_sdk import Scope
from pytest_sentry import Client

@pytest.mark.sentry_client(None)
Expand All @@ -108,7 +108,7 @@ object from the `Sentry SDK for Python

@pytest.mark.sentry_client(Client("CUSTOM DSN"))
@pytest.mark.sentry_client(lambda: Client("CUSTOM DSN"))
@pytest.mark.sentry_client(Hub(Client("CUSTOM DSN")))
@pytest.mark.sentry_client(Scope(Client("CUSTOM DSN")))
@pytest.mark.sentry_client({"dsn": ..., "debug": True})


Expand All @@ -125,12 +125,12 @@ you configured this plugin with. That's because ``pytest-sentry`` goes to
extreme lenghts to keep its own SDK setup separate from the SDK setup of the
tested code.

``pytest-sentry`` exposes the ``sentry_test_hub`` fixture whose return value is
the ``Hub`` being used to send events to Sentry. Use ``with sentry_test_hub:``
``pytest-sentry`` exposes the ``sentry_test_scope`` fixture whose return value is
the ``Scope`` being used to send events to Sentry. Use ``with sentry_test_scope:``
to temporarily switch context. You can use this to set custom tags like so::

def test_foo(sentry_test_hub):
with sentry_test_hub:
def test_foo(sentry_test_scope):
with sentry_test_scope:
szokeasaurusrex marked this conversation as resolved.
Show resolved Hide resolved
sentry_sdk.set_tag("pull_request", os.environ['EXAMPLE_CI_PULL_REQUEST'])


Expand Down
75 changes: 40 additions & 35 deletions pytest_sentry.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
import sentry_sdk
from sentry_sdk.integrations import Integration

from sentry_sdk import Hub, capture_exception
from sentry_sdk import Scope, capture_exception
from sentry_sdk.tracing import Transaction
from sentry_sdk.scope import add_global_event_processor
from sentry_sdk.scope import add_global_event_processor, use_scope

_ENVVARS_AS_TAGS = frozenset(
[
Expand Down Expand Up @@ -59,7 +59,7 @@ def __init__(self, always_report=None):
def setup_once():
@add_global_event_processor
def procesor(event, hint):
if Hub.current.get_integration(PytestIntegration) is None:
if Scope.get_client().get_integration(PytestIntegration) is None:
return event

for key in _ENVVARS_AS_TAGS:
Expand All @@ -82,7 +82,10 @@ def procesor(event, hint):
class Client(sentry_sdk.Client):
def __init__(self, *args, **kwargs):
kwargs.setdefault("dsn", os.environ.get("PYTEST_SENTRY_DSN", None))
kwargs.setdefault("traces_sample_rate", float(os.environ.get("PYTEST_SENTRY_TRACES_SAMPLE_RATE", 1.0)))
kwargs.setdefault(
"traces_sample_rate",
float(os.environ.get("PYTEST_SENTRY_TRACES_SAMPLE_RATE", 1.0)),
)
kwargs.setdefault("_experiments", {}).setdefault(
"auto_enabling_integrations", True
)
Expand All @@ -94,52 +97,52 @@ def __init__(self, *args, **kwargs):

def hookwrapper(itemgetter, **kwargs):
"""
A version of pytest.hookimpl that sets the current hub to the correct one
A version of pytest.hookimpl that sets the current scope to the correct one
and skips the hook if the integration is disabled.

Assumes the function is a hookwrapper, ie yields once
"""

@wrapt.decorator
def _with_hub(wrapped, instance, args, kwargs):
def _with_scope(wrapped, instance, args, kwargs):
item = itemgetter(*args, **kwargs)
hub = _resolve_hub_marker_value(item.get_closest_marker("sentry_client"))
scope = _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))

if hub.get_integration(PytestIntegration) is None:
if scope.client.get_integration(PytestIntegration) is None:
yield
else:
with hub:
with use_scope(scope):
gen = wrapped(*args, **kwargs)

while True:
try:
with hub:
with use_scope(scope):
chunk = next(gen)

y = yield chunk

with hub:
with use_scope(scope):
gen.send(y)

except StopIteration:
break

def inner(f):
return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_hub(f))
return pytest.hookimpl(hookwrapper=True, **kwargs)(_with_scope(f))

return inner


def pytest_load_initial_conftests(early_config, parser, args):
early_config.addinivalue_line(
"markers",
"sentry_client(client=None): Use this client instance for reporting tests. You can also pass a DSN string directly, or a `Hub` if you need it.",
"sentry_client(client=None): Use this client instance for reporting tests. You can also pass a DSN string directly, or a `Scope` if you need it.",
)


def _start_transaction(**kwargs):
transaction = Transaction.continue_from_headers(
dict(Hub.current.iter_trace_propagation_headers()), **kwargs
dict(Scope.get_current_scope().iter_trace_propagation_headers()), **kwargs
)
transaction.same_process_as_parent = True
return sentry_sdk.start_transaction(transaction)
Expand All @@ -154,7 +157,7 @@ def pytest_runtest_protocol(item):
# We use the full name including parameters because then we can identify
# how often a single test has run as part of the same GITHUB_RUN_ID.

with _start_transaction(op=op, name=u"{} {}".format(op, name)) as tx:
with _start_transaction(op=op, name="{} {}".format(op, name)) as tx:
yield

# Purposefully drop transaction to spare quota. We only created it to
Expand All @@ -171,14 +174,16 @@ def pytest_runtest_call(item):
# We use the full name including parameters because then we can identify
# how often a single test has run as part of the same GITHUB_RUN_ID.

with _start_transaction(op=op, name=u"{} {}".format(op, name)):
with _start_transaction(op=op, name="{} {}".format(op, name)):
yield


@hookwrapper(itemgetter=lambda fixturedef, request: request._pyfuncitem)
def pytest_fixture_setup(fixturedef, request):
op = "pytest.fixture.setup"
with _start_transaction(op=op, name=u"{} {}".format(op, fixturedef.argname)) as transaction:
with _start_transaction(
op=op, name="{} {}".format(op, fixturedef.argname)
) as transaction:
transaction.set_tag("pytest.fixture.scope", fixturedef.scope)
yield

Expand All @@ -198,31 +203,31 @@ def pytest_runtest_makereport(item, call):
call.excinfo
]

integration = Hub.current.get_integration(PytestIntegration)
integration = Scope.get_client().get_integration(PytestIntegration)

if (cur_exc_chain and call.excinfo is None) or integration.always_report:
for exc_info in cur_exc_chain:
capture_exception((exc_info.type, exc_info.value, exc_info.tb))


DEFAULT_HUB = Hub(Client())
DEFAULT_SCOPE = Scope(client=Client())

_hub_cache = {}
_scope_cache = {}


def _resolve_hub_marker_value(marker_value):
if id(marker_value) not in _hub_cache:
_hub_cache[id(marker_value)] = rv = _resolve_hub_marker_value_uncached(
def _resolve_scope_marker_value(marker_value):
if id(marker_value) not in _scope_cache:
_scope_cache[id(marker_value)] = rv = _resolve_scope_marker_value_uncached(
marker_value
)
return rv

return _hub_cache[id(marker_value)]
return _scope_cache[id(marker_value)]


def _resolve_hub_marker_value_uncached(marker_value):
def _resolve_scope_marker_value_uncached(marker_value):
if marker_value is None:
marker_value = DEFAULT_HUB
marker_value = DEFAULT_SCOPE
else:
marker_value = marker_value.args[0]

Expand All @@ -231,35 +236,35 @@ def _resolve_hub_marker_value_uncached(marker_value):

if marker_value is None:
# user explicitly disabled reporting
return Hub()
return Scope()

if isinstance(marker_value, str):
return Hub(Client(marker_value))
return Scope(client=Client(marker_value))

if isinstance(marker_value, dict):
return Hub(Client(**marker_value))
return Scope(client=Client(**marker_value))

if isinstance(marker_value, Client):
return Hub(marker_value)
return Scope(client=marker_value)

if isinstance(marker_value, Hub):
if isinstance(marker_value, Scope):
return marker_value

raise RuntimeError(
"The `sentry_client` value must be a client, hub or string, not {}".format(
"The `sentry_client` value must be a client, scope or string, not {}".format(
repr(type(marker_value))
)
)


@pytest.fixture
def sentry_test_hub(request):
def sentry_test_scope(request):
"""
Gives back the current hub.
Gives back the current scope.
"""

item = request.node
return _resolve_hub_marker_value(item.get_closest_marker("sentry_client"))
return _resolve_scope_marker_value(item.get_closest_marker("sentry_client"))


def _process_stacktrace(stacktrace):
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ classifiers =
py_modules = pytest_sentry
install_requires =
pytest
sentry-sdk
sentry-sdk==2.0.0a2
wrapt

[options.entry_points]
Expand Down
7 changes: 4 additions & 3 deletions tests/test_envvars.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import pytest
import sentry_sdk
from sentry_sdk.scope import use_scope
import pytest_sentry

events = []
Expand All @@ -26,9 +27,9 @@ def clear_events(monkeypatch):
pytestmark = pytest.mark.sentry_client(pytest_sentry.Client(transport=MyTransport()))


def test_basic(sentry_test_hub):
with sentry_test_hub:
sentry_test_hub.capture_message("hi")
def test_basic(sentry_test_scope):
with use_scope(sentry_test_scope):
sentry_test_scope.capture_message("hi")

(event,) = events
assert event["tags"]["pytest_environ.GITHUB_RUN_ID"] == "123abc"
Expand Down
8 changes: 4 additions & 4 deletions tests/test_fixture.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
pytestmark = pytest.mark.sentry_client(GLOBAL_CLIENT)


def test_basic(sentry_test_hub):
assert sentry_test_hub.client is GLOBAL_CLIENT
def test_basic(sentry_test_scope):
assert sentry_test_scope.client is GLOBAL_CLIENT


@pytest.mark.sentry_client(None)
def test_func(sentry_test_hub):
assert sentry_test_hub.client is None
def test_func(sentry_test_scope):
assert not sentry_test_scope.client.is_active()
42 changes: 0 additions & 42 deletions tests/test_hub.py

This file was deleted.

53 changes: 53 additions & 0 deletions tests/test_scope.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
from __future__ import absolute_import

import pytest
import unittest
import sentry_sdk
import pytest_sentry

pytestmark = pytest.mark.sentry_client(pytest_sentry.Client())

_DEFAULT_GLOBAL_SCOPE = sentry_sdk.Scope.get_global_scope()
_DEFAULT_ISOLATION_SCOPE = sentry_sdk.Scope.get_isolation_scope()


def _assert_right_scopes():
global_scope = sentry_sdk.Scope.get_global_scope()
isolation_scope = sentry_sdk.Scope.get_isolation_scope()

assert not global_scope.get_client().is_active()
assert global_scope is _DEFAULT_GLOBAL_SCOPE

assert not isolation_scope.get_client().is_active()
assert isolation_scope is _DEFAULT_ISOLATION_SCOPE


def test_basic():
_assert_right_scopes()


def test_sentry_test_scope(sentry_test_scope):
# Ensure that we are within a transaction (started by the fixture)
assert sentry_test_scope.span is not None


class TestSimpleClass(object):
def setup_method(self):
_assert_right_scopes()

def test_basic(self):
_assert_right_scopes()

def teardown_method(self):
_assert_right_scopes()


class TestUnittestClass(unittest.TestCase):
def setUp(self):
_assert_right_scopes()

def test_basic(self):
_assert_right_scopes()

def tearDown(self):
_assert_right_scopes()
Loading