From 2d436a5fa199ac26c35ef87d0481ac375c0589e1 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 26 Oct 2020 16:41:41 -0700 Subject: [PATCH 1/5] asgi --- sentry_sdk/integrations/asgi.py | 4 ++- tests/integrations/asgi/test_asgi.py | 49 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/asgi.py b/sentry_sdk/integrations/asgi.py index 7a0d0bd339..6bd1c146a0 100644 --- a/sentry_sdk/integrations/asgi.py +++ b/sentry_sdk/integrations/asgi.py @@ -139,7 +139,9 @@ async def _run_app(self, scope, callback): transaction.name = _DEFAULT_TRANSACTION_NAME transaction.set_tag("asgi.type", ty) - with hub.start_transaction(transaction): + with hub.start_transaction( + transaction, custom_sampling_context={"asgi_scope": scope} + ): # XXX: Would be cool to have correct span status, but we # would have to wrap send(). That is a bit hard to do with # the current abstraction over ASGI 2/3. diff --git a/tests/integrations/asgi/test_asgi.py b/tests/integrations/asgi/test_asgi.py index 521c7c8302..b698f619e1 100644 --- a/tests/integrations/asgi/test_asgi.py +++ b/tests/integrations/asgi/test_asgi.py @@ -8,6 +8,11 @@ from starlette.testclient import TestClient from starlette.websockets import WebSocket +try: + from unittest import mock # python 3.3 and above +except ImportError: + import mock # python < 3.3 + @pytest.fixture def app(): @@ -202,3 +207,47 @@ def handler(*args, **kwargs): (exception,) = event["exception"]["values"] assert exception["type"] == "ValueError" assert exception["value"] == "oh no" + + +def test_transaction(app, sentry_init, capture_events): + sentry_init(traces_sample_rate=1.0) + events = capture_events() + + @app.route("/tricks/kangaroo") + def kangaroo_handler(request): + return PlainTextResponse("dogs are great") + + client = TestClient(app) + client.get("/tricks/kangaroo") + + event = events[0] + assert event["type"] == "transaction" + assert ( + event["transaction"] + == "tests.integrations.asgi.test_asgi.test_transaction..kangaroo_handler" + ) + + +def test_traces_sampler_gets_scope_in_sampling_context( + app, sentry_init, DictionaryContaining # noqa: N803 +): + traces_sampler = mock.Mock() + sentry_init(traces_sampler=traces_sampler) + + @app.route("/tricks/kangaroo") + def kangaroo_handler(request): + return PlainTextResponse("dogs are great") + + client = TestClient(app) + client.get("/tricks/kangaroo") + + traces_sampler.assert_any_call( + DictionaryContaining( + { + # starlette just uses a dictionary to hold the scope + "asgi_scope": DictionaryContaining( + {"method": "GET", "path": "/tricks/kangaroo"} + ) + } + ) + ) From ada6032f16ce3cddb19077c592fdfd40f2c649b6 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 26 Oct 2020 19:57:12 -0700 Subject: [PATCH 2/5] celery --- sentry_sdk/integrations/celery.py | 13 ++++++++++- tests/integrations/celery/test_celery.py | 28 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/celery.py b/sentry_sdk/integrations/celery.py index 2b51fe1f00..49b572d795 100644 --- a/sentry_sdk/integrations/celery.py +++ b/sentry_sdk/integrations/celery.py @@ -159,7 +159,18 @@ def _inner(*args, **kwargs): if transaction is None: return f(*args, **kwargs) - with hub.start_transaction(transaction): + with hub.start_transaction( + transaction, + custom_sampling_context={ + "celery_job": { + "task": task.name, + # for some reason, args[1] is a list if non-empty but a + # tuple if empty + "args": list(args[1]), + "kwargs": args[2], + } + }, + ): return f(*args, **kwargs) return _inner # type: ignore diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index 32b3021b1a..a146db24ef 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -11,6 +11,11 @@ from celery import Celery, VERSION from celery.bin import worker +try: + from unittest import mock # python 3.3 and above +except ImportError: + import mock # python < 3.3 + @pytest.fixture def connect_signal(request): @@ -379,3 +384,26 @@ def dummy_task(self, x, y): assert dummy_task.apply(kwargs={"x": 1, "y": 1}).wait() == 1 assert celery_invocation(dummy_task, 1, 1)[0].wait() == 1 + + +def test_traces_sampler_gets_task_info_in_sampling_context( + init_celery, celery_invocation, DictionaryContaining # noqa:N803 +): + traces_sampler = mock.Mock() + celery = init_celery(traces_sampler=traces_sampler) + + @celery.task(name="dog_walk") + def walk_dogs(x, y): + dogs, route = x + num_loops = y + return dogs, route, num_loops + + _, args_kwargs = celery_invocation( + walk_dogs, [["Maisey", "Charlie", "Bodhi", "Cory"], "Dog park round trip"], 1 + ) + + traces_sampler.assert_any_call( + # depending on the iteration of celery_invocation, the data might be + # passed as args or as kwargs, so make this generic + DictionaryContaining({"celery_job": {"task": "dog_walk", **args_kwargs}}) + ) From 23af17105bb9bced243ee1698919b2bfbc8acbd5 Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 2 Nov 2020 10:14:02 -0800 Subject: [PATCH 3/5] rq --- sentry_sdk/integrations/rq.py | 4 +- tests/integrations/rq/test_rq.py | 106 +++++++++++++++++++++++++++++++ 2 files changed, 109 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/rq.py b/sentry_sdk/integrations/rq.py index fa583c8bdc..1af4b0babd 100644 --- a/sentry_sdk/integrations/rq.py +++ b/sentry_sdk/integrations/rq.py @@ -70,7 +70,9 @@ def sentry_patched_perform_job(self, job, *args, **kwargs): with capture_internal_exceptions(): transaction.name = job.func_name - with hub.start_transaction(transaction): + with hub.start_transaction( + transaction, custom_sampling_context={"rq_job": job} + ): rv = old_perform_job(self, job, *args, **kwargs) if self.is_horse: diff --git a/tests/integrations/rq/test_rq.py b/tests/integrations/rq/test_rq.py index b98b6be7c3..ee3e5f51fa 100644 --- a/tests/integrations/rq/test_rq.py +++ b/tests/integrations/rq/test_rq.py @@ -5,6 +5,11 @@ from fakeredis import FakeStrictRedis import rq +try: + from unittest import mock # python 3.3 and above +except ImportError: + import mock # python < 3.3 + @pytest.fixture(autouse=True) def _patch_rq_get_server_version(monkeypatch): @@ -28,6 +33,14 @@ def crashing_job(foo): 1 / 0 +def chew_up_shoes(dog, human, shoes): + raise Exception("{}!! Why did you eat {}'s {}??".format(dog, human, shoes)) + + +def do_trick(dog, trick): + return "{}, can you {}? Good dog!".format(dog, trick) + + def test_basic(sentry_init, capture_events): sentry_init(integrations=[RqIntegration()]) events = capture_events() @@ -71,3 +84,96 @@ def test_transport_shutdown(sentry_init, capture_events_forksafe): (exception,) = event["exception"]["values"] assert exception["type"] == "ZeroDivisionError" + + +def test_transaction_with_error( + sentry_init, capture_events, DictionaryContaining # noqa:N803 +): + + sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0) + events = capture_events() + + queue = rq.Queue(connection=FakeStrictRedis()) + worker = rq.SimpleWorker([queue], connection=queue.connection) + + queue.enqueue(chew_up_shoes, "Charlie", "Katie", shoes="flip-flops") + worker.work(burst=True) + + error_event, envelope = events + + assert error_event["transaction"] == "tests.integrations.rq.test_rq.chew_up_shoes" + assert error_event["contexts"]["trace"]["op"] == "rq.task" + assert error_event["exception"]["values"][0]["type"] == "Exception" + assert ( + error_event["exception"]["values"][0]["value"] + == "Charlie!! Why did you eat Katie's flip-flops??" + ) + + assert envelope["type"] == "transaction" + assert envelope["contexts"]["trace"] == error_event["contexts"]["trace"] + assert envelope["transaction"] == error_event["transaction"] + assert envelope["extra"]["rq-job"] == DictionaryContaining( + { + "args": ["Charlie", "Katie"], + "kwargs": {"shoes": "flip-flops"}, + "func": "tests.integrations.rq.test_rq.chew_up_shoes", + "description": "tests.integrations.rq.test_rq.chew_up_shoes('Charlie', 'Katie', shoes='flip-flops')", + } + ) + + +def test_transaction_no_error( + sentry_init, capture_events, DictionaryContaining # noqa:N803 +): + sentry_init(integrations=[RqIntegration()], traces_sample_rate=1.0) + events = capture_events() + + queue = rq.Queue(connection=FakeStrictRedis()) + worker = rq.SimpleWorker([queue], connection=queue.connection) + + queue.enqueue(do_trick, "Maisey", trick="kangaroo") + worker.work(burst=True) + + envelope = events[0] + + assert envelope["type"] == "transaction" + assert envelope["contexts"]["trace"]["op"] == "rq.task" + assert envelope["transaction"] == "tests.integrations.rq.test_rq.do_trick" + assert envelope["extra"]["rq-job"] == DictionaryContaining( + { + "args": ["Maisey"], + "kwargs": {"trick": "kangaroo"}, + "func": "tests.integrations.rq.test_rq.do_trick", + "description": "tests.integrations.rq.test_rq.do_trick('Maisey', trick='kangaroo')", + } + ) + + +def test_traces_sampler_gets_correct_values_in_sampling_context( + sentry_init, DictionaryContaining, ObjectDescribedBy # noqa:N803 +): + traces_sampler = mock.Mock(return_value=True) + sentry_init(integrations=[RqIntegration()], traces_sampler=traces_sampler) + + queue = rq.Queue(connection=FakeStrictRedis()) + worker = rq.SimpleWorker([queue], connection=queue.connection) + + queue.enqueue(do_trick, "Bodhi", trick="roll over") + worker.work(burst=True) + + traces_sampler.assert_any_call( + DictionaryContaining( + { + "rq_job": ObjectDescribedBy( + type=rq.job.Job, + attrs={ + "description": "tests.integrations.rq.test_rq.do_trick('Bodhi', trick='roll over')", + "result": "Bodhi, can you roll over? Good dog!", + "func_name": "tests.integrations.rq.test_rq.do_trick", + "args": ("Bodhi",), + "kwargs": {"trick": "roll over"}, + }, + ), + } + ) + ) From bd11e56074793c8ae2b67752e9c6e3f57244404b Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 2 Nov 2020 12:27:38 -0800 Subject: [PATCH 4/5] wsgi --- sentry_sdk/integrations/wsgi.py | 4 +- tests/integrations/wsgi/test_wsgi.py | 92 ++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+), 1 deletion(-) diff --git a/sentry_sdk/integrations/wsgi.py b/sentry_sdk/integrations/wsgi.py index ee359c7925..13b960a713 100644 --- a/sentry_sdk/integrations/wsgi.py +++ b/sentry_sdk/integrations/wsgi.py @@ -117,7 +117,9 @@ def __call__(self, environ, start_response): environ, op="http.server", name="generic WSGI request" ) - with hub.start_transaction(transaction): + with hub.start_transaction( + transaction, custom_sampling_context={"wsgi_environ": environ} + ): try: rv = self.app( environ, diff --git a/tests/integrations/wsgi/test_wsgi.py b/tests/integrations/wsgi/test_wsgi.py index 67bfe055d1..1f9613997a 100644 --- a/tests/integrations/wsgi/test_wsgi.py +++ b/tests/integrations/wsgi/test_wsgi.py @@ -3,6 +3,11 @@ from sentry_sdk.integrations.wsgi import SentryWsgiMiddleware +try: + from unittest import mock # python 3.3 and above +except ImportError: + import mock # python < 3.3 + @pytest.fixture def crashing_app(): @@ -109,3 +114,90 @@ def test_keyboard_interrupt_is_captured(sentry_init, capture_events): assert exc["type"] == "KeyboardInterrupt" assert exc["value"] == "" assert event["level"] == "error" + + +def test_transaction_with_error( + sentry_init, crashing_app, capture_events, DictionaryContaining # noqa:N803 +): + def dogpark(environ, start_response): + raise Exception("Fetch aborted. The ball was not returned.") + + sentry_init(send_default_pii=True, traces_sample_rate=1.0) + app = SentryWsgiMiddleware(dogpark) + client = Client(app) + events = capture_events() + + with pytest.raises(Exception): + client.get("http://dogs.are.great/sit/stay/rollover/") + + error_event, envelope = events + + assert error_event["transaction"] == "generic WSGI request" + assert error_event["contexts"]["trace"]["op"] == "http.server" + assert error_event["exception"]["values"][0]["type"] == "Exception" + assert ( + error_event["exception"]["values"][0]["value"] + == "Fetch aborted. The ball was not returned." + ) + + assert envelope["type"] == "transaction" + + # event trace context is a subset of envelope trace context + assert envelope["contexts"]["trace"] == DictionaryContaining( + error_event["contexts"]["trace"] + ) + assert envelope["contexts"]["trace"]["status"] == "internal_error" + assert envelope["transaction"] == error_event["transaction"] + assert envelope["request"] == error_event["request"] + + +def test_transaction_no_error( + sentry_init, capture_events, DictionaryContaining # noqa:N803 +): + def dogpark(environ, start_response): + start_response("200 OK", []) + return ["Go get the ball! Good dog!"] + + sentry_init(send_default_pii=True, traces_sample_rate=1.0) + app = SentryWsgiMiddleware(dogpark) + client = Client(app) + events = capture_events() + + client.get("/dogs/are/great/") + + envelope = events[0] + + assert envelope["type"] == "transaction" + assert envelope["transaction"] == "generic WSGI request" + assert envelope["contexts"]["trace"]["op"] == "http.server" + assert envelope["request"] == DictionaryContaining( + {"method": "GET", "url": "http://localhost/dogs/are/great/"} + ) + + +def test_traces_sampler_gets_correct_values_in_sampling_context( + sentry_init, DictionaryContaining, ObjectDescribedBy # noqa:N803 +): + def app(environ, start_response): + start_response("200 OK", []) + return ["Go get the ball! Good dog!"] + + traces_sampler = mock.Mock(return_value=True) + sentry_init(send_default_pii=True, traces_sampler=traces_sampler) + app = SentryWsgiMiddleware(app) + client = Client(app) + + client.get("/dogs/are/great/") + + traces_sampler.assert_any_call( + DictionaryContaining( + { + "wsgi_environ": DictionaryContaining( + { + "PATH_INFO": "/dogs/are/great/", + "REQUEST_METHOD": "GET", + }, + ), + } + ) + ) From 729d1d88c0abfe8e90c8599249558a66f3c97f1e Mon Sep 17 00:00:00 2001 From: Katie Byers Date: Mon, 2 Nov 2020 14:01:43 -0800 Subject: [PATCH 5/5] fix syntax for older python versions --- tests/integrations/celery/test_celery.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integrations/celery/test_celery.py b/tests/integrations/celery/test_celery.py index a146db24ef..a405e53fd9 100644 --- a/tests/integrations/celery/test_celery.py +++ b/tests/integrations/celery/test_celery.py @@ -405,5 +405,5 @@ def walk_dogs(x, y): traces_sampler.assert_any_call( # depending on the iteration of celery_invocation, the data might be # passed as args or as kwargs, so make this generic - DictionaryContaining({"celery_job": {"task": "dog_walk", **args_kwargs}}) + DictionaryContaining({"celery_job": dict(task="dog_walk", **args_kwargs)}) )