From 53315b6104af0044f7d0ed0745959773ae9ed2be Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 4 Nov 2026 16:05:16 +0100 Subject: [PATCH 1/2] feat(crons): Add before_send_check_in, don't run before_send on check-ins Check-ins must not go through before_send according to the check-in spec. Add a dedicated before_send_check_in option instead and record check-ins dropped by it under the monitor data category. --- MIGRATION_GUIDE.md | 1 + sentry_sdk/client.py | 13 +++++++++-- sentry_sdk/consts.py | 5 ++++ tests/test_crons.py | 55 ++++++++++++++++++++++++++++++++++++++++---- 4 files changed, 68 insertions(+), 6 deletions(-) diff --git a/MIGRATION_GUIDE.md b/MIGRATION_GUIDE.md index 49a37b2178..e71c2450c0 100644 --- a/MIGRATION_GUIDE.md +++ b/MIGRATION_GUIDE.md @@ -10,6 +10,7 @@ Looking to upgrade from Sentry SDK 2.x to 3.x? Here's a comprehensive list of wh - The `@trace` decorator doesn't accept a `template` parameter anymore. - The option `attach_stacktrace` is now `True` by default, meaning the SDK will attach stack traces to messages. - `add_attachment` no longer accepts an `add_to_transactions` argument. +- Cron check-ins no longer go through `before_send`. Use the new `before_send_check_in` option to modify or drop check-ins before they're sent. - If a `before_breadcrumb` callback, an event processor, or an error processor raises an exception, the breadcrumb or event is now dropped. Previously, the SDK would log the exception and continue processing the unmodified breadcrumb or event. - The top-level manual tracing API is now wired to span streaming tracing: - `sentry_sdk.start_transaction` doesn't exist anymore. Use `sentry_sdk.start_span` instead. diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index 141dba29d1..be95455cda 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -547,7 +547,14 @@ def _prepare_event( ), ) - before_send = self.options["before_send"] + # Check-ins don't go through before_send, they have their own callback + if is_checkin: + before_send = self.options["before_send_check_in"] + data_category: "EventDataCategory" = "monitor" + else: + before_send = self.options["before_send"] + data_category = "error" + if before_send is not None and event is not None: new_event = None exception_raised_in_before_send = False @@ -565,7 +572,9 @@ def _prepare_event( if exception_raised_in_before_send else "before_send" ) - self.transport.record_lost_event(reason, data_category="error") + self.transport.record_lost_event( + reason, data_category=data_category + ) event = new_event diff --git a/sentry_sdk/consts.py b/sentry_sdk/consts.py index 59627fa1c5..1a6fff3baf 100644 --- a/sentry_sdk/consts.py +++ b/sentry_sdk/consts.py @@ -1581,6 +1581,7 @@ def __init__( before_send_log: "Optional[Callable[[Log, Hint], Optional[Log]]]" = None, before_send_metric: "Optional[Callable[[Metric, Hint], Optional[Metric]]]" = None, before_send_span: "Optional[Callable[[SpanJSON, Hint], Optional[SpanJSON]]]" = None, + before_send_check_in: "Optional[EventProcessor]" = None, org_id: "Optional[str]" = None, strict_trace_continuation: bool = False, ) -> None: @@ -1968,6 +1969,10 @@ def __init__( Modifications to the span's attributes and name will be retained. Unlike ``before_send_log`` and ``before_send_metric``, spans cannot be dropped by returning None. + :param before_send_check_in: An optional function to modify or filter out cron check-ins before they're + sent to Sentry. Check-ins don't go through `before_send`. Any modifications to the check-in in this + function will be retained. If the function returns None, the check-in will not be sent to Sentry. + :param ignore_spans: A sequence of span-matching rules. Matching spans are ignored. :param _experiments: Dictionary of experimental, opt-in features that are not yet stable. diff --git a/tests/test_crons.py b/tests/test_crons.py index c5e53170e2..dabd7e4710 100644 --- a/tests/test_crons.py +++ b/tests/test_crons.py @@ -325,22 +325,69 @@ def test_capture_checkin_sdk_not_initialized(): assert check_in_id is None -def test_capture_checkin_dropped_by_before_send(sentry_init, capture_envelopes): - sentry_init(before_send=lambda event, hint: None) +def test_capture_checkin_dropped_by_before_send_check_in( + sentry_init, capture_envelopes, capture_record_lost_event_calls +): + sentry_init(before_send_check_in=lambda check_in, hint: None) envelopes = capture_envelopes() + record_lost_event_calls = capture_record_lost_event_calls() check_in_id = capture_checkin(monitor_slug="abc123", status="in_progress") assert check_in_id is None assert envelopes == [] + assert record_lost_event_calls == [("before_send", "monitor", None, 1)] + + +def test_before_send_check_in_only_applies_to_check_ins(sentry_init, capture_envelopes): + def before_send(event, hint): + event["extra"] = {"before_send_called": True} + return event + + def before_send_check_in(check_in, hint): + check_in["environment"] = "from_before_send_check_in" + return check_in + + sentry_init(before_send=before_send, before_send_check_in=before_send_check_in) + envelopes = capture_envelopes() + + capture_checkin(monitor_slug="abc123", status="ok") + sentry_sdk.capture_message("hello") + + check_in_envelope, event_envelope = envelopes + + check_in = check_in_envelope.items[0].payload.json + assert check_in["monitor_slug"] == "abc123" + assert check_in["environment"] == "from_before_send_check_in" + assert "extra" not in check_in + + event = event_envelope.items[0].payload.json + assert event["message"] == "hello" + assert event["extra"] == {"before_send_called": True} + assert event["environment"] != "from_before_send_check_in" + + +def test_event_processors_not_applied_to_check_ins(sentry_init, capture_envelopes): + def event_processor(event, hint): + event["extra"] = {"event_processor_called": True} + return event + + sentry_init() + sentry_sdk.get_isolation_scope().add_event_processor(event_processor) + envelopes = capture_envelopes() + + capture_checkin(monitor_slug="abc123", status="ok") + + (envelope,) = envelopes + assert "extra" not in envelope.items[0].payload.json def test_context_monitor_exit_successfully_sends_checkin_when_enter_checkin_dropped( sentry_init, capture_envelopes ): sentry_init( - before_send=lambda event, hint: ( - None if event.get("status") == "in_progress" else event + before_send_check_in=lambda check_in, hint: ( + None if check_in.get("status") == "in_progress" else check_in ) ) envelopes = capture_envelopes() From 542810cdcc41f697c550cc1b1922854d85aa898a Mon Sep 17 00:00:00 2001 From: Ivana Kellyer Date: Wed, 4 Nov 2026 16:10:24 +0100 Subject: [PATCH 2/2] Remove comment --- sentry_sdk/client.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry_sdk/client.py b/sentry_sdk/client.py index be95455cda..6621553c1d 100644 --- a/sentry_sdk/client.py +++ b/sentry_sdk/client.py @@ -547,7 +547,6 @@ def _prepare_event( ), ) - # Check-ins don't go through before_send, they have their own callback if is_checkin: before_send = self.options["before_send_check_in"] data_category: "EventDataCategory" = "monitor"