-
Notifications
You must be signed in to change notification settings - Fork 642
fix(tracing): handle exceptions raised within traces_sampler and other callbacks #6853
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
Changes from all commits
bfe9623
7bf8fd7
0f8a64a
99fe440
6f95c1f
227a4c4
f3e440e
b89171c
99fb778
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1244,15 +1244,31 @@ | |
| serialized = telemetry._to_json() # type: ignore[union-attr] | ||
|
|
||
| if before_send is not None: | ||
| serialized = before_send(serialized, {}) # type: ignore[arg-type] | ||
| exception_raised_in_before_send_func = False | ||
| with capture_internal_exceptions(): | ||
| try: | ||
| serialized = before_send(serialized, {}) # type: ignore[arg-type] | ||
| except Exception: | ||
| exception_raised_in_before_send_func = True | ||
|
Check warning on line 1252 in sentry_sdk/client.py
|
||
| raise | ||
|
|
||
| if ty in ("log", "metric"): | ||
| # We are ok with dropping metrics and logs when an exception is raised | ||
| # because we allow users to drop them in their respect before_send_* | ||
| # functions. | ||
| if exception_raised_in_before_send_func: | ||
| return | ||
|
Comment on lines
+1258
to
+1260
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Bug: The Suggested FixModify the exception handling to catch Prompt for AI Agent |
||
| # Logs and metrics can be dropped in their respective | ||
| # before_send, so if we get None, don't queue them for sending. | ||
| if serialized is None: | ||
|
sentry[bot] marked this conversation as resolved.
|
||
| return | ||
|
|
||
| elif ty == "span" and isinstance(telemetry, StreamedSpan): | ||
| # Reset the span to its original value before we attempted | ||
| # to call the `before_send_span` callback | ||
| if exception_raised_in_before_send_func: | ||
| serialized = telemetry._to_json() | ||
|
|
||
| # Spans can't be dropped in before_send_span by design. They can | ||
| # be altered though (e.g. to sanitize). Only allow changes to | ||
| # name and attributes. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -195,15 +195,124 @@ def test_tolerates_traces_sampler_returning_a_boolean_span_streaming( | |
| ): | ||
| sentry_init( | ||
| traces_sampler=mock.Mock(return_value=traces_sampler_return_value), | ||
| _experiments={ | ||
| "trace_lifecycle": "stream", | ||
| }, | ||
| trace_lifecycle="stream", | ||
| ) | ||
|
|
||
| with sentry_sdk.traces.start_span(name="dogpark") as span: | ||
| assert span.sampled is traces_sampler_return_value | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [not related to this specific line or testcase] One case I'm not quite sure of: there is no incoming trace,
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Will add a couple, where the assertion is that the span/transaction isn't sampled |
||
| "traces_sample_rate,expected_decision", | ||
| [(0.0, False), (0.25, False), (0.75, True), (1.00, True)], | ||
| ) | ||
| def test_traces_sampler_raising_falls_back_to_traces_sample_rate( | ||
| sentry_init, | ||
| traces_sample_rate, | ||
| expected_decision, | ||
| ): | ||
| sentry_init( | ||
| traces_sampler=mock.Mock(side_effect=ValueError("boom")), | ||
| traces_sample_rate=traces_sample_rate, | ||
| ) | ||
|
|
||
| baggage = Baggage(sentry_items={"sample_rand": "0.500000"}) | ||
| transaction = start_transaction(name="dogpark", baggage=baggage) | ||
| assert transaction.sampled is expected_decision | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("parent_sampling_decision", [True, False]) | ||
| def test_traces_sampler_raising_falls_back_to_parent_sampling_decision( | ||
| sentry_init, parent_sampling_decision | ||
| ): | ||
| # set traces_sample_rate to produce the opposite of the parent decision, | ||
| # to prove the parent's decision takes precedence in the fallback | ||
| sentry_init( | ||
| traces_sampler=mock.Mock(side_effect=ValueError("boom")), | ||
| traces_sample_rate=0.0 if parent_sampling_decision else 1.0, | ||
| ) | ||
|
|
||
| baggage = Baggage(sentry_items={"sample_rand": "0.500000"}) | ||
| transaction = start_transaction( | ||
| name="dogpark", baggage=baggage, parent_sampled=parent_sampling_decision | ||
| ) | ||
| assert transaction.sampled is parent_sampling_decision | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "traces_sample_rate,expected_decision", | ||
| [(0.0, False), (0.25, False), (0.75, True), (1.00, True)], | ||
| ) | ||
| def test_traces_sampler_raising_falls_back_to_traces_sample_rate_span_streaming( | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can we also add a span streaming test case with no incoming trace propagation headers/no continue_trace and check that we fall back to |
||
| sentry_init, | ||
| traces_sample_rate, | ||
| expected_decision, | ||
| ): | ||
| sentry_init( | ||
| traces_sampler=mock.Mock(side_effect=ValueError("boom")), | ||
| traces_sample_rate=traces_sample_rate, | ||
| trace_lifecycle="stream", | ||
| ) | ||
|
|
||
| sentry_sdk.traces.continue_trace( | ||
| { | ||
| "sentry-trace": "0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331", | ||
| "baggage": "sentry-sample_rand=0.500000", | ||
| } | ||
| ) | ||
|
|
||
| with sentry_sdk.traces.start_span(name="dogpark") as span: | ||
| assert span.sampled is expected_decision | ||
|
|
||
|
|
||
| @pytest.mark.parametrize( | ||
| "traces_sample_rate,expected_decision", | ||
| [(0.0, False), (0.25, False), (0.75, True), (1.00, True)], | ||
| ) | ||
| def test_traces_sampler_raising_no_incoming_trace_falls_back_to_traces_sample_rate_span_streaming( | ||
| sentry_init, | ||
| traces_sample_rate, | ||
| expected_decision, | ||
| monkeypatch, | ||
| ): | ||
| sentry_init( | ||
| traces_sampler=mock.Mock(side_effect=ValueError("boom")), | ||
| traces_sample_rate=traces_sample_rate, | ||
| trace_lifecycle="stream", | ||
| ) | ||
|
|
||
| # no continue_trace, so no propagated sample_rand; make it deterministic | ||
| monkeypatch.setattr( | ||
| "sentry_sdk.tracing_utils._generate_sample_rand", lambda *a, **kw: 0.5 | ||
| ) | ||
|
|
||
| with sentry_sdk.traces.start_span(name="dogpark") as span: | ||
| assert span.sampled is expected_decision | ||
|
|
||
|
|
||
| def test_traces_sampler_raising_no_incoming_trace_and_no_traces_sample_rate( | ||
| sentry_init, | ||
| ): | ||
| sentry_init( | ||
| traces_sampler=mock.Mock(side_effect=ValueError("boom")), | ||
| ) | ||
|
|
||
| transaction = start_transaction(name="dogpark") | ||
| assert transaction.sampled is False | ||
|
|
||
|
|
||
| def test_traces_sampler_raising_no_incoming_trace_and_no_traces_sample_rate_span_streaming( | ||
| sentry_init, | ||
| ): | ||
| sentry_init( | ||
| traces_sampler=mock.Mock(side_effect=ValueError("boom")), | ||
| trace_lifecycle="stream", | ||
| ) | ||
|
|
||
| with sentry_sdk.traces.start_span(name="dogpark") as span: | ||
| assert span.sampled is False | ||
|
|
||
|
|
||
| @pytest.mark.parametrize("sampling_decision", [True, False]) | ||
| def test_only_captures_transaction_when_sampled_is_true( | ||
| sentry_init, sampling_decision, capture_events | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bug: The
capture_internal_exceptionscontext manager suppressesBaseExceptionsubclasses likeKeyboardInterrupt, preventing process termination when raised inbefore_sendcallbacks.Severity: CRITICAL
Suggested Fix
Modify the
__exit__method ofCaptureInternalExceptionto not suppressBaseExceptionsubclasses. It should check if the exception typetyis a subclass ofExceptionbefore suppressing it, allowing critical signals likeKeyboardInterruptandSystemExitto propagate and terminate the process as expected.Prompt for AI Agent
Also affects:
sentry_sdk/utils.py:208~223