Skip to content

fix(spring-jakarta): [Queue Instrumentation 11] Guard entire span lifecycle in Kafka producer interceptor#5281

Open
adinauer wants to merge 1 commit intofix/queue-instrumentation-root-scopesfrom
fix/queue-instrumentation-producer-guard
Open

fix(spring-jakarta): [Queue Instrumentation 11] Guard entire span lifecycle in Kafka producer interceptor#5281
adinauer wants to merge 1 commit intofix/queue-instrumentation-root-scopesfrom
fix/queue-instrumentation-producer-guard

Conversation

@adinauer
Copy link
Copy Markdown
Member

@adinauer adinauer commented Apr 10, 2026

PR Stack (Queue Instrumentation)


📜 Description

Wrap the entire span lifecycle in SentryProducerInterceptor.onSend() in a single try-catch so instrumentation can never break the customer's Kafka send. The ProducerRecord is always returned regardless of any exception in Sentry code.

Previously, only header injection was guarded. Span creation (startChild), setData(), and finish() were unguarded — any exception from these would propagate and fail the customer's Kafka send per the ProducerInterceptor.onSend() contract.

The inner try-catch around injectHeaders is removed since the outer catch now covers it.

💡 Motivation and Context

Kafka's ProducerInterceptor.onSend() contract requires that interceptors not throw exceptions — any exception propagates and fails the send. Observability code must never cause customer message loss.

💚 How did you test it?

  • All 8 existing SentryProducerInterceptorTest tests pass
  • The change is purely defensive — wraps existing logic in try-catch without changing behavior

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

Nothing.

⚠️ Merge this PR using a merge commit (not squash). Only the collection branch is squash-merged into main.

#skip-changelog

…terceptor

Wrap all span operations (startChild, setData, injectHeaders, finish)
in a single try-catch so instrumentation can never break the customer's
Kafka send. The record is always returned regardless of any exception
in Sentry code.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 10, 2026

Semver Impact of This PR

🟢 Patch (bug fixes)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


This PR will not appear in the changelog.


🤖 This preview updates automatically when you update the PR.

@sentry
Copy link
Copy Markdown

sentry Bot commented Apr 10, 2026

Sentry Build Distribution

App Name App ID Version Configuration Install Page
SDK Size io.sentry.tests.size 8.37.1 (1) release Install Build

Configure sentry-android build distribution settings

@github-actions
Copy link
Copy Markdown
Contributor

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 347.98 ms 413.21 ms 65.23 ms
Size 0 B 0 B 0 B

Baseline results on branch: fix/queue-instrumentation-root-scopes

Startup times

Revision Plain With Sentry Diff
22ae1be 333.60 ms 393.04 ms 59.44 ms

App size

Revision Plain With Sentry Diff
22ae1be 0 B 0 B 0 B

span.finish();
} catch (Throwable ignored) {
// Header injection must not break the send
// Instrumentation must never break the customer's Kafka send
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as in the original, should we log anything here?

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 6d91bdc. Configure here.


span.setStatus(SpanStatus.OK);
span.finish();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Span leaks unfinished when exception occurs after creation

Medium Severity

If an exception is thrown by setData(), injectHeaders(), or setStatus() after startChild() successfully creates a span, the catch block swallows the exception but never calls span.finish(). This leaves an orphaned, unfinished span. This is a regression from the previous code where injectHeaders failures were caught independently and span.finish() still ran. The pattern used elsewhere in this codebase (e.g., SentryCacheWrapper) uses finally { span.finish() } to guarantee cleanup.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 6d91bdc. Configure here.

Comment on lines +62 to 72
return record;
}

span.setData(SpanDataConvention.MESSAGING_SYSTEM, "kafka");
span.setData(SpanDataConvention.MESSAGING_DESTINATION_NAME, record.topic());
span.setData(SpanDataConvention.MESSAGING_SYSTEM, "kafka");
span.setData(SpanDataConvention.MESSAGING_DESTINATION_NAME, record.topic());

try {
injectHeaders(record.headers(), span);

span.setStatus(SpanStatus.OK);
span.finish();
} catch (Throwable ignored) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: If injectHeaders() throws an exception in onSend(), the created span is never finished, causing a span leak. The catch block bypasses the span.finish() call.
Severity: MEDIUM

Suggested Fix

Ensure span.finish() is always called after a span is created, even if an exception occurs. This can be done by moving the span creation and finishing logic into a try-finally block, ensuring span.finish() is called in the finally section.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location:
sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/kafka/SentryProducerInterceptor.java#L56-L72

Potential issue: In `SentryProducerInterceptor.onSend()`, the refactored exception
handling introduces a potential span leak. The `span.finish()` call is now inside a
`try` block. If `injectHeaders()` throws an exception, control will jump to the `catch`
block, skipping the `span.finish()` call. The `catch` block does not finish the span,
causing it to remain unfinished in the parent transaction's span list. This is a
regression from the previous code, which correctly ensured the span was always finished
by placing the call outside the `try` block for header injection.

Did we get this right? 👍 / 👎 to inform future reviews.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants