From 72c08e6bd2b0f721306377602c708c1d12ca2aa2 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 8 May 2024 12:03:54 +0200 Subject: [PATCH 1/4] feat(client): Add last_event_id to `Client` ref #3049 --- sentry_sdk/client.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index dc31e5ce1b..02081e71c5 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -205,6 +205,16 @@ def flush(self, *args, **kwargs): # type: (*Any, **Any) -> None return None + def last_event_id(self): + # type: () -> Optional[str] + """ + .. versionadded:: 2.1.2 + + Returns the most recently captured error event's ID. If this client has + not captured an error event yet, returns `None`. + """ + return None + def __enter__(self): # type: () -> BaseClient return self @@ -379,6 +389,8 @@ def _capture_envelope(envelope): except Exception as e: logger.debug("Can not set up profiler. (%s)", e) + self._last_event_id = None # type: Optional[str] + finally: _client_init_debug.set(old_debug) @@ -709,6 +721,7 @@ def capture_event( is_transaction = event_opt.get("type") == "transaction" is_checkin = event_opt.get("type") == "check_in" + is_error = not is_transaction and not is_checkin if ( not is_transaction @@ -750,6 +763,9 @@ def capture_event( if self.transport is None: return None + if is_error: + self._last_event_id = event_id + self.transport.capture_envelope(envelope) return event_id @@ -820,6 +836,10 @@ def flush( self.metrics_aggregator.flush() self.transport.flush(timeout=timeout, callback=callback) + def last_event_id(self): + # type: () -> Optional[str] + return self._last_event_id + def __enter__(self): # type: () -> _Client return self From bc11a8e1f0bb2991b016abe5d15283525fc8cd0f Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 8 May 2024 12:37:15 +0200 Subject: [PATCH 2/4] test(client): Add unit tests for last_event_id ref #3049 --- tests/test_client.py | 65 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/tests/test_client.py b/tests/test_client.py index 0464f32b5e..7c9295274f 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -18,7 +18,9 @@ capture_exception, capture_event, set_tag, + get_client, ) +from sentry_sdk.client import NonRecordingClient from sentry_sdk.integrations.executing import ExecutingIntegration from sentry_sdk.transport import Transport from sentry_sdk.serializer import MAX_DATABAG_BREADTH @@ -1205,3 +1207,66 @@ def test_uwsgi_warnings(sentry_init, recwarn, opt, missing_flags): assert flag in str(record.message) else: assert not recwarn + + +def test_last_event_id_non_recording(): + client = NonRecordingClient() + + assert client.last_event_id() is None + + +def test_last_event_id_nothing_sent(sentry_init): + sentry_init() + client = get_client() + + assert client.last_event_id() is None + + +def test_last_event_id_manually_set(sentry_init): + sentry_init() + client = get_client() + + client.capture_event({"event_id": "42"}) + + assert client.last_event_id() == "42" + + +def test_last_event_id_auto_generated(sentry_init): + sentry_init() + client = get_client() + + client.capture_event({}) + + # We can't predict the event ID, but it should be set + assert client.last_event_id() is not None + + +def test_last_event_id_no_transport(sentry_init): + sentry_init() + client = get_client() + client.transport = None + + client.capture_event({"event_id": "42"}) + + assert client.last_event_id() is None + + +@pytest.mark.parametrize("type", ("check_in", "transaction")) +def test_last_event_id_not_error(type, sentry_init): + sentry_init() + client = get_client() + + client.capture_event({"type": type, "event_id": "42"}) + + assert client.last_event_id() is None + + +@pytest.mark.parametrize("type", ("check_in", "transaction")) +def test_last_event_id_not_error_after_error(type, sentry_init): + sentry_init() + client = get_client() + + client.capture_event({"event_id": "error_id"}) + client.capture_event({"type": type, "event_id": "not_error_id"}) + + assert client.last_event_id() == "error_id" From 91afec7cce2d094089809f33f5ce10f8ce0f49fc Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 8 May 2024 12:12:39 +0200 Subject: [PATCH 3/4] feat: Add `last_event_id` to top-level api Closes #3049 --- sentry_sdk/__init__.py | 1 + sentry_sdk/api.py | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/sentry_sdk/__init__.py b/sentry_sdk/__init__.py index 1b646992ff..74fab6c785 100644 --- a/sentry_sdk/__init__.py +++ b/sentry_sdk/__init__.py @@ -34,6 +34,7 @@ "is_initialized", "isolation_scope", "new_scope", + "last_event_id", "push_scope", "set_context", "set_extra", diff --git a/sentry_sdk/api.py b/sentry_sdk/api.py index 37c81afcc5..e2d38c2111 100644 --- a/sentry_sdk/api.py +++ b/sentry_sdk/api.py @@ -60,6 +60,7 @@ def overload(x): "is_initialized", "isolation_scope", "new_scope", + "last_event_id", "push_scope", "set_context", "set_extra", @@ -375,3 +376,9 @@ def continue_trace(environ_or_headers, op=None, name=None, source=None): return Scope.get_isolation_scope().continue_trace( environ_or_headers, op, name, source ) + + +@clientmethod +def last_event_id(): + # type: () -> Optional[str] + return get_client().last_event_id() From 751f6ca0e14bff879a1a00d6e214717d73ac9087 Mon Sep 17 00:00:00 2001 From: Daniel Szoke Date: Wed, 8 May 2024 14:26:46 +0200 Subject: [PATCH 4/4] test: Unit test top-level `last_event_id` ref #3049 --- tests/test_basics.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/test_basics.py b/tests/test_basics.py index bf42634710..f511242195 100644 --- a/tests/test_basics.py +++ b/tests/test_basics.py @@ -18,6 +18,7 @@ add_breadcrumb, Hub, Scope, + last_event_id, ) from sentry_sdk.integrations import ( _AUTO_ENABLING_INTEGRATIONS, @@ -778,3 +779,24 @@ def test_classmethod_tracing(sentry_init): with patch_start_tracing_child() as fake_start_child: assert instance_or_class.class_(1) == (TracingTestClass, 1) assert fake_start_child.call_count == 1 + + +def test_last_event_id(sentry_init): + sentry_init() + + assert last_event_id() is None + + capture_exception(ValueError("foo")) + + assert last_event_id() is not None + + +def test_last_event_id_transaction(sentry_init): + sentry_init() + + assert last_event_id() is None + + with start_transaction(): + pass + + assert last_event_id() is None