Skip to content

Commit

Permalink
Add Annotation SentryCaptureException for org.springframework.web.bin…
Browse files Browse the repository at this point in the history
…d.annotation.ExceptionHandler (#2764)

Co-authored-by: Alexander Dinauer <alexander.dinauer@sentry.io>
  • Loading branch information
xeromank and adinauer authored Oct 9, 2023
1 parent bb0533f commit 99a51e2
Show file tree
Hide file tree
Showing 21 changed files with 662 additions and 2 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
### Features

- Add `CheckInUtils.withCheckIn` which abstracts away some of the manual check-ins complexity ([#2959](https://github.com/getsentry/sentry-java/pull/2959))
- Add `@SentryCaptureExceptionParameter` annotation which captures exceptions passed into an annotated method ([#2764](https://github.com/getsentry/sentry-java/pull/2764))
- This can be used to replace `Sentry.captureException` calls in `@ExceptionHandler` of a `@ControllerAdvice`

## 6.30.0

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration;
import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration;
import io.sentry.spring.jakarta.checkin.SentryQuartzConfiguration;
import io.sentry.spring.jakarta.exception.SentryCaptureExceptionParameterPointcutConfiguration;
import io.sentry.spring.jakarta.exception.SentryExceptionParameterAdviceConfiguration;
import io.sentry.spring.jakarta.graphql.SentryGraphqlConfiguration;
import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration;
import io.sentry.spring.jakarta.tracing.SentrySpanPointcutConfiguration;
Expand Down Expand Up @@ -304,6 +306,22 @@ public FilterRegistrationBean<SentryTracingFilter> sentryTracingFilter(
}
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ProceedingJoinPoint.class)
@ConditionalOnProperty(
value = "sentry.enable-aot-compatibility",
havingValue = "false",
matchIfMissing = true)
@Import(SentryExceptionParameterAdviceConfiguration.class)
@Open
static class SentryErrorAspectsConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "sentryCaptureExceptionParameterPointcut")
@Import(SentryCaptureExceptionParameterPointcutConfiguration.class)
@Open
static class SentryCaptureExceptionParameterPointcutAutoConfiguration {}
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(
value = "sentry.enable-aot-compatibility",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,23 @@ class SentryAutoConfigurationTest {
}
}

@Test
fun `creates AOP beans to support @SentryCaptureExceptionParameter`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj")
.run {
assertThat(it).hasSentryExceptionParameterAdviceBeans()
}
}

@Test
fun `does not create AOP beans to support @SentryCaptureExceptionParameter if AOP class is missing`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj")
.withClassLoader(FilteredClassLoader(ProceedingJoinPoint::class.java))
.run {
assertThat(it).doesNotHaveSentryExceptionParameterAdviceBeans()
}
}

@Test
fun `when tracing is enabled creates AOP beans to support @SentryTransaction`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0")
Expand Down Expand Up @@ -957,6 +974,20 @@ class SentryAutoConfigurationTest {
return this
}

private fun <C : ApplicationContext> ApplicationContextAssert<C>.hasSentryExceptionParameterAdviceBeans(): ApplicationContextAssert<C> {
this.hasBean("sentryCaptureExceptionParameterPointcut")
this.hasBean("sentryCaptureExceptionParameterAdvice")
this.hasBean("sentryCaptureExceptionParameterAdvisor")
return this
}

private fun <C : ApplicationContext> ApplicationContextAssert<C>.doesNotHaveSentryExceptionParameterAdviceBeans(): ApplicationContextAssert<C> {
this.doesNotHaveBean("sentryCaptureExceptionParameterPointcut")
this.doesNotHaveBean("sentryCaptureExceptionParameterAdvice")
this.doesNotHaveBean("sentryCaptureExceptionParameterAdvisor")
return this
}

private fun ApplicationContext.getSentryUserProviders(): List<SentryUserProvider> {
val userFilter = this.getBean("sentryUserFilter", FilterRegistrationBean::class.java).filter as SentryUserFilter
return userFilter.sentryUserProviders
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration;
import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration;
import io.sentry.spring.checkin.SentryQuartzConfiguration;
import io.sentry.spring.exception.SentryCaptureExceptionParameterPointcutConfiguration;
import io.sentry.spring.exception.SentryExceptionParameterAdviceConfiguration;
import io.sentry.spring.graphql.SentryGraphqlConfiguration;
import io.sentry.spring.tracing.SentryAdviceConfiguration;
import io.sentry.spring.tracing.SentrySpanPointcutConfiguration;
Expand Down Expand Up @@ -304,6 +306,22 @@ public FilterRegistrationBean<SentryTracingFilter> sentryTracingFilter(
}
}

@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ProceedingJoinPoint.class)
@ConditionalOnProperty(
value = "sentry.enable-aot-compatibility",
havingValue = "false",
matchIfMissing = true)
@Import(SentryExceptionParameterAdviceConfiguration.class)
@Open
static class SentryErrorAspectsConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = "sentryCaptureExceptionParameterPointcut")
@Import(SentryCaptureExceptionParameterPointcutConfiguration.class)
@Open
static class SentryCaptureExceptionParameterPointcutAutoConfiguration {}
}

@Configuration(proxyBeanMethods = false)
@Conditional(SentryTracingCondition.class)
@ConditionalOnClass(ProceedingJoinPoint.class)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,23 @@ class SentryAutoConfigurationTest {
}
}

@Test
fun `creates AOP beans to support @SentryCaptureExceptionParameter`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj")
.run {
assertThat(it).hasSentryExceptionParameterAdviceBeans()
}
}

@Test
fun `does not create AOP beans to support @SentryCaptureExceptionParameter if AOP class is missing`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj")
.withClassLoader(FilteredClassLoader(ProceedingJoinPoint::class.java))
.run {
assertThat(it).doesNotHaveSentryExceptionParameterAdviceBeans()
}
}

@Test
fun `when tracing is enabled creates AOP beans to support @SentryTransaction`() {
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0")
Expand Down Expand Up @@ -957,6 +974,20 @@ class SentryAutoConfigurationTest {
return this
}

private fun <C : ApplicationContext> ApplicationContextAssert<C>.hasSentryExceptionParameterAdviceBeans(): ApplicationContextAssert<C> {
this.hasBean("sentryCaptureExceptionParameterPointcut")
this.hasBean("sentryCaptureExceptionParameterAdvice")
this.hasBean("sentryCaptureExceptionParameterAdvisor")
return this
}

private fun <C : ApplicationContext> ApplicationContextAssert<C>.doesNotHaveSentryExceptionParameterAdviceBeans(): ApplicationContextAssert<C> {
this.doesNotHaveBean("sentryCaptureExceptionParameterPointcut")
this.doesNotHaveBean("sentryCaptureExceptionParameterAdvice")
this.doesNotHaveBean("sentryCaptureExceptionParameterAdvisor")
return this
}

private fun ApplicationContext.getSentryUserProviders(): List<SentryUserProvider> {
val userFilter = this.getBean("sentryUserFilter", FilterRegistrationBean::class.java).filter as SentryUserFilter
return userFilter.sentryUserProviders
Expand Down
23 changes: 23 additions & 0 deletions sentry-spring-jakarta/api/sentry-spring-jakarta.api
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,29 @@ public final class io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCu
public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V
}

public abstract interface annotation class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter : java/lang/annotation/Annotation {
}

public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice : org/aopalliance/intercept/MethodInterceptor {
public fun <init> (Lio/sentry/IHub;)V
public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object;
}

public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration {
public fun <init> ()V
}

public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration {
public fun <init> ()V
public fun sentryCaptureExceptionParameterPointcut ()Lorg/springframework/aop/Pointcut;
}

public class io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration {
public fun <init> ()V
public fun sentryCaptureExceptionParameterAdvice (Lio/sentry/IHub;)Lorg/aopalliance/aop/Advice;
public fun sentryCaptureExceptionParameterAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor;
}

public final class io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry : org/springframework/graphql/execution/BatchLoaderRegistry {
public fun forName (Ljava/lang/String;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec;
public fun forTypePair (Ljava/lang/Class;Ljava/lang/Class;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package io.sentry.spring.jakarta.exception;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* Captures an exception passed to an annotated method. Can be used to capture exceptions from your
* {@link org.springframework.web.bind.annotation.ExceptionHandler} but can also be used on other
* methods.
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface SentryCaptureExceptionParameter {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.sentry.spring.jakarta.exception;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.IHub;
import io.sentry.exception.ExceptionMechanismException;
import io.sentry.protocol.Mechanism;
import io.sentry.util.Objects;
import java.lang.reflect.Method;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.support.AopUtils;
import org.springframework.core.annotation.AnnotationUtils;

/**
* Captures an exception passed to a bean method annotated with {@link
* SentryCaptureExceptionParameter}.
*/
@ApiStatus.Internal
@Open
public class SentryCaptureExceptionParameterAdvice implements MethodInterceptor {
private static final String MECHANISM_TYPE = "SentrySpring6CaptureExceptionParameterAdvice";
private final @NotNull IHub hub;

public SentryCaptureExceptionParameterAdvice(final @NotNull IHub hub) {
this.hub = Objects.requireNonNull(hub, "hub is required");
}

@Override
public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable {
final Method mostSpecificMethod =
AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass());
SentryCaptureExceptionParameter sentryCaptureExceptionParameter =
AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCaptureExceptionParameter.class);

if (sentryCaptureExceptionParameter != null) {
Object[] args = invocation.getArguments();
for (Object arg : args) {
if (arg instanceof Exception) {
captureException((Exception) arg);
break;
}
}
}

return invocation.proceed();
}

private void captureException(final @NotNull Throwable throwable) {
final Mechanism mechanism = new Mechanism();
mechanism.setType(MECHANISM_TYPE);
mechanism.setHandled(true);
final Throwable mechanismException =
new ExceptionMechanismException(mechanism, throwable, Thread.currentThread());
hub.captureException(mechanismException);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package io.sentry.spring.jakarta.exception;

import com.jakewharton.nopen.annotation.Open;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
* Provides infrastructure beans for capturing exceptions passed to bean methods annotated with
* {@link SentryCaptureExceptionParameter}.
*/
@Configuration
@Import({
SentryExceptionParameterAdviceConfiguration.class,
SentryCaptureExceptionParameterPointcutConfiguration.class
})
@Open
public class SentryCaptureExceptionParameterConfiguration {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package io.sentry.spring.jakarta.exception;

import com.jakewharton.nopen.annotation.Open;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.ComposablePointcut;
import org.springframework.aop.support.annotation.AnnotationClassFilter;
import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** AOP pointcut configuration for {@link SentryCaptureExceptionParameter}. */
@Configuration(proxyBeanMethods = false)
@Open
public class SentryCaptureExceptionParameterPointcutConfiguration {

/**
* Pointcut around which spans are created.
*
* @return pointcut used by {@link SentryCaptureExceptionParameterAdvice}.
*/
@Bean
public @NotNull Pointcut sentryCaptureExceptionParameterPointcut() {
return new ComposablePointcut(
new AnnotationClassFilter(SentryCaptureExceptionParameter.class, true))
.union(new AnnotationMatchingPointcut(null, SentryCaptureExceptionParameter.class));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package io.sentry.spring.jakarta.exception;

import com.jakewharton.nopen.annotation.Open;
import io.sentry.IHub;
import org.aopalliance.aop.Advice;
import org.jetbrains.annotations.NotNull;
import org.springframework.aop.Advisor;
import org.springframework.aop.Pointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** Creates advice infrastructure for {@link SentryCaptureExceptionParameter}. */
@Configuration(proxyBeanMethods = false)
@Open
public class SentryExceptionParameterAdviceConfiguration {

@Bean
public @NotNull Advice sentryCaptureExceptionParameterAdvice(final @NotNull IHub hub) {
return new SentryCaptureExceptionParameterAdvice(hub);
}

@Bean
public @NotNull Advisor sentryCaptureExceptionParameterAdvisor(
final @NotNull @Qualifier("sentryCaptureExceptionParameterPointcut") Pointcut
sentryCaptureExceptionParameterPointcut,
final @NotNull @Qualifier("sentryCaptureExceptionParameterAdvice") Advice
sentryCaptureExceptionParameterAdvice) {
return new DefaultPointcutAdvisor(
sentryCaptureExceptionParameterPointcut, sentryCaptureExceptionParameterAdvice);
}
}
Loading

0 comments on commit 99a51e2

Please sign in to comment.