Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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