@@ -37,9 +37,11 @@ public class SentryTracingFilter extends OncePerRequestFilter {
3737 private static final String TRANSACTION_OP = "http.server" ;
3838
3939 private static final String TRACE_ORIGIN = "auto.http.spring_jakarta.webmvc" ;
40+ private static final String TRANSACTION_ATTR = "sentry.transaction" ;
4041
4142 private final @ NotNull TransactionNameProvider transactionNameProvider ;
4243 private final @ NotNull IScopes scopes ;
44+ private final boolean isAsyncSupportEnabled ;
4345
4446 /**
4547 * Creates filter that resolves transaction name using {@link SpringMvcTransactionNameProvider}.
@@ -63,28 +65,52 @@ public SentryTracingFilter() {
6365 public SentryTracingFilter (
6466 final @ NotNull IScopes scopes ,
6567 final @ NotNull TransactionNameProvider transactionNameProvider ) {
68+ this (scopes , transactionNameProvider , false );
69+ }
70+
71+ /**
72+ * Creates filter that resolves transaction name using transaction name provider given by
73+ * parameter.
74+ *
75+ * @param scopes - the scopes
76+ * @param transactionNameProvider - transaction name provider.
77+ * @param isAsyncSupportEnabled - whether transactions should be kept open until async handling is
78+ * done
79+ */
80+ public SentryTracingFilter (
81+ final @ NotNull IScopes scopes ,
82+ final @ NotNull TransactionNameProvider transactionNameProvider ,
83+ final boolean isAsyncSupportEnabled ) {
6684 this .scopes = Objects .requireNonNull (scopes , "Scopes are required" );
6785 this .transactionNameProvider =
6886 Objects .requireNonNull (transactionNameProvider , "transactionNameProvider is required" );
87+ this .isAsyncSupportEnabled = isAsyncSupportEnabled ;
6988 }
7089
7190 public SentryTracingFilter (final @ NotNull IScopes scopes ) {
7291 this (scopes , new SpringMvcTransactionNameProvider ());
7392 }
7493
94+ @ Override
95+ protected boolean shouldNotFilterAsyncDispatch () {
96+ return !isAsyncSupportEnabled ;
97+ }
98+
7599 @ Override
76100 protected void doFilterInternal (
77101 final @ NotNull HttpServletRequest httpRequest ,
78102 final @ NotNull HttpServletResponse httpResponse ,
79103 final @ NotNull FilterChain filterChain )
80104 throws ServletException , IOException {
81105 if (scopes .isEnabled () && !isIgnored ()) {
82- final @ Nullable String sentryTraceHeader =
83- httpRequest .getHeader (SentryTraceHeader .SENTRY_TRACE_HEADER );
84- final @ Nullable List <String > baggageHeader =
85- Collections .list (httpRequest .getHeaders (BaggageHeader .BAGGAGE_HEADER ));
86- final @ Nullable TransactionContext transactionContext =
87- scopes .continueTrace (sentryTraceHeader , baggageHeader );
106+ @ Nullable TransactionContext transactionContext = null ;
107+ if (shouldContinueTrace (httpRequest )) {
108+ final @ Nullable String sentryTraceHeader =
109+ httpRequest .getHeader (SentryTraceHeader .SENTRY_TRACE_HEADER );
110+ final @ Nullable List <String > baggageHeader =
111+ Collections .list (httpRequest .getHeaders (BaggageHeader .BAGGAGE_HEADER ));
112+ transactionContext = scopes .continueTrace (sentryTraceHeader , baggageHeader );
113+ }
88114 if (scopes .getOptions ().isTracingEnabled () && shouldTraceRequest (httpRequest )) {
89115 doFilterWithTransaction (httpRequest , httpResponse , filterChain , transactionContext );
90116 } else {
@@ -105,35 +131,85 @@ private void doFilterWithTransaction(
105131 FilterChain filterChain ,
106132 final @ Nullable TransactionContext transactionContext )
107133 throws IOException , ServletException {
108- // at this stage we are not able to get real transaction name
109- final ITransaction transaction = startTransaction (httpRequest , transactionContext );
134+ final @ Nullable ITransaction transaction =
135+ getOrStartTransaction (httpRequest , transactionContext );
110136
111137 try {
112138 filterChain .doFilter (httpRequest , httpResponse );
113139 } catch (Throwable e ) {
114- // exceptions that are not handled by Spring
115- transaction .setStatus (SpanStatus .INTERNAL_ERROR );
140+ if (transaction != null ) {
141+ // exceptions that are not handled by Spring
142+ transaction .setStatus (SpanStatus .INTERNAL_ERROR );
143+ }
116144 throw e ;
117145 } finally {
118- // after all filters run, templated path pattern is available in request attribute
119- final String transactionName = transactionNameProvider .provideTransactionName (httpRequest );
120- final TransactionNameSource transactionNameSource =
121- transactionNameProvider .provideTransactionSource ();
122- // if transaction name is not resolved, the request has not been processed by a controller
123- // and we should not report it to Sentry
124- if (transactionName != null ) {
125- transaction .setName (transactionName , transactionNameSource );
126- transaction .setOperation (TRANSACTION_OP );
127- // if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and
128- // httpResponse.getStatus() returns 200.
129- if (transaction .getStatus () == null ) {
130- transaction .setStatus (SpanStatus .fromHttpStatusCode (httpResponse .getStatus ()));
146+ if (shouldFinishTransaction (httpRequest ) && transaction != null ) {
147+ // after all filters run, templated path pattern is available in request attribute
148+ final String transactionName = transactionNameProvider .provideTransactionName (httpRequest );
149+ final TransactionNameSource transactionNameSource =
150+ transactionNameProvider .provideTransactionSource ();
151+ // if transaction name is not resolved, the request has not been processed by a controller
152+ // and we should not report it to Sentry
153+ if (transactionName != null ) {
154+ transaction .setName (transactionName , transactionNameSource );
155+ transaction .setOperation (TRANSACTION_OP );
156+ // if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and
157+ // httpResponse.getStatus() returns 200.
158+ if (transaction .getStatus () == null ) {
159+ transaction .setStatus (SpanStatus .fromHttpStatusCode (httpResponse .getStatus ()));
160+ }
161+ transaction .finish ();
131162 }
132- transaction .finish ();
133163 }
134164 }
135165 }
136166
167+ private ITransaction getOrStartTransaction (
168+ final @ NotNull HttpServletRequest httpRequest ,
169+ final @ Nullable TransactionContext transactionContext ) {
170+ if (isAsyncDispatch (httpRequest )) {
171+ // second invocation of this filter for the same async request already has the transaction
172+ // in the attributes
173+ return (ITransaction ) httpRequest .getAttribute (TRANSACTION_ATTR );
174+ } else {
175+ // at this stage we are not able to get real transaction name
176+ final @ NotNull ITransaction transaction = startTransaction (httpRequest , transactionContext );
177+ if (shouldStoreTransactionForAsyncProcessing ()) {
178+ httpRequest .setAttribute (TRANSACTION_ATTR , transaction );
179+ }
180+ return transaction ;
181+ }
182+ }
183+
184+ /**
185+ * Returns false if an async request is being dispatched (second invocation of the filter for the
186+ * same async request).
187+ *
188+ * <p>Returns true if not an async request or this is the first invocation of the filter for the
189+ * same async request
190+ */
191+ private boolean shouldContinueTrace (HttpServletRequest httpRequest ) {
192+ return !isAsyncSupportEnabled || !isAsyncDispatch (httpRequest );
193+ }
194+
195+ private boolean shouldStoreTransactionForAsyncProcessing () {
196+ return isAsyncSupportEnabled ;
197+ }
198+
199+ /**
200+ * Returns false if async request handling has only been started but not yet finished (first
201+ * invocation of this filter for the same async request).
202+ *
203+ * <p>Returns true if not an async request or async request handling has finished (second
204+ * invocation of this filter for the same async request)
205+ *
206+ * <p>Note: isAsyncStarted changes its return value after filterChain.doFilter() of the first
207+ * async invocation
208+ */
209+ private boolean shouldFinishTransaction (HttpServletRequest httpRequest ) {
210+ return !isAsyncSupportEnabled || !isAsyncStarted (httpRequest );
211+ }
212+
137213 private boolean shouldTraceRequest (final @ NotNull HttpServletRequest request ) {
138214 return scopes .getOptions ().isTraceOptionsRequests ()
139215 || !HttpMethod .OPTIONS .name ().equals (request .getMethod ());
0 commit comments