From 3ec789b56240f468aacb62a1339e59e54a7f1dda Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 4 Aug 2025 14:32:37 +0200 Subject: [PATCH 01/10] Add new modules for Spring 7 and Spring Boot 4 --- .craft.yml | 3 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 3 + .github/workflows/system-tests-backend.yml | 15 + build.gradle.kts | 5 + buildSrc/src/main/java/Config.kt | 3 + gradle/libs.versions.toml | 14 + .../sentry-samples-spring-7/README.md | 19 + .../sentry-samples-spring-7/build.gradle.kts | 52 + .../samples/spring/jakarta/AppConfig.java | 20 + .../spring/jakarta/AppInitializer.java | 45 + .../spring/jakarta/SecurityConfiguration.java | 42 + .../samples/spring/jakarta/SentryConfig.java | 37 + .../samples/spring/jakarta/WebConfig.java | 34 + .../samples/spring/jakarta/web/Person.java | 24 + .../spring/jakarta/web/PersonController.java | 42 + .../spring/jakarta/web/PersonService.java | 39 + .../src/main/resources/logback.xml | 14 + .../src/main/resources/sentry.properties | 2 + .../README.md | 122 ++ .../build.gradle.kts | 93 ++ .../boot/jakarta/CustomEventProcessor.java | 35 + .../spring/boot/jakarta/CustomJob.java | 26 + .../jakarta/DistributedTracingController.java | 51 + .../samples/spring/boot/jakarta/Person.java | 24 + .../spring/boot/jakarta/PersonController.java | 66 + .../spring/boot/jakarta/PersonService.java | 41 + .../boot/jakarta/SecurityConfiguration.java | 40 + .../boot/jakarta/SentryDemoApplication.java | 78 + .../samples/spring/boot/jakarta/Todo.java | 25 + .../spring/boot/jakarta/TodoController.java | 86 ++ .../jakarta/graphql/AssigneeController.java | 34 + .../jakarta/graphql/GreetingController.java | 17 + .../jakarta/graphql/ProjectController.java | 140 ++ .../graphql/TaskCreatorController.java | 49 + .../spring/boot/jakarta/quartz/SampleJob.java | 19 + .../src/main/resources/application.properties | 39 + .../main/resources/graphql/schema.graphqls | 68 + .../src/main/resources/quartz.properties | 1 + .../src/main/resources/schema.sql | 5 + .../src/test/kotlin/io/sentry/DummyTest.kt | 12 + .../DistributedTracingSystemTest.kt | 197 +++ .../systemtest/GraphqlGreetingSystemTest.kt | 46 + .../systemtest/GraphqlProjectSystemTest.kt | 66 + .../systemtest/GraphqlTaskSystemTest.kt | 40 + .../io/sentry/systemtest/PersonSystemTest.kt | 115 ++ .../io/sentry/systemtest/TodoSystemTest.kt | 65 + .../src/test/resources/logback.xml | 17 + .../README.md | 126 ++ .../build.gradle.kts | 120 ++ .../boot/jakarta/CustomEventProcessor.java | 35 + .../spring/boot/jakarta/CustomJob.java | 25 + .../jakarta/DistributedTracingController.java | 49 + .../samples/spring/boot/jakarta/Person.java | 24 + .../spring/boot/jakarta/PersonController.java | 64 + .../spring/boot/jakarta/PersonService.java | 41 + .../boot/jakarta/SecurityConfiguration.java | 40 + .../boot/jakarta/SentryDemoApplication.java | 78 + .../samples/spring/boot/jakarta/Todo.java | 25 + .../spring/boot/jakarta/TodoController.java | 86 ++ .../jakarta/graphql/AssigneeController.java | 34 + .../jakarta/graphql/GreetingController.java | 17 + .../jakarta/graphql/ProjectController.java | 140 ++ .../graphql/TaskCreatorController.java | 49 + .../spring/boot/jakarta/quartz/SampleJob.java | 19 + .../src/main/resources/application.properties | 33 + .../main/resources/graphql/schema.graphqls | 68 + .../src/main/resources/quartz.properties | 1 + .../src/main/resources/schema.sql | 5 + .../src/test/kotlin/io/sentry/DummyTest.kt | 12 + .../DistributedTracingSystemTest.kt | 197 +++ .../systemtest/GraphqlGreetingSystemTest.kt | 46 + .../systemtest/GraphqlProjectSystemTest.kt | 66 + .../systemtest/GraphqlTaskSystemTest.kt | 40 + .../io/sentry/systemtest/PersonSystemTest.kt | 115 ++ .../io/sentry/systemtest/TodoSystemTest.kt | 61 + .../src/test/resources/logback.xml | 17 + .../README.md | 44 + .../build.gradle.kts | 73 + .../jakarta/DistributedTracingController.java | 52 + .../samples/spring/boot/jakarta/Person.java | 24 + .../spring/boot/jakarta/PersonController.java | 37 + .../spring/boot/jakarta/PersonService.java | 21 + .../boot/jakarta/SentryDemoApplication.java | 18 + .../samples/spring/boot/jakarta/Todo.java | 25 + .../spring/boot/jakarta/TodoController.java | 26 + .../jakarta/graphql/GreetingController.java | 19 + .../src/main/resources/application.properties | 14 + .../main/resources/graphql/schema.graphqls | 70 + .../src/test/kotlin/io/sentry/DummyTest.kt | 12 + .../DistributedTracingSystemTest.kt | 197 +++ .../systemtest/GraphqlGreetingSystemTest.kt | 46 + .../io/sentry/systemtest/PersonSystemTest.kt | 50 + .../io/sentry/systemtest/TodoSystemTest.kt | 31 + .../src/test/resources/logback.xml | 17 + .../sentry-samples-spring-boot-4/README.md | 122 ++ .../build.gradle.kts | 94 ++ .../boot/jakarta/CustomEventProcessor.java | 35 + .../spring/boot/jakarta/CustomJob.java | 25 + .../jakarta/DistributedTracingController.java | 49 + .../samples/spring/boot/jakarta/Person.java | 24 + .../spring/boot/jakarta/PersonController.java | 49 + .../spring/boot/jakarta/PersonService.java | 41 + .../boot/jakarta/SecurityConfiguration.java | 40 + .../boot/jakarta/SentryDemoApplication.java | 71 + .../samples/spring/boot/jakarta/Todo.java | 25 + .../spring/boot/jakarta/TodoController.java | 57 + .../jakarta/graphql/AssigneeController.java | 34 + .../jakarta/graphql/GreetingController.java | 17 + .../jakarta/graphql/ProjectController.java | 140 ++ .../graphql/TaskCreatorController.java | 50 + .../spring/boot/jakarta/quartz/SampleJob.java | 19 + .../src/main/resources/application.properties | 34 + .../main/resources/graphql/schema.graphqls | 68 + .../src/main/resources/quartz.properties | 1 + .../src/main/resources/schema.sql | 5 + .../src/test/kotlin/io/sentry/DummyTest.kt | 12 + .../DistributedTracingSystemTest.kt | 197 +++ .../systemtest/GraphqlGreetingSystemTest.kt | 46 + .../systemtest/GraphqlProjectSystemTest.kt | 66 + .../systemtest/GraphqlTaskSystemTest.kt | 50 + .../io/sentry/systemtest/PersonSystemTest.kt | 55 + .../io/sentry/systemtest/TodoSystemTest.kt | 61 + .../src/test/resources/logback.xml | 17 + sentry-spring-7/api/sentry-spring-jakarta.api | 363 +++++ sentry-spring-7/build.gradle.kts | 127 ++ .../jakarta/ContextTagsEventProcessor.java | 46 + .../sentry/spring/jakarta/EnableSentry.java | 52 + .../HttpServletRequestSentryUserProvider.java | 55 + .../jakarta/RequestPayloadExtractor.java | 34 + .../jakarta/SentryExceptionResolver.java | 93 ++ .../spring/jakarta/SentryHubRegistrar.java | 103 ++ .../jakarta/SentryInitBeanPostProcessor.java | 91 ++ ...tryRequestHttpServletRequestProcessor.java | 39 + .../spring/jakarta/SentryRequestResolver.java | 114 ++ .../spring/jakarta/SentrySpringFilter.java | 165 +++ ...ntrySpringServletContainerInitializer.java | 38 + .../spring/jakarta/SentryTaskDecorator.java | 27 + .../spring/jakarta/SentryUserFilter.java | 82 ++ .../spring/jakarta/SentryUserProvider.java | 14 + .../jakarta/SentryWebConfiguration.java | 23 + .../jakarta/SpringProfilesEventProcessor.java | 51 + .../SpringSecuritySentryUserProvider.java | 35 + .../spring/jakarta/checkin/SentryCheckIn.java | 41 + .../jakarta/checkin/SentryCheckInAdvice.java | 119 ++ .../SentryCheckInAdviceConfiguration.java | 35 + .../SentryCheckInPointcutConfiguration.java | 33 + .../checkin/SentryQuartzConfiguration.java | 21 + .../SentrySchedulerFactoryBeanCustomizer.java | 14 + .../SentryCaptureExceptionParameter.java | 15 + ...SentryCaptureExceptionParameterAdvice.java | 63 + ...aptureExceptionParameterConfiguration.java | 17 + ...ceptionParameterPointcutConfiguration.java | 32 + ...ExceptionParameterAdviceConfiguration.java | 37 + .../graphql/SentryBatchLoaderRegistry.java | 119 ++ ...ryDataFetcherExceptionResolverAdapter.java | 48 + .../graphql/SentryDgsSubscriptionHandler.java | 34 + .../graphql/SentryGraphql22Configuration.java | 64 + .../SentryGraphqlBeanPostProcessor.java | 25 + .../graphql/SentryGraphqlConfiguration.java | 64 + .../SentrySpringSubscriptionHandler.java | 35 + ...etryAgentWithoutAutoInitConfiguration.java | 27 + ...ntryOpenTelemetryNoAgentConfiguration.java | 38 + .../CombinedTransactionNameProvider.java | 55 + .../tracing/SentryAdviceConfiguration.java | 48 + .../spring/jakarta/tracing/SentrySpan.java | 41 + .../jakarta/tracing/SentrySpanAdvice.java | 82 ++ ...entrySpanClientHttpRequestInterceptor.java | 143 ++ .../SentrySpanClientWebRequestFilter.java | 126 ++ .../SentrySpanPointcutConfiguration.java | 31 + .../tracing/SentryTracingConfiguration.java | 18 + .../jakarta/tracing/SentryTracingFilter.java | 245 ++++ .../jakarta/tracing/SentryTransaction.java | 39 + .../tracing/SentryTransactionAdvice.java | 125 ++ ...entryTransactionPointcutConfiguration.java | 31 + .../SpringMvcTransactionNameProvider.java | 35 + .../SpringServletTransactionNameProvider.java | 22 + .../tracing/TransactionNameProvider.java | 38 + .../tracing/TransactionNameWithSource.java | 27 + .../webflux/AbstractSentryWebFilter.java | 168 +++ .../webflux/SentryRequestResolver.java | 70 + .../jakarta/webflux/SentryScheduleHook.java | 26 + .../webflux/SentryWebExceptionHandler.java | 73 + .../jakarta/webflux/SentryWebFilter.java | 45 + ...entryWebFilterWithThreadLocalAccessor.java | 53 + .../webflux/TransactionNameProvider.java | 32 + .../jakarta/webflux/reactor/ReactorUtils.java | 9 + ...akarta.servlet.ServletContainerInitializer | 1 + .../jakarta/ContextTagsEventProcessorTest.kt | 86 ++ .../sentry/spring/jakarta/EnableSentryTest.kt | 239 +++ ...ttpServletRequestSentryUserProviderTest.kt | 63 + .../spring/jakarta/SentryCheckInAdviceTest.kt | 310 ++++ .../jakarta/SentryExceptionResolverTest.kt | 117 ++ .../SentryInitBeanPostProcessorTest.kt | 26 + ...yRequestHttpServletRequestProcessorTest.kt | 64 + .../spring/jakarta/SentrySpringFilterTest.kt | 317 ++++ .../spring/jakarta/SentryTaskDecoratorTest.kt | 58 + .../spring/jakarta/SentryUserFilterTest.kt | 128 ++ .../SpringProfilesEventProcessorTest.kt | 83 ++ .../SpringSecuritySentryUserProviderTest.kt | 56 + ...ntryCaptureExceptionParameterAdviceTest.kt | 69 + .../SentrySpringSubscriptionHandlerTest.kt | 98 ++ .../mvc/SentrySpringIntegrationTest.kt | 537 +++++++ .../jakarta/tracing/SentrySpanAdviceTest.kt | 180 +++ .../tracing/SentryTracingFilterTest.kt | 464 ++++++ .../tracing/SentryTransactionAdviceTest.kt | 201 +++ .../jakarta/webflux/SentryScheduleHookTest.kt | 58 + .../webflux/SentryWebFluxTracingFilterTest.kt | 386 +++++ .../webflux/SentryWebfluxIntegrationTest.kt | 196 +++ sentry-spring-boot-4-starter/.gitignore | 1 + .../sentry-spring-boot-starter-jakarta.api | 0 sentry-spring-boot-4-starter/build.gradle.kts | 80 + sentry-spring-boot-4/.gitignore | 1 + .../api/sentry-spring-boot-jakarta.api | 93 ++ sentry-spring-boot-4/build.gradle.kts | 143 ++ .../boot/jakarta/InAppIncludesResolver.java | 43 + .../boot/jakarta/SentryAutoConfiguration.java | 500 +++++++ ...entryLogbackAppenderAutoConfiguration.java | 26 + .../jakarta/SentryLogbackInitializer.java | 85 ++ .../spring/boot/jakarta/SentryProperties.java | 218 +++ .../SentrySpanRestClientCustomizer.java | 30 + .../SentrySpanRestTemplateCustomizer.java | 31 + .../SentrySpanWebClientCustomizer.java | 22 + .../jakarta/SentrySpringVersionChecker.java | 30 + .../SentryWebfluxAutoConfiguration.java | 120 ++ .../SentryGraphql22AutoConfiguration.java | 72 + .../SentryGraphqlAutoConfiguration.java | 72 + .../io.sentry/sentry/proxy-config.json | 10 + .../main/resources/META-INF/spring.factories | 1 + ...ot.autoconfigure.AutoConfiguration.imports | 3 + .../src/test/kotlin/com/acme/MainBootClass.kt | 5 + .../jakarta/SentryAutoConfigurationTest.kt | 1286 +++++++++++++++++ ...tryLogbackAppenderAutoConfigurationTest.kt | 142 ++ .../SentrySpanRestClientCustomizerTest.kt | 386 +++++ .../SentrySpanRestTemplateCustomizerTest.kt | 331 +++++ .../SentrySpanWebClientCustomizerTest.kt | 371 +++++ .../SentryWebfluxAutoConfigurationTest.kt | 96 ++ .../jakarta/it/SentrySpringIntegrationTest.kt | 350 +++++ .../org.mockito.plugins.MockMaker | 1 + settings.gradle.kts | 8 + test/system-test-runner.py | 5 + 240 files changed, 18257 insertions(+) create mode 100644 sentry-samples/sentry-samples-spring-7/README.md create mode 100644 sentry-samples/sentry-samples-spring-7/build.gradle.kts create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppConfig.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppInitializer.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SecurityConfiguration.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/WebConfig.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonService.java create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/resources/logback.xml create mode 100644 sentry-samples/sentry-samples-spring-7/src/main/resources/sentry.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/README.md create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/application.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/graphql/schema.graphqls create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/quartz.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/schema.sql create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/resources/logback.xml create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/README.md create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/graphql/schema.graphqls create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/quartz.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/schema.sql create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/resources/logback.xml create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/README.md create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/graphql/schema.graphqls create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/resources/logback.xml create mode 100644 sentry-samples/sentry-samples-spring-boot-4/README.md create mode 100644 sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/resources/graphql/schema.graphqls create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/resources/quartz.properties create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/resources/schema.sql create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/DummyTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/test/resources/logback.xml create mode 100644 sentry-spring-7/api/sentry-spring-jakarta.api create mode 100644 sentry-spring-7/build.gradle.kts create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/ContextTagsEventProcessor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/EnableSentry.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/RequestPayloadExtractor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryExceptionResolver.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringServletContainerInitializer.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryTaskDecorator.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryWebConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringProfilesEventProcessor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringSecuritySentryUserProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpan.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransaction.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionAdvice.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameWithSource.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/TransactionNameProvider.java create mode 100644 sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java create mode 100644 sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/ContextTagsEventProcessorTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/HttpServletRequestSentryUserProviderTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryExceptionResolverTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryInitBeanPostProcessorTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessorTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryTaskDecoratorTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringProfilesEventProcessorTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringSecuritySentryUserProviderTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdviceTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandlerTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanAdviceTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTransactionAdviceTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt create mode 100644 sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt create mode 100644 sentry-spring-boot-4-starter/.gitignore create mode 100644 sentry-spring-boot-4-starter/api/sentry-spring-boot-starter-jakarta.api create mode 100644 sentry-spring-boot-4-starter/build.gradle.kts create mode 100644 sentry-spring-boot-4/.gitignore create mode 100644 sentry-spring-boot-4/api/sentry-spring-boot-jakarta.api create mode 100644 sentry-spring-boot-4/build.gradle.kts create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/InAppIncludesResolver.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackInitializer.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpringVersionChecker.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java create mode 100644 sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java create mode 100644 sentry-spring-boot-4/src/main/resources/META-INF/native-image/io.sentry/sentry/proxy-config.json create mode 100644 sentry-spring-boot-4/src/main/resources/META-INF/spring.factories create mode 100644 sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports create mode 100644 sentry-spring-boot-4/src/test/kotlin/com/acme/MainBootClass.kt create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfigurationTest.kt create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfigurationTest.kt create mode 100644 sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt create mode 100644 sentry-spring-boot-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/.craft.yml b/.craft.yml index c23f88a66c1..2fc0cc110da 100644 --- a/.craft.yml +++ b/.craft.yml @@ -19,10 +19,13 @@ targets: maven:io.sentry:sentry: maven:io.sentry:sentry-spring: maven:io.sentry:sentry-spring-jakarta: +# maven:io.sentry:sentry-spring-7: maven:io.sentry:sentry-spring-boot: maven:io.sentry:sentry-spring-boot-jakarta: maven:io.sentry:sentry-spring-boot-starter: maven:io.sentry:sentry-spring-boot-starter-jakarta: +# maven:io.sentry:sentry-spring-boot-4: +# maven:io.sentry:sentry-spring-boot-4-starter: maven:io.sentry:sentry-servlet: maven:io.sentry:sentry-servlet-jakarta: maven:io.sentry:sentry-logback: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index c6817a5a0c8..a7ca3cbb770 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -24,8 +24,11 @@ body: - sentry-spring-boot-jakarta - sentry-spring-boot-starter - sentry-spring-boot-starter-jakarta + - sentry-spring-boot-4 + - sentry-spring-boot-4-starter - sentry-spring - sentry-spring-jakarta + - sentry-spring-7 - sentry-logback - sentry-log4j2 - sentry-graphql diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index e6bf48a139f..5df5f3ec1d8 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -54,6 +54,21 @@ jobs: - sample: "sentry-samples-console" agent: "false" agent-auto-init: "true" + - sample: "sentry-samples-spring-boot-jakarta" + agent: "false" + agent-auto-init: "true" + - sample: "sentry-samples-spring-boot-4-webflux" + agent: "false" + agent-auto-init: "true" + - sample: "sentry-samples-spring-boot-4-opentelemetry-noagent" + agent: "false" + agent-auto-init: "true" + - sample: "sentry-samples-spring-boot-4-opentelemetry" + agent: "true" + agent-auto-init: "true" + - sample: "sentry-samples-spring-boot-4-opentelemetry" + agent: "true" + agent-auto-init: "false" steps: - uses: actions/checkout@v4 with: diff --git a/build.gradle.kts b/build.gradle.kts index 6476bfbb652..820624fa1d4 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -61,6 +61,7 @@ apiValidation { "sentry-samples-servlet", "sentry-samples-spring", "sentry-samples-spring-jakarta", + "sentry-samples-spring-7", "sentry-samples-spring-boot", "sentry-samples-spring-boot-opentelemetry", "sentry-samples-spring-boot-opentelemetry-noagent", @@ -69,6 +70,10 @@ apiValidation { "sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "sentry-samples-spring-boot-webflux", "sentry-samples-spring-boot-webflux-jakarta", + "sentry-samples-spring-boot-4", + "sentry-samples-spring-boot-4-opentelemetry", + "sentry-samples-spring-boot-4-opentelemetry-noagent", + "sentry-samples-spring-boot-4-webflux", "sentry-samples-ktor-client", "sentry-uitest-android", "sentry-uitest-android-benchmark", diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index f0d59239075..a892af8e920 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -53,10 +53,13 @@ object Config { val SENTRY_LOG4J2_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.log4j2" val SENTRY_SPRING_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring" val SENTRY_SPRING_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring.jakarta" + val SENTRY_SPRING_7_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-7" val SENTRY_SPRING_BOOT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot" val SENTRY_SPRING_BOOT_STARTER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot-starter" val SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot.jakarta" val SENTRY_SPRING_BOOT_STARTER_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot-starter.jakarta" + val SENTRY_SPRING_BOOT_4_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot-4" + val SENTRY_SPRING_BOOT_4_STARTER_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.spring-boot-4-starter" val SENTRY_OPENTELEMETRY_BOOTSTRAP_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.bootstrap" val SENTRY_OPENTELEMETRY_CORE_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.core" val SENTRY_OPENTELEMETRY_AGENT_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.opentelemetry.agent" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4379c63d615..62a952dd6a0 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -30,6 +30,7 @@ retrofit = "2.9.0" slf4j = "1.7.30" springboot2 = "2.7.18" springboot3 = "3.5.0" +springboot4 = "4.0.0-M1" # Android targetSdk = "34" compileSdk = "34" @@ -53,6 +54,7 @@ kover = { id = "org.jetbrains.kotlinx.kover", version = "0.7.3" } vanniktech-maven-publish = { id = "com.vanniktech.maven.publish", version = "0.30.0" } springboot2 = { id = "org.springframework.boot", version.ref = "springboot2" } springboot3 = { id = "org.springframework.boot", version.ref = "springboot3" } +springboot4 = { id = "org.springframework.boot", version.ref = "springboot4" } spring-dependency-management = { id = "io.spring.dependency-management", version = "1.0.11.RELEASE" } gretty = { id = "org.gretty", version = "4.0.0" } @@ -150,6 +152,18 @@ springboot3-starter-aop = { module = "org.springframework.boot:spring-boot-start springboot3-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "springboot3" } springboot3-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot3" } springboot3-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot3" } +springboot4-otel = { module = "io.opentelemetry.instrumentation:opentelemetry-spring-boot-starter", version.ref = "otelInstrumentation" } +springboot4-starter = { module = "org.springframework.boot:spring-boot-starter", version.ref = "springboot4" } +springboot4-starter-graphql = { module = "org.springframework.boot:spring-boot-starter-graphql", version.ref = "springboot4" } +springboot4-starter-quartz = { module = "org.springframework.boot:spring-boot-starter-quartz", version.ref = "springboot4" } +springboot4-starter-test = { module = "org.springframework.boot:spring-boot-starter-test", version.ref = "springboot4" } +springboot4-starter-web = { module = "org.springframework.boot:spring-boot-starter-web", version.ref = "springboot4" } +springboot4-starter-websocket = { module = "org.springframework.boot:spring-boot-starter-websocket", version.ref = "springboot4" } +springboot4-starter-webflux = { module = "org.springframework.boot:spring-boot-starter-webflux", version.ref = "springboot4" } +springboot4-starter-aop = { module = "org.springframework.boot:spring-boot-starter-aop", version.ref = "springboot4" } +springboot4-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "springboot4" } +springboot4-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot4" } +springboot4-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot4" } timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" } # test libraries diff --git a/sentry-samples/sentry-samples-spring-7/README.md b/sentry-samples/sentry-samples-spring-7/README.md new file mode 100644 index 00000000000..cece0aecba5 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/README.md @@ -0,0 +1,19 @@ +# Sentry Sample Spring 6.0+ + +Sample application showing how to use Sentry with [Spring](http://spring.io/) from version `6.0` onwards. + +## How to run? + +To see events triggered in this sample application in your Sentry dashboard, go to `src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java` and replace the test DSN with your own DSN. + +Then, execute a command from the module directory: + +``` +../../gradlew appRun +``` + +Make an HTTP request that will trigger events: + +``` +curl -XPOST --user user:password http://localhost:8080/sentry-samples-spring-jakarta/person/ -H "Content-Type:application/json" -d '{"firstName":"John","lastName":"Smith"}' +``` diff --git a/sentry-samples/sentry-samples-spring-7/build.gradle.kts b/sentry-samples/sentry-samples-spring-7/build.gradle.kts new file mode 100644 index 00000000000..bf5eecea94b --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/build.gradle.kts @@ -0,0 +1,52 @@ +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.springframework.boot.gradle.plugin.SpringBootPlugin + +plugins { + alias(libs.plugins.springboot4) apply false + alias(libs.plugins.spring.dependency.management) + kotlin("jvm") + alias(libs.plugins.kotlin.spring) + id("war") + alias(libs.plugins.gretty) +} + +group = "io.sentry.sample.spring-7" + +version = "0.0.1-SNAPSHOT" + +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + +dependencyManagement { imports { mavenBom(SpringBootPlugin.BOM_COORDINATES) } } + +dependencies { + implementation(Config.Libs.springWeb) + implementation(Config.Libs.springAop) + implementation(Config.Libs.aspectj) + implementation(Config.Libs.springSecurityWeb) + implementation(Config.Libs.springSecurityConfig) + implementation(Config.Libs.kotlinReflect) + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + implementation(projects.sentrySpring7) + implementation(projects.sentryLogback) + implementation(libs.jackson.databind) + implementation(libs.logback.classic) + implementation(libs.servlet.jakarta.api) + implementation(libs.slf4j2.api) + testImplementation(libs.springboot.starter.test) { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } +} + +tasks.withType().configureEach { useJUnitPlatform() } + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppConfig.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppConfig.java new file mode 100644 index 00000000000..72ecb14e2f4 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppConfig.java @@ -0,0 +1,20 @@ +package io.sentry.samples.spring.jakarta; + +import io.sentry.IScopes; +import io.sentry.spring.jakarta.SentryUserFilter; +import io.sentry.spring.jakarta.SentryUserProvider; +import java.util.List; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +@Configuration +@Import(SentryConfig.class) +public class AppConfig { + + @Bean + SentryUserFilter sentryUserFilter( + final IScopes scopes, final List sentryUserProviders) { + return new SentryUserFilter(scopes, sentryUserProviders); + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppInitializer.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppInitializer.java new file mode 100644 index 00000000000..374a416c3ce --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppInitializer.java @@ -0,0 +1,45 @@ +package io.sentry.samples.spring.jakarta; + +import io.sentry.spring.jakarta.tracing.SentryTracingFilter; +import jakarta.servlet.Filter; +import org.springframework.web.filter.DelegatingFilterProxy; +import org.springframework.web.filter.RequestContextFilter; +import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer; + +public class AppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer { + + @Override + protected String[] getServletMappings() { + return new String[] {"/*"}; + } + + @Override + protected Class[] getRootConfigClasses() { + return new Class[] {AppConfig.class, SecurityConfiguration.class}; + } + + @Override + protected Class[] getServletConfigClasses() { + return new Class[] {WebConfig.class}; + } + + @Override + protected Filter[] getServletFilters() { + // creates Sentry transactions around incoming HTTP requests + SentryTracingFilter sentryTracingFilter = new SentryTracingFilter(); + + // filter required by Spring Security + DelegatingFilterProxy springSecurityFilterChain = new DelegatingFilterProxy(); + springSecurityFilterChain.setTargetBeanName("springSecurityFilterChain"); + // sets request on RequestContextHolder + RequestContextFilter requestContextFilter = new RequestContextFilter(); + + // sets Sentry user on the scope + DelegatingFilterProxy sentryUserFilterProxy = new DelegatingFilterProxy(); + sentryUserFilterProxy.setTargetBeanName("sentryUserFilter"); + + return new Filter[] { + sentryTracingFilter, springSecurityFilterChain, requestContextFilter, sentryUserFilterProxy + }; + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SecurityConfiguration.java new file mode 100644 index 00000000000..d6c8a315583 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SecurityConfiguration.java @@ -0,0 +1,42 @@ +package io.sentry.samples.spring.jakarta; + +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +@EnableWebSecurity +public class SecurityConfiguration { + + // this API is meant to be consumed by non-browser clients thus the CSRF protection is not needed. + @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) + @Bean + public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { + http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); + + return http.build(); + } + + @Bean + public @NotNull InMemoryUserDetailsManager userDetailsService() { + final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + + final UserDetails user = + User.builder() + .passwordEncoder(encoder::encode) + .username("user") + .password("password") + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(user); + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java new file mode 100644 index 00000000000..443299048f3 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java @@ -0,0 +1,37 @@ +package io.sentry.samples.spring.jakarta; + +import io.sentry.SentryOptions; +import io.sentry.SentryOptions.TracesSamplerCallback; +import io.sentry.spring.jakarta.EnableSentry; +import io.sentry.spring.jakarta.tracing.SentryTracingConfiguration; +import jakarta.servlet.http.HttpServletRequest; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +// NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry +// project/dashboard +@EnableSentry( + dsn = "https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563", + sendDefaultPii = true, + maxRequestBodySize = SentryOptions.RequestSize.MEDIUM) +@Import(SentryTracingConfiguration.class) +public class SentryConfig { + + /** + * Configures callback used to determine if transaction should be sampled. + * + * @return traces sampler callback + */ + @Bean + TracesSamplerCallback tracesSamplerCallback() { + return samplingContext -> { + HttpServletRequest request = + (HttpServletRequest) samplingContext.getCustomSamplingContext().get("request"); + if ("/error".equals(request.getRequestURI())) { + return 0.5d; + } else { + return 1.0d; + } + }; + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/WebConfig.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/WebConfig.java new file mode 100644 index 00000000000..73d425b2868 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/WebConfig.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.jakarta; + +import io.sentry.IScopes; +import io.sentry.spring.jakarta.tracing.SentrySpanClientHttpRequestInterceptor; +import java.util.Collections; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +@Configuration +@EnableAspectJAutoProxy(proxyTargetClass = true) +@ComponentScan("io.sentry.samples.spring.jakarta") +@EnableWebMvc +public class WebConfig { + + /** + * Creates a {@link RestTemplate} which calls are intercepted with {@link + * SentrySpanClientHttpRequestInterceptor} to create spans around HTTP calls. + * + * @param scopes - sentry scopes + * @return RestTemplate + */ + @Bean + RestTemplate restTemplate(IScopes scopes) { + RestTemplate restTemplate = new RestTemplate(); + SentrySpanClientHttpRequestInterceptor sentryRestTemplateInterceptor = + new SentrySpanClientHttpRequestInterceptor(scopes); + restTemplate.setInterceptors(Collections.singletonList(sentryRestTemplateInterceptor)); + return restTemplate; + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java new file mode 100644 index 00000000000..ee1f4f0827e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java @@ -0,0 +1,24 @@ +package io.sentry.samples.spring.jakarta.web; + +public class Person { + private final String firstName; + private final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java new file mode 100644 index 00000000000..c5ee953810c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java @@ -0,0 +1,42 @@ +package io.sentry.samples.spring.jakarta.web; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/person/") +public class PersonController { + private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); + + private final PersonService personService; + + public PersonController(PersonService personService) { + this.personService = personService; + } + + @GetMapping("{id}") + Person person(@PathVariable Long id) { + Sentry.logger().warn("warn Sentry logging"); + Sentry.logger().error("error Sentry logging"); + Sentry.logger().info("hello %s %s", "there", "world!"); + LOGGER.info("Loading person with id={}", id); + if (id > 10L) { + throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); + } else { + return personService.find(id); + } + } + + @PostMapping + Person create(@RequestBody Person person) { + LOGGER.warn("Creating person: {}", person); + return personService.create(person); + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonService.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonService.java new file mode 100644 index 00000000000..e8dcb8c1e30 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonService.java @@ -0,0 +1,39 @@ +package io.sentry.samples.spring.jakarta.web; + +import io.sentry.spring.jakarta.tracing.SentrySpan; +import java.util.Map; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +@Service +public class PersonService { + private final RestTemplate restTemplate; + + public PersonService(RestTemplate restTemplate) { + this.restTemplate = restTemplate; + } + + @SentrySpan + @SuppressWarnings("unchecked") + Person find(Long id) { + Map result = + restTemplate.getForObject("https://jsonplaceholder.typicode.com/users/{id}", Map.class, id); + String name = (String) result.get("name"); + if (name != null) { + String[] nameParts = name.split(" "); + return new Person(nameParts[0], nameParts[1]); + } else { + return null; + } + } + + @SentrySpan + Person create(Person person) { + try { + Thread.sleep(100); + } catch (InterruptedException e) { + // ignored + } + return person; + } +} diff --git a/sentry-samples/sentry-samples-spring-7/src/main/resources/logback.xml b/sentry-samples/sentry-samples-spring-7/src/main/resources/logback.xml new file mode 100644 index 00000000000..26c88f3494f --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/resources/logback.xml @@ -0,0 +1,14 @@ + + + + + + + WARN + + + + + + + diff --git a/sentry-samples/sentry-samples-spring-7/src/main/resources/sentry.properties b/sentry-samples/sentry-samples-spring-7/src/main/resources/sentry.properties new file mode 100644 index 00000000000..3cd4a7b9b08 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-7/src/main/resources/sentry.properties @@ -0,0 +1,2 @@ +debug=true +in-app-includes="io.sentry.samples" diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/README.md b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/README.md new file mode 100644 index 00000000000..4a5f2a7739c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/README.md @@ -0,0 +1,122 @@ +# Sentry Sample Spring Boot 3.0+ + +Sample application showing how to use Sentry with [Spring boot](http://spring.io/projects/spring-boot) from version `3.0` onwards integrated with the [OpenTelemetry Spring Boot Starter](https://opentelemetry.io/docs/zero-code/java/spring-boot-starter/) without an agent. + +## How to run? + +To see events triggered in this sample application in your Sentry dashboard, go to `src/main/resources/application.properties` and replace the test DSN with your own DSN. + +Then, execute a command from the module directory: + +``` +../../gradlew bootRun +``` + +Make an HTTP request that will trigger events: + +``` +curl -XPOST --user user:password http://localhost:8080/person/ -H "Content-Type:application/json" -d '{"firstName":"John","lastName":"Smith"}' +``` + +## GraphQL + +The following queries can be used to test the GraphQL integration. + +### Greeting +``` +{ + greeting(name: "crash") +} +``` + +### Greeting with variables + +``` +query GreetingQuery($name: String) { + greeting(name: $name) +} +``` +variables: +``` +{ + "name": "crash" +} +``` + +### Project + +``` +query ProjectQuery($slug: ID!) { + project(slug: $slug) { + slug + name + repositoryUrl + status + } +} +``` +variables: +``` +{ + "slug": "statuscrash" +} +``` + +### Mutation + +``` +mutation AddProjectMutation($slug: ID!) { + addProject(slug: $slug) +} +``` +variables: +``` +{ + "slug": "nocrash", + "name": "nocrash" +} +``` + +### Subscription + +``` +subscription SubscriptionNotifyNewTask($slug: ID!) { + notifyNewTask(projectSlug: $slug) { + id + name + assigneeId + assignee { + id + name + } + } +} +``` +variables: +``` +{ + "slug": "crash" +} +``` + +### Data loader + +``` +query TasksAndAssigneesQuery($slug: ID!) { + tasks(projectSlug: $slug) { + id + name + assigneeId + assignee { + id + name + } + } +} +``` +variables: +``` +{ + "slug": "crash" +} +``` diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts new file mode 100644 index 00000000000..05964eb76dc --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts @@ -0,0 +1,93 @@ +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.springboot4) + alias(libs.plugins.spring.dependency.management) + kotlin("jvm") + alias(libs.plugins.kotlin.spring) +} + +group = "io.sentry.sample.spring-boot-4" + +version = "0.0.1-SNAPSHOT" + +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + +configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + implementation(libs.springboot4.starter) + implementation(libs.springboot4.starter.actuator) + implementation(libs.springboot4.starter.aop) + implementation(libs.springboot4.starter.graphql) + implementation(libs.springboot4.starter.jdbc) + implementation(libs.springboot4.starter.quartz) + implementation(libs.springboot4.starter.security) + implementation(libs.springboot4.starter.web) + implementation(libs.springboot4.starter.webflux) + implementation(libs.springboot4.starter.websocket) + implementation(Config.Libs.aspectj) + implementation(Config.Libs.kotlinReflect) + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + implementation(projects.sentrySpringBoot4Starter) + implementation(projects.sentryLogback) + implementation(projects.sentryGraphql22) + implementation(projects.sentryQuartz) + implementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentlessSpring) + + // database query tracing + implementation(projects.sentryJdbc) + runtimeOnly(libs.hsqldb) + + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.apollo3.kotlin) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.slf4j2.api) + testImplementation(libs.springboot4.starter.test) { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + testImplementation("ch.qos.logback:logback-classic:1.5.16") + testImplementation("ch.qos.logback:logback-core:1.5.16") +} + +dependencyManagement { imports { mavenBom(libs.otel.instrumentation.bom.get().toString()) } } + +configure { test { java.srcDir("src/test/java") } } + +tasks.register("systemTest").configure { + group = "verification" + description = "Runs the System tests" + + outputs.upToDateWhen { false } + + maxParallelForks = 1 + + // Cap JVM args per test + minHeapSize = "128m" + maxHeapSize = "1g" + + filter { includeTestsMatching("io.sentry.systemtest*") } +} + +tasks.named("test").configure { + require(this is Test) + + filter { excludeTestsMatching("io.sentry.systemtest.*") } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java new file mode 100644 index 00000000000..51451a5d77e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java @@ -0,0 +1,35 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.SentryEvent; +import io.sentry.protocol.SentryRuntime; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.SpringBootVersion; +import org.springframework.stereotype.Component; + +/** + * Custom {@link EventProcessor} implementation lets modifying {@link SentryEvent}s before they are + * sent to Sentry. + */ +@Component +public class CustomEventProcessor implements EventProcessor { + private final String springBootVersion; + + public CustomEventProcessor(String springBootVersion) { + this.springBootVersion = springBootVersion; + } + + public CustomEventProcessor() { + this(SpringBootVersion.getVersion()); + } + + @Override + public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { + final SentryRuntime runtime = new SentryRuntime(); + runtime.setVersion(springBootVersion); + runtime.setName("Spring Boot"); + event.getContexts().setRuntime(runtime); + return event; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java new file mode 100644 index 00000000000..cac83e6d797 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java @@ -0,0 +1,26 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.spring.jakarta.checkin.SentryCheckIn; +import io.sentry.spring.jakarta.tracing.SentryTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +/** + * {@link SentryTransaction} added on the class level, creates transaction around each method + * execution of every method of the annotated class. + */ +@Component +@SentryTransaction(operation = "scheduled") +public class CustomJob { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class); + + @SentryCheckIn("monitor_slug_1") + @Scheduled(fixedRate = 3 * 60 * 1000L) + void execute() throws InterruptedException { + LOGGER.info("Executing scheduled job"); + Thread.sleep(2000L); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java new file mode 100644 index 00000000000..cfff0be4702 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java @@ -0,0 +1,51 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.opentelemetry.instrumentation.annotations.WithSpan; +import java.nio.charset.Charset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; + +@RestController +@RequestMapping("/tracing/") +public class DistributedTracingController { + private static final Logger LOGGER = LoggerFactory.getLogger(DistributedTracingController.class); + private final RestClient restClient; + + public DistributedTracingController(RestClient restClient) { + this.restClient = restClient; + } + + @GetMapping("{id}") + @WithSpan("tracingSpanThroughOtelAnnotation") + Person person(@PathVariable Long id) { + return restClient + .get() + .uri("http://localhost:8080/person/{id}", id) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } + + @PostMapping + Person create(@RequestBody Person person) { + return restClient + .post() + .uri("http://localhost:8080/person/") + .body(person) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java new file mode 100644 index 00000000000..2c8bdc33c62 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java @@ -0,0 +1,24 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Person { + private final String firstName; + private final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java new file mode 100644 index 00000000000..b3f22fd7fd4 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -0,0 +1,66 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.annotations.WithSpan; +import io.sentry.ISpan; +import io.sentry.Sentry; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/person/") +public class PersonController { + private final PersonService personService; + private final Tracer tracer; + private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); + + public PersonController(PersonService personService, Tracer tracer) { + this.personService = personService; + this.tracer = tracer; + } + + @GetMapping("{id}") + @WithSpan("personSpanThroughOtelAnnotation") + Person person(@PathVariable Long id) { + Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); + try (final @NotNull Scope spanScope = span.makeCurrent()) { + Sentry.logger().warn("warn Sentry logging"); + Sentry.logger().error("error Sentry logging"); + Sentry.logger().info("hello %s %s", "there", "world!"); + ISpan currentSpan = Sentry.getSpan(); + ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); + try { + LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading")); + throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); + } finally { + sentrySpan.finish(); + } + } finally { + span.end(); + } + } + + @PostMapping + Person create(@RequestBody Person person) { + Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); + try (final @NotNull Scope spanScope = span.makeCurrent()) { + ISpan sentrySpan = Sentry.getSpan().startChild("spanCreatedThroughSentryApi"); + try { + return personService.create(person); + } finally { + sentrySpan.finish(); + } + } finally { + span.end(); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java new file mode 100644 index 00000000000..50cdc9dd4e7 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java @@ -0,0 +1,41 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.spring.jakarta.tracing.SentrySpan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +/** + * {@link SentrySpan} can be added either on the class or the method to create spans around method + * executions. + */ +@Service +@SentrySpan +public class PersonService { + private static final Logger LOGGER = LoggerFactory.getLogger(PersonService.class); + + private final JdbcTemplate jdbcTemplate; + private int createCount = 0; + + public PersonService(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + Person create(Person person) { + createCount++; + final ISpan span = Sentry.getSpan(); + if (span != null) { + span.setMeasurement("create_count", createCount); + } + + jdbcTemplate.update( + "insert into person (firstName, lastName) values (?, ?)", + person.getFirstName(), + person.getLastName()); + + return person; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java new file mode 100644 index 00000000000..e5987c8f4a2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java @@ -0,0 +1,40 @@ +package io.sentry.samples.spring.boot.jakarta; + +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfiguration { + + // this API is meant to be consumed by non-browser clients thus the CSRF protection is not needed. + @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) + @Bean + public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { + http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); + + return http.build(); + } + + @Bean + public @NotNull InMemoryUserDetailsManager userDetailsService() { + final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + + final UserDetails user = + User.builder() + .passwordEncoder(encoder::encode) + .username("user") + .password("password") + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(user); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java new file mode 100644 index 00000000000..7f412eaa0d6 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -0,0 +1,78 @@ +package io.sentry.samples.spring.boot.jakarta; + +import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; + +import io.opentelemetry.api.OpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import java.util.Collections; +import org.quartz.JobDetail; +import org.quartz.SimpleTrigger; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; +import org.springframework.scheduling.quartz.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +@SpringBootApplication +@EnableScheduling +public class SentryDemoApplication { + public static void main(String[] args) { + SpringApplication.run(SentryDemoApplication.class, args); + } + + @Bean + RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); + } + + @Bean + WebClient webClient(WebClient.Builder builder) { + return builder.build(); + } + + @Bean + RestClient restClient(RestClient.Builder builder) { + return builder.build(); + } + + @Bean + public JobDetailFactoryBean jobDetail() { + JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); + jobDetailFactory.setJobClass(SampleJob.class); + jobDetailFactory.setDurability(true); + jobDetailFactory.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_job_detail")); + return jobDetailFactory; + } + + @Bean + public SimpleTriggerFactoryBean trigger(JobDetail job) { + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setRepeatInterval(2 * 60 * 1000); // every two minutes + trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + trigger.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_simple_trigger")); + return trigger; + } + + @Bean + public CronTriggerFactoryBean cronTrigger(JobDetail job) { + CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setCronExpression("0 0/5 * ? * *"); // every five minutes + return trigger; + } + + @Bean + public Tracer tracer(OpenTelemetry openTelemetry) { + return openTelemetry.getTracer("tracerForSpringBootDemo"); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java new file mode 100644 index 00000000000..5fc4164d1b0 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java @@ -0,0 +1,25 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Todo { + private final Long id; + private final String title; + private final boolean completed; + + public Todo(Long id, String title, boolean completed) { + this.id = id; + this.title = title; + this.completed = completed; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public boolean isCompleted() { + return completed; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java new file mode 100644 index 00000000000..0fa450a879c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java @@ -0,0 +1,86 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.reactor.SentryReactorUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@RestController +public class TodoController { + private final RestTemplate restTemplate; + private final WebClient webClient; + private final RestClient restClient; + private final Tracer tracer; + + public TodoController( + RestTemplate restTemplate, WebClient webClient, RestClient restClient, Tracer tracer) { + this.restTemplate = restTemplate; + this.webClient = webClient; + this.restClient = restClient; + this.tracer = tracer; + } + + @GetMapping("/todo/{id}") + Todo todo(@PathVariable Long id) { + Span otelSpan = tracer.spanBuilder("todoSpanOtelApi").startSpan(); + try (final @NotNull Scope spanScope = otelSpan.makeCurrent()) { + ISpan sentrySpan = Sentry.getSpan().startChild("todoSpanSentryApi"); + try { + return restTemplate.getForObject( + "https://jsonplaceholder.typicode.com/todos/{id}", Todo.class, id); + } finally { + sentrySpan.finish(); + } + } finally { + otelSpan.end(); + } + } + + @GetMapping("/todo-webclient/{id}") + Todo todoWebClient(@PathVariable Long id) { + Hooks.enableAutomaticContextPropagation(); + return SentryReactorUtils.withSentry( + Mono.just(true) + .publishOn(Schedulers.boundedElastic()) + .flatMap( + x -> + webClient + .get() + .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) + .retrieve() + .bodyToMono(Todo.class) + .map(response -> response))) + .block(); + } + + @GetMapping("/todo-restclient/{id}") + Todo todoRestClient(@PathVariable Long id) { + Span span = tracer.spanBuilder("todoRestClientSpanOtelApi").startSpan(); + try (final @NotNull Scope spanScope = span.makeCurrent()) { + ISpan sentrySpan = Sentry.getSpan().startChild("todoRestClientSpanSentryApi"); + try { + return restClient + .get() + .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) + .retrieve() + .body(Todo.class); + } finally { + sentrySpan.finish(); + } + } finally { + span.end(); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java new file mode 100644 index 00000000000..6fdf96506c8 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.springframework.graphql.data.method.annotation.BatchMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +@Controller +public class AssigneeController { + + @BatchMapping(typeName = "Task", field = "assignee") + public Mono> assignee( + final @NotNull Set tasks) { + return Mono.fromCallable( + () -> { + final @NotNull Map map = + new HashMap<>(); + for (final @NotNull ProjectController.Task task : tasks) { + if ("Acrash".equalsIgnoreCase(task.assigneeId)) { + throw new RuntimeException("Causing an error while loading assignee"); + } + if (task.assigneeId != null) { + map.put( + task, new ProjectController.Assignee(task.assigneeId, "Name" + task.assigneeId)); + } + } + + return map; + }); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java new file mode 100644 index 00000000000..bfc383c9122 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java @@ -0,0 +1,17 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +@Controller +public class GreetingController { + + @QueryMapping + public String greeting(final @Argument String name) { + if ("crash".equalsIgnoreCase(name)) { + throw new RuntimeException("causing an error for " + name); + } + return "Hello " + name + "!"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java new file mode 100644 index 00000000000..63790bca628 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java @@ -0,0 +1,140 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.nio.file.NoSuchFileException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.jetbrains.annotations.NotNull; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.graphql.data.method.annotation.SubscriptionMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Flux; + +@Controller +public class ProjectController { + + @QueryMapping + public Project project(final @Argument String slug) throws Exception { + if ("crash".equalsIgnoreCase(slug) || "projectcrash".equalsIgnoreCase(slug)) { + throw new RuntimeException("causing a project error for " + slug); + } + if ("notfound".equalsIgnoreCase(slug)) { + throw new IllegalStateException("not found"); + } + if ("nofile".equals(slug)) { + throw new NoSuchFileException("no such file"); + } + Project project = new Project(); + project.slug = slug; + return project; + } + + @SchemaMapping(typeName = "Project", field = "status") + public ProjectStatus projectStatus(final Project project) { + if ("crash".equalsIgnoreCase(project.slug) || "statuscrash".equalsIgnoreCase(project.slug)) { + throw new RuntimeException("causing a project status error for " + project.slug); + } + return ProjectStatus.COMMUNITY; + } + + @MutationMapping + public String addProject(@Argument String slug) { + if ("crash".equalsIgnoreCase(slug) || "addprojectcrash".equalsIgnoreCase(slug)) { + throw new RuntimeException("causing a project add error for " + slug); + } + return UUID.randomUUID().toString(); + } + + @QueryMapping + public List tasks(final @Argument String projectSlug) { + List tasks = new ArrayList<>(); + tasks.add(new Task("T1", "Create a new API", "A3", "C3")); + tasks.add(new Task("T2", "Update dependencies", "A1", "C1")); + tasks.add(new Task("T3", "Document API", "A1", "C1")); + tasks.add(new Task("T4", "Merge community PRs", "A2", "C2")); + tasks.add(new Task("T5", "Plan more work", null, null)); + if ("crash".equalsIgnoreCase(projectSlug)) { + tasks.add(new Task("T6", "Fix crash", "Acrash", "Ccrash")); + } + return tasks; + } + + @SubscriptionMapping + public Flux notifyNewTask(@Argument String projectSlug) { + if ("crash".equalsIgnoreCase(projectSlug)) { + throw new RuntimeException("causing error for subscription"); + } + if ("fluxerror".equalsIgnoreCase(projectSlug)) { + return Flux.error(new RuntimeException("causing flux error for subscription")); + } + final String assigneeId = "assigneecrash".equalsIgnoreCase(projectSlug) ? "Acrash" : "A1"; + final String creatorId = "creatorcrash".equalsIgnoreCase(projectSlug) ? "Ccrash" : "C1"; + final @NotNull AtomicInteger counter = new AtomicInteger(1000); + return Flux.interval(Duration.ofSeconds(1)) + .map( + num -> { + int i = counter.incrementAndGet(); + if ("produceerror".equalsIgnoreCase(projectSlug) && i % 2 == 0) { + throw new RuntimeException("causing produce error for subscription"); + } + return new Task("T" + i, "A new task arrived ", assigneeId, creatorId); + }); + } + + public static class Task { + public String id; + public String name; + public String assigneeId; + public String creatorId; + + public Task( + final String id, final String name, final String assigneeId, final String creatorId) { + this.id = id; + this.name = name; + this.assigneeId = assigneeId; + this.creatorId = creatorId; + } + + @Override + public String toString() { + return "Task{id=" + id + "}"; + } + } + + public static class Assignee { + public String id; + public String name; + + public Assignee(final String id, final String name) { + this.id = id; + this.name = name; + } + } + + public static class Creator { + public String id; + public String name; + + public Creator(final String id, final String name) { + this.id = id; + this.name = name; + } + } + + public static class Project { + public String slug; + } + + public enum ProjectStatus { + ACTIVE, + COMMUNITY, + INCUBATING, + ATTIC, + EOL; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java new file mode 100644 index 00000000000..cb6677c0c37 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java @@ -0,0 +1,49 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.DataLoader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.graphql.execution.BatchLoaderRegistry; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +@Controller +class TaskCreatorController { + + public TaskCreatorController(final BatchLoaderRegistry batchLoaderRegistry) { + // using mapped BatchLoader to not have to deal with correct ordering of items + batchLoaderRegistry + .forTypePair(String.class, ProjectController.Creator.class) + .registerMappedBatchLoader( + (Set keys, BatchLoaderEnvironment env) -> { + return Mono.fromCallable( + () -> { + final @NotNull Map map = new HashMap<>(); + for (String key : keys) { + if ("Ccrash".equalsIgnoreCase(key)) { + throw new RuntimeException("Causing an error while loading creator"); + } + map.put(key, new ProjectController.Creator(key, "Name" + key)); + } + + return map; + }); + }); + } + + @SchemaMapping(typeName = "Task") + public @Nullable CompletableFuture creator( + final ProjectController.Task task, + final DataLoader dataLoader) { + if (task.creatorId == null) { + return null; + } + return dataLoader.load(task.creatorId); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java new file mode 100644 index 00000000000..d0f0973c864 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java @@ -0,0 +1,19 @@ +package io.sentry.samples.spring.boot.jakarta.quartz; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.stereotype.Component; + +@Component +public class SampleJob implements Job { + + public void execute(JobExecutionContext context) throws JobExecutionException { + System.out.println("running job"); + try { + Thread.sleep(15000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/application.properties new file mode 100644 index 00000000000..f9c648d6ff4 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/application.properties @@ -0,0 +1,39 @@ +# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard +sentry.dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +sentry.send-default-pii=true +sentry.max-request-body-size=medium +# Sentry Spring Boot integration allows more fine-grained SentryOptions configuration +sentry.max-breadcrumbs=150 +# Logback integration configuration options +sentry.logging.minimum-event-level=info +sentry.logging.minimum-breadcrumb-level=debug +# Performance configuration +sentry.traces-sample-rate=1.0 +sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 +sentry.debug=true +sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR +sentry.enable-backpressure-handling=true +sentry.enable-spotlight=true +sentry.enablePrettySerializationOutput=false +sentry.logs.enabled=true +in-app-includes="io.sentry.samples" + +# Uncomment and set to true to enable aot compatibility +# This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) +# to successfully compile to GraalVM +# sentry.enable-aot-compatibility=false + +# Database configuration +spring.datasource.url=jdbc:p6spy:hsqldb:mem:testdb +spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver +spring.datasource.username=sa +spring.datasource.password= +spring.graphql.graphiql.enabled=true +spring.graphql.websocket.path=/graphql +spring.quartz.job-store-type=memory + +# OTEL configuration +otel.propagators=tracecontext,baggage,sentry +otel.logs.exporter=none +otel.metrics.exporter=none +otel.traces.exporter=none diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/graphql/schema.graphqls b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/graphql/schema.graphqls new file mode 100644 index 00000000000..aeea62357bd --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,68 @@ +type Query { + greeting(name: String! = "Spring"): String! + project(slug: ID!): Project + tasks(projectSlug: ID!): [Task] +} + +type Mutation { + addProject(slug: ID!): String! +} + +type Subscription { + notifyNewTask(projectSlug: ID!): Task +} + +""" A Project in the Spring portfolio """ +type Project { + """ Unique string id used in URLs """ + slug: ID! + """ Project name """ + name: String + """ Current support status """ + status: ProjectStatus! +} + +""" A task """ +type Task { + """ ID """ + id: String! + """ Name """ + name: String! + """ ID of the Assignee """ + assigneeId: String + """ Assignee """ + assignee: Assignee + """ ID of the Creator """ + creatorId: String + """ Creator """ + creator: Creator +} + +""" An Assignee """ +type Assignee { + """ ID """ + id: String! + """ Name """ + name: String! +} + +""" An Creator """ +type Creator { + """ ID """ + id: String! + """ Name """ + name: String! +} + +enum ProjectStatus { + """ Actively supported by the Spring team """ + ACTIVE + """ Supported by the community """ + COMMUNITY + """ Prototype, not officially supported yet """ + INCUBATING + """ Project being retired, in maintenance mode """ + ATTIC + """ End-Of-Lifed """ + EOL +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/quartz.properties b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/quartz.properties new file mode 100644 index 00000000000..6e302ce765a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/quartz.properties @@ -0,0 +1 @@ +org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/schema.sql b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/schema.sql new file mode 100644 index 00000000000..7ca8a5cbf42 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/resources/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE person ( + id INTEGER IDENTITY PRIMARY KEY, + firstName VARCHAR(50) NOT NULL, + lastName VARCHAR(50) NOT NULL +); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/DummyTest.kt @@ -0,0 +1,12 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertTrue + +class DummyTest { + @Test + fun `the only test`() { + // only needed to have more than 0 tests and not fail the build + assertTrue(true) + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt new file mode 100644 index 00000000000..3cd16003024 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt @@ -0,0 +1,197 @@ +package io.sentry.systemtest + +import io.sentry.protocol.SentryId +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import org.junit.Before + +class DistributedTracingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } + + @Test + fun `get person distributed tracing with sampled false`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-0", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=false,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" + } + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" + } + } + + @Test + fun `get person distributed tracing without sample_rand`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRand1: String? = null + var sampleRand2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand1 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand2 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + assertEquals(sampleRand1, sampleRand2) + } + + @Test + fun `get person distributed tracing updates sample_rate on deferred decision`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRate1: String? = null + var sampleRate2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate1 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate2 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + assertEquals(sampleRate1, sampleRate2) + assertNotEquals(sampleRate1, "0.5") + } + + @Test + fun `create person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPersonDistributedTracing( + person, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /tracing/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /person/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt new file mode 100644 index 00000000000..76a6024decc --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt @@ -0,0 +1,46 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import org.junit.Before + +class GraphqlGreetingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `greeting works`() { + val response = testHelper.graphqlClient.greet("world") + + testHelper.ensureNoErrors(response) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.greeting", + ) + } + } + + @Test + fun `greeting error`() { + val response = testHelper.graphqlClient.greet("crash") + + testHelper.ensureErrorCount(response, 1) + testHelper.ensureErrorReceived { error -> + error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.greeting", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt new file mode 100644 index 00000000000..fca3956717c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt @@ -0,0 +1,66 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.Before + +class GraphqlProjectSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `project query works`() { + val response = testHelper.graphqlClient.project("proj-slug") + + testHelper.ensureNoErrors(response) + assertEquals("proj-slug", response?.data?.project?.slug) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.project", + ) + } + } + + @Test + fun `project mutation works`() { + val response = testHelper.graphqlClient.addProject("proj-slug") + + testHelper.ensureNoErrors(response) + assertNotNull(response?.data?.addProject) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Mutation.addProject", + ) + } + } + + @Test + fun `project mutation error`() { + val response = testHelper.graphqlClient.addProject("addprojectcrash") + + testHelper.ensureErrorCount(response, 1) + assertNull(response?.data?.addProject) + testHelper.ensureErrorReceived { error -> + error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Mutation.addProject", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt new file mode 100644 index 00000000000..f9359ae1496 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt @@ -0,0 +1,40 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class GraphqlTaskSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `tasks and assignees query works`() { + val response = testHelper.graphqlClient.tasksAndAssignees("project-slug") + + testHelper.ensureNoErrors(response) + + assertEquals(5, response?.data?.tasks?.size) + + val firstTask = response?.data?.tasks?.firstOrNull() ?: throw RuntimeException("no task") + assertEquals("T1", firstTask.id) + assertEquals("A3", firstTask.assigneeId) + assertEquals("A3", firstTask.assignee?.id) + assertEquals("C3", firstTask.creatorId) + assertEquals("C3", firstTask.creator?.id) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.tasks", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt new file mode 100644 index 00000000000..707b5025dcf --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -0,0 +1,115 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class PersonSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person fails`() { + val restClient = testHelper.restClient + restClient.getPerson(1L) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") + } + + Thread.sleep(10000) + + testHelper.ensureLogsReceived { logs, envelopeHeader -> + testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && + testHelper.doesContainLogWithBody(logs, "error Sentry logging") && + testHelper.doesContainLogWithBody(logs, "hello there world!") + } + } + + @Test + fun `create person works`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = restClient.createPerson(person) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "db", + "insert into person (firstName, lastName) values (?, ?)", + ) + } + } + + @Test + fun `create person creates transaction if no sampled flag in sentry-trace header`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPerson( + person, + mapOf( + "sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "db", + "insert into person (firstName, lastName) values (?, ?)", + ) + } + } + + @Test + fun `create person creates transaction if sampled true in sentry-trace header`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPerson( + person, + mapOf( + "sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "db", + "insert into person (firstName, lastName) values (?, ?)", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt new file mode 100644 index 00000000000..e92ad0a17f9 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt @@ -0,0 +1,65 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class TodoSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get todo works`() { + val restClient = testHelper.restClient + restClient.getTodo(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } + + @Test + fun `get todo webclient works`() { + val restClient = testHelper.restClient + restClient.getTodoWebclient(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } + + @Test + fun `get todo restclient works`() { + val restClient = testHelper.restClient + restClient.getTodoRestClient(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/resources/logback.xml b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/resources/logback.xml new file mode 100644 index 00000000000..a36b8f80f76 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + + %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n + + + + + + + diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/README.md b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/README.md new file mode 100644 index 00000000000..e8f55f8926b --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/README.md @@ -0,0 +1,126 @@ +# Sentry Sample Spring Boot 3.0+ + +Sample application showing how to use Sentry with [Spring boot](http://spring.io/projects/spring-boot) from version `3.0` onwards and the Sentry OpenTelemetry agent. + +## How to run? + +Make sure the `sentry-opentelemetry` module is built (`../../gradlew :sentry-opentelemetry:sentry-opentelemetry-agent:assemble`). + +Then, execute a command from the module directory: + +``` +SENTRY_DSN="https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563" SENTRY_DEBUG=true ../../gradlew bootRunWithAgent +``` + +To see events triggered in this sample application in your Sentry dashboard, replace the `SENTRY_DSN` above with your own. + +## Http + +Make an HTTP request that will trigger events: + +``` +curl -XPOST --user user:password http://localhost:8080/person/ -H "Content-Type:application/json" -d '{"firstName":"John","lastName":"Smith"}' +``` + +## GraphQL + +The following queries can be used to test the GraphQL integration. + +### Greeting +``` +{ + greeting(name: "crash") +} +``` + +### Greeting with variables + +``` +query GreetingQuery($name: String) { + greeting(name: $name) +} +``` +variables: +``` +{ + "name": "crash" +} +``` + +### Project + +``` +query ProjectQuery($slug: ID!) { + project(slug: $slug) { + slug + name + repositoryUrl + status + } +} +``` +variables: +``` +{ + "slug": "statuscrash" +} +``` + +### Mutation + +``` +mutation AddProjectMutation($slug: ID!) { + addProject(slug: $slug) +} +``` +variables: +``` +{ + "slug": "nocrash", + "name": "nocrash" +} +``` + +### Subscription + +``` +subscription SubscriptionNotifyNewTask($slug: ID!) { + notifyNewTask(projectSlug: $slug) { + id + name + assigneeId + assignee { + id + name + } + } +} +``` +variables: +``` +{ + "slug": "crash" +} +``` + +### Data loader + +``` +query TasksAndAssigneesQuery($slug: ID!) { + tasks(projectSlug: $slug) { + id + name + assigneeId + assignee { + id + name + } + } +} +``` +variables: +``` +{ + "slug": "crash" +} +``` diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts new file mode 100644 index 00000000000..23a73cfd991 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts @@ -0,0 +1,120 @@ +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.springframework.boot.gradle.tasks.run.BootRun + +plugins { + alias(libs.plugins.springboot4) + alias(libs.plugins.spring.dependency.management) + kotlin("jvm") + alias(libs.plugins.kotlin.spring) +} + +group = "io.sentry.sample.spring-boot-4" + +version = "0.0.1-SNAPSHOT" + +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + +configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + implementation(libs.springboot4.starter) + implementation(libs.springboot4.starter.actuator) + implementation(libs.springboot4.starter.aop) + implementation(libs.springboot4.starter.graphql) + implementation(libs.springboot4.starter.jdbc) + implementation(libs.springboot4.starter.quartz) + implementation(libs.springboot4.starter.security) + implementation(libs.springboot4.starter.web) + implementation(libs.springboot4.starter.webflux) + implementation(libs.springboot4.starter.websocket) + implementation(Config.Libs.aspectj) + implementation(Config.Libs.kotlinReflect) + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + implementation(projects.sentrySpringBoot4Starter) + implementation(projects.sentryLogback) + implementation(projects.sentryGraphql22) + implementation(projects.sentryQuartz) + implementation(libs.otel) + + // database query tracing + implementation(projects.sentryJdbc) + runtimeOnly(libs.hsqldb) + + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.apollo3.kotlin) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.slf4j2.api) + testImplementation(libs.springboot4.starter.test) { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + testImplementation("ch.qos.logback:logback-classic:1.5.16") + testImplementation("ch.qos.logback:logback-core:1.5.16") +} + +configure { test { java.srcDir("src/test/java") } } + +tasks.register("bootRunWithAgent").configure { + group = "application" + + val mainBootRunTask = tasks.getByName("bootRun") + mainClass = mainBootRunTask.mainClass + classpath = mainBootRunTask.classpath + + val versionName = project.properties["versionName"] as String + val agentJarPath = + "$rootDir/sentry-opentelemetry/sentry-opentelemetry-agent/build/libs/sentry-opentelemetry-agent-$versionName.jar" + + val dsn = + System.getenv("SENTRY_DSN") + ?: "https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563" + val tracesSampleRate = System.getenv("SENTRY_TRACES_SAMPLE_RATE") ?: "1" + + environment("SENTRY_DSN", dsn) + environment("SENTRY_TRACES_SAMPLE_RATE", tracesSampleRate) + environment("OTEL_TRACES_EXPORTER", "none") + environment("OTEL_METRICS_EXPORTER", "none") + environment("OTEL_LOGS_EXPORTER", "none") + + jvmArgs = listOf("-Dotel.javaagent.debug=true", "-javaagent:$agentJarPath") +} + +tasks.register("systemTest").configure { + group = "verification" + description = "Runs the System tests" + + outputs.upToDateWhen { false } + + maxParallelForks = 1 + + // Cap JVM args per test + minHeapSize = "128m" + maxHeapSize = "1g" + + filter { includeTestsMatching("io.sentry.systemtest*") } +} + +tasks.named("test").configure { + require(this is Test) + + filter { excludeTestsMatching("io.sentry.systemtest.*") } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java new file mode 100644 index 00000000000..51451a5d77e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java @@ -0,0 +1,35 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.SentryEvent; +import io.sentry.protocol.SentryRuntime; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.SpringBootVersion; +import org.springframework.stereotype.Component; + +/** + * Custom {@link EventProcessor} implementation lets modifying {@link SentryEvent}s before they are + * sent to Sentry. + */ +@Component +public class CustomEventProcessor implements EventProcessor { + private final String springBootVersion; + + public CustomEventProcessor(String springBootVersion) { + this.springBootVersion = springBootVersion; + } + + public CustomEventProcessor() { + this(SpringBootVersion.getVersion()); + } + + @Override + public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { + final SentryRuntime runtime = new SentryRuntime(); + runtime.setVersion(springBootVersion); + runtime.setName("Spring Boot"); + event.getContexts().setRuntime(runtime); + return event; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java new file mode 100644 index 00000000000..4cd609d67ce --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java @@ -0,0 +1,25 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.spring.jakarta.checkin.SentryCheckIn; +import io.sentry.spring.jakarta.tracing.SentryTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * {@link SentryTransaction} added on the class level, creates transaction around each method + * execution of every method of the annotated class. + */ +@Component +@SentryTransaction(operation = "scheduled") +public class CustomJob { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class); + + @SentryCheckIn("monitor_slug_1") + // @Scheduled(fixedRate = 3 * 60 * 1000L) + void execute() throws InterruptedException { + LOGGER.info("Executing scheduled job"); + Thread.sleep(2000L); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java new file mode 100644 index 00000000000..d67059abb68 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java @@ -0,0 +1,49 @@ +package io.sentry.samples.spring.boot.jakarta; + +import java.nio.charset.Charset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; + +@RestController +@RequestMapping("/tracing/") +public class DistributedTracingController { + private static final Logger LOGGER = LoggerFactory.getLogger(DistributedTracingController.class); + private final RestClient restClient; + + public DistributedTracingController(RestClient restClient) { + this.restClient = restClient; + } + + @GetMapping("{id}") + Person person(@PathVariable Long id) { + return restClient + .get() + .uri("http://localhost:8080/person/{id}", id) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } + + @PostMapping + Person create(@RequestBody Person person) { + return restClient + .post() + .uri("http://localhost:8080/person/") + .body(person) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java new file mode 100644 index 00000000000..2c8bdc33c62 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java @@ -0,0 +1,24 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Person { + private final String firstName; + private final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java new file mode 100644 index 00000000000..1584a9e823b --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -0,0 +1,64 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.sentry.ISpan; +import io.sentry.Sentry; +import org.jetbrains.annotations.NotNull; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/person/") +public class PersonController { + private final PersonService personService; + private final Tracer tracer; + private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); + + public PersonController(PersonService personService, Tracer tracer) { + this.personService = personService; + this.tracer = tracer; + } + + @GetMapping("{id}") + Person person(@PathVariable Long id) { + Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); + try (final @NotNull Scope spanScope = span.makeCurrent()) { + Sentry.logger().warn("warn Sentry logging"); + Sentry.logger().error("error Sentry logging"); + Sentry.logger().info("hello %s %s", "there", "world!"); + ISpan currentSpan = Sentry.getSpan(); + ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); + try { + LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading")); + throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); + } finally { + sentrySpan.finish(); + } + } finally { + span.end(); + } + } + + @PostMapping + Person create(@RequestBody Person person) { + Span span = tracer.spanBuilder("spanCreatedThroughOtelApi").startSpan(); + try (final @NotNull Scope spanScope = span.makeCurrent()) { + ISpan sentrySpan = Sentry.getSpan().startChild("spanCreatedThroughSentryApi"); + try { + return personService.create(person); + } finally { + sentrySpan.finish(); + } + } finally { + span.end(); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java new file mode 100644 index 00000000000..50cdc9dd4e7 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java @@ -0,0 +1,41 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.spring.jakarta.tracing.SentrySpan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +/** + * {@link SentrySpan} can be added either on the class or the method to create spans around method + * executions. + */ +@Service +@SentrySpan +public class PersonService { + private static final Logger LOGGER = LoggerFactory.getLogger(PersonService.class); + + private final JdbcTemplate jdbcTemplate; + private int createCount = 0; + + public PersonService(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + Person create(Person person) { + createCount++; + final ISpan span = Sentry.getSpan(); + if (span != null) { + span.setMeasurement("create_count", createCount); + } + + jdbcTemplate.update( + "insert into person (firstName, lastName) values (?, ?)", + person.getFirstName(), + person.getLastName()); + + return person; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java new file mode 100644 index 00000000000..e5987c8f4a2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java @@ -0,0 +1,40 @@ +package io.sentry.samples.spring.boot.jakarta; + +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfiguration { + + // this API is meant to be consumed by non-browser clients thus the CSRF protection is not needed. + @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) + @Bean + public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { + http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); + + return http.build(); + } + + @Bean + public @NotNull InMemoryUserDetailsManager userDetailsService() { + final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + + final UserDetails user = + User.builder() + .passwordEncoder(encoder::encode) + .username("user") + .password("password") + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(user); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java new file mode 100644 index 00000000000..a6eb46f4c74 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -0,0 +1,78 @@ +package io.sentry.samples.spring.boot.jakarta; + +import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; + +import io.opentelemetry.api.GlobalOpenTelemetry; +import io.opentelemetry.api.trace.Tracer; +import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import java.util.Collections; +import org.quartz.JobDetail; +import org.quartz.SimpleTrigger; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; +import org.springframework.scheduling.quartz.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +@SpringBootApplication +@EnableScheduling +public class SentryDemoApplication { + public static void main(String[] args) { + SpringApplication.run(SentryDemoApplication.class, args); + } + + @Bean + RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); + } + + @Bean + WebClient webClient(WebClient.Builder builder) { + return builder.build(); + } + + @Bean + RestClient restClient(RestClient.Builder builder) { + return builder.build(); + } + + @Bean + public JobDetailFactoryBean jobDetail() { + JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); + jobDetailFactory.setJobClass(SampleJob.class); + jobDetailFactory.setDurability(true); + jobDetailFactory.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_job_detail")); + return jobDetailFactory; + } + + @Bean + public SimpleTriggerFactoryBean trigger(JobDetail job) { + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setRepeatInterval(2 * 60 * 1000); // every two minutes + trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + trigger.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_simple_trigger")); + return trigger; + } + + @Bean + public CronTriggerFactoryBean cronTrigger(JobDetail job) { + CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setCronExpression("0 0/5 * ? * *"); // every five minutes + return trigger; + } + + @Bean + public Tracer tracer() { + return GlobalOpenTelemetry.get().getTracer("tracerForSpringBootDemo"); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java new file mode 100644 index 00000000000..5fc4164d1b0 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java @@ -0,0 +1,25 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Todo { + private final Long id; + private final String title; + private final boolean completed; + + public Todo(Long id, String title, boolean completed) { + this.id = id; + this.title = title; + this.completed = completed; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public boolean isCompleted() { + return completed; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java new file mode 100644 index 00000000000..0fa450a879c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java @@ -0,0 +1,86 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.opentelemetry.api.trace.Span; +import io.opentelemetry.api.trace.Tracer; +import io.opentelemetry.context.Scope; +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.reactor.SentryReactorUtils; +import org.jetbrains.annotations.NotNull; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@RestController +public class TodoController { + private final RestTemplate restTemplate; + private final WebClient webClient; + private final RestClient restClient; + private final Tracer tracer; + + public TodoController( + RestTemplate restTemplate, WebClient webClient, RestClient restClient, Tracer tracer) { + this.restTemplate = restTemplate; + this.webClient = webClient; + this.restClient = restClient; + this.tracer = tracer; + } + + @GetMapping("/todo/{id}") + Todo todo(@PathVariable Long id) { + Span otelSpan = tracer.spanBuilder("todoSpanOtelApi").startSpan(); + try (final @NotNull Scope spanScope = otelSpan.makeCurrent()) { + ISpan sentrySpan = Sentry.getSpan().startChild("todoSpanSentryApi"); + try { + return restTemplate.getForObject( + "https://jsonplaceholder.typicode.com/todos/{id}", Todo.class, id); + } finally { + sentrySpan.finish(); + } + } finally { + otelSpan.end(); + } + } + + @GetMapping("/todo-webclient/{id}") + Todo todoWebClient(@PathVariable Long id) { + Hooks.enableAutomaticContextPropagation(); + return SentryReactorUtils.withSentry( + Mono.just(true) + .publishOn(Schedulers.boundedElastic()) + .flatMap( + x -> + webClient + .get() + .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) + .retrieve() + .bodyToMono(Todo.class) + .map(response -> response))) + .block(); + } + + @GetMapping("/todo-restclient/{id}") + Todo todoRestClient(@PathVariable Long id) { + Span span = tracer.spanBuilder("todoRestClientSpanOtelApi").startSpan(); + try (final @NotNull Scope spanScope = span.makeCurrent()) { + ISpan sentrySpan = Sentry.getSpan().startChild("todoRestClientSpanSentryApi"); + try { + return restClient + .get() + .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) + .retrieve() + .body(Todo.class); + } finally { + sentrySpan.finish(); + } + } finally { + span.end(); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java new file mode 100644 index 00000000000..6fdf96506c8 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.springframework.graphql.data.method.annotation.BatchMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +@Controller +public class AssigneeController { + + @BatchMapping(typeName = "Task", field = "assignee") + public Mono> assignee( + final @NotNull Set tasks) { + return Mono.fromCallable( + () -> { + final @NotNull Map map = + new HashMap<>(); + for (final @NotNull ProjectController.Task task : tasks) { + if ("Acrash".equalsIgnoreCase(task.assigneeId)) { + throw new RuntimeException("Causing an error while loading assignee"); + } + if (task.assigneeId != null) { + map.put( + task, new ProjectController.Assignee(task.assigneeId, "Name" + task.assigneeId)); + } + } + + return map; + }); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java new file mode 100644 index 00000000000..bfc383c9122 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java @@ -0,0 +1,17 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +@Controller +public class GreetingController { + + @QueryMapping + public String greeting(final @Argument String name) { + if ("crash".equalsIgnoreCase(name)) { + throw new RuntimeException("causing an error for " + name); + } + return "Hello " + name + "!"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java new file mode 100644 index 00000000000..63790bca628 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java @@ -0,0 +1,140 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.nio.file.NoSuchFileException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.jetbrains.annotations.NotNull; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.graphql.data.method.annotation.SubscriptionMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Flux; + +@Controller +public class ProjectController { + + @QueryMapping + public Project project(final @Argument String slug) throws Exception { + if ("crash".equalsIgnoreCase(slug) || "projectcrash".equalsIgnoreCase(slug)) { + throw new RuntimeException("causing a project error for " + slug); + } + if ("notfound".equalsIgnoreCase(slug)) { + throw new IllegalStateException("not found"); + } + if ("nofile".equals(slug)) { + throw new NoSuchFileException("no such file"); + } + Project project = new Project(); + project.slug = slug; + return project; + } + + @SchemaMapping(typeName = "Project", field = "status") + public ProjectStatus projectStatus(final Project project) { + if ("crash".equalsIgnoreCase(project.slug) || "statuscrash".equalsIgnoreCase(project.slug)) { + throw new RuntimeException("causing a project status error for " + project.slug); + } + return ProjectStatus.COMMUNITY; + } + + @MutationMapping + public String addProject(@Argument String slug) { + if ("crash".equalsIgnoreCase(slug) || "addprojectcrash".equalsIgnoreCase(slug)) { + throw new RuntimeException("causing a project add error for " + slug); + } + return UUID.randomUUID().toString(); + } + + @QueryMapping + public List tasks(final @Argument String projectSlug) { + List tasks = new ArrayList<>(); + tasks.add(new Task("T1", "Create a new API", "A3", "C3")); + tasks.add(new Task("T2", "Update dependencies", "A1", "C1")); + tasks.add(new Task("T3", "Document API", "A1", "C1")); + tasks.add(new Task("T4", "Merge community PRs", "A2", "C2")); + tasks.add(new Task("T5", "Plan more work", null, null)); + if ("crash".equalsIgnoreCase(projectSlug)) { + tasks.add(new Task("T6", "Fix crash", "Acrash", "Ccrash")); + } + return tasks; + } + + @SubscriptionMapping + public Flux notifyNewTask(@Argument String projectSlug) { + if ("crash".equalsIgnoreCase(projectSlug)) { + throw new RuntimeException("causing error for subscription"); + } + if ("fluxerror".equalsIgnoreCase(projectSlug)) { + return Flux.error(new RuntimeException("causing flux error for subscription")); + } + final String assigneeId = "assigneecrash".equalsIgnoreCase(projectSlug) ? "Acrash" : "A1"; + final String creatorId = "creatorcrash".equalsIgnoreCase(projectSlug) ? "Ccrash" : "C1"; + final @NotNull AtomicInteger counter = new AtomicInteger(1000); + return Flux.interval(Duration.ofSeconds(1)) + .map( + num -> { + int i = counter.incrementAndGet(); + if ("produceerror".equalsIgnoreCase(projectSlug) && i % 2 == 0) { + throw new RuntimeException("causing produce error for subscription"); + } + return new Task("T" + i, "A new task arrived ", assigneeId, creatorId); + }); + } + + public static class Task { + public String id; + public String name; + public String assigneeId; + public String creatorId; + + public Task( + final String id, final String name, final String assigneeId, final String creatorId) { + this.id = id; + this.name = name; + this.assigneeId = assigneeId; + this.creatorId = creatorId; + } + + @Override + public String toString() { + return "Task{id=" + id + "}"; + } + } + + public static class Assignee { + public String id; + public String name; + + public Assignee(final String id, final String name) { + this.id = id; + this.name = name; + } + } + + public static class Creator { + public String id; + public String name; + + public Creator(final String id, final String name) { + this.id = id; + this.name = name; + } + } + + public static class Project { + public String slug; + } + + public enum ProjectStatus { + ACTIVE, + COMMUNITY, + INCUBATING, + ATTIC, + EOL; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java new file mode 100644 index 00000000000..cb6677c0c37 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java @@ -0,0 +1,49 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.DataLoader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.graphql.execution.BatchLoaderRegistry; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +@Controller +class TaskCreatorController { + + public TaskCreatorController(final BatchLoaderRegistry batchLoaderRegistry) { + // using mapped BatchLoader to not have to deal with correct ordering of items + batchLoaderRegistry + .forTypePair(String.class, ProjectController.Creator.class) + .registerMappedBatchLoader( + (Set keys, BatchLoaderEnvironment env) -> { + return Mono.fromCallable( + () -> { + final @NotNull Map map = new HashMap<>(); + for (String key : keys) { + if ("Ccrash".equalsIgnoreCase(key)) { + throw new RuntimeException("Causing an error while loading creator"); + } + map.put(key, new ProjectController.Creator(key, "Name" + key)); + } + + return map; + }); + }); + } + + @SchemaMapping(typeName = "Task") + public @Nullable CompletableFuture creator( + final ProjectController.Task task, + final DataLoader dataLoader) { + if (task.creatorId == null) { + return null; + } + return dataLoader.load(task.creatorId); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java new file mode 100644 index 00000000000..d0f0973c864 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java @@ -0,0 +1,19 @@ +package io.sentry.samples.spring.boot.jakarta.quartz; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.stereotype.Component; + +@Component +public class SampleJob implements Job { + + public void execute(JobExecutionContext context) throws JobExecutionException { + System.out.println("running job"); + try { + Thread.sleep(15000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties new file mode 100644 index 00000000000..c1499111d31 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/application.properties @@ -0,0 +1,33 @@ +# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard +sentry.dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +sentry.send-default-pii=true +sentry.max-request-body-size=medium +# Sentry Spring Boot integration allows more fine-grained SentryOptions configuration +sentry.max-breadcrumbs=150 +# Logback integration configuration options +sentry.logging.minimum-event-level=info +sentry.logging.minimum-breadcrumb-level=debug +# Performance configuration +sentry.traces-sample-rate=1.0 +sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 +sentry.debug=true +sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR +sentry.enable-backpressure-handling=true +sentry.enable-spotlight=true +sentry.enablePrettySerializationOutput=false +sentry.logs.enabled=true +in-app-includes="io.sentry.samples" + +# Uncomment and set to true to enable aot compatibility +# This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) +# to successfully compile to GraalVM +# sentry.enable-aot-compatibility=false + +# Database configuration +spring.datasource.url=jdbc:p6spy:hsqldb:mem:testdb +spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver +spring.datasource.username=sa +spring.datasource.password= +spring.graphql.graphiql.enabled=true +spring.graphql.websocket.path=/graphql +spring.quartz.job-store-type=memory diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/graphql/schema.graphqls b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/graphql/schema.graphqls new file mode 100644 index 00000000000..aeea62357bd --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,68 @@ +type Query { + greeting(name: String! = "Spring"): String! + project(slug: ID!): Project + tasks(projectSlug: ID!): [Task] +} + +type Mutation { + addProject(slug: ID!): String! +} + +type Subscription { + notifyNewTask(projectSlug: ID!): Task +} + +""" A Project in the Spring portfolio """ +type Project { + """ Unique string id used in URLs """ + slug: ID! + """ Project name """ + name: String + """ Current support status """ + status: ProjectStatus! +} + +""" A task """ +type Task { + """ ID """ + id: String! + """ Name """ + name: String! + """ ID of the Assignee """ + assigneeId: String + """ Assignee """ + assignee: Assignee + """ ID of the Creator """ + creatorId: String + """ Creator """ + creator: Creator +} + +""" An Assignee """ +type Assignee { + """ ID """ + id: String! + """ Name """ + name: String! +} + +""" An Creator """ +type Creator { + """ ID """ + id: String! + """ Name """ + name: String! +} + +enum ProjectStatus { + """ Actively supported by the Spring team """ + ACTIVE + """ Supported by the community """ + COMMUNITY + """ Prototype, not officially supported yet """ + INCUBATING + """ Project being retired, in maintenance mode """ + ATTIC + """ End-Of-Lifed """ + EOL +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/quartz.properties b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/quartz.properties new file mode 100644 index 00000000000..6e302ce765a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/quartz.properties @@ -0,0 +1 @@ +org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/schema.sql b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/schema.sql new file mode 100644 index 00000000000..7ca8a5cbf42 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/resources/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE person ( + id INTEGER IDENTITY PRIMARY KEY, + firstName VARCHAR(50) NOT NULL, + lastName VARCHAR(50) NOT NULL +); diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/DummyTest.kt @@ -0,0 +1,12 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertTrue + +class DummyTest { + @Test + fun `the only test`() { + // only needed to have more than 0 tests and not fail the build + assertTrue(true) + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt new file mode 100644 index 00000000000..3cd16003024 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt @@ -0,0 +1,197 @@ +package io.sentry.systemtest + +import io.sentry.protocol.SentryId +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import org.junit.Before + +class DistributedTracingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } + + @Test + fun `get person distributed tracing with sampled false`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-0", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=false,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" + } + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" + } + } + + @Test + fun `get person distributed tracing without sample_rand`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRand1: String? = null + var sampleRand2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand1 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand2 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + assertEquals(sampleRand1, sampleRand2) + } + + @Test + fun `get person distributed tracing updates sample_rate on deferred decision`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRate1: String? = null + var sampleRate2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate1 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate2 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + assertEquals(sampleRate1, sampleRate2) + assertNotEquals(sampleRate1, "0.5") + } + + @Test + fun `create person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPersonDistributedTracing( + person, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /tracing/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /person/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt new file mode 100644 index 00000000000..4286cfa1a86 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt @@ -0,0 +1,46 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import org.junit.Before + +class GraphqlGreetingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `greeting works`() { + val response = testHelper.graphqlClient.greet("world") + + testHelper.ensureNoErrors(response) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "query GreetingQuery", + "query GreetingQuery", + ) + } + } + + @Test + fun `greeting error`() { + val response = testHelper.graphqlClient.greet("crash") + + testHelper.ensureErrorCount(response, 1) + testHelper.ensureErrorReceived { error -> + error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "query GreetingQuery", + "query GreetingQuery", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt new file mode 100644 index 00000000000..f592b2a08ec --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt @@ -0,0 +1,66 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.Before + +class GraphqlProjectSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `project query works`() { + val response = testHelper.graphqlClient.project("proj-slug") + + testHelper.ensureNoErrors(response) + assertEquals("proj-slug", response?.data?.project?.slug) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "query ProjectQuery", + "query ProjectQuery", + ) + } + } + + @Test + fun `project mutation works`() { + val response = testHelper.graphqlClient.addProject("proj-slug") + + testHelper.ensureNoErrors(response) + assertNotNull(response?.data?.addProject) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "mutation AddProjectMutation", + "mutation AddProjectMutation", + ) + } + } + + @Test + fun `project mutation error`() { + val response = testHelper.graphqlClient.addProject("addprojectcrash") + + testHelper.ensureErrorCount(response, 1) + assertNull(response?.data?.addProject) + testHelper.ensureErrorReceived { error -> + error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "mutation AddProjectMutation", + "mutation AddProjectMutation", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt new file mode 100644 index 00000000000..f811da5a082 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt @@ -0,0 +1,40 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class GraphqlTaskSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `tasks and assignees query works`() { + val response = testHelper.graphqlClient.tasksAndAssignees("project-slug") + + testHelper.ensureNoErrors(response) + + assertEquals(5, response?.data?.tasks?.size) + + val firstTask = response?.data?.tasks?.firstOrNull() ?: throw RuntimeException("no task") + assertEquals("T1", firstTask.id) + assertEquals("A3", firstTask.assigneeId) + assertEquals("A3", firstTask.assignee?.id) + assertEquals("C3", firstTask.creatorId) + assertEquals("C3", firstTask.creator?.id) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "query TasksAndAssigneesQuery", + "query TasksAndAssigneesQuery", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt new file mode 100644 index 00000000000..707b5025dcf --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -0,0 +1,115 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class PersonSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person fails`() { + val restClient = testHelper.restClient + restClient.getPerson(1L) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") + } + + Thread.sleep(10000) + + testHelper.ensureLogsReceived { logs, envelopeHeader -> + testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && + testHelper.doesContainLogWithBody(logs, "error Sentry logging") && + testHelper.doesContainLogWithBody(logs, "hello there world!") + } + } + + @Test + fun `create person works`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = restClient.createPerson(person) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "db", + "insert into person (firstName, lastName) values (?, ?)", + ) + } + } + + @Test + fun `create person creates transaction if no sampled flag in sentry-trace header`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPerson( + person, + mapOf( + "sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "db", + "insert into person (firstName, lastName) values (?, ?)", + ) + } + } + + @Test + fun `create person creates transaction if sampled true in sentry-trace header`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPerson( + person, + mapOf( + "sentry-trace" to "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=f9118105af4a2d42b4124532cd1065ff,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "spanCreatedThroughSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "db", + "insert into person (firstName, lastName) values (?, ?)", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt new file mode 100644 index 00000000000..18dc7d3f552 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt @@ -0,0 +1,61 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class TodoSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get todo works`() { + val restClient = testHelper.restClient + restClient.getTodo(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "todoSpanSentryApi") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } + + @Test + fun `get todo webclient works`() { + val restClient = testHelper.restClient + restClient.getTodoWebclient(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } + + @Test + fun `get todo restclient works`() { + val restClient = testHelper.restClient + restClient.getTodoRestClient(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanOtelApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "todoRestClientSpanSentryApi") && + testHelper.doesTransactionContainSpanWithOp(transaction, "http.client") + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/resources/logback.xml b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/resources/logback.xml new file mode 100644 index 00000000000..a36b8f80f76 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + + %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n + + + + + + + diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/README.md b/sentry-samples/sentry-samples-spring-boot-4-webflux/README.md new file mode 100644 index 00000000000..7a4a36df50c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/README.md @@ -0,0 +1,44 @@ +# Sentry Sample Spring Boot 3 Webflux + +Sample application showing how to use Sentry with [Spring Webflux](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html) and [Spring boot](http://spring.io/projects/spring-boot). + +## How to run? + +To see events triggered in this sample application in your Sentry dashboard, go to `src/main/resources/application.properties` and replace the test DSN with your own DSN. + +Then, execute a command from the module directory: + +``` +../../gradlew bootRun +``` + +Make an HTTP request that will trigger events: + +``` +curl -XPOST --user user:password http://localhost:8080/person/ -H "Content-Type:application/json" -d '{"firstName":"John","lastName":"Smith"}' +``` + +## GraphQL + +The following queries can be used to test the GraphQL integration. + +### Greeting +``` +{ + greeting(name: "crash") +} +``` + +### Greeting with variables + +``` +query GreetingQuery($name: String) { + greeting(name: $name) +} +``` +variables: +``` +{ + "name": "crash" +} +``` diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts new file mode 100644 index 00000000000..1d68ad1a83e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts @@ -0,0 +1,73 @@ +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.springboot4) + alias(libs.plugins.spring.dependency.management) + kotlin("jvm") + alias(libs.plugins.kotlin.spring) +} + +group = "io.sentry.sample.spring-boot-4-webflux" + +version = "0.0.1-SNAPSHOT" + +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + +dependencies { + implementation(Config.Libs.kotlinReflect) + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + implementation(projects.sentrySpringBoot4Starter) + implementation(projects.sentryLogback) + implementation(projects.sentryJdbc) + implementation(projects.sentryGraphql22) + implementation(libs.context.propagation) + implementation(libs.springboot4.starter.actuator) + implementation(libs.springboot4.starter.graphql) + implementation(libs.springboot4.starter.webflux) + + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.apollo3.kotlin) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.slf4j2.api) + testImplementation(libs.springboot4.starter.test) { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + testImplementation("ch.qos.logback:logback-classic:1.5.16") + testImplementation("ch.qos.logback:logback-core:1.5.16") +} + +configure { test { java.srcDir("src/test/java") } } + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +tasks.register("systemTest").configure { + group = "verification" + description = "Runs the System tests" + + outputs.upToDateWhen { false } + + maxParallelForks = 1 + + // Cap JVM args per test + minHeapSize = "128m" + maxHeapSize = "1g" + + filter { includeTestsMatching("io.sentry.systemtest*") } +} + +tasks.named("test").configure { + require(this is Test) + + filter { excludeTestsMatching("io.sentry.systemtest.*") } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java new file mode 100644 index 00000000000..38409509905 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java @@ -0,0 +1,52 @@ +package io.sentry.samples.spring.boot.jakarta; + +import java.nio.charset.Charset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/tracing/") +public class DistributedTracingController { + private static final Logger LOGGER = LoggerFactory.getLogger(DistributedTracingController.class); + private final WebClient webClient; + + public DistributedTracingController(WebClient webClient) { + this.webClient = webClient; + } + + @GetMapping("{id}") + Mono person(@PathVariable Long id) { + return webClient + .get() + .uri("http://localhost:8080/person/{id}", id) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .bodyToMono(Person.class) + .map(response -> response); + } + + @PostMapping + Mono create(@RequestBody Person person) { + return webClient + .post() + .uri("http://localhost:8080/person/") + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .body(Mono.just(person), Person.class) + .retrieve() + .bodyToMono(Person.class) + .map(response -> response); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java new file mode 100644 index 00000000000..2c8bdc33c62 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java @@ -0,0 +1,24 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Person { + private final String firstName; + private final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java new file mode 100644 index 00000000000..a7b7752806d --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -0,0 +1,37 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; + +@RestController +@RequestMapping("/person/") +public class PersonController { + private final PersonService personService; + private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); + + public PersonController(PersonService personService) { + this.personService = personService; + } + + @GetMapping("{id}") + Person person(@PathVariable Long id) { + Sentry.logger().warn("warn Sentry logging"); + Sentry.logger().error("error Sentry logging"); + Sentry.logger().info("hello %s %s", "there", "world!"); + LOGGER.info("Loading person with id={}", id); + throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); + } + + @PostMapping + Mono create(@RequestBody Person person) { + return personService.create(person); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java new file mode 100644 index 00000000000..75b0e38b125 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java @@ -0,0 +1,21 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.Sentry; +import java.time.Duration; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@Service +public class PersonService { + + Mono create(Person person) { + return Mono.delay(Duration.ofMillis(100)) + .publishOn(Schedulers.boundedElastic()) + .doOnNext( + __ -> { + Sentry.captureMessage("Creating person"); + }) + .map(__ -> person); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java new file mode 100644 index 00000000000..926298bb97b --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -0,0 +1,18 @@ +package io.sentry.samples.spring.boot.jakarta; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.web.reactive.function.client.WebClient; + +@SpringBootApplication +public class SentryDemoApplication { + public static void main(String[] args) { + SpringApplication.run(SentryDemoApplication.class, args); + } + + @Bean + WebClient webClient(WebClient.Builder builder) { + return builder.build(); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java new file mode 100644 index 00000000000..5fc4164d1b0 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java @@ -0,0 +1,25 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Todo { + private final Long id; + private final String title; + private final boolean completed; + + public Todo(Long id, String title, boolean completed) { + this.id = id; + this.title = title; + this.completed = completed; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public boolean isCompleted() { + return completed; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java new file mode 100644 index 00000000000..de29c206ecb --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java @@ -0,0 +1,26 @@ +package io.sentry.samples.spring.boot.jakarta; + +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +@RestController +public class TodoController { + private final WebClient webClient; + + public TodoController(WebClient webClient) { + this.webClient = webClient; + } + + @GetMapping("/todo-webclient/{id}") + Mono todoWebClient(@PathVariable Long id) { + return webClient + .get() + .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) + .retrieve() + .bodyToMono(Todo.class) + .map(response -> response); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java new file mode 100644 index 00000000000..421631ca7a5 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java @@ -0,0 +1,19 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +@Controller +public class GreetingController { + + @QueryMapping + public Mono greeting(final @Argument String name) { + if ("crash".equalsIgnoreCase(name)) { + // return Mono.error(new RuntimeException("causing an error for " + name)); + throw new RuntimeException("causing an error for " + name); + } + return Mono.just("Hello " + name + "!"); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties new file mode 100644 index 00000000000..e7944f4a962 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/application.properties @@ -0,0 +1,14 @@ +# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard +sentry.dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +sentry.send-default-pii=true +sentry.debug=true +# Sentry Spring Boot integration allows more fine-grained SentryOptions configuration +sentry.max-breadcrumbs=150 +# Logback integration configuration options +sentry.logging.minimum-event-level=info +sentry.logging.minimum-breadcrumb-level=debug +sentry.reactive.thread-local-accessor-enabled=true +sentry.traces-sample-rate=1.0 +sentry.enable-backpressure-handling=true +sentry.logs.enabled=true +sentry.enable-spotlight=true diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/graphql/schema.graphqls b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/graphql/schema.graphqls new file mode 100644 index 00000000000..d76aca4756a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,70 @@ +type Query { + greeting(name: String! = "Spring"): String! + project(slug: ID!): Project + tasks(projectSlug: ID!): [Task] +} + +type Mutation { + addProject(slug: ID!): String! +} + +type Subscription { + notifyNewTask(projectSlug: ID!): Task +} + +""" A Project in the Spring portfolio """ +type Project { + """ Unique string id used in URLs """ + slug: ID! + """ Project name """ + name: String + """ URL of the git repository """ + repositoryUrl: String! + """ Current support status """ + status: ProjectStatus! +} + +""" A task """ +type Task { + """ ID """ + id: String! + """ Name """ + name: String! + """ ID of the Assignee """ + assigneeId: String + """ Assignee """ + assignee: Assignee + """ ID of the Creator """ + creatorId: String + """ Creator """ + creator: Creator +} + +""" An Assignee """ +type Assignee { + """ ID """ + id: String! + """ Name """ + name: String! +} + +""" An Creator """ +type Creator { + """ ID """ + id: String! + """ Name """ + name: String! +} + +enum ProjectStatus { + """ Actively supported by the Spring team """ + ACTIVE + """ Supported by the community """ + COMMUNITY + """ Prototype, not officially supported yet """ + INCUBATING + """ Project being retired, in maintenance mode """ + ATTIC + """ End-Of-Lifed """ + EOL +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/DummyTest.kt @@ -0,0 +1,12 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertTrue + +class DummyTest { + @Test + fun `the only test`() { + // only needed to have more than 0 tests and not fail the build + assertTrue(true) + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt new file mode 100644 index 00000000000..3cd16003024 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt @@ -0,0 +1,197 @@ +package io.sentry.systemtest + +import io.sentry.protocol.SentryId +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import org.junit.Before + +class DistributedTracingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } + + @Test + fun `get person distributed tracing with sampled false`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-0", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=false,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" + } + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" + } + } + + @Test + fun `get person distributed tracing without sample_rand`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRand1: String? = null + var sampleRand2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand1 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand2 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + assertEquals(sampleRand1, sampleRand2) + } + + @Test + fun `get person distributed tracing updates sample_rate on deferred decision`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRate1: String? = null + var sampleRate2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate1 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate2 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + assertEquals(sampleRate1, sampleRate2) + assertNotEquals(sampleRate1, "0.5") + } + + @Test + fun `create person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPersonDistributedTracing( + person, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /tracing/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /person/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt new file mode 100644 index 00000000000..76a6024decc --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt @@ -0,0 +1,46 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import org.junit.Before + +class GraphqlGreetingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `greeting works`() { + val response = testHelper.graphqlClient.greet("world") + + testHelper.ensureNoErrors(response) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.greeting", + ) + } + } + + @Test + fun `greeting error`() { + val response = testHelper.graphqlClient.greet("crash") + + testHelper.ensureErrorCount(response, 1) + testHelper.ensureErrorReceived { error -> + error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.greeting", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt new file mode 100644 index 00000000000..26c3282e7a8 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -0,0 +1,50 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class PersonSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person fails`() { + val restClient = testHelper.restClient + restClient.getPerson(1L) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionHaveOp(transaction, "http.server") + } + + Thread.sleep(10000) + + testHelper.ensureLogsReceived { logs, envelopeHeader -> + testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && + testHelper.doesContainLogWithBody(logs, "error Sentry logging") && + testHelper.doesContainLogWithBody(logs, "hello there world!") + } + } + + @Test + fun `create person works`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = restClient.createPerson(person) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionHaveOp(transaction, "http.server") + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt new file mode 100644 index 00000000000..b06b82a62ee --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt @@ -0,0 +1,31 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class TodoSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get todo webclient works`() { + val restClient = testHelper.restClient + restClient.getTodoWebclient(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/resources/logback.xml b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/resources/logback.xml new file mode 100644 index 00000000000..a36b8f80f76 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + + %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n + + + + + + + diff --git a/sentry-samples/sentry-samples-spring-boot-4/README.md b/sentry-samples/sentry-samples-spring-boot-4/README.md new file mode 100644 index 00000000000..58b94ba8997 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/README.md @@ -0,0 +1,122 @@ +# Sentry Sample Spring Boot 3.0+ + +Sample application showing how to use Sentry with [Spring boot](http://spring.io/projects/spring-boot) from version `3.0` onwards. + +## How to run? + +To see events triggered in this sample application in your Sentry dashboard, go to `src/main/resources/application.properties` and replace the test DSN with your own DSN. + +Then, execute a command from the module directory: + +``` +../../gradlew bootRun +``` + +Make an HTTP request that will trigger events: + +``` +curl -XPOST --user user:password http://localhost:8080/person/ -H "Content-Type:application/json" -d '{"firstName":"John","lastName":"Smith"}' +``` + +## GraphQL + +The following queries can be used to test the GraphQL integration. + +### Greeting +``` +{ + greeting(name: "crash") +} +``` + +### Greeting with variables + +``` +query GreetingQuery($name: String) { + greeting(name: $name) +} +``` +variables: +``` +{ + "name": "crash" +} +``` + +### Project + +``` +query ProjectQuery($slug: ID!) { + project(slug: $slug) { + slug + name + repositoryUrl + status + } +} +``` +variables: +``` +{ + "slug": "statuscrash" +} +``` + +### Mutation + +``` +mutation AddProjectMutation($slug: ID!) { + addProject(slug: $slug) +} +``` +variables: +``` +{ + "slug": "nocrash", + "name": "nocrash" +} +``` + +### Subscription + +``` +subscription SubscriptionNotifyNewTask($slug: ID!) { + notifyNewTask(projectSlug: $slug) { + id + name + assigneeId + assignee { + id + name + } + } +} +``` +variables: +``` +{ + "slug": "crash" +} +``` + +### Data loader + +``` +query TasksAndAssigneesQuery($slug: ID!) { + tasks(projectSlug: $slug) { + id + name + assigneeId + assignee { + id + name + } + } +} +``` +variables: +``` +{ + "slug": "crash" +} +``` diff --git a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts new file mode 100644 index 00000000000..95aa93c1ed0 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts @@ -0,0 +1,94 @@ +import org.jetbrains.kotlin.config.KotlinCompilerVersion +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.springboot4) + alias(libs.plugins.spring.dependency.management) + kotlin("jvm") + alias(libs.plugins.kotlin.spring) +} + +group = "io.sentry.sample.spring-boot-4" + +version = "0.0.1-SNAPSHOT" + +java.sourceCompatibility = JavaVersion.VERSION_17 + +java.targetCompatibility = JavaVersion.VERSION_17 + +repositories { mavenCentral() } + +configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() +} + +tasks.withType().configureEach { + kotlinOptions { + freeCompilerArgs = listOf("-Xjsr305=strict") + jvmTarget = JavaVersion.VERSION_17.toString() + } +} + +dependencies { + implementation(libs.springboot4.starter) + implementation(libs.springboot4.starter.actuator) + implementation(libs.springboot4.starter.aop) + implementation(libs.springboot4.starter.graphql) + implementation(libs.springboot4.starter.jdbc) + implementation(libs.springboot4.starter.quartz) + implementation(libs.springboot4.starter.security) + implementation(libs.springboot4.starter.web) + implementation(libs.springboot4.starter.webflux) + implementation(libs.springboot4.starter.websocket) + implementation(Config.Libs.aspectj) + implementation(Config.Libs.kotlinReflect) + implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + implementation(projects.sentrySpringBoot4Starter) + implementation(projects.sentryLogback) + implementation(projects.sentryGraphql22) + implementation(projects.sentryQuartz) + + // database query tracing + implementation(projects.sentryJdbc) + runtimeOnly(libs.hsqldb) + + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(projects.sentry) + testImplementation(projects.sentrySystemTestSupport) + testImplementation(libs.apollo3.kotlin) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.slf4j2.api) + testImplementation(libs.springboot4.starter.test) { + exclude(group = "org.junit.vintage", module = "junit-vintage-engine") + } + testImplementation("ch.qos.logback:logback-classic:1.5.16") + testImplementation("ch.qos.logback:logback-core:1.5.16") +} + +configure { test { java.srcDir("src/test/java") } } + +tasks.register("systemTest").configure { + group = "verification" + description = "Runs the System tests" + + outputs.upToDateWhen { false } + + maxParallelForks = 1 + + // Cap JVM args per test + minHeapSize = "128m" + maxHeapSize = "1g" + + filter { includeTestsMatching("io.sentry.systemtest*") } +} + +tasks.named("test").configure { + require(this is Test) + + filter { excludeTestsMatching("io.sentry.systemtest.*") } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java new file mode 100644 index 00000000000..51451a5d77e --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java @@ -0,0 +1,35 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.SentryEvent; +import io.sentry.protocol.SentryRuntime; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.SpringBootVersion; +import org.springframework.stereotype.Component; + +/** + * Custom {@link EventProcessor} implementation lets modifying {@link SentryEvent}s before they are + * sent to Sentry. + */ +@Component +public class CustomEventProcessor implements EventProcessor { + private final String springBootVersion; + + public CustomEventProcessor(String springBootVersion) { + this.springBootVersion = springBootVersion; + } + + public CustomEventProcessor() { + this(SpringBootVersion.getVersion()); + } + + @Override + public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { + final SentryRuntime runtime = new SentryRuntime(); + runtime.setVersion(springBootVersion); + runtime.setName("Spring Boot"); + event.getContexts().setRuntime(runtime); + return event; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java new file mode 100644 index 00000000000..4cd609d67ce --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java @@ -0,0 +1,25 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.spring.jakarta.checkin.SentryCheckIn; +import io.sentry.spring.jakarta.tracing.SentryTransaction; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +/** + * {@link SentryTransaction} added on the class level, creates transaction around each method + * execution of every method of the annotated class. + */ +@Component +@SentryTransaction(operation = "scheduled") +public class CustomJob { + + private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class); + + @SentryCheckIn("monitor_slug_1") + // @Scheduled(fixedRate = 3 * 60 * 1000L) + void execute() throws InterruptedException { + LOGGER.info("Executing scheduled job"); + Thread.sleep(2000L); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java new file mode 100644 index 00000000000..d67059abb68 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java @@ -0,0 +1,49 @@ +package io.sentry.samples.spring.boot.jakarta; + +import java.nio.charset.Charset; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpHeaders; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; + +@RestController +@RequestMapping("/tracing/") +public class DistributedTracingController { + private static final Logger LOGGER = LoggerFactory.getLogger(DistributedTracingController.class); + private final RestClient restClient; + + public DistributedTracingController(RestClient restClient) { + this.restClient = restClient; + } + + @GetMapping("{id}") + Person person(@PathVariable Long id) { + return restClient + .get() + .uri("http://localhost:8080/person/{id}", id) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } + + @PostMapping + Person create(@RequestBody Person person) { + return restClient + .post() + .uri("http://localhost:8080/person/") + .body(person) + .header( + HttpHeaders.AUTHORIZATION, + "Basic " + HttpHeaders.encodeBasicAuth("user", "password", Charset.defaultCharset())) + .retrieve() + .body(Person.class); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java new file mode 100644 index 00000000000..2c8bdc33c62 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java @@ -0,0 +1,24 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Person { + private final String firstName; + private final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java new file mode 100644 index 00000000000..94a4b9b8520 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java @@ -0,0 +1,49 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.ISpan; +import io.sentry.Sentry; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/person/") +public class PersonController { + private final PersonService personService; + private static final Logger LOGGER = LoggerFactory.getLogger(PersonController.class); + + public PersonController(PersonService personService) { + this.personService = personService; + } + + @GetMapping("{id}") + Person person(@PathVariable Long id) { + ISpan currentSpan = Sentry.getSpan(); + ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); + try { + Sentry.logger().warn("warn Sentry logging"); + Sentry.logger().error("error Sentry logging"); + Sentry.logger().info("hello %s %s", "there", "world!"); + LOGGER.error("Trying person with id={}", id, new RuntimeException("error while loading")); + throw new IllegalArgumentException("Something went wrong [id=" + id + "]"); + } finally { + sentrySpan.finish(); + } + } + + @PostMapping + Person create(@RequestBody Person person) { + ISpan currentSpan = Sentry.getSpan(); + ISpan sentrySpan = currentSpan.startChild("spanCreatedThroughSentryApi"); + try { + return personService.create(person); + } finally { + sentrySpan.finish(); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java new file mode 100644 index 00000000000..50cdc9dd4e7 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java @@ -0,0 +1,41 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.ISpan; +import io.sentry.Sentry; +import io.sentry.spring.jakarta.tracing.SentrySpan; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Service; + +/** + * {@link SentrySpan} can be added either on the class or the method to create spans around method + * executions. + */ +@Service +@SentrySpan +public class PersonService { + private static final Logger LOGGER = LoggerFactory.getLogger(PersonService.class); + + private final JdbcTemplate jdbcTemplate; + private int createCount = 0; + + public PersonService(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + Person create(Person person) { + createCount++; + final ISpan span = Sentry.getSpan(); + if (span != null) { + span.setMeasurement("create_count", createCount); + } + + jdbcTemplate.update( + "insert into person (firstName, lastName) values (?, ?)", + person.getFirstName(), + person.getLastName()); + + return person; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java new file mode 100644 index 00000000000..e5987c8f4a2 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java @@ -0,0 +1,40 @@ +package io.sentry.samples.spring.boot.jakarta; + +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.core.userdetails.User; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.crypto.factory.PasswordEncoderFactories; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.provisioning.InMemoryUserDetailsManager; +import org.springframework.security.web.SecurityFilterChain; + +@Configuration +public class SecurityConfiguration { + + // this API is meant to be consumed by non-browser clients thus the CSRF protection is not needed. + @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) + @Bean + public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { + http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); + + return http.build(); + } + + @Bean + public @NotNull InMemoryUserDetailsManager userDetailsService() { + final PasswordEncoder encoder = PasswordEncoderFactories.createDelegatingPasswordEncoder(); + + final UserDetails user = + User.builder() + .passwordEncoder(encoder::encode) + .username("user") + .password("password") + .roles("USER") + .build(); + + return new InMemoryUserDetailsManager(user); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java new file mode 100644 index 00000000000..8050cb8e74c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -0,0 +1,71 @@ +package io.sentry.samples.spring.boot.jakarta; + +import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; + +import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import java.util.Collections; +import org.quartz.JobDetail; +import org.quartz.SimpleTrigger; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.quartz.CronTriggerFactoryBean; +import org.springframework.scheduling.quartz.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; + +@SpringBootApplication +@EnableScheduling +public class SentryDemoApplication { + public static void main(String[] args) { + SpringApplication.run(SentryDemoApplication.class, args); + } + + @Bean + RestTemplate restTemplate(RestTemplateBuilder builder) { + return builder.build(); + } + + @Bean + WebClient webClient(WebClient.Builder builder) { + return builder.build(); + } + + @Bean + RestClient restClient(RestClient.Builder builder) { + return builder.build(); + } + + @Bean + public JobDetailFactoryBean jobDetail() { + JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); + jobDetailFactory.setJobClass(SampleJob.class); + jobDetailFactory.setDurability(true); + jobDetailFactory.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_job_detail")); + return jobDetailFactory; + } + + @Bean + public SimpleTriggerFactoryBean trigger(JobDetail job) { + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setRepeatInterval(2 * 60 * 1000); // every two minutes + trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + trigger.setJobDataAsMap( + Collections.singletonMap(SENTRY_SLUG_KEY, "monitor_slug_simple_trigger")); + return trigger; + } + + @Bean + public CronTriggerFactoryBean cronTrigger(JobDetail job) { + CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setCronExpression("0 0/5 * ? * *"); // every five minutes + return trigger; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java new file mode 100644 index 00000000000..5fc4164d1b0 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java @@ -0,0 +1,25 @@ +package io.sentry.samples.spring.boot.jakarta; + +public class Todo { + private final Long id; + private final String title; + private final boolean completed; + + public Todo(Long id, String title, boolean completed) { + this.id = id; + this.title = title; + this.completed = completed; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public boolean isCompleted() { + return completed; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java new file mode 100644 index 00000000000..987d516936b --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java @@ -0,0 +1,57 @@ +package io.sentry.samples.spring.boot.jakarta; + +import io.sentry.reactor.SentryReactorUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Hooks; +import reactor.core.publisher.Mono; +import reactor.core.scheduler.Schedulers; + +@RestController +public class TodoController { + private final RestTemplate restTemplate; + private final WebClient webClient; + private final RestClient restClient; + + public TodoController(RestTemplate restTemplate, WebClient webClient, RestClient restClient) { + this.restTemplate = restTemplate; + this.webClient = webClient; + this.restClient = restClient; + } + + @GetMapping("/todo/{id}") + Todo todo(@PathVariable Long id) { + return restTemplate.getForObject( + "https://jsonplaceholder.typicode.com/todos/{id}", Todo.class, id); + } + + @GetMapping("/todo-webclient/{id}") + Todo todoWebClient(@PathVariable Long id) { + Hooks.enableAutomaticContextPropagation(); + return SentryReactorUtils.withSentry( + Mono.just(true) + .publishOn(Schedulers.boundedElastic()) + .flatMap( + x -> + webClient + .get() + .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) + .retrieve() + .bodyToMono(Todo.class) + .map(response -> response))) + .block(); + } + + @GetMapping("/todo-restclient/{id}") + Todo todoRestClient(@PathVariable Long id) { + return restClient + .get() + .uri("https://jsonplaceholder.typicode.com/todos/{id}", id) + .retrieve() + .body(Todo.class); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java new file mode 100644 index 00000000000..6fdf96506c8 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java @@ -0,0 +1,34 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.springframework.graphql.data.method.annotation.BatchMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +@Controller +public class AssigneeController { + + @BatchMapping(typeName = "Task", field = "assignee") + public Mono> assignee( + final @NotNull Set tasks) { + return Mono.fromCallable( + () -> { + final @NotNull Map map = + new HashMap<>(); + for (final @NotNull ProjectController.Task task : tasks) { + if ("Acrash".equalsIgnoreCase(task.assigneeId)) { + throw new RuntimeException("Causing an error while loading assignee"); + } + if (task.assigneeId != null) { + map.put( + task, new ProjectController.Assignee(task.assigneeId, "Name" + task.assigneeId)); + } + } + + return map; + }); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java new file mode 100644 index 00000000000..bfc383c9122 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java @@ -0,0 +1,17 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.stereotype.Controller; + +@Controller +public class GreetingController { + + @QueryMapping + public String greeting(final @Argument String name) { + if ("crash".equalsIgnoreCase(name)) { + throw new RuntimeException("causing an error for " + name); + } + return "Hello " + name + "!"; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java new file mode 100644 index 00000000000..63790bca628 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java @@ -0,0 +1,140 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.nio.file.NoSuchFileException; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.jetbrains.annotations.NotNull; +import org.springframework.graphql.data.method.annotation.Argument; +import org.springframework.graphql.data.method.annotation.MutationMapping; +import org.springframework.graphql.data.method.annotation.QueryMapping; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.graphql.data.method.annotation.SubscriptionMapping; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Flux; + +@Controller +public class ProjectController { + + @QueryMapping + public Project project(final @Argument String slug) throws Exception { + if ("crash".equalsIgnoreCase(slug) || "projectcrash".equalsIgnoreCase(slug)) { + throw new RuntimeException("causing a project error for " + slug); + } + if ("notfound".equalsIgnoreCase(slug)) { + throw new IllegalStateException("not found"); + } + if ("nofile".equals(slug)) { + throw new NoSuchFileException("no such file"); + } + Project project = new Project(); + project.slug = slug; + return project; + } + + @SchemaMapping(typeName = "Project", field = "status") + public ProjectStatus projectStatus(final Project project) { + if ("crash".equalsIgnoreCase(project.slug) || "statuscrash".equalsIgnoreCase(project.slug)) { + throw new RuntimeException("causing a project status error for " + project.slug); + } + return ProjectStatus.COMMUNITY; + } + + @MutationMapping + public String addProject(@Argument String slug) { + if ("crash".equalsIgnoreCase(slug) || "addprojectcrash".equalsIgnoreCase(slug)) { + throw new RuntimeException("causing a project add error for " + slug); + } + return UUID.randomUUID().toString(); + } + + @QueryMapping + public List tasks(final @Argument String projectSlug) { + List tasks = new ArrayList<>(); + tasks.add(new Task("T1", "Create a new API", "A3", "C3")); + tasks.add(new Task("T2", "Update dependencies", "A1", "C1")); + tasks.add(new Task("T3", "Document API", "A1", "C1")); + tasks.add(new Task("T4", "Merge community PRs", "A2", "C2")); + tasks.add(new Task("T5", "Plan more work", null, null)); + if ("crash".equalsIgnoreCase(projectSlug)) { + tasks.add(new Task("T6", "Fix crash", "Acrash", "Ccrash")); + } + return tasks; + } + + @SubscriptionMapping + public Flux notifyNewTask(@Argument String projectSlug) { + if ("crash".equalsIgnoreCase(projectSlug)) { + throw new RuntimeException("causing error for subscription"); + } + if ("fluxerror".equalsIgnoreCase(projectSlug)) { + return Flux.error(new RuntimeException("causing flux error for subscription")); + } + final String assigneeId = "assigneecrash".equalsIgnoreCase(projectSlug) ? "Acrash" : "A1"; + final String creatorId = "creatorcrash".equalsIgnoreCase(projectSlug) ? "Ccrash" : "C1"; + final @NotNull AtomicInteger counter = new AtomicInteger(1000); + return Flux.interval(Duration.ofSeconds(1)) + .map( + num -> { + int i = counter.incrementAndGet(); + if ("produceerror".equalsIgnoreCase(projectSlug) && i % 2 == 0) { + throw new RuntimeException("causing produce error for subscription"); + } + return new Task("T" + i, "A new task arrived ", assigneeId, creatorId); + }); + } + + public static class Task { + public String id; + public String name; + public String assigneeId; + public String creatorId; + + public Task( + final String id, final String name, final String assigneeId, final String creatorId) { + this.id = id; + this.name = name; + this.assigneeId = assigneeId; + this.creatorId = creatorId; + } + + @Override + public String toString() { + return "Task{id=" + id + "}"; + } + } + + public static class Assignee { + public String id; + public String name; + + public Assignee(final String id, final String name) { + this.id = id; + this.name = name; + } + } + + public static class Creator { + public String id; + public String name; + + public Creator(final String id, final String name) { + this.id = id; + this.name = name; + } + } + + public static class Project { + public String slug; + } + + public enum ProjectStatus { + ACTIVE, + COMMUNITY, + INCUBATING, + ATTIC, + EOL; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java new file mode 100644 index 00000000000..434a4822084 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java @@ -0,0 +1,50 @@ +package io.sentry.samples.spring.boot.jakarta.graphql; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletableFuture; +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.DataLoader; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.graphql.data.method.annotation.SchemaMapping; +import org.springframework.graphql.execution.BatchLoaderRegistry; +import org.springframework.stereotype.Controller; +import reactor.core.publisher.Mono; + +@Controller +class TaskCreatorController { + + public TaskCreatorController(final BatchLoaderRegistry batchLoaderRegistry) { + // using mapped BatchLoader to not have to deal with correct ordering of items + batchLoaderRegistry + .forTypePair(String.class, ProjectController.Creator.class) + .withOptions((builder) -> builder.setBatchingEnabled(true)) + .registerMappedBatchLoader( + (Set keys, BatchLoaderEnvironment env) -> { + return Mono.fromCallable( + () -> { + final @NotNull Map map = new HashMap<>(); + for (String key : keys) { + if ("Ccrash".equalsIgnoreCase(key)) { + throw new RuntimeException("Causing an error while loading creator"); + } + map.put(key, new ProjectController.Creator(key, "Name" + key)); + } + + return map; + }); + }); + } + + @SchemaMapping(typeName = "Task") + public @Nullable CompletableFuture creator( + final ProjectController.Task task, + final DataLoader dataLoader) { + if (task.creatorId == null) { + return null; + } + return dataLoader.load(task.creatorId); + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java new file mode 100644 index 00000000000..d0f0973c864 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java @@ -0,0 +1,19 @@ +package io.sentry.samples.spring.boot.jakarta.quartz; + +import org.quartz.Job; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.springframework.stereotype.Component; + +@Component +public class SampleJob implements Job { + + public void execute(JobExecutionContext context) throws JobExecutionException { + System.out.println("running job"); + try { + Thread.sleep(15000); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties new file mode 100644 index 00000000000..8e383764a43 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/application.properties @@ -0,0 +1,34 @@ +# NOTE: Replace the test DSN below with YOUR OWN DSN to see the events from this app in your Sentry project/dashboard +sentry.dsn=https://502f25099c204a2fbf4cb16edc5975d1@o447951.ingest.sentry.io/5428563 +sentry.send-default-pii=true +sentry.max-request-body-size=medium +# Sentry Spring Boot integration allows more fine-grained SentryOptions configuration +sentry.max-breadcrumbs=150 +# Logback integration configuration options +sentry.logging.minimum-event-level=info +sentry.logging.minimum-breadcrumb-level=debug +# Performance configuration +sentry.traces-sample-rate=1.0 +sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 +sentry.debug=true +sentry.graphql.ignored-error-types=SOME_ERROR,ANOTHER_ERROR +sentry.enable-backpressure-handling=true +sentry.enable-spotlight=true +sentry.enablePrettySerializationOutput=false +in-app-includes="io.sentry.samples" +sentry.logs.enabled=true + +# Uncomment and set to true to enable aot compatibility +# This flag disables all AOP related features (i.e. @SentryTransaction, @SentrySpan) +# to successfully compile to GraalVM +# sentry.enable-aot-compatibility=false + +# Database configuration +spring.datasource.url=jdbc:p6spy:hsqldb:mem:testdb +spring.datasource.driver-class-name=com.p6spy.engine.spy.P6SpyDriver +spring.datasource.username=sa +spring.datasource.password= +spring.graphql.graphiql.enabled=true +spring.graphql.websocket.path=/graphql +spring.quartz.job-store-type=memory + diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/graphql/schema.graphqls b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/graphql/schema.graphqls new file mode 100644 index 00000000000..aeea62357bd --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/graphql/schema.graphqls @@ -0,0 +1,68 @@ +type Query { + greeting(name: String! = "Spring"): String! + project(slug: ID!): Project + tasks(projectSlug: ID!): [Task] +} + +type Mutation { + addProject(slug: ID!): String! +} + +type Subscription { + notifyNewTask(projectSlug: ID!): Task +} + +""" A Project in the Spring portfolio """ +type Project { + """ Unique string id used in URLs """ + slug: ID! + """ Project name """ + name: String + """ Current support status """ + status: ProjectStatus! +} + +""" A task """ +type Task { + """ ID """ + id: String! + """ Name """ + name: String! + """ ID of the Assignee """ + assigneeId: String + """ Assignee """ + assignee: Assignee + """ ID of the Creator """ + creatorId: String + """ Creator """ + creator: Creator +} + +""" An Assignee """ +type Assignee { + """ ID """ + id: String! + """ Name """ + name: String! +} + +""" An Creator """ +type Creator { + """ ID """ + id: String! + """ Name """ + name: String! +} + +enum ProjectStatus { + """ Actively supported by the Spring team """ + ACTIVE + """ Supported by the community """ + COMMUNITY + """ Prototype, not officially supported yet """ + INCUBATING + """ Project being retired, in maintenance mode """ + ATTIC + """ End-Of-Lifed """ + EOL +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/quartz.properties b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/quartz.properties new file mode 100644 index 00000000000..6e302ce765a --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/quartz.properties @@ -0,0 +1 @@ +org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/schema.sql b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/schema.sql new file mode 100644 index 00000000000..7ca8a5cbf42 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/resources/schema.sql @@ -0,0 +1,5 @@ +CREATE TABLE person ( + id INTEGER IDENTITY PRIMARY KEY, + firstName VARCHAR(50) NOT NULL, + lastName VARCHAR(50) NOT NULL +); diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/DummyTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/DummyTest.kt new file mode 100644 index 00000000000..6f762b7e453 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/DummyTest.kt @@ -0,0 +1,12 @@ +package io.sentry + +import kotlin.test.Test +import kotlin.test.assertTrue + +class DummyTest { + @Test + fun `the only test`() { + // only needed to have more than 0 tests and not fail the build + assertTrue(true) + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt new file mode 100644 index 00000000000..3cd16003024 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/DistributedTracingSystemTest.kt @@ -0,0 +1,197 @@ +package io.sentry.systemtest + +import io.sentry.protocol.SentryId +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals +import org.junit.Before + +class DistributedTracingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } + + @Test + fun `get person distributed tracing with sampled false`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-0", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=false,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /tracing/{id}" + } + + testHelper.ensureNoTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "GET /person/{id}" + } + } + + @Test + fun `get person distributed tracing without sample_rand`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRand1: String? = null + var sampleRand2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand1 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRand2 = envelopeHeader.traceContext?.sampleRand + } + + matches + } + + assertEquals(sampleRand1, sampleRand2) + } + + @Test + fun `get person distributed tracing updates sample_rate on deferred decision`() { + val traceId = SentryId() + val restClient = testHelper.restClient + restClient.getPersonDistributedTracing( + 1L, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=0.5,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(500, restClient.lastKnownStatusCode) + + var sampleRate1: String? = null + var sampleRate2: String? = null + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /tracing/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate1 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + val matches = + transaction.transaction == "GET /person/{id}" && + envelopeHeader.traceContext!!.traceId == traceId && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + + if (matches) { + testHelper.logObject(envelopeHeader) + testHelper.logObject(transaction) + sampleRate2 = envelopeHeader.traceContext?.sampleRate + } + + matches + } + + assertEquals(sampleRate1, sampleRate2) + assertNotEquals(sampleRate1, "0.5") + } + + @Test + fun `create person distributed tracing`() { + val traceId = SentryId() + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = + restClient.createPersonDistributedTracing( + person, + mapOf( + "sentry-trace" to "$traceId-424cffc8f94feeee-1", + "baggage" to + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=$traceId,sentry-transaction=HTTP%20GET", + ), + ) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /tracing/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + transaction.transaction == "POST /person/" && + testHelper.doesTransactionHaveTraceId(transaction, traceId.toString()) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt new file mode 100644 index 00000000000..76a6024decc --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlGreetingSystemTest.kt @@ -0,0 +1,46 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import org.junit.Before + +class GraphqlGreetingSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `greeting works`() { + val response = testHelper.graphqlClient.greet("world") + + testHelper.ensureNoErrors(response) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.greeting", + ) + } + } + + @Test + fun `greeting error`() { + val response = testHelper.graphqlClient.greet("crash") + + testHelper.ensureErrorCount(response, 1) + testHelper.ensureErrorReceived { error -> + error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.greeting", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt new file mode 100644 index 00000000000..fca3956717c --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlProjectSystemTest.kt @@ -0,0 +1,66 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.junit.Before + +class GraphqlProjectSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `project query works`() { + val response = testHelper.graphqlClient.project("proj-slug") + + testHelper.ensureNoErrors(response) + assertEquals("proj-slug", response?.data?.project?.slug) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.project", + ) + } + } + + @Test + fun `project mutation works`() { + val response = testHelper.graphqlClient.addProject("proj-slug") + + testHelper.ensureNoErrors(response) + assertNotNull(response?.data?.addProject) + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Mutation.addProject", + ) + } + } + + @Test + fun `project mutation error`() { + val response = testHelper.graphqlClient.addProject("addprojectcrash") + + testHelper.ensureErrorCount(response, 1) + assertNull(response?.data?.addProject) + testHelper.ensureErrorReceived { error -> + error.message?.message?.startsWith("Unresolved RuntimeException for executionId ") ?: false + } + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Mutation.addProject", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt new file mode 100644 index 00000000000..940709c0778 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/GraphqlTaskSystemTest.kt @@ -0,0 +1,50 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class GraphqlTaskSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `tasks and assignees query works`() { + val response = testHelper.graphqlClient.tasksAndAssignees("project-slug") + + testHelper.ensureNoErrors(response) + + assertEquals(5, response?.data?.tasks?.size) + + val firstTask = response?.data?.tasks?.firstOrNull() ?: throw RuntimeException("no task") + assertEquals("T1", firstTask.id) + assertEquals("A3", firstTask.assigneeId) + assertEquals("A3", firstTask.assignee?.id) + assertEquals("C3", firstTask.creatorId) + assertEquals("C3", firstTask.creator?.id) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Query.tasks", + ) && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Task.assignee", + ) && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "graphql", + "Task.creator", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt new file mode 100644 index 00000000000..7d6e0182530 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/PersonSystemTest.kt @@ -0,0 +1,55 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class PersonSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get person fails`() { + val restClient = testHelper.restClient + restClient.getPerson(1L) + assertEquals(500, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionHaveOp(transaction, "http.server") + } + + Thread.sleep(10000) + + testHelper.ensureLogsReceived { logs, envelopeHeader -> + testHelper.doesContainLogWithBody(logs, "warn Sentry logging") && + testHelper.doesContainLogWithBody(logs, "error Sentry logging") && + testHelper.doesContainLogWithBody(logs, "hello there world!") + } + } + + @Test + fun `create person works`() { + val restClient = testHelper.restClient + val person = Person("firstA", "lastB") + val returnedPerson = restClient.createPerson(person) + assertEquals(200, restClient.lastKnownStatusCode) + + assertEquals(person.firstName, returnedPerson!!.firstName) + assertEquals(person.lastName, returnedPerson!!.lastName) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOp(transaction, "PersonService.create") && + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "db.query", + "insert into person (firstName, lastName) values (?, ?)", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt new file mode 100644 index 00000000000..d34485e1388 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/kotlin/io/sentry/systemtest/TodoSystemTest.kt @@ -0,0 +1,61 @@ +package io.sentry.systemtest + +import io.sentry.systemtest.util.TestHelper +import kotlin.test.Test +import kotlin.test.assertEquals +import org.junit.Before + +class TodoSystemTest { + lateinit var testHelper: TestHelper + + @Before + fun setup() { + testHelper = TestHelper("http://localhost:8080") + testHelper.reset() + } + + @Test + fun `get todo works`() { + val restClient = testHelper.restClient + restClient.getTodo(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } + + @Test + fun `get todo webclient works`() { + val restClient = testHelper.restClient + restClient.getTodoWebclient(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } + + @Test + fun `get todo restclient works`() { + val restClient = testHelper.restClient + restClient.getTodoRestClient(1L) + assertEquals(200, restClient.lastKnownStatusCode) + + testHelper.ensureTransactionReceived { transaction, envelopeHeader -> + testHelper.doesTransactionContainSpanWithOpAndDescription( + transaction, + "http.client", + "GET https://jsonplaceholder.typicode.com/todos/1", + ) + } + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/test/resources/logback.xml b/sentry-samples/sentry-samples-spring-boot-4/src/test/resources/logback.xml new file mode 100644 index 00000000000..a36b8f80f76 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/test/resources/logback.xml @@ -0,0 +1,17 @@ + + + + + + + + + + %-4relative [%thread] %-5level %logger{35} -%kvp- %msg %n + + + + + + + diff --git a/sentry-spring-7/api/sentry-spring-jakarta.api b/sentry-spring-7/api/sentry-spring-jakarta.api new file mode 100644 index 00000000000..3c1db200cbb --- /dev/null +++ b/sentry-spring-7/api/sentry-spring-jakarta.api @@ -0,0 +1,363 @@ +public final class io/sentry/spring/jakarta/BuildConfig { + public static final field SENTRY_SPRING_JAKARTA_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/spring/jakarta/ContextTagsEventProcessor : io/sentry/EventProcessor { + public fun (Lio/sentry/SentryOptions;)V + public fun getOrder ()Ljava/lang/Long; + public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; +} + +public abstract interface annotation class io/sentry/spring/jakarta/EnableSentry : java/lang/annotation/Annotation { + public abstract fun dsn ()Ljava/lang/String; + public abstract fun exceptionResolverOrder ()I + public abstract fun maxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; + public abstract fun sendDefaultPii ()Z +} + +public final class io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider : io/sentry/spring/jakarta/SentryUserProvider { + public fun (Lio/sentry/SentryOptions;)V + public fun provideUser ()Lio/sentry/protocol/User; +} + +public class io/sentry/spring/jakarta/SentryExceptionResolver : org/springframework/core/Ordered, org/springframework/web/servlet/HandlerExceptionResolver { + public static final field MECHANISM_TYPE Ljava/lang/String; + public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;I)V + protected fun createEvent (Ljakarta/servlet/http/HttpServletRequest;Ljava/lang/Exception;)Lio/sentry/SentryEvent; + protected fun createHint (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;)Lio/sentry/Hint; + public fun getOrder ()I + public fun resolveException (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljava/lang/Object;Ljava/lang/Exception;)Lorg/springframework/web/servlet/ModelAndView; +} + +public class io/sentry/spring/jakarta/SentryHubRegistrar : org/springframework/context/annotation/ImportBeanDefinitionRegistrar { + public fun ()V + public fun registerBeanDefinitions (Lorg/springframework/core/type/AnnotationMetadata;Lorg/springframework/beans/factory/support/BeanDefinitionRegistry;)V +} + +public class io/sentry/spring/jakarta/SentryInitBeanPostProcessor : org/springframework/beans/factory/DisposableBean, org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/context/ApplicationContextAware { + public fun ()V + public fun destroy ()V + public fun postProcessAfterInitialization (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; + public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V +} + +public class io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor { + public fun (Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V + public fun getOrder ()Ljava/lang/Long; + public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; +} + +public class io/sentry/spring/jakarta/SentryRequestResolver { + protected static final field staticLock Lio/sentry/util/AutoClosableReentrantLock; + public fun (Lio/sentry/IScopes;)V + public fun resolveSentryRequest (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/protocol/Request; +} + +public class io/sentry/spring/jakarta/SentrySpringFilter : org/springframework/web/filter/OncePerRequestFilter { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/SentryRequestResolver;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;)V + protected fun doFilterInternal (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljakarta/servlet/FilterChain;)V +} + +public class io/sentry/spring/jakarta/SentrySpringServletContainerInitializer : jakarta/servlet/ServletContainerInitializer { + public fun ()V + public fun onStartup (Ljava/util/Set;Ljakarta/servlet/ServletContext;)V +} + +public final class io/sentry/spring/jakarta/SentryTaskDecorator : org/springframework/core/task/TaskDecorator { + public fun ()V + public fun decorate (Ljava/lang/Runnable;)Ljava/lang/Runnable; +} + +public class io/sentry/spring/jakarta/SentryUserFilter : org/springframework/web/filter/OncePerRequestFilter { + public fun (Lio/sentry/IScopes;Ljava/util/List;)V + protected fun doFilterInternal (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljakarta/servlet/FilterChain;)V + public fun getSentryUserProviders ()Ljava/util/List; +} + +public abstract interface class io/sentry/spring/jakarta/SentryUserProvider { + public abstract fun provideUser ()Lio/sentry/protocol/User; +} + +public class io/sentry/spring/jakarta/SentryWebConfiguration { + public fun ()V + public fun httpServletRequestSentryUserProvider (Lio/sentry/SentryOptions;)Lio/sentry/spring/jakarta/HttpServletRequestSentryUserProvider; +} + +public final class io/sentry/spring/jakarta/SpringProfilesEventProcessor : io/sentry/EventProcessor { + public fun (Lorg/springframework/core/env/Environment;)V + public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; + public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent; + public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; +} + +public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : io/sentry/spring/jakarta/SentryUserProvider { + public fun (Lio/sentry/SentryOptions;)V + public fun provideUser ()Lio/sentry/protocol/User; +} + +public abstract interface annotation class io/sentry/spring/jakarta/checkin/SentryCheckIn : java/lang/annotation/Annotation { + public abstract fun heartbeat ()Z + public abstract fun monitorSlug ()Ljava/lang/String; + public abstract fun value ()Ljava/lang/String; +} + +public class io/sentry/spring/jakarta/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor, org/springframework/context/EmbeddedValueResolverAware { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; + public fun setEmbeddedValueResolver (Lorg/springframework/util/StringValueResolver;)V +} + +public class io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration { + public fun ()V + public fun sentryCheckInAdvice ()Lorg/aopalliance/aop/Advice; + public fun sentryCheckInAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; +} + +public class io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration { + public fun ()V + public fun sentryCheckInPointcut ()Lorg/springframework/aop/Pointcut; +} + +public class io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration { + public fun ()V + public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; +} + +public final class io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer { + public fun ()V + 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 ()V + public fun (Lio/sentry/IScopes;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration { + public fun ()V +} + +public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration { + public fun ()V + public fun sentryCaptureExceptionParameterPointcut ()Lorg/springframework/aop/Pointcut; +} + +public class io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration { + public fun ()V + public fun sentryCaptureExceptionParameterAdvice ()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; + public fun registerDataLoaders (Lorg/dataloader/DataLoaderRegistry;Lgraphql/GraphQLContext;)V +} + +public final class io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry$SentryRegistrationSpec : org/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec { + public fun (Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec;Ljava/lang/Class;Ljava/lang/Class;)V + public fun (Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec;Ljava/lang/String;)V + public fun registerBatchLoader (Ljava/util/function/BiFunction;)V + public fun registerMappedBatchLoader (Ljava/util/function/BiFunction;)V + public fun withName (Ljava/lang/String;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; + public fun withOptions (Ljava/util/function/Consumer;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; + public fun withOptions (Lorg/dataloader/DataLoaderOptions;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; +} + +public final class io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter : org/springframework/graphql/execution/DataFetcherExceptionResolverAdapter { + public fun ()V + public fun isThreadLocalContextAware ()Z +} + +public final class io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { + public fun ()V + public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration { + public fun ()V + public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; + public fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; + public fun sentryInstrumentationWebMvc (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; + public fun sentryInstrumentationWebflux (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; +} + +public final class io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor : org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/core/PriorityOrdered { + public fun ()V + public fun getOrder ()I + public fun postProcessAfterInitialization (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration { + public fun ()V + public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; + public fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; + public fun sentryInstrumentationWebMvc (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; + public fun sentryInstrumentationWebflux (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; +} + +public final class io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { + public fun ()V + public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration { + public fun ()V + public fun sentryOpenTelemetryOptionsConfiguration ()Lio/sentry/Sentry$OptionsConfiguration; +} + +public class io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration { + public fun ()V + public static fun openTelemetrySpanFactory (Lio/opentelemetry/api/OpenTelemetry;)Lio/sentry/ISpanFactory; + public fun sentryOpenTelemetryOptionsConfiguration ()Lio/sentry/Sentry$OptionsConfiguration; +} + +public final class io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider { + public fun (Ljava/util/List;)V + public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; + public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring/jakarta/tracing/TransactionNameWithSource; + public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; +} + +public class io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration { + public fun ()V + public fun sentrySpanAdvice ()Lorg/aopalliance/aop/Advice; + public fun sentrySpanAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; + public fun sentryTransactionAdvice ()Lorg/aopalliance/aop/Advice; + public fun sentryTransactionAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; +} + +public abstract interface annotation class io/sentry/spring/jakarta/tracing/SentrySpan : java/lang/annotation/Annotation { + public abstract fun description ()Ljava/lang/String; + public abstract fun operation ()Ljava/lang/String; + public abstract fun value ()Ljava/lang/String; +} + +public class io/sentry/spring/jakarta/tracing/SentrySpanAdvice : org/aopalliance/intercept/MethodInterceptor { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor : org/springframework/http/client/ClientHttpRequestInterceptor { + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Z)V + public fun intercept (Lorg/springframework/http/HttpRequest;[BLorg/springframework/http/client/ClientHttpRequestExecution;)Lorg/springframework/http/client/ClientHttpResponse; +} + +public class io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter : org/springframework/web/reactive/function/client/ExchangeFilterFunction { + public fun (Lio/sentry/IScopes;)V + public fun filter (Lorg/springframework/web/reactive/function/client/ClientRequest;Lorg/springframework/web/reactive/function/client/ExchangeFunction;)Lreactor/core/publisher/Mono; +} + +public class io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration { + public fun ()V + public fun sentrySpanPointcut ()Lorg/springframework/aop/Pointcut; +} + +public class io/sentry/spring/jakarta/tracing/SentryTracingConfiguration { + public fun ()V +} + +public class io/sentry/spring/jakarta/tracing/SentryTracingFilter : org/springframework/web/filter/OncePerRequestFilter { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;)V + public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;Z)V + protected fun doFilterInternal (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljakarta/servlet/FilterChain;)V + protected fun shouldNotFilterAsyncDispatch ()Z +} + +public abstract interface annotation class io/sentry/spring/jakarta/tracing/SentryTransaction : java/lang/annotation/Annotation { + public abstract fun name ()Ljava/lang/String; + public abstract fun operation ()Ljava/lang/String; + public abstract fun value ()Ljava/lang/String; +} + +public class io/sentry/spring/jakarta/tracing/SentryTransactionAdvice : org/aopalliance/intercept/MethodInterceptor { + public fun ()V + public fun (Lio/sentry/IScopes;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration { + public fun ()V + public fun sentryTransactionPointcut ()Lorg/springframework/aop/Pointcut; +} + +public final class io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider { + public fun ()V + public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; + public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; +} + +public final class io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider { + public fun ()V + public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; + public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; +} + +public abstract interface class io/sentry/spring/jakarta/tracing/TransactionNameProvider { + public abstract fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; + public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring/jakarta/tracing/TransactionNameWithSource; + public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; +} + +public final class io/sentry/spring/jakarta/tracing/TransactionNameWithSource { + public fun (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V + public fun getTransactionName ()Ljava/lang/String; + public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; +} + +public abstract class io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter : org/springframework/web/server/WebFilter { + public static final field SENTRY_HUB_KEY Ljava/lang/String; + public static final field SENTRY_SCOPES_KEY Ljava/lang/String; + public fun (Lio/sentry/IScopes;)V + protected fun doFinally (Lorg/springframework/web/server/ServerWebExchange;Lio/sentry/IScopes;Lio/sentry/ITransaction;)V + protected fun doFirst (Lorg/springframework/web/server/ServerWebExchange;Lio/sentry/IScopes;)V + protected fun doOnError (Lio/sentry/ITransaction;Ljava/lang/Throwable;)V + protected fun maybeStartTransaction (Lio/sentry/IScopes;Lorg/springframework/http/server/reactive/ServerHttpRequest;Ljava/lang/String;)Lio/sentry/ITransaction; + protected fun shouldTraceRequest (Lio/sentry/IScopes;Lorg/springframework/http/server/reactive/ServerHttpRequest;)Z + protected fun startTransaction (Lio/sentry/IScopes;Lorg/springframework/http/server/reactive/ServerHttpRequest;Lio/sentry/TransactionContext;Ljava/lang/String;)Lio/sentry/ITransaction; +} + +public class io/sentry/spring/jakarta/webflux/SentryRequestResolver { + public fun (Lio/sentry/IScopes;)V + public fun resolveSentryRequest (Lorg/springframework/http/server/reactive/ServerHttpRequest;)Lio/sentry/protocol/Request; +} + +public final class io/sentry/spring/jakarta/webflux/SentryScheduleHook : java/util/function/Function { + public fun ()V + public synthetic fun apply (Ljava/lang/Object;)Ljava/lang/Object; + public fun apply (Ljava/lang/Runnable;)Ljava/lang/Runnable; +} + +public final class io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler : org/springframework/web/server/WebExceptionHandler { + public static final field MECHANISM_TYPE Ljava/lang/String; + public fun (Lio/sentry/IScopes;)V + public fun handle (Lorg/springframework/web/server/ServerWebExchange;Ljava/lang/Throwable;)Lreactor/core/publisher/Mono; +} + +public class io/sentry/spring/jakarta/webflux/SentryWebFilter : io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter { + public fun (Lio/sentry/IScopes;)V + public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono; +} + +public final class io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor : io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter { + public static final field TRACE_ORIGIN Ljava/lang/String; + public fun (Lio/sentry/IScopes;)V + public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono; +} + +public final class io/sentry/spring/jakarta/webflux/reactor/ReactorUtils : io/sentry/reactor/SentryReactorUtils { + public fun ()V +} + diff --git a/sentry-spring-7/build.gradle.kts b/sentry-spring-7/build.gradle.kts new file mode 100644 index 00000000000..483549330a7 --- /dev/null +++ b/sentry-spring-7/build.gradle.kts @@ -0,0 +1,127 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.springframework.boot.gradle.plugin.SpringBootPlugin + +plugins { + `java-library` + id("io.sentry.javadoc") + kotlin("jvm") + jacoco + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.buildconfig) + alias(libs.plugins.springboot4) apply false +} + +configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions { + jvmTarget = JavaVersion.VERSION_17.toString() + languageVersion = libs.versions.kotlin.compatible.version.get() + freeCompilerArgs = listOf("-Xjsr305=strict") + } +} + +dependencies { + api(projects.sentry) + compileOnly(platform(SpringBootPlugin.BOM_COORDINATES)) + compileOnly(Config.Libs.springWeb) + compileOnly(Config.Libs.springAop) + compileOnly(Config.Libs.springSecurityWeb) + compileOnly(Config.Libs.aspectj) + compileOnly(libs.context.propagation) + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + compileOnly(libs.otel) + compileOnly(libs.servlet.jakarta.api) + compileOnly(libs.slf4j.api) + compileOnly(libs.springboot4.starter.graphql) + compileOnly(libs.springboot4.starter.quartz) + + compileOnly(Config.Libs.springWebflux) + compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryGraphql22) + compileOnly(projects.sentryQuartz) + compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) + compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) + api(projects.sentryReactor) + + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) + + // tests + testImplementation(projects.sentryTestSupport) + testImplementation(projects.sentryGraphql) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(libs.awaitility.kotlin) + testImplementation(libs.context.propagation) + testImplementation(libs.graphql.java24) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.mockito.inline) + testImplementation(libs.springboot4.starter.aop) + testImplementation(libs.springboot4.starter.graphql) + testImplementation(libs.springboot4.starter.security) + testImplementation(libs.springboot4.starter.test) + testImplementation(libs.springboot4.starter.web) + testImplementation(libs.springboot4.starter.webflux) + testImplementation(projects.sentryReactor) +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.spring7") + buildConfigField( + "String", + "SENTRY_SPRING_7_SDK_NAME", + "\"${Config.Sentry.SENTRY_SPRING_7_SDK_NAME}\"", + ) + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +tasks.withType().configureEach { + dependsOn(tasks.generateBuildConfig) + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_SPRING_7_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-spring-7", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/ContextTagsEventProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/ContextTagsEventProcessor.java new file mode 100644 index 00000000000..94f49d83190 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/ContextTagsEventProcessor.java @@ -0,0 +1,46 @@ +package io.sentry.spring.jakarta; + +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.SentryEvent; +import io.sentry.SentryOptions; +import io.sentry.util.CollectionUtils; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.MDC; + +/** + * Attaches context tags defined in {@link SentryOptions#getContextTags()} from {@link MDC} to + * {@link SentryEvent#getTags()}. + */ +public final class ContextTagsEventProcessor implements EventProcessor { + private final SentryOptions options; + + public ContextTagsEventProcessor(final @NotNull SentryOptions options) { + this.options = options; + } + + @Override + public @NotNull SentryEvent process(@NotNull SentryEvent event, @Nullable Hint hint) { + final Map contextMap = MDC.getCopyOfContextMap(); + if (contextMap != null) { + final Map mdcProperties = + CollectionUtils.filterMapEntries(contextMap, entry -> entry.getValue() != null); + if (!mdcProperties.isEmpty() && !options.getContextTags().isEmpty()) { + for (final String contextTag : options.getContextTags()) { + // if mdc tag is listed in SentryOptions, apply as event tag + if (mdcProperties.containsKey(contextTag)) { + event.setTag(contextTag, mdcProperties.get(contextTag)); + } + } + } + } + return event; + } + + @Override + public @Nullable Long getOrder() { + return 14000L; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/EnableSentry.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/EnableSentry.java new file mode 100644 index 00000000000..e8cd91f3f44 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/EnableSentry.java @@ -0,0 +1,52 @@ +package io.sentry.spring.jakarta; + +import io.sentry.SentryOptions; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.context.annotation.Import; + +/** + * Enables Sentry error handling capabilities. + * + *
    + *
  • creates bean of type {@link io.sentry.SentryOptions} + *
  • registers {@link io.sentry.IScopes} for sending Sentry events + *
  • registers {@link SentryExceptionResolver} to send Sentry event for any uncaught exception + * in Spring MVC flow. + *
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Import({SentryHubRegistrar.class, SentryInitBeanPostProcessor.class, SentryWebConfiguration.class}) +@Target(ElementType.TYPE) +public @interface EnableSentry { + + /** + * The DSN tells the SDK where to send the events to. If this value is not provided, the SDK will + * just not send any events. + * + * @return the Sentry DSN + */ + String dsn() default ""; + + /** + * Whether to send personal identifiable information along with events. + * + * @return true if send default PII or false otherwise. + */ + boolean sendDefaultPii() default false; + + /** + * Determines whether all web exceptions are reported or only uncaught exceptions. + * + * @return the order to use for {@link SentryExceptionResolver} + */ + int exceptionResolverOrder() default 1; + + /** + * Controls the size of the request body to extract if any. No truncation is done by the SDK. If + * the request body is larger than the accepted size, nothing is sent. + */ + SentryOptions.RequestSize maxRequestBodySize() default SentryOptions.RequestSize.NONE; +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider.java new file mode 100644 index 00000000000..6174da0dc5f --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider.java @@ -0,0 +1,55 @@ +package io.sentry.spring.jakarta; + +import io.sentry.SentryOptions; +import io.sentry.protocol.User; +import io.sentry.util.Objects; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; + +/** + * Resolves user information from {@link HttpServletRequest} obtained via {@link + * RequestContextHolder}. + */ +public final class HttpServletRequestSentryUserProvider implements SentryUserProvider { + private final @NotNull SentryOptions options; + + public HttpServletRequestSentryUserProvider(final @NotNull SentryOptions options) { + this.options = Objects.requireNonNull(options, "options are required"); + } + + @Override + public @Nullable User provideUser() { + if (options.isSendDefaultPii()) { + final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes(); + if (requestAttributes instanceof ServletRequestAttributes) { + final ServletRequestAttributes servletRequestAttributes = + (ServletRequestAttributes) requestAttributes; + final HttpServletRequest request = servletRequestAttributes.getRequest(); + + final User user = new User(); + user.setIpAddress(toIpAddress(request)); + if (request.getUserPrincipal() != null) { + user.setUsername(request.getUserPrincipal().getName()); + } + return user; + } + } + return null; + } + + // it is advised to not use `String#split` method but since we do not have 3rd party libraries + // this is our only option. + @SuppressWarnings("StringSplitter") + private static @NotNull String toIpAddress(final @NotNull HttpServletRequest request) { + final String ipAddress = request.getHeader("X-FORWARDED-FOR"); + if (ipAddress != null) { + return ipAddress.contains(",") ? ipAddress.split(",")[0].trim() : ipAddress; + } else { + return request.getRemoteAddr(); + } + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/RequestPayloadExtractor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/RequestPayloadExtractor.java new file mode 100644 index 00000000000..2eb41462f8a --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/RequestPayloadExtractor.java @@ -0,0 +1,34 @@ +package io.sentry.spring.jakarta; + +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import jakarta.servlet.http.HttpServletRequest; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.util.StreamUtils; +import org.springframework.web.util.ContentCachingRequestWrapper; + +final class RequestPayloadExtractor { + + @Nullable + String extract(final @NotNull HttpServletRequest request, final @NotNull SentryOptions options) { + // request body can be read only once from the stream + // original request can be replaced with ContentCachingRequestWrapper in SentrySpringFilter + if (request instanceof ContentCachingRequestWrapper cachedRequest) { + try { + final byte[] body = + cachedRequest.getInputStream().isFinished() + ? cachedRequest.getContentAsByteArray() + : StreamUtils.copyToByteArray(cachedRequest.getInputStream()); + return new String(body, StandardCharsets.UTF_8); + } catch (IOException e) { + options.getLogger().log(SentryLevel.ERROR, "Failed to set request body", e); + return null; + } + } else { + return null; + } + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryExceptionResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryExceptionResolver.java new file mode 100644 index 00000000000..efa98ef581b --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryExceptionResolver.java @@ -0,0 +1,93 @@ +package io.sentry.spring.jakarta; + +import static io.sentry.TypeCheckHint.SPRING_RESOLVER_REQUEST; +import static io.sentry.TypeCheckHint.SPRING_RESOLVER_RESPONSE; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.Hint; +import io.sentry.IScopes; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.exception.ExceptionMechanismException; +import io.sentry.protocol.Mechanism; +import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.util.Objects; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.core.Ordered; +import org.springframework.web.servlet.HandlerExceptionResolver; +import org.springframework.web.servlet.ModelAndView; + +/** + * {@link HandlerExceptionResolver} implementation that will record any exception that a Spring + * {@link org.springframework.web.servlet.mvc.Controller} throws to Sentry. It then returns null, + * which will let the other (default or custom) exception resolvers handle the actual error. + */ +@Open +public class SentryExceptionResolver implements HandlerExceptionResolver, Ordered { + public static final String MECHANISM_TYPE = "Spring6ExceptionResolver"; + + private final @NotNull IScopes scopes; + private final @NotNull TransactionNameProvider transactionNameProvider; + private final int order; + + public SentryExceptionResolver( + final @NotNull IScopes scopes, + final @NotNull TransactionNameProvider transactionNameProvider, + final int order) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); + this.transactionNameProvider = + Objects.requireNonNull(transactionNameProvider, "transactionNameProvider is required"); + this.order = order; + } + + @Override + public @Nullable ModelAndView resolveException( + final @NotNull HttpServletRequest request, + final @NotNull HttpServletResponse response, + final @Nullable Object handler, + final @NotNull Exception ex) { + + final SentryEvent event = createEvent(request, ex); + final Hint hint = createHint(request, response); + + scopes.captureEvent(event, hint); + + // null = run other HandlerExceptionResolvers to actually handle the exception + return null; + } + + @Override + public int getOrder() { + return order; + } + + @NotNull + protected SentryEvent createEvent( + final @NotNull HttpServletRequest request, final @NotNull Exception ex) { + + final Mechanism mechanism = new Mechanism(); + mechanism.setHandled(false); + mechanism.setType(MECHANISM_TYPE); + final Throwable throwable = + new ExceptionMechanismException(mechanism, ex, Thread.currentThread()); + final SentryEvent event = new SentryEvent(throwable); + event.setLevel(SentryLevel.FATAL); + event.setTransaction(transactionNameProvider.provideTransactionName(request)); + + return event; + } + + @Nullable + protected Hint createHint( + final @NotNull HttpServletRequest request, final @NotNull HttpServletResponse response) { + + final Hint hint = new Hint(); + hint.set(SPRING_RESOLVER_REQUEST, request); + hint.set(SPRING_RESOLVER_RESPONSE, response); + + return hint; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java new file mode 100644 index 00000000000..cca74ae2a2d --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java @@ -0,0 +1,103 @@ +package io.sentry.spring.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.InitPriority; +import io.sentry.ScopesAdapter; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryOptions; +import io.sentry.protocol.SdkVersion; +import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.ImportBeanDefinitionRegistrar; +import org.springframework.core.annotation.AnnotationAttributes; +import org.springframework.core.type.AnnotationMetadata; + +/** Registers beans required to use Sentry core features. */ +@Configuration +@Open +public class SentryHubRegistrar implements ImportBeanDefinitionRegistrar { + + static { + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-spring-jakarta", BuildConfig.VERSION_NAME); + } + + @Override + public void registerBeanDefinitions( + final @NotNull AnnotationMetadata importingClassMetadata, + final @NotNull BeanDefinitionRegistry registry) { + final AnnotationAttributes annotationAttributes = + AnnotationAttributes.fromMap( + importingClassMetadata.getAnnotationAttributes(EnableSentry.class.getName())); + if (annotationAttributes != null && annotationAttributes.containsKey("dsn")) { + registerSentryOptions(registry, annotationAttributes); + registerSentryHubBean(registry); + registerSentryExceptionResolver(registry, annotationAttributes); + } + } + + private void registerSentryOptions( + final @NotNull BeanDefinitionRegistry registry, + final @NotNull AnnotationAttributes annotationAttributes) { + final BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(SentryOptions.class); + + if (registry.containsBeanDefinition("mockTransportFactory")) { + builder.addPropertyReference("transportFactory", "mockTransportFactory"); + } + builder.addPropertyValue("dsn", annotationAttributes.getString("dsn")); + builder.addPropertyValue("enableExternalConfiguration", true); + builder.addPropertyValue("sentryClientName", BuildConfig.SENTRY_SPRING_JAKARTA_SDK_NAME); + builder.addPropertyValue("sdkVersion", createSdkVersion()); + builder.addPropertyValue("initPriority", InitPriority.LOW); + addPackageAndIntegrationInfo(); + if (annotationAttributes.containsKey("sendDefaultPii")) { + builder.addPropertyValue("sendDefaultPii", annotationAttributes.getBoolean("sendDefaultPii")); + } + if (annotationAttributes.containsKey("maxRequestBodySize")) { + builder.addPropertyValue( + "maxRequestBodySize", annotationAttributes.get("maxRequestBodySize")); + } + + registry.registerBeanDefinition("sentryOptions", builder.getBeanDefinition()); + } + + private void registerSentryHubBean(final @NotNull BeanDefinitionRegistry registry) { + final BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(ScopesAdapter.class); + builder.setInitMethodName("getInstance"); + + registry.registerBeanDefinition("sentryHub", builder.getBeanDefinition()); + } + + private void registerSentryExceptionResolver( + final @NotNull BeanDefinitionRegistry registry, + final @NotNull AnnotationAttributes annotationAttributes) { + final BeanDefinitionBuilder builder = + BeanDefinitionBuilder.genericBeanDefinition(SentryExceptionResolver.class); + builder.addConstructorArgReference("sentryHub"); + builder.addConstructorArgValue(new SpringMvcTransactionNameProvider()); + int order = annotationAttributes.getNumber("exceptionResolverOrder"); + builder.addConstructorArgValue(order); + + registry.registerBeanDefinition("sentryExceptionResolver", builder.getBeanDefinition()); + } + + private static @NotNull SdkVersion createSdkVersion() { + final SentryOptions defaultOptions = new SentryOptions(); + SdkVersion sdkVersion = defaultOptions.getSdkVersion(); + + final String name = BuildConfig.SENTRY_SPRING_JAKARTA_SDK_NAME; + final String version = BuildConfig.VERSION_NAME; + sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version); + + return sdkVersion; + } + + private static void addPackageAndIntegrationInfo() { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6"); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java new file mode 100644 index 00000000000..d33dfca8d8a --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java @@ -0,0 +1,91 @@ +package io.sentry.spring.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.EventProcessor; +import io.sentry.IScopes; +import io.sentry.ITransportFactory; +import io.sentry.Integration; +import io.sentry.ScopesAdapter; +import io.sentry.Sentry; +import io.sentry.SentryOptions; +import io.sentry.SentryOptions.TracesSamplerCallback; +import io.sentry.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Initializes Sentry after all beans are registered. Closes Sentry on Spring application context + * destroy. + */ +@Open +public class SentryInitBeanPostProcessor + implements BeanPostProcessor, ApplicationContextAware, DisposableBean { + private @Nullable ApplicationContext applicationContext; + + private final @NotNull IScopes scopes; + + public SentryInitBeanPostProcessor() { + this(ScopesAdapter.getInstance()); + } + + SentryInitBeanPostProcessor(final @NotNull IScopes scopes) { + Objects.requireNonNull(scopes, "Scopes are required"); + this.scopes = scopes; + } + + @Override + @SuppressWarnings({"unchecked", "deprecation"}) + public @NotNull Object postProcessAfterInitialization( + final @NotNull Object bean, @NotNull final String beanName) throws BeansException { + if (bean instanceof SentryOptions) { + final SentryOptions options = (SentryOptions) bean; + + if (applicationContext != null) { + applicationContext + .getBeanProvider(TracesSamplerCallback.class) + .ifAvailable(options::setTracesSampler); + applicationContext + .getBeanProvider(ITransportFactory.class) + .ifAvailable(options::setTransportFactory); + applicationContext + .getBeanProvider(SentryOptions.BeforeSendCallback.class) + .ifAvailable(options::setBeforeSend); + applicationContext + .getBeanProvider(SentryOptions.BeforeSendTransactionCallback.class) + .ifAvailable(options::setBeforeSendTransaction); + applicationContext + .getBeanProvider(SentryOptions.BeforeBreadcrumbCallback.class) + .ifAvailable(options::setBeforeBreadcrumb); + applicationContext + .getBeansOfType(EventProcessor.class) + .values() + .forEach(options::addEventProcessor); + applicationContext + .getBeansOfType(Integration.class) + .values() + .forEach(options::addIntegration); + applicationContext + .getBeanProvider(Sentry.OptionsConfiguration.class) + .ifAvailable(optionsConfiguration -> optionsConfiguration.configure(options)); + } + Sentry.init(options); + } + return bean; + } + + @Override + public void setApplicationContext(final @NotNull ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } + + @Override + public void destroy() { + scopes.close(); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor.java new file mode 100644 index 00000000000..91b27ddeac7 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor.java @@ -0,0 +1,39 @@ +package io.sentry.spring.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.SentryEvent; +import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.util.Objects; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Attaches transaction name from the HTTP request to {@link SentryEvent}. */ +@Open +public class SentryRequestHttpServletRequestProcessor implements EventProcessor { + private final @NotNull TransactionNameProvider transactionNameProvider; + private final @NotNull HttpServletRequest request; + + public SentryRequestHttpServletRequestProcessor( + final @NotNull TransactionNameProvider transactionNameProvider, + final @NotNull HttpServletRequest request) { + this.transactionNameProvider = + Objects.requireNonNull(transactionNameProvider, "transactionNameProvider is required"); + this.request = Objects.requireNonNull(request, "request is required"); + } + + @Override + public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { + if (event.getTransaction() == null) { + event.setTransaction(transactionNameProvider.provideTransactionName(request)); + } + return event; + } + + @Override + public @Nullable Long getOrder() { + return 5000L; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java new file mode 100644 index 00000000000..4bb2ad312bb --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java @@ -0,0 +1,114 @@ +package io.sentry.spring.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.SentryLevel; +import io.sentry.protocol.Request; +import io.sentry.util.AutoClosableReentrantLock; +import io.sentry.util.HttpUtils; +import io.sentry.util.Objects; +import io.sentry.util.UrlUtils; +import jakarta.servlet.ServletContext; +import jakarta.servlet.SessionCookieConfig; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@Open +public class SentryRequestResolver { + protected static final @NotNull AutoClosableReentrantLock staticLock = + new AutoClosableReentrantLock(); + private final @NotNull IScopes scopes; + private volatile @Nullable List extraSecurityCookies; + + public SentryRequestResolver(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "options is required"); + } + + // httpRequest.getRequestURL() returns StringBuffer which is considered an obsolete class. + @SuppressWarnings("JdkObsolete") + public @NotNull Request resolveSentryRequest(final @NotNull HttpServletRequest httpRequest) { + final Request sentryRequest = new Request(); + sentryRequest.setMethod(httpRequest.getMethod()); + final @NotNull UrlUtils.UrlDetails urlDetails = + UrlUtils.parse(httpRequest.getRequestURL().toString()); + urlDetails.applyToRequest(sentryRequest); + sentryRequest.setQueryString(httpRequest.getQueryString()); + final @NotNull List additionalSecurityCookieNames = + extractSecurityCookieNamesOrUseCached(httpRequest); + sentryRequest.setHeaders(resolveHeadersMap(httpRequest, additionalSecurityCookieNames)); + + if (scopes.getOptions().isSendDefaultPii()) { + String cookieName = HttpUtils.COOKIE_HEADER_NAME; + final @Nullable List filteredHeaders = + HttpUtils.filterOutSecurityCookiesFromHeader( + httpRequest.getHeaders(cookieName), cookieName, additionalSecurityCookieNames); + sentryRequest.setCookies(toString(filteredHeaders)); + } + return sentryRequest; + } + + @NotNull + Map resolveHeadersMap( + final @NotNull HttpServletRequest request, + final @NotNull List additionalSecurityCookieNames) { + final Map headersMap = new HashMap<>(); + for (String headerName : Collections.list(request.getHeaderNames())) { + // do not copy personal information identifiable headers + if (scopes.getOptions().isSendDefaultPii() + || !HttpUtils.containsSensitiveHeader(headerName)) { + final @Nullable List filteredHeaders = + HttpUtils.filterOutSecurityCookiesFromHeader( + request.getHeaders(headerName), headerName, additionalSecurityCookieNames); + headersMap.put(headerName, toString(filteredHeaders)); + } + } + return headersMap; + } + + private List extractSecurityCookieNamesOrUseCached( + final @NotNull HttpServletRequest httpRequest) { + if (extraSecurityCookies == null) { + try (final @NotNull ISentryLifecycleToken ignored = staticLock.acquire()) { + if (extraSecurityCookies == null) { + extraSecurityCookies = extractSecurityCookieNames(httpRequest); + } + } + } + + return extraSecurityCookies; + } + + private List extractSecurityCookieNames(final @NotNull HttpServletRequest httpRequest) { + try { + final @Nullable ServletContext servletContext = httpRequest.getServletContext(); + if (servletContext != null) { + final @Nullable SessionCookieConfig sessionCookieConfig = + servletContext.getSessionCookieConfig(); + if (sessionCookieConfig != null) { + final @Nullable String cookieName = sessionCookieConfig.getName(); + if (cookieName != null) { + return Arrays.asList(cookieName); + } + } + } + } catch (Throwable t) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.WARNING, "Failed to extract session cookie name from request.", t); + } + + return Collections.emptyList(); + } + + private static @Nullable String toString(final @Nullable List list) { + return list != null ? String.join(",", list) : null; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java new file mode 100644 index 00000000000..34dd5f0c469 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java @@ -0,0 +1,165 @@ +package io.sentry.spring.jakarta; + +import static io.sentry.SentryOptions.RequestSize.*; +import static io.sentry.TypeCheckHint.SPRING_REQUEST_FILTER_REQUEST; +import static io.sentry.TypeCheckHint.SPRING_REQUEST_FILTER_RESPONSE; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.Breadcrumb; +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ScopesAdapter; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.SentryOptions; +import io.sentry.SentryOptions.RequestSize; +import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; +import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.util.Objects; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.MediaType; +import org.springframework.util.MimeType; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.util.ContentCachingRequestWrapper; + +@Open +public class SentrySpringFilter extends OncePerRequestFilter { + private final @NotNull IScopes scopesBeforeForking; + private final @NotNull SentryRequestResolver requestResolver; + private final @NotNull TransactionNameProvider transactionNameProvider; + + public SentrySpringFilter( + final @NotNull IScopes scopes, + final @NotNull SentryRequestResolver requestResolver, + final @NotNull TransactionNameProvider transactionNameProvider) { + this.scopesBeforeForking = Objects.requireNonNull(scopes, "scopes are required"); + this.requestResolver = Objects.requireNonNull(requestResolver, "requestResolver is required"); + this.transactionNameProvider = + Objects.requireNonNull(transactionNameProvider, "transactionNameProvider is required"); + } + + public SentrySpringFilter(final @NotNull IScopes scopes) { + this(scopes, new SentryRequestResolver(scopes), new SpringMvcTransactionNameProvider()); + } + + public SentrySpringFilter() { + this(ScopesAdapter.getInstance()); + } + + @Override + protected void doFilterInternal( + final @NotNull HttpServletRequest servletRequest, + final @NotNull HttpServletResponse response, + final @NotNull FilterChain filterChain) + throws ServletException, IOException { + if (scopesBeforeForking.isEnabled()) { + // request may qualify for caching request body, if so resolve cached request + final HttpServletRequest request = + resolveHttpServletRequest(scopesBeforeForking, servletRequest); + final @NotNull IScopes forkedScopes = scopesBeforeForking.forkedScopes("SentrySpringFilter"); + try (final @NotNull ISentryLifecycleToken ignored = forkedScopes.makeCurrent()) { + final Hint hint = new Hint(); + hint.set(SPRING_REQUEST_FILTER_REQUEST, servletRequest); + hint.set(SPRING_REQUEST_FILTER_RESPONSE, response); + + forkedScopes.addBreadcrumb( + Breadcrumb.http(request.getRequestURI(), request.getMethod()), hint); + configureScope(forkedScopes, request); + filterChain.doFilter(request, response); + } + } else { + filterChain.doFilter(servletRequest, response); + } + } + + private void configureScope( + final @NotNull IScopes scopes, final @NotNull HttpServletRequest request) { + try { + scopes.configureScope( + scope -> { + // set basic request information on the scope + scope.setRequest(requestResolver.resolveSentryRequest(request)); + // transaction name is known at the later stage of request processing, thus it cannot + // be set on the scope + scope.addEventProcessor( + new SentryRequestHttpServletRequestProcessor(transactionNameProvider, request)); + // only if request caches body, add an event processor that sets body on the event + // body is not on the scope, to avoid using memory when no event is triggered during + // request processing + if (request instanceof ContentCachingRequestWrapper) { + scope.addEventProcessor( + new RequestBodyExtractingEventProcessor(request, scopes.getOptions())); + } + }); + } catch (Throwable e) { + scopes + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Failed to set scope for HTTP request", e); + } + } + + private @NotNull HttpServletRequest resolveHttpServletRequest( + final @NotNull IScopes scopes, final @NotNull HttpServletRequest request) { + if (scopes.getOptions().isSendDefaultPii() + && qualifiesForCaching(request, scopes.getOptions().getMaxRequestBodySize())) { + return new ContentCachingRequestWrapper(request); + } + return request; + } + + private static boolean qualifiesForCaching( + final @NotNull HttpServletRequest request, final @NotNull RequestSize maxRequestBodySize) { + final int contentLength = request.getContentLength(); + final String contentType = request.getContentType(); + + return maxRequestBodySize != RequestSize.NONE + && contentLength != -1 + && contentType != null + && shouldCacheMimeType(contentType) + && ((maxRequestBodySize == SMALL && contentLength < 1000) + || (maxRequestBodySize == MEDIUM && contentLength < 10000) + || maxRequestBodySize == ALWAYS); + } + + private static boolean shouldCacheMimeType(String contentType) { + return MimeType.valueOf(contentType).isCompatibleWith(MediaType.APPLICATION_JSON) + || MimeType.valueOf(contentType).isCompatibleWith(MediaType.APPLICATION_FORM_URLENCODED); + } + + static final class RequestBodyExtractingEventProcessor implements EventProcessor { + private final @NotNull RequestPayloadExtractor requestPayloadExtractor = + new RequestPayloadExtractor(); + private final @NotNull HttpServletRequest request; + private final @NotNull SentryOptions options; + + public RequestBodyExtractingEventProcessor( + final @NotNull HttpServletRequest request, final @NotNull SentryOptions options) { + this.request = Objects.requireNonNull(request, "request is required"); + this.options = Objects.requireNonNull(options, "options is required"); + } + + @Override + public @NotNull SentryEvent process(@NotNull SentryEvent event, @NotNull Hint hint) { + if (event.getRequest() != null + && options.isSendDefaultPii() + && qualifiesForCaching(request, options.getMaxRequestBodySize())) { + event.getRequest().setData(requestPayloadExtractor.extract(request, options)); + } + return event; + } + + @Override + public @Nullable Long getOrder() { + return 3000L; + } + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringServletContainerInitializer.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringServletContainerInitializer.java new file mode 100644 index 00000000000..9a71bc6910b --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringServletContainerInitializer.java @@ -0,0 +1,38 @@ +package io.sentry.spring.jakarta; + +import static io.sentry.util.ClassLoaderUtils.classLoaderOrDefault; + +import com.jakewharton.nopen.annotation.Open; +import jakarta.servlet.DispatcherType; +import jakarta.servlet.FilterRegistration; +import jakarta.servlet.ServletContainerInitializer; +import jakarta.servlet.ServletContext; +import jakarta.servlet.ServletException; +import java.util.EnumSet; +import java.util.Set; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Servlet container initializer used to add the {@link SentrySpringFilter} to the {@link + * ServletContext}. + */ +@Open +public class SentrySpringServletContainerInitializer implements ServletContainerInitializer { + @Override + public void onStartup(final @Nullable Set> c, final @NotNull ServletContext ctx) + throws ServletException { + try { + Class.forName( + "org.springframework.boot.SpringApplication", + false, + classLoaderOrDefault(getClass().getClassLoader())); + } catch (ClassNotFoundException e) { + final FilterRegistration.Dynamic dynamic = + ctx.addFilter("sentrySpringFilter", SentrySpringFilter.class); + if (dynamic != null) { + dynamic.addMappingForUrlPatterns(EnumSet.allOf(DispatcherType.class), false, "/*"); + } + } + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryTaskDecorator.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryTaskDecorator.java new file mode 100644 index 00000000000..ba757952606 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryTaskDecorator.java @@ -0,0 +1,27 @@ +package io.sentry.spring.jakarta; + +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.Sentry; +import java.util.concurrent.Callable; +import org.jetbrains.annotations.NotNull; +import org.springframework.core.task.TaskDecorator; +import org.springframework.scheduling.annotation.Async; + +/** + * Forks scopes for a thread running a {@link Runnable} given by parameter. Used to propagate the + * current {@link IScopes} on the thread executing async task - like MVC controller methods + * returning a {@link Callable} or Spring beans methods annotated with {@link Async}. + */ +public final class SentryTaskDecorator implements TaskDecorator { + @Override + public @NotNull Runnable decorate(final @NotNull Runnable runnable) { + final IScopes newScopes = Sentry.getCurrentScopes().forkedScopes("SentryTaskDecorator"); + + return () -> { + try (final @NotNull ISentryLifecycleToken ignored = newScopes.makeCurrent()) { + runnable.run(); + } + }; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java new file mode 100644 index 00000000000..31cc73a3468 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java @@ -0,0 +1,82 @@ +package io.sentry.spring.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScope; +import io.sentry.IScopes; +import io.sentry.IpAddressUtils; +import io.sentry.protocol.User; +import io.sentry.util.Objects; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; +import org.springframework.web.filter.OncePerRequestFilter; + +/** + * Sets the {@link User} on the {@link IScope} with information retrieved from {@link + * SentryUserProvider}s. + */ +@Open +public class SentryUserFilter extends OncePerRequestFilter { + private final @NotNull IScopes scopes; + private final @NotNull List sentryUserProviders; + + public SentryUserFilter( + final @NotNull IScopes scopes, final @NotNull List sentryUserProviders) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); + this.sentryUserProviders = + Objects.requireNonNull(sentryUserProviders, "sentryUserProviders list is required"); + } + + @Override + protected void doFilterInternal( + final @NotNull HttpServletRequest request, + final @NotNull HttpServletResponse response, + final @NotNull FilterChain chain) + throws ServletException, IOException { + final User user = new User(); + for (final SentryUserProvider provider : sentryUserProviders) { + apply(user, provider.provideUser()); + } + if (scopes.getOptions().isSendDefaultPii()) { + if (IpAddressUtils.isDefault(user.getIpAddress())) { + // unset {{auto}} as it would set the server's ip address as a user ip address + user.setIpAddress(null); + } + } + scopes.setUser(user); + chain.doFilter(request, response); + } + + private void apply(final @NotNull User existingUser, final @Nullable User userFromProvider) { + if (userFromProvider != null) { + Optional.ofNullable(userFromProvider.getEmail()).ifPresent(existingUser::setEmail); + Optional.ofNullable(userFromProvider.getId()).ifPresent(existingUser::setId); + Optional.ofNullable(userFromProvider.getIpAddress()).ifPresent(existingUser::setIpAddress); + Optional.ofNullable(userFromProvider.getUsername()).ifPresent(existingUser::setUsername); + if (userFromProvider.getData() != null && !userFromProvider.getData().isEmpty()) { + Map existingUserData = existingUser.getData(); + if (existingUserData == null) { + existingUserData = new ConcurrentHashMap<>(); + } + for (final Map.Entry entry : userFromProvider.getData().entrySet()) { + existingUserData.put(entry.getKey(), entry.getValue()); + } + existingUser.setData(existingUserData); + } + } + } + + @VisibleForTesting + public @NotNull List getSentryUserProviders() { + return sentryUserProviders; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserProvider.java new file mode 100644 index 00000000000..9a87dbc78a5 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserProvider.java @@ -0,0 +1,14 @@ +package io.sentry.spring.jakarta; + +import io.sentry.protocol.User; +import org.jetbrains.annotations.Nullable; + +/** + * Out of the box Spring integration configures single {@link SentryUserProvider} - {@link + * HttpServletRequestSentryUserProvider}. + */ +@FunctionalInterface +public interface SentryUserProvider { + @Nullable + User provideUser(); +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryWebConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryWebConfiguration.java new file mode 100644 index 00000000000..3f4d356505e --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryWebConfiguration.java @@ -0,0 +1,23 @@ +package io.sentry.spring.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.SentryOptions; +import org.jetbrains.annotations.NotNull; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Lazy; +import org.springframework.core.annotation.Order; + +/** Registers Spring Web specific Sentry beans. */ +@Configuration(proxyBeanMethods = false) +@Open +public class SentryWebConfiguration { + + @Bean + @Lazy + @Order(0) + public @NotNull HttpServletRequestSentryUserProvider httpServletRequestSentryUserProvider( + final @NotNull SentryOptions sentryOptions) { + return new HttpServletRequestSentryUserProvider(sentryOptions); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringProfilesEventProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringProfilesEventProcessor.java new file mode 100644 index 00000000000..48957e88507 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringProfilesEventProcessor.java @@ -0,0 +1,51 @@ +package io.sentry.spring.jakarta; + +import io.sentry.EventProcessor; +import io.sentry.Hint; +import io.sentry.SentryBaseEvent; +import io.sentry.SentryEvent; +import io.sentry.SentryReplayEvent; +import io.sentry.protocol.SentryTransaction; +import io.sentry.protocol.Spring; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.core.env.Environment; + +/** + * Attaches the list of active Spring profiles (an empty list if only the default profile is active) + * to the {@link io.sentry.TraceContext} associated with the event. + */ +public final class SpringProfilesEventProcessor implements EventProcessor { + private final @NotNull Environment environment; + + @Override + public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) { + processInternal(event); + return event; + } + + @Override + public @NotNull SentryTransaction process( + final @NotNull SentryTransaction transaction, final @NotNull Hint hint) { + processInternal(transaction); + return transaction; + } + + @Override + public @NotNull SentryReplayEvent process( + final @NotNull SentryReplayEvent event, final @NotNull Hint hint) { + processInternal(event); + return event; + } + + private void processInternal(final @NotNull SentryBaseEvent event) { + @Nullable String[] activeProfiles = environment.getActiveProfiles(); + @NotNull Spring springContext = new Spring(); + springContext.setActiveProfiles(activeProfiles); + event.getContexts().setSpring(springContext); + } + + public SpringProfilesEventProcessor(final @NotNull Environment environment) { + this.environment = environment; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringSecuritySentryUserProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringSecuritySentryUserProvider.java new file mode 100644 index 00000000000..d36bc4bf2b0 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringSecuritySentryUserProvider.java @@ -0,0 +1,35 @@ +package io.sentry.spring.jakarta; + +import io.sentry.SentryOptions; +import io.sentry.protocol.User; +import io.sentry.util.Objects; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +/** + * Resolves user information from Spring Security {@link Authentication} obtained via {@link + * SecurityContextHolder}. + */ +public final class SpringSecuritySentryUserProvider implements SentryUserProvider { + private final @NotNull SentryOptions options; + + public SpringSecuritySentryUserProvider(final @NotNull SentryOptions options) { + this.options = Objects.requireNonNull(options, "options is required"); + } + + @Override + public @Nullable User provideUser() { + if (options.isSendDefaultPii()) { + final SecurityContext context = SecurityContextHolder.getContext(); + if (context != null && context.getAuthentication() != null) { + final User user = new User(); + user.setUsername(context.getAuthentication().getName()); + return user; + } + } + return null; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java new file mode 100644 index 00000000000..a2f53da955c --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java @@ -0,0 +1,41 @@ +package io.sentry.spring.jakarta.checkin; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.jetbrains.annotations.ApiStatus; +import org.springframework.core.annotation.AliasFor; + +/** Sends a {@link io.sentry.CheckIn} for the annotated method. */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD}) +@ApiStatus.Experimental +public @interface SentryCheckIn { + + /** + * Monitor slug. If not set, no check-in will be sent. + * + * @return monitor slug + */ + @AliasFor("value") + String monitorSlug() default ""; + + /** + * Whether to send only send heartbeat events. + * + *

A hearbeat check-in means there's no separate IN_PROGRESS check-in at the start of the jobs + * execution. Only the check-in after finishing the job will be sent. + * + * @return true if only heartbeat check-ins should be sent. + */ + boolean heartbeat() default false; + + /** + * Monitor slug. If not set, no check-in will be sent. + * + * @return monitor slug + */ + @AliasFor("monitorSlug") + String value() default ""; +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java new file mode 100644 index 00000000000..dd22f4dc5dc --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java @@ -0,0 +1,119 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.CheckIn; +import io.sentry.CheckInStatus; +import io.sentry.DateUtils; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ScopesAdapter; +import io.sentry.SentryLevel; +import io.sentry.protocol.SentryId; +import io.sentry.util.Objects; +import io.sentry.util.TracingUtils; +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.jetbrains.annotations.Nullable; +import org.springframework.aop.support.AopUtils; +import org.springframework.context.EmbeddedValueResolverAware; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringValueResolver; + +/** + * Reports execution of every bean method annotated with {@link SentryCheckIn} as a monitor + * check-in. + */ +@ApiStatus.Internal +@ApiStatus.Experimental +@Open +public class SentryCheckInAdvice implements MethodInterceptor, EmbeddedValueResolverAware { + private final @NotNull IScopes scopes; + + private @Nullable StringValueResolver resolver; + + public SentryCheckInAdvice() { + this(ScopesAdapter.getInstance()); + } + + public SentryCheckInAdvice(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); + } + + @Override + public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { + final Method mostSpecificMethod = + AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()); + + @Nullable + SentryCheckIn checkInAnnotation = + AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCheckIn.class); + if (checkInAnnotation == null) { + return invocation.proceed(); + } + + final boolean isHeartbeatOnly = checkInAnnotation.heartbeat(); + + @Nullable String monitorSlug = checkInAnnotation.value(); + + if (resolver != null) { + try { + monitorSlug = resolver.resolveStringValue(checkInAnnotation.value()); + } catch (Throwable e) { + // When resolving fails, we fall back to the original string which may contain unresolved + // expressions. Testing shows this can also happen if properties cannot be resolved (without + // an exception being thrown). Sentry should alert the user about missed checkins in this + // case since the monitor slug won't match what is configured in Sentry. + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Slug for method annotated with @SentryCheckIn could not be resolved from properties.", + e); + } + } + + if (ObjectUtils.isEmpty(monitorSlug)) { + scopes + .getOptions() + .getLogger() + .log( + SentryLevel.WARNING, + "Not capturing check-in for method annotated with @SentryCheckIn because it does not specify a monitor slug."); + return invocation.proceed(); + } + + try (final @NotNull ISentryLifecycleToken ignored = + scopes.forkedScopes("SentryCheckInAdvice").makeCurrent()) { + TracingUtils.startNewTrace(scopes); + + @Nullable SentryId checkInId = null; + final long startTime = System.currentTimeMillis(); + boolean didError = false; + + try { + if (!isHeartbeatOnly) { + checkInId = scopes.captureCheckIn(new CheckIn(monitorSlug, CheckInStatus.IN_PROGRESS)); + } + return invocation.proceed(); + } catch (Throwable e) { + didError = true; + throw e; + } finally { + final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; + CheckIn checkIn = new CheckIn(checkInId, monitorSlug, status); + checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); + scopes.captureCheckIn(checkIn); + } + } + } + + @Override + public void setEmbeddedValueResolver(StringValueResolver resolver) { + this.resolver = resolver; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java new file mode 100644 index 00000000000..1f948581f18 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java @@ -0,0 +1,35 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.aopalliance.aop.Advice; +import org.jetbrains.annotations.ApiStatus; +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.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SentryCheckInAdviceConfiguration { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advice sentryCheckInAdvice() { + return new SentryCheckInAdvice(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advisor sentryCheckInAdvisor( + final @NotNull @Qualifier("sentryCheckInPointcut") Pointcut sentryCheckInPointcut, + final @NotNull @Qualifier("sentryCheckInAdvice") Advice sentryCheckInAdvice) { + return new DefaultPointcutAdvisor(sentryCheckInPointcut, sentryCheckInAdvice); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java new file mode 100644 index 00000000000..d5c7b8e97e6 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java @@ -0,0 +1,33 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.jetbrains.annotations.ApiStatus; +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.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** AOP pointcut configuration for {@link SentryCheckIn}. */ +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SentryCheckInPointcutConfiguration { + + /** + * Pointcut around which check-ins are created. + * + * @return pointcut used by {@link SentryCheckInAdvice}. + */ + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Pointcut sentryCheckInPointcut() { + return new ComposablePointcut(new AnnotationClassFilter(SentryCheckIn.class, true)) + .union(new AnnotationMatchingPointcut(null, SentryCheckIn.class)); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java new file mode 100644 index 00000000000..e4e9104eb05 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java @@ -0,0 +1,21 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.jetbrains.annotations.ApiStatus; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryQuartzConfiguration { + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() { + return new SentrySchedulerFactoryBeanCustomizer(); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java new file mode 100644 index 00000000000..e37f5ac4674 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -0,0 +1,14 @@ +package io.sentry.spring.jakarta.checkin; + +import io.sentry.quartz.SentryJobListener; +import org.jetbrains.annotations.ApiStatus; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +@ApiStatus.Experimental +public final class SentrySchedulerFactoryBeanCustomizer implements SchedulerFactoryBeanCustomizer { + @Override + public void customize(SchedulerFactoryBean schedulerFactoryBean) { + schedulerFactoryBean.setGlobalJobListeners(new SentryJobListener()); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter.java new file mode 100644 index 00000000000..4911419c0b9 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter.java @@ -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 {} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice.java new file mode 100644 index 00000000000..c6537f853c2 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice.java @@ -0,0 +1,63 @@ +package io.sentry.spring.jakarta.exception; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.ScopesAdapter; +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 IScopes scopes; + + public SentryCaptureExceptionParameterAdvice() { + this(ScopesAdapter.getInstance()); + } + + public SentryCaptureExceptionParameterAdvice(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "scopes are 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()); + scopes.captureException(mechanismException); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration.java new file mode 100644 index 00000000000..0d4f4274be1 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration.java @@ -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 {} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration.java new file mode 100644 index 00000000000..3744182a306 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration.java @@ -0,0 +1,32 @@ +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.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** AOP pointcut configuration for {@link SentryCaptureExceptionParameter}. */ +@Configuration(proxyBeanMethods = false) +@Open +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SentryCaptureExceptionParameterPointcutConfiguration { + + /** + * Pointcut around which spans are created. + * + * @return pointcut used by {@link SentryCaptureExceptionParameterAdvice}. + */ + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Pointcut sentryCaptureExceptionParameterPointcut() { + return new ComposablePointcut( + new AnnotationClassFilter(SentryCaptureExceptionParameter.class, true)) + .union(new AnnotationMatchingPointcut(null, SentryCaptureExceptionParameter.class)); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration.java new file mode 100644 index 00000000000..aa75510c0a2 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration.java @@ -0,0 +1,37 @@ +package io.sentry.spring.jakarta.exception; + +import com.jakewharton.nopen.annotation.Open; +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.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** Creates advice infrastructure for {@link SentryCaptureExceptionParameter}. */ +@Configuration(proxyBeanMethods = false) +@Open +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SentryExceptionParameterAdviceConfiguration { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advice sentryCaptureExceptionParameterAdvice() { + return new SentryCaptureExceptionParameterAdvice(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advisor sentryCaptureExceptionParameterAdvisor( + final @NotNull @Qualifier("sentryCaptureExceptionParameterPointcut") Pointcut + sentryCaptureExceptionParameterPointcut, + final @NotNull @Qualifier("sentryCaptureExceptionParameterAdvice") Advice + sentryCaptureExceptionParameterAdvice) { + return new DefaultPointcutAdvisor( + sentryCaptureExceptionParameterPointcut, sentryCaptureExceptionParameterAdvice); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java new file mode 100644 index 00000000000..a75aa281349 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java @@ -0,0 +1,119 @@ +package io.sentry.spring.jakarta.graphql; + +import static io.sentry.graphql.SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; + +import graphql.GraphQLContext; +import io.sentry.Breadcrumb; +import io.sentry.IScopes; +import io.sentry.NoOpScopes; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Consumer; +import org.dataloader.BatchLoaderEnvironment; +import org.dataloader.DataLoaderOptions; +import org.dataloader.DataLoaderRegistry; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.graphql.execution.BatchLoaderRegistry; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +@ApiStatus.Internal +public final class SentryBatchLoaderRegistry implements BatchLoaderRegistry { + + private final @NotNull BatchLoaderRegistry delegate; + + SentryBatchLoaderRegistry(final @NotNull BatchLoaderRegistry delegate) { + this.delegate = delegate; + } + + @Override + public RegistrationSpec forTypePair(Class keyType, Class valueType) { + return new SentryRegistrationSpec( + delegate.forTypePair(keyType, valueType), keyType, valueType); + } + + @Override + public RegistrationSpec forName(String name) { + return new SentryRegistrationSpec(delegate.forName(name), name); + } + + @Override + public void registerDataLoaders(DataLoaderRegistry registry, GraphQLContext context) { + delegate.registerDataLoaders(registry, context); + } + + public static final class SentryRegistrationSpec + implements BatchLoaderRegistry.RegistrationSpec { + + private final @NotNull RegistrationSpec delegate; + private final @Nullable String name; + private final @Nullable Class keyType; + private final @Nullable Class valueType; + + public SentryRegistrationSpec( + final @NotNull RegistrationSpec delegate, Class keyType, Class valueType) { + this.delegate = delegate; + this.keyType = keyType; + this.valueType = valueType; + this.name = null; + } + + public SentryRegistrationSpec(final @NotNull RegistrationSpec delegate, String name) { + this.delegate = delegate; + this.name = name; + this.keyType = null; + this.valueType = null; + } + + @Override + public BatchLoaderRegistry.RegistrationSpec withName(String name) { + return delegate.withName(name); + } + + @Override + public BatchLoaderRegistry.RegistrationSpec withOptions( + Consumer optionsConsumer) { + return delegate.withOptions(optionsConsumer); + } + + @Override + public BatchLoaderRegistry.RegistrationSpec withOptions(DataLoaderOptions options) { + return delegate.withOptions(options); + } + + @Override + public void registerBatchLoader(BiFunction, BatchLoaderEnvironment, Flux> loader) { + delegate.registerBatchLoader( + (keys, batchLoaderEnvironment) -> { + scopesFromContext(batchLoaderEnvironment) + .addBreadcrumb(Breadcrumb.graphqlDataLoader(keys, keyType, valueType, name)); + return loader.apply(keys, batchLoaderEnvironment); + }); + } + + @Override + public void registerMappedBatchLoader( + BiFunction, BatchLoaderEnvironment, Mono>> loader) { + delegate.registerMappedBatchLoader( + (keys, batchLoaderEnvironment) -> { + scopesFromContext(batchLoaderEnvironment) + .addBreadcrumb(Breadcrumb.graphqlDataLoader(keys, keyType, valueType, name)); + return loader.apply(keys, batchLoaderEnvironment); + }); + } + + private @NotNull IScopes scopesFromContext(final @NotNull BatchLoaderEnvironment environment) { + Object context = environment.getContext(); + if (context instanceof GraphQLContext) { + GraphQLContext graphqlContext = (GraphQLContext) context; + return graphqlContext.getOrDefault(SENTRY_SCOPES_CONTEXT_KEY, NoOpScopes.getInstance()); + } + + return NoOpScopes.getInstance(); + } + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter.java new file mode 100644 index 00000000000..3f34931147c --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter.java @@ -0,0 +1,48 @@ +package io.sentry.spring.jakarta.graphql; + +import graphql.GraphQLError; +import graphql.execution.DataFetcherExceptionHandlerResult; +import graphql.schema.DataFetchingEnvironment; +import io.sentry.graphql.SentryGraphqlExceptionHandler; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; + +@ApiStatus.Internal +public final class SentryDataFetcherExceptionResolverAdapter + extends DataFetcherExceptionResolverAdapter { + private final @NotNull SentryGraphqlExceptionHandler handler; + + public SentryDataFetcherExceptionResolverAdapter() { + this.handler = new SentryGraphqlExceptionHandler(null); + } + + @Override + public boolean isThreadLocalContextAware() { + return true; + } + + @Override + protected @Nullable GraphQLError resolveToSingleError(Throwable ex, DataFetchingEnvironment env) { + List errors = resolveToMultipleErrors(ex, env); + if (errors != null && !errors.isEmpty()) { + return errors.get(0); + } + return null; + } + + @Override + protected @Nullable List resolveToMultipleErrors( + Throwable ex, DataFetchingEnvironment env) { + @Nullable + CompletableFuture result = + handler.handleException(ex, env, null); + if (result != null) { + return result.join().getErrors(); + } + return null; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler.java new file mode 100644 index 00000000000..a7a6cccd3e3 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler.java @@ -0,0 +1,34 @@ +package io.sentry.spring.jakarta.graphql; + +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import io.sentry.IScopes; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.ExceptionReporter; +import io.sentry.graphql.SentrySubscriptionHandler; +import org.jetbrains.annotations.NotNull; +import reactor.core.publisher.Flux; + +public final class SentryDgsSubscriptionHandler implements SentrySubscriptionHandler { + + public SentryDgsSubscriptionHandler() { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6NetflixDGSGrahQL"); + } + + @Override + public @NotNull Object onSubscriptionResult( + final @NotNull Object result, + final @NotNull IScopes scopes, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull InstrumentationFieldFetchParameters parameters) { + if (result instanceof Flux) { + final @NotNull Flux flux = (Flux) result; + return flux.doOnError( + throwable -> { + final @NotNull ExceptionReporter.ExceptionDetails exceptionDetails = + new ExceptionReporter.ExceptionDetails(scopes, parameters.getEnvironment(), true); + exceptionReporter.captureThrowable(throwable, exceptionDetails, null); + }); + } + return result; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration.java new file mode 100644 index 00000000000..be94017201e --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration.java @@ -0,0 +1,64 @@ +package io.sentry.spring.jakarta.graphql; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.SentryGraphqlInstrumentation; +import io.sentry.graphql22.SentryInstrumentation; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryGraphql22Configuration { + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean(name = "sentryInstrumentation") + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) + public SentryInstrumentation sentryInstrumentationWebMvc( + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + return createInstrumentation(beforeSpanCallback, false); + } + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean(name = "sentryInstrumentation") + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + public SentryInstrumentation sentryInstrumentationWebflux( + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + return createInstrumentation(beforeSpanCallback, true); + } + + /** + * We're not setting defaultDataFetcherExceptionHandler here on purpose and instead use the + * resolver adapter below. This way Springs handler can still forward to other resolver adapters. + */ + private SentryInstrumentation createInstrumentation( + final @NotNull ObjectProvider + beforeSpanCallback, + final boolean captureRequestBody) { + return new SentryInstrumentation( + beforeSpanCallback.getIfAvailable(), + new SentrySpringSubscriptionHandler(), + captureRequestBody); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SentryDataFetcherExceptionResolverAdapter exceptionResolverAdapter() { + return new SentryDataFetcherExceptionResolverAdapter(); + } + + @Bean + public SentryGraphqlBeanPostProcessor graphqlBeanPostProcessor() { + return new SentryGraphqlBeanPostProcessor(); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor.java new file mode 100644 index 00000000000..5eb4374bb1f --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor.java @@ -0,0 +1,25 @@ +package io.sentry.spring.jakarta.graphql; + +import org.jetbrains.annotations.ApiStatus; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.PriorityOrdered; +import org.springframework.graphql.execution.BatchLoaderRegistry; + +@ApiStatus.Internal +public final class SentryGraphqlBeanPostProcessor implements BeanPostProcessor, PriorityOrdered { + + @Override + public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + if (bean instanceof BatchLoaderRegistry) { + return new SentryBatchLoaderRegistry((BatchLoaderRegistry) bean); + } + return bean; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration.java new file mode 100644 index 00000000000..611e6bf5279 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration.java @@ -0,0 +1,64 @@ +package io.sentry.spring.jakarta.graphql; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.SentryGraphqlInstrumentation; +import io.sentry.graphql.SentryInstrumentation; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryGraphqlConfiguration { + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) + public SentryInstrumentation sentryInstrumentationWebMvc( + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + return createInstrumentation(beforeSpanCallback, false); + } + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + public SentryInstrumentation sentryInstrumentationWebflux( + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + return createInstrumentation(beforeSpanCallback, true); + } + + /** + * We're not setting defaultDataFetcherExceptionHandler here on purpose and instead use the + * resolver adapter below. This way Springs handler can still forward to other resolver adapters. + */ + private SentryInstrumentation createInstrumentation( + final @NotNull ObjectProvider + beforeSpanCallback, + final boolean captureRequestBody) { + return new SentryInstrumentation( + beforeSpanCallback.getIfAvailable(), + new SentrySpringSubscriptionHandler(), + captureRequestBody); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SentryDataFetcherExceptionResolverAdapter exceptionResolverAdapter() { + return new SentryDataFetcherExceptionResolverAdapter(); + } + + @Bean + public SentryGraphqlBeanPostProcessor graphqlBeanPostProcessor() { + return new SentryGraphqlBeanPostProcessor(); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler.java new file mode 100644 index 00000000000..eec86f5e8b7 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler.java @@ -0,0 +1,35 @@ +package io.sentry.spring.jakarta.graphql; + +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; +import io.sentry.IScopes; +import io.sentry.graphql.ExceptionReporter; +import io.sentry.graphql.SentrySubscriptionHandler; +import org.jetbrains.annotations.NotNull; +import org.springframework.graphql.execution.SubscriptionPublisherException; +import reactor.core.publisher.Flux; + +public final class SentrySpringSubscriptionHandler implements SentrySubscriptionHandler { + + @Override + public @NotNull Object onSubscriptionResult( + final @NotNull Object result, + final @NotNull IScopes scopes, + final @NotNull ExceptionReporter exceptionReporter, + final @NotNull InstrumentationFieldFetchParameters parameters) { + if (result instanceof Flux) { + final @NotNull Flux flux = (Flux) result; + return flux.doOnError( + throwable -> { + final @NotNull ExceptionReporter.ExceptionDetails exceptionDetails = + new ExceptionReporter.ExceptionDetails(scopes, parameters.getEnvironment(), true); + if (throwable instanceof SubscriptionPublisherException + && throwable.getCause() != null) { + exceptionReporter.captureThrowable(throwable.getCause(), exceptionDetails, null); + } else { + exceptionReporter.captureThrowable(throwable, exceptionDetails, null); + } + }); + } + return result; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java new file mode 100644 index 00000000000..bac77cfafc1 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java @@ -0,0 +1,27 @@ +package io.sentry.spring.jakarta.opentelemetry; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.Sentry; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryOpenTelemetryMode; +import io.sentry.SentryOptions; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryOpenTelemetryAgentWithoutAutoInitConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "sentryOpenTelemetryOptionsConfiguration") + public @NotNull Sentry.OptionsConfiguration + sentryOpenTelemetryOptionsConfiguration() { + return options -> { + SentryIntegrationPackageStorage.getInstance() + .addIntegration("SpringBoot3OpenTelemetryAgentWithoutAutoInit"); + options.setOpenTelemetryMode(SentryOpenTelemetryMode.AGENT); + }; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java new file mode 100644 index 00000000000..3e3ef9e2afd --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java @@ -0,0 +1,38 @@ +package io.sentry.spring.jakarta.opentelemetry; + +import com.jakewharton.nopen.annotation.Open; +import io.opentelemetry.api.OpenTelemetry; +import io.sentry.ISpanFactory; +import io.sentry.Sentry; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryOpenTelemetryMode; +import io.sentry.SentryOptions; +import io.sentry.opentelemetry.OtelSpanFactory; +import io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryOpenTelemetryNoAgentConfiguration { + + @Bean + @ConditionalOnMissingBean + public static ISpanFactory openTelemetrySpanFactory(OpenTelemetry openTelemetry) { + return new OtelSpanFactory(openTelemetry); + } + + @Bean + @ConditionalOnMissingBean(name = "sentryOpenTelemetryOptionsConfiguration") + public @NotNull Sentry.OptionsConfiguration + sentryOpenTelemetryOptionsConfiguration() { + return options -> { + SentryIntegrationPackageStorage.getInstance() + .addIntegration("SpringBoot3OpenTelemetryNoAgent"); + SentryAutoConfigurationCustomizerProvider.skipInit = true; + options.setOpenTelemetryMode(SentryOpenTelemetryMode.AGENTLESS_SPRING); + }; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider.java new file mode 100644 index 00000000000..1e142e958dc --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider.java @@ -0,0 +1,55 @@ +package io.sentry.spring.jakarta.tracing; + +import io.sentry.protocol.TransactionNameSource; +import jakarta.servlet.http.HttpServletRequest; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Resolves transaction name using other transaction name providers by invoking them in order. If a + * provider returns no transaction name, the next one is invoked. + */ +@ApiStatus.Internal +public final class CombinedTransactionNameProvider implements TransactionNameProvider { + + private final @NotNull List providers; + + public CombinedTransactionNameProvider(final @NotNull List providers) { + this.providers = providers; + } + + @Override + public @Nullable String provideTransactionName(@NotNull HttpServletRequest request) { + for (TransactionNameProvider provider : providers) { + String transactionName = provider.provideTransactionName(request); + if (transactionName != null) { + return transactionName; + } + } + + return null; + } + + @Override + @ApiStatus.Internal + public @NotNull TransactionNameSource provideTransactionSource() { + return TransactionNameSource.CUSTOM; + } + + @ApiStatus.Internal + @Override + public @NotNull TransactionNameWithSource provideTransactionNameAndSource( + @NotNull HttpServletRequest request) { + for (TransactionNameProvider provider : providers) { + String transactionName = provider.provideTransactionName(request); + if (transactionName != null) { + final @NotNull TransactionNameSource source = provider.provideTransactionSource(); + return new TransactionNameWithSource(transactionName, source); + } + } + + return new TransactionNameWithSource(null, TransactionNameSource.CUSTOM); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration.java new file mode 100644 index 00000000000..c3e6e1ef6af --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration.java @@ -0,0 +1,48 @@ +package io.sentry.spring.jakarta.tracing; + +import com.jakewharton.nopen.annotation.Open; +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.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** Creates advice infrastructure for {@link SentrySpan} and {@link SentryTransaction}. */ +@Configuration(proxyBeanMethods = false) +@Open +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SentryAdviceConfiguration { + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advice sentryTransactionAdvice() { + return new SentryTransactionAdvice(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advisor sentryTransactionAdvisor( + final @NotNull @Qualifier("sentryTransactionPointcut") Pointcut sentryTransactionPointcut, + final @NotNull @Qualifier("sentryTransactionAdvice") Advice sentryTransactionAdvice) { + return new DefaultPointcutAdvisor(sentryTransactionPointcut, sentryTransactionAdvice); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advice sentrySpanAdvice() { + return new SentrySpanAdvice(); + } + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Advisor sentrySpanAdvisor( + final @NotNull @Qualifier("sentrySpanPointcut") Pointcut sentrySpanPointcut, + final @NotNull @Qualifier("sentrySpanAdvice") Advice sentrySpanAdvice) { + return new DefaultPointcutAdvisor(sentrySpanPointcut, sentrySpanAdvice); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpan.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpan.java new file mode 100644 index 00000000000..846cf53283a --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpan.java @@ -0,0 +1,41 @@ +package io.sentry.spring.jakarta.tracing; + +import io.sentry.protocol.SentryTransaction; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + +/** + * Makes annotated method execution or a method execution within a class annotated with {@link + * SentrySpan} executed within running {@link SentryTransaction} to get wrapped into {@link + * io.sentry.Span}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface SentrySpan { + + /** + * Span description. + * + * @return description + */ + String description() default ""; + + /** + * Span operation. If not set, operation is resolved as a class name and a method name. + * + * @return operation. + */ + @AliasFor("value") + String operation() default ""; + + /** + * Span operation. If not set, transaction name is resolved as a class name and a method name. + * + * @return operation. + */ + @AliasFor("operation") + String value() default ""; +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java new file mode 100644 index 00000000000..668c8d1b0b8 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java @@ -0,0 +1,82 @@ +package io.sentry.spring.jakarta.tracing; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.ScopesAdapter; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.util.Objects; +import java.lang.reflect.Method; +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StringUtils; + +/** + * Creates a span from every bean method executed within {@link SentryTransaction}. Depending on the + * configured pointcut, method either must or can be annotated with {@link SentrySpan}. + */ +@Open +public class SentrySpanAdvice implements MethodInterceptor { + private static final String TRACE_ORIGIN = "auto.function.spring_jakarta.advice"; + private final @NotNull IScopes scopes; + + public SentrySpanAdvice() { + this(ScopesAdapter.getInstance()); + } + + public SentrySpanAdvice(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); + } + + @SuppressWarnings("deprecation") + @Override + public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { + final ISpan activeSpan = scopes.getSpan(); + + if (activeSpan == null || activeSpan.isNoOp()) { + // there is no active transaction, we do not start new span + return invocation.proceed(); + } else { + final Method mostSpecificMethod = + AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()); + final Class targetClass = invocation.getMethod().getDeclaringClass(); + SentrySpan sentrySpan = AnnotationUtils.findAnnotation(mostSpecificMethod, SentrySpan.class); + if (sentrySpan == null) { + sentrySpan = + AnnotationUtils.findAnnotation( + mostSpecificMethod.getDeclaringClass(), SentrySpan.class); + } + final String operation = resolveSpanOperation(targetClass, mostSpecificMethod, sentrySpan); + SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final ISpan span = activeSpan.startChild(operation, null, spanOptions); + if (sentrySpan != null && !StringUtils.isEmpty(sentrySpan.description())) { + span.setDescription(sentrySpan.description()); + } + try { + final Object result = invocation.proceed(); + span.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + span.setStatus(SpanStatus.INTERNAL_ERROR); + span.setThrowable(e); + throw e; + } finally { + span.finish(); + } + } + } + + @SuppressWarnings("deprecation") + private String resolveSpanOperation( + Class targetClass, Method method, @Nullable SentrySpan sentrySpan) { + return sentrySpan == null || StringUtils.isEmpty(sentrySpan.value()) + ? targetClass.getSimpleName() + "." + method.getName() + : sentrySpan.value(); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java new file mode 100644 index 00000000000..f50c93976e5 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -0,0 +1,143 @@ +package io.sentry.spring.jakarta.tracing; + +import static io.sentry.TypeCheckHint.SPRING_REQUEST_INTERCEPTOR_REQUEST; +import static io.sentry.TypeCheckHint.SPRING_REQUEST_INTERCEPTOR_REQUEST_BODY; +import static io.sentry.TypeCheckHint.SPRING_REQUEST_INTERCEPTOR_RESPONSE; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.BaggageHeader; +import io.sentry.Breadcrumb; +import io.sentry.Hint; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.SpanDataConvention; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.util.Objects; +import io.sentry.util.SpanUtils; +import io.sentry.util.TracingUtils; +import io.sentry.util.UrlUtils; +import java.io.IOException; +import java.util.Locale; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.HttpRequest; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; + +@Open +public class SentrySpanClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + private static final String TRACE_ORIGIN_REST_TEMPLATE = "auto.http.spring_jakarta.resttemplate"; + private static final String TRACE_ORIGIN_REST_CLIENT = "auto.http.spring_jakarta.restclient"; + private final @NotNull IScopes scopes; + private final @NotNull String traceOrigin; + + public SentrySpanClientHttpRequestInterceptor(final @NotNull IScopes scopes) { + this(scopes, true); + } + + public SentrySpanClientHttpRequestInterceptor( + final @NotNull IScopes scopes, final @NotNull boolean isRestTemplate) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + this.traceOrigin = isRestTemplate ? TRACE_ORIGIN_REST_TEMPLATE : TRACE_ORIGIN_REST_CLIENT; + } + + @Override + public @NotNull ClientHttpResponse intercept( + @NotNull HttpRequest request, + @NotNull byte[] body, + @NotNull ClientHttpRequestExecution execution) + throws IOException { + Integer responseStatusCode = null; + ClientHttpResponse response = null; + try { + final ISpan activeSpan = scopes.getSpan(); + if (activeSpan == null) { + maybeAddTracingHeaders(request, null); + return execution.execute(request, body); + } + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(traceOrigin); + final ISpan span = activeSpan.startChild("http.client", null, spanOptions); + final String methodName = + request.getMethod() != null ? request.getMethod().name() : "unknown"; + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(request.getURI().toString()); + span.setDescription(methodName + " " + urlDetails.getUrlOrFallback()); + span.setData(SpanDataConvention.HTTP_METHOD_KEY, methodName.toUpperCase(Locale.ROOT)); + urlDetails.applyToSpan(span); + + maybeAddTracingHeaders(request, span); + + try { + response = execution.execute(request, body); + // handles both success and error responses + span.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.getStatusCode().value()); + span.setStatus(SpanStatus.fromHttpStatusCode(response.getStatusCode().value())); + responseStatusCode = response.getStatusCode().value(); + return response; + } catch (Throwable e) { + // handles cases like connection errors + span.setThrowable(e); + span.setStatus(SpanStatus.INTERNAL_ERROR); + throw e; + } finally { + span.finish(); + } + } finally { + addBreadcrumb(request, body, responseStatusCode, response); + } + } + + private void maybeAddTracingHeaders( + final @NotNull HttpRequest request, final @Nullable ISpan span) { + if (isIgnored()) { + return; + } + + final @Nullable TracingUtils.TracingHeaders tracingHeaders = + TracingUtils.traceIfAllowed( + scopes, + request.getURI().toString(), + request.getHeaders().get(BaggageHeader.BAGGAGE_HEADER), + span); + + if (tracingHeaders != null) { + request + .getHeaders() + .add( + tracingHeaders.getSentryTraceHeader().getName(), + tracingHeaders.getSentryTraceHeader().getValue()); + + final @Nullable BaggageHeader baggageHeader = tracingHeaders.getBaggageHeader(); + if (baggageHeader != null) { + request.getHeaders().set(baggageHeader.getName(), baggageHeader.getValue()); + } + } + } + + private boolean isIgnored() { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), traceOrigin); + } + + private void addBreadcrumb( + final @NotNull HttpRequest request, + final @NotNull byte[] body, + final @Nullable Integer responseStatusCode, + final @Nullable ClientHttpResponse response) { + final String methodName = request.getMethod() != null ? request.getMethod().name() : "unknown"; + + final Breadcrumb breadcrumb = + Breadcrumb.http(request.getURI().toString(), methodName, responseStatusCode); + breadcrumb.setData("request_body_size", body.length); + + final Hint hint = new Hint(); + hint.set(SPRING_REQUEST_INTERCEPTOR_REQUEST, request); + hint.set(SPRING_REQUEST_INTERCEPTOR_REQUEST_BODY, body); + if (response != null) { + hint.set(SPRING_REQUEST_INTERCEPTOR_RESPONSE, response); + } + + scopes.addBreadcrumb(breadcrumb, hint); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java new file mode 100644 index 00000000000..1189532c0c4 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java @@ -0,0 +1,126 @@ +package io.sentry.spring.jakarta.tracing; + +import static io.sentry.TypeCheckHint.SPRING_EXCHANGE_FILTER_REQUEST; +import static io.sentry.TypeCheckHint.SPRING_EXCHANGE_FILTER_RESPONSE; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.BaggageHeader; +import io.sentry.Breadcrumb; +import io.sentry.Hint; +import io.sentry.IScopes; +import io.sentry.ISpan; +import io.sentry.SpanDataConvention; +import io.sentry.SpanOptions; +import io.sentry.SpanStatus; +import io.sentry.util.Objects; +import io.sentry.util.SpanUtils; +import io.sentry.util.TracingUtils; +import java.util.Locale; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.web.reactive.function.client.ClientRequest; +import org.springframework.web.reactive.function.client.ClientResponse; +import org.springframework.web.reactive.function.client.ExchangeFilterFunction; +import org.springframework.web.reactive.function.client.ExchangeFunction; +import reactor.core.publisher.Mono; + +@Open +public class SentrySpanClientWebRequestFilter implements ExchangeFilterFunction { + private static final String TRACE_ORIGIN = "auto.http.spring_jakarta.webclient"; + private final @NotNull IScopes scopes; + + public SentrySpanClientWebRequestFilter(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + } + + @Override + public @NotNull Mono filter( + final @NotNull ClientRequest request, final @NotNull ExchangeFunction next) { + final ISpan activeSpan = scopes.getSpan(); + if (activeSpan == null) { + final @NotNull ClientRequest modifiedRequest = maybeAddTracingHeaders(request, null); + addBreadcrumb(modifiedRequest, null); + return next.exchange(modifiedRequest); + } + final @NotNull SpanOptions spanOptions = new SpanOptions(); + spanOptions.setOrigin(TRACE_ORIGIN); + final ISpan span = activeSpan.startChild("http.client", null, spanOptions); + final @NotNull String method = request.method().name(); + span.setDescription(method + " " + request.url()); + span.setData(SpanDataConvention.HTTP_METHOD_KEY, method.toUpperCase(Locale.ROOT)); + + final @NotNull ClientRequest modifiedRequest = maybeAddTracingHeaders(request, span); + + return next.exchange(modifiedRequest) + .flatMap( + response -> { + span.setData(SpanDataConvention.HTTP_STATUS_CODE_KEY, response.statusCode().value()); + span.setStatus(SpanStatus.fromHttpStatusCode(response.statusCode().value())); + addBreadcrumb(request, response); + span.finish(); + return Mono.just(response); + }) + .onErrorMap( + throwable -> { + span.setThrowable(throwable); + span.setStatus(SpanStatus.INTERNAL_ERROR); + addBreadcrumb(request, null); + span.finish(); + return throwable; + }); + } + + private @NotNull ClientRequest maybeAddTracingHeaders( + final @NotNull ClientRequest request, final @Nullable ISpan span) { + if (isIgnored()) { + return request; + } + + final ClientRequest.Builder requestBuilder = ClientRequest.from(request); + + final @Nullable TracingUtils.TracingHeaders tracingHeaders = + TracingUtils.traceIfAllowed( + scopes, + request.url().toString(), + request.headers().get(BaggageHeader.BAGGAGE_HEADER), + span); + + if (tracingHeaders != null) { + requestBuilder.header( + tracingHeaders.getSentryTraceHeader().getName(), + tracingHeaders.getSentryTraceHeader().getValue()); + + final @Nullable BaggageHeader baggageHeader = tracingHeaders.getBaggageHeader(); + if (baggageHeader != null) { + requestBuilder.headers( + httpHeaders -> { + httpHeaders.remove(BaggageHeader.BAGGAGE_HEADER); + httpHeaders.add(baggageHeader.getName(), baggageHeader.getValue()); + }); + } + } + + return requestBuilder.build(); + } + + private boolean isIgnored() { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN); + } + + private void addBreadcrumb( + final @NotNull ClientRequest request, final @Nullable ClientResponse response) { + final Breadcrumb breadcrumb = + Breadcrumb.http( + request.url().toString(), + request.method().name(), + response != null ? response.statusCode().value() : null); + + final Hint hint = new Hint(); + hint.set(SPRING_EXCHANGE_FILTER_REQUEST, request); + if (response != null) { + hint.set(SPRING_EXCHANGE_FILTER_RESPONSE, response); + } + + scopes.addBreadcrumb(breadcrumb, hint); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration.java new file mode 100644 index 00000000000..95544c7992d --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration.java @@ -0,0 +1,31 @@ +package io.sentry.spring.jakarta.tracing; + +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.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** AOP pointcut configuration for {@link SentrySpan}. */ +@Configuration(proxyBeanMethods = false) +@Open +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SentrySpanPointcutConfiguration { + + /** + * Pointcut around which spans are created. + * + * @return pointcut used by {@link SentrySpanAdvice}. + */ + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Pointcut sentrySpanPointcut() { + return new ComposablePointcut(new AnnotationClassFilter(SentrySpan.class, true)) + .union(new AnnotationMatchingPointcut(null, SentrySpan.class)); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingConfiguration.java new file mode 100644 index 00000000000..ffd3d1d0d79 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingConfiguration.java @@ -0,0 +1,18 @@ +package io.sentry.spring.jakarta.tracing; + +import com.jakewharton.nopen.annotation.Open; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; + +/** + * Provides infrastructure beans for creating transactions and spans around bean methods annotated + * with {@link SentryTransaction} and {@link SentrySpan}. + */ +@Configuration +@Import({ + SentryAdviceConfiguration.class, + SentrySpanPointcutConfiguration.class, + SentryTransactionPointcutConfiguration.class +}) +@Open +public class SentryTracingConfiguration {} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java new file mode 100644 index 00000000000..d059338c0dc --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java @@ -0,0 +1,245 @@ +package io.sentry.spring.jakarta.tracing; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.BaggageHeader; +import io.sentry.CustomSamplingContext; +import io.sentry.IScopes; +import io.sentry.ITransaction; +import io.sentry.ScopesAdapter; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanStatus; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; +import io.sentry.protocol.TransactionNameSource; +import io.sentry.util.Objects; +import io.sentry.util.SpanUtils; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.HttpMethod; +import org.springframework.web.filter.OncePerRequestFilter; +import org.springframework.web.servlet.HandlerMapping; +import org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping; + +/** + * Creates {@link ITransaction} around HTTP request executions if performance is enabled. Otherwise + * just reads tracing information from incoming request. + */ +@Open +public class SentryTracingFilter extends OncePerRequestFilter { + /** Operation used by {@link SentryTransaction} created in {@link SentryTracingFilter}. */ + private static final String TRANSACTION_OP = "http.server"; + + private static final String TRACE_ORIGIN = "auto.http.spring_jakarta.webmvc"; + private static final String TRANSACTION_ATTR = "sentry.transaction"; + + private final @NotNull TransactionNameProvider transactionNameProvider; + private final @NotNull IScopes scopes; + private final boolean isAsyncSupportEnabled; + + /** + * Creates filter that resolves transaction name using {@link SpringMvcTransactionNameProvider}. + * + *

Only requests that have {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} request + * attribute set are turned into transactions. This attribute is set in {@link + * RequestMappingInfoHandlerMapping} on request that have not been dropped with any {@link + * jakarta.servlet.Filter}. + */ + public SentryTracingFilter() { + this(ScopesAdapter.getInstance()); + } + + /** + * Creates filter that resolves transaction name using transaction name provider given by + * parameter. + * + * @param scopes - the scopes + * @param transactionNameProvider - transaction name provider. + */ + public SentryTracingFilter( + final @NotNull IScopes scopes, + final @NotNull TransactionNameProvider transactionNameProvider) { + this(scopes, transactionNameProvider, false); + } + + /** + * Creates filter that resolves transaction name using transaction name provider given by + * parameter. + * + * @param scopes - the scopes + * @param transactionNameProvider - transaction name provider. + * @param isAsyncSupportEnabled - whether transactions should be kept open until async handling is + * done + */ + public SentryTracingFilter( + final @NotNull IScopes scopes, + final @NotNull TransactionNameProvider transactionNameProvider, + final boolean isAsyncSupportEnabled) { + this.scopes = Objects.requireNonNull(scopes, "Scopes are required"); + this.transactionNameProvider = + Objects.requireNonNull(transactionNameProvider, "transactionNameProvider is required"); + this.isAsyncSupportEnabled = isAsyncSupportEnabled; + } + + public SentryTracingFilter(final @NotNull IScopes scopes) { + this(scopes, new SpringMvcTransactionNameProvider()); + } + + @Override + protected boolean shouldNotFilterAsyncDispatch() { + return !isAsyncSupportEnabled; + } + + @Override + protected void doFilterInternal( + final @NotNull HttpServletRequest httpRequest, + final @NotNull HttpServletResponse httpResponse, + final @NotNull FilterChain filterChain) + throws ServletException, IOException { + if (scopes.isEnabled() && !isIgnored()) { + @Nullable TransactionContext transactionContext = null; + if (shouldContinueTrace(httpRequest)) { + final @Nullable String sentryTraceHeader = + httpRequest.getHeader(SentryTraceHeader.SENTRY_TRACE_HEADER); + final @Nullable List baggageHeader = + Collections.list(httpRequest.getHeaders(BaggageHeader.BAGGAGE_HEADER)); + transactionContext = scopes.continueTrace(sentryTraceHeader, baggageHeader); + } + if (scopes.getOptions().isTracingEnabled() && shouldTraceRequest(httpRequest)) { + doFilterWithTransaction(httpRequest, httpResponse, filterChain, transactionContext); + } else { + filterChain.doFilter(httpRequest, httpResponse); + } + } else { + filterChain.doFilter(httpRequest, httpResponse); + } + } + + private boolean isIgnored() { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), TRACE_ORIGIN); + } + + private void doFilterWithTransaction( + HttpServletRequest httpRequest, + HttpServletResponse httpResponse, + FilterChain filterChain, + final @Nullable TransactionContext transactionContext) + throws IOException, ServletException { + final @Nullable ITransaction transaction = + getOrStartTransaction(httpRequest, transactionContext); + + try { + filterChain.doFilter(httpRequest, httpResponse); + } catch (Throwable e) { + if (transaction != null) { + // exceptions that are not handled by Spring + transaction.setStatus(SpanStatus.INTERNAL_ERROR); + } + throw e; + } finally { + if (shouldFinishTransaction(httpRequest) && transaction != null) { + // after all filters run, templated path pattern is available in request attribute + final @NotNull TransactionNameWithSource transactionNameWithSource = + transactionNameProvider.provideTransactionNameAndSource(httpRequest); + final @Nullable String transactionName = transactionNameWithSource.getTransactionName(); + final @NotNull TransactionNameSource transactionNameSource = + transactionNameWithSource.getTransactionNameSource(); + // if transaction name is not resolved, the request has not been processed by a controller + // and we should not report it to Sentry + if (transactionName != null) { + transaction.setName(transactionName, transactionNameSource); + transaction.setOperation(TRANSACTION_OP); + // if exception has been thrown, transaction status is already set to INTERNAL_ERROR, and + // httpResponse.getStatus() returns 200. + if (transaction.getStatus() == null) { + transaction.setStatus(SpanStatus.fromHttpStatusCode(httpResponse.getStatus())); + } + transaction.finish(); + } + } + } + } + + private ITransaction getOrStartTransaction( + final @NotNull HttpServletRequest httpRequest, + final @Nullable TransactionContext transactionContext) { + if (isAsyncDispatch(httpRequest)) { + // second invocation of this filter for the same async request already has the transaction + // in the attributes + return (ITransaction) httpRequest.getAttribute(TRANSACTION_ATTR); + } else { + // at this stage we are not able to get real transaction name + final @NotNull ITransaction transaction = startTransaction(httpRequest, transactionContext); + if (shouldStoreTransactionForAsyncProcessing()) { + httpRequest.setAttribute(TRANSACTION_ATTR, transaction); + } + return transaction; + } + } + + /** + * Returns false if an async request is being dispatched (second invocation of the filter for the + * same async request). + * + *

Returns true if not an async request or this is the first invocation of the filter for the + * same async request + */ + private boolean shouldContinueTrace(HttpServletRequest httpRequest) { + return !isAsyncSupportEnabled || !isAsyncDispatch(httpRequest); + } + + private boolean shouldStoreTransactionForAsyncProcessing() { + return isAsyncSupportEnabled; + } + + /** + * Returns false if async request handling has only been started but not yet finished (first + * invocation of this filter for the same async request). + * + *

Returns true if not an async request or async request handling has finished (second + * invocation of this filter for the same async request) + * + *

Note: isAsyncStarted changes its return value after filterChain.doFilter() of the first + * async invocation + */ + private boolean shouldFinishTransaction(HttpServletRequest httpRequest) { + return !isAsyncSupportEnabled || !isAsyncStarted(httpRequest); + } + + private boolean shouldTraceRequest(final @NotNull HttpServletRequest request) { + return scopes.getOptions().isTraceOptionsRequests() + || !HttpMethod.OPTIONS.name().equals(request.getMethod()); + } + + private ITransaction startTransaction( + final @NotNull HttpServletRequest request, + final @Nullable TransactionContext transactionContext) { + + final String name = request.getMethod() + " " + request.getRequestURI(); + + final CustomSamplingContext customSamplingContext = new CustomSamplingContext(); + customSamplingContext.set("request", request); + + final TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setCustomSamplingContext(customSamplingContext); + transactionOptions.setBindToScope(true); + transactionOptions.setOrigin(TRACE_ORIGIN); + + if (transactionContext != null) { + transactionContext.setName(name); + transactionContext.setTransactionNameSource(TransactionNameSource.URL); + transactionContext.setOperation("http.server"); + + return scopes.startTransaction(transactionContext, transactionOptions); + } + + return scopes.startTransaction( + new TransactionContext(name, TransactionNameSource.URL, "http.server"), transactionOptions); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransaction.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransaction.java new file mode 100644 index 00000000000..01d2e9e146b --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransaction.java @@ -0,0 +1,39 @@ +package io.sentry.spring.jakarta.tracing; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import org.springframework.core.annotation.AliasFor; + +/** + * Makes annotated method execution or a method execution within an annotated class to get wrapped + * into {@link io.sentry.protocol.SentryTransaction}. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target({ElementType.METHOD, ElementType.TYPE}) +public @interface SentryTransaction { + + /** + * Transaction name. If not set, transaction name is resolved as a class name and a method name. + * + * @return transaction name + */ + @AliasFor("value") + String name() default ""; + + /** + * A transaction operation, for example "http". + * + * @return transaction operation + */ + String operation(); + + /** + * Transaction name. If not set, transaction name is resolved as a class name and a method name. + * + * @return transaction name + */ + @AliasFor("name") + String value() default ""; +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionAdvice.java new file mode 100644 index 00000000000..95618f76fd7 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionAdvice.java @@ -0,0 +1,125 @@ +package io.sentry.spring.jakarta.tracing; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.ITransaction; +import io.sentry.ScopesAdapter; +import io.sentry.SpanStatus; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; +import io.sentry.protocol.TransactionNameSource; +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.jetbrains.annotations.Nullable; +import org.springframework.aop.support.AopUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.StringUtils; + +/** + * Reports execution of every bean method annotated with {@link SentryTransaction} or a execution of + * a bean method within a class annotated with {@link SentryTransaction}. + */ +@ApiStatus.Internal +@Open +public class SentryTransactionAdvice implements MethodInterceptor { + private static final String TRACE_ORIGIN = "auto.function.spring_jakarta.advice"; + + private final @NotNull IScopes scopesBeforeForking; + + public SentryTransactionAdvice() { + this(ScopesAdapter.getInstance()); + } + + public SentryTransactionAdvice(final @NotNull IScopes scopes) { + this.scopesBeforeForking = Objects.requireNonNull(scopes, "scopes are required"); + } + + @SuppressWarnings("deprecation") + @Override + public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { + final Method mostSpecificMethod = + AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()); + + @Nullable + SentryTransaction sentryTransaction = + AnnotationUtils.findAnnotation(mostSpecificMethod, SentryTransaction.class); + if (sentryTransaction == null) { + sentryTransaction = + AnnotationUtils.findAnnotation( + mostSpecificMethod.getDeclaringClass(), SentryTransaction.class); + } + + final TransactionNameAndSource nameAndSource = + resolveTransactionName(invocation, sentryTransaction); + + final boolean isTransactionActive = isTransactionActive(scopesBeforeForking); + + if (isTransactionActive) { + // transaction is already active, we do not start new transaction + return invocation.proceed(); + } else { + String operation; + if (sentryTransaction != null && !StringUtils.isEmpty(sentryTransaction.operation())) { + operation = sentryTransaction.operation(); + } else { + operation = "bean"; + } + final @NotNull IScopes forkedScopes = + scopesBeforeForking.forkedScopes("SentryTransactionAdvice"); + try (final @NotNull ISentryLifecycleToken ignored = forkedScopes.makeCurrent()) { + final TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setBindToScope(true); + transactionOptions.setOrigin(TRACE_ORIGIN); + final ITransaction transaction = + forkedScopes.startTransaction( + new TransactionContext(nameAndSource.name, nameAndSource.source, operation), + transactionOptions); + try { + final Object result = invocation.proceed(); + transaction.setStatus(SpanStatus.OK); + return result; + } catch (Throwable e) { + transaction.setStatus(SpanStatus.INTERNAL_ERROR); + transaction.setThrowable(e); + throw e; + } finally { + transaction.finish(); + } + } + } + } + + @SuppressWarnings("deprecation") + private @NotNull TransactionNameAndSource resolveTransactionName( + MethodInvocation invocation, @Nullable SentryTransaction sentryTransaction) { + if (sentryTransaction == null || StringUtils.isEmpty(sentryTransaction.value())) { + final String name = + invocation.getMethod().getDeclaringClass().getSimpleName() + + "." + + invocation.getMethod().getName(); + return new TransactionNameAndSource(name, TransactionNameSource.COMPONENT); + } else { + return new TransactionNameAndSource(sentryTransaction.value(), TransactionNameSource.CUSTOM); + } + } + + private boolean isTransactionActive(final @NotNull IScopes scopes) { + return scopes.getSpan() != null; + } + + private static class TransactionNameAndSource { + private final @NotNull String name; + private final @NotNull TransactionNameSource source; + + public TransactionNameAndSource( + final @NotNull String name, final @NotNull TransactionNameSource source) { + this.name = name; + this.source = source; + } + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration.java new file mode 100644 index 00000000000..6f2d886b750 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration.java @@ -0,0 +1,31 @@ +package io.sentry.spring.jakarta.tracing; + +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.beans.factory.config.BeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** AOP pointcut configuration for {@link SentryTransaction}. */ +@Configuration(proxyBeanMethods = false) +@Open +@Role(BeanDefinition.ROLE_INFRASTRUCTURE) +public class SentryTransactionPointcutConfiguration { + + /** + * Pointcut around which transactions are created. + * + * @return pointcut used by {@link SentryTransactionAdvice}. + */ + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public @NotNull Pointcut sentryTransactionPointcut() { + return new ComposablePointcut(new AnnotationClassFilter(SentryTransaction.class, true)) + .union(new AnnotationMatchingPointcut(null, SentryTransaction.class)); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider.java new file mode 100644 index 00000000000..0dc8dd8a8c9 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider.java @@ -0,0 +1,35 @@ +package io.sentry.spring.jakarta.tracing; + +import io.sentry.protocol.TransactionNameSource; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.web.servlet.HandlerMapping; + +/** + * Resolves transaction name using {@link HttpServletRequest#getMethod()} and templated route that + * handled the request. To return correct transaction name, it must be used after request is + * processed by {@link org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping} + * where {@link HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} is set. + */ +@ApiStatus.Internal +public final class SpringMvcTransactionNameProvider implements TransactionNameProvider { + @Override + public @Nullable String provideTransactionName(final @NotNull HttpServletRequest request) { + final String pattern = + (String) request.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + + if (pattern != null) { + return request.getMethod() + " " + pattern; + } else { + return null; + } + } + + @Override + @ApiStatus.Internal + public @NotNull TransactionNameSource provideTransactionSource() { + return TransactionNameSource.ROUTE; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java new file mode 100644 index 00000000000..e0beda65a0b --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java @@ -0,0 +1,22 @@ +package io.sentry.spring.jakarta.tracing; + +import io.sentry.protocol.TransactionNameSource; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Fallback TransactionNameProvider when Spring is used in servlet mode (without MVC). */ +@ApiStatus.Internal +public final class SpringServletTransactionNameProvider implements TransactionNameProvider { + @Override + public @Nullable String provideTransactionName(final @NotNull HttpServletRequest request) { + return request.getMethod() + " " + request.getRequestURI(); + } + + @Override + @ApiStatus.Internal + public @NotNull TransactionNameSource provideTransactionSource() { + return TransactionNameSource.URL; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameProvider.java new file mode 100644 index 00000000000..7c0eac8152f --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameProvider.java @@ -0,0 +1,38 @@ +package io.sentry.spring.jakarta.tracing; + +import io.sentry.protocol.TransactionNameSource; +import jakarta.servlet.http.HttpServletRequest; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * Resolves transaction name from {@link HttpServletRequest}. + * + *

With Spring MVC - use {@link SpringMvcTransactionNameProvider}. + */ +public interface TransactionNameProvider { + /** + * Resolves transaction name from {@link HttpServletRequest}. + * + * @param request - the http request + * @return transaction name or {@code null} if not resolved + */ + @Nullable + String provideTransactionName(@NotNull HttpServletRequest request); + + /** Returns the source of the transaction name. Only to be used internally. */ + @NotNull + @ApiStatus.Internal + default TransactionNameSource provideTransactionSource() { + return TransactionNameSource.CUSTOM; + } + + @NotNull + @ApiStatus.Internal + default TransactionNameWithSource provideTransactionNameAndSource( + final @NotNull HttpServletRequest request) { + return new TransactionNameWithSource( + provideTransactionName(request), provideTransactionSource()); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameWithSource.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameWithSource.java new file mode 100644 index 00000000000..f913b895cf7 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameWithSource.java @@ -0,0 +1,27 @@ +package io.sentry.spring.jakarta.tracing; + +import io.sentry.protocol.TransactionNameSource; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +@ApiStatus.Internal +public final class TransactionNameWithSource { + private final @Nullable String transactionName; + private final @NotNull TransactionNameSource transactionNameSource; + + public TransactionNameWithSource( + final @Nullable String transactionName, + final @NotNull TransactionNameSource transactionNameSource) { + this.transactionName = transactionName; + this.transactionNameSource = transactionNameSource; + } + + public @Nullable String getTransactionName() { + return transactionName; + } + + public @NotNull TransactionNameSource getTransactionNameSource() { + return transactionNameSource; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java new file mode 100644 index 00000000000..57b7b86e40f --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java @@ -0,0 +1,168 @@ +package io.sentry.spring.jakarta.webflux; + +import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_REQUEST; +import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_RESPONSE; + +import io.sentry.BaggageHeader; +import io.sentry.Breadcrumb; +import io.sentry.CustomSamplingContext; +import io.sentry.Hint; +import io.sentry.IScope; +import io.sentry.IScopes; +import io.sentry.ITransaction; +import io.sentry.NoOpScopes; +import io.sentry.Sentry; +import io.sentry.SentryTraceHeader; +import io.sentry.SpanStatus; +import io.sentry.TransactionContext; +import io.sentry.TransactionOptions; +import io.sentry.protocol.TransactionNameSource; +import io.sentry.util.Objects; +import io.sentry.util.SpanUtils; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatusCode; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.http.server.reactive.ServerHttpResponse; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilter; + +/** Manages {@link IScope} in Webflux request processing. */ +@ApiStatus.Experimental +public abstract class AbstractSentryWebFilter implements WebFilter { + private final @NotNull SentryRequestResolver sentryRequestResolver; + public static final String SENTRY_SCOPES_KEY = "sentry-scopes"; + + /** + * @deprecated please use {@link AbstractSentryWebFilter#SENTRY_SCOPES_KEY} instead. + */ + @Deprecated public static final String SENTRY_HUB_KEY = SENTRY_SCOPES_KEY; + + private static final String TRANSACTION_OP = "http.server"; + + public AbstractSentryWebFilter(final @NotNull IScopes scopes) { + Objects.requireNonNull(scopes, "scopes are required"); + this.sentryRequestResolver = new SentryRequestResolver(scopes); + } + + protected @Nullable ITransaction maybeStartTransaction( + final @NotNull IScopes requestScopes, + final @NotNull ServerHttpRequest request, + final @NotNull String origin) { + if (requestScopes.isEnabled() && !isIgnored(requestScopes, origin)) { + final @NotNull HttpHeaders headers = request.getHeaders(); + final @Nullable String sentryTraceHeader = + headers.getFirst(SentryTraceHeader.SENTRY_TRACE_HEADER); + final @Nullable List baggageHeaders = headers.get(BaggageHeader.BAGGAGE_HEADER); + final @Nullable TransactionContext transactionContext = + requestScopes.continueTrace(sentryTraceHeader, baggageHeaders); + + if (requestScopes.getOptions().isTracingEnabled() + && shouldTraceRequest(requestScopes, request)) { + return startTransaction(requestScopes, request, transactionContext, origin); + } + } + + return null; + } + + private boolean isIgnored(final @NotNull IScopes scopes, final @NotNull String origin) { + return SpanUtils.isIgnored(scopes.getOptions().getIgnoredSpanOrigins(), origin); + } + + protected void doFinally( + final @NotNull ServerWebExchange serverWebExchange, + final @NotNull IScopes requestScopes, + final @Nullable ITransaction transaction) { + if (transaction != null) { + finishTransaction(serverWebExchange, transaction); + } + Sentry.setCurrentScopes(NoOpScopes.getInstance()); + } + + protected void doFirst( + final @NotNull ServerWebExchange serverWebExchange, final @NotNull IScopes requestScopes) { + if (requestScopes.isEnabled()) { + serverWebExchange.getAttributes().put(SENTRY_SCOPES_KEY, requestScopes); + final ServerHttpRequest request = serverWebExchange.getRequest(); + final ServerHttpResponse response = serverWebExchange.getResponse(); + + final Hint hint = new Hint(); + hint.set(WEBFLUX_FILTER_REQUEST, request); + hint.set(WEBFLUX_FILTER_RESPONSE, response); + final String methodName = + request.getMethod() != null ? request.getMethod().name() : "unknown"; + requestScopes.addBreadcrumb(Breadcrumb.http(request.getURI().toString(), methodName), hint); + requestScopes.configureScope( + scope -> scope.setRequest(sentryRequestResolver.resolveSentryRequest(request))); + } + } + + protected void doOnError(final @Nullable ITransaction transaction, final @NotNull Throwable e) { + if (transaction != null) { + transaction.setStatus(SpanStatus.INTERNAL_ERROR); + transaction.setThrowable(e); + } + } + + protected boolean shouldTraceRequest( + final @NotNull IScopes scopes, final @NotNull ServerHttpRequest request) { + return scopes.getOptions().isTraceOptionsRequests() + || !HttpMethod.OPTIONS.equals(request.getMethod()); + } + + private void finishTransaction(ServerWebExchange exchange, ITransaction transaction) { + String transactionName = TransactionNameProvider.provideTransactionName(exchange); + if (transactionName != null) { + transaction.setName(transactionName, TransactionNameSource.ROUTE); + transaction.setOperation(TRANSACTION_OP); + } + final @Nullable ServerHttpResponse response = exchange.getResponse(); + if (response != null) { + final @Nullable HttpStatusCode statusCode = response.getStatusCode(); + if (statusCode != null) { + transaction + .getContexts() + .withResponse( + (sentryResponse) -> { + sentryResponse.setStatusCode(statusCode.value()); + }); + if (transaction.getStatus() == null) { + transaction.setStatus(SpanStatus.fromHttpStatusCode(statusCode.value())); + } + } + } + transaction.finish(); + } + + protected @NotNull ITransaction startTransaction( + final @NotNull IScopes scopes, + final @NotNull ServerHttpRequest request, + final @Nullable TransactionContext transactionContext, + final @NotNull String origin) { + final @NotNull String name = request.getMethod() + " " + request.getURI().getPath(); + final @NotNull CustomSamplingContext customSamplingContext = new CustomSamplingContext(); + customSamplingContext.set("request", request); + + final TransactionOptions transactionOptions = new TransactionOptions(); + transactionOptions.setCustomSamplingContext(customSamplingContext); + transactionOptions.setBindToScope(true); + transactionOptions.setOrigin(origin); + + if (transactionContext != null) { + transactionContext.setName(name); + transactionContext.setTransactionNameSource(TransactionNameSource.URL); + transactionContext.setOperation(TRANSACTION_OP); + + return scopes.startTransaction(transactionContext, transactionOptions); + } + + return scopes.startTransaction( + new TransactionContext(name, TransactionNameSource.URL, TRANSACTION_OP), + transactionOptions); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java new file mode 100644 index 00000000000..d58291ade6e --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java @@ -0,0 +1,70 @@ +package io.sentry.spring.jakarta.webflux; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.protocol.Request; +import io.sentry.util.HttpUtils; +import io.sentry.util.Objects; +import io.sentry.util.UrlUtils; +import java.net.URI; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.HttpHeaders; +import org.springframework.http.server.reactive.ServerHttpRequest; + +@Open +@ApiStatus.Experimental +public class SentryRequestResolver { + private final @NotNull IScopes scopes; + + public SentryRequestResolver(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); + } + + public @NotNull Request resolveSentryRequest(final @NotNull ServerHttpRequest httpRequest) { + final Request sentryRequest = new Request(); + final String methodName = + httpRequest.getMethod() != null ? httpRequest.getMethod().name() : "unknown"; + sentryRequest.setMethod(methodName); + final @NotNull URI uri = httpRequest.getURI(); + final @NotNull UrlUtils.UrlDetails urlDetails = UrlUtils.parse(uri.toString()); + urlDetails.applyToRequest(sentryRequest); + sentryRequest.setHeaders(resolveHeadersMap(httpRequest.getHeaders())); + + if (scopes.getOptions().isSendDefaultPii()) { + String headerName = HttpUtils.COOKIE_HEADER_NAME; + sentryRequest.setCookies( + toString( + HttpUtils.filterOutSecurityCookiesFromHeader( + httpRequest.getHeaders().get(headerName), headerName, Collections.emptyList()))); + } + return sentryRequest; + } + + @NotNull + Map resolveHeadersMap(final HttpHeaders request) { + final Map headersMap = new HashMap<>(); + for (Map.Entry> entry : request.entrySet()) { + // do not copy personal information identifiable headers + String headerName = entry.getKey(); + if (scopes.getOptions().isSendDefaultPii() + || !HttpUtils.containsSensitiveHeader(headerName)) { + headersMap.put( + headerName, + toString( + HttpUtils.filterOutSecurityCookiesFromHeader( + entry.getValue(), headerName, Collections.emptyList()))); + } + } + return headersMap; + } + + private static @Nullable String toString(final @Nullable List enumeration) { + return enumeration != null ? String.join(",", enumeration) : null; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java new file mode 100644 index 00000000000..57a74732ea8 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java @@ -0,0 +1,26 @@ +package io.sentry.spring.jakarta.webflux; + +import io.sentry.IScopes; +import io.sentry.ISentryLifecycleToken; +import io.sentry.Sentry; +import java.util.function.Function; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; + +/** + * Hook meant to used with {@link reactor.core.scheduler.Schedulers#onScheduleHook(String, + * Function)} to configure Reactor to copy correct scopes into the operating thread. + */ +@ApiStatus.Experimental +public final class SentryScheduleHook implements Function { + @Override + public Runnable apply(final @NotNull Runnable runnable) { + final IScopes newScopes = Sentry.getCurrentScopes().forkedCurrentScope("spring.scheduleHook"); + + return () -> { + try (final @NotNull ISentryLifecycleToken ignored = newScopes.makeCurrent()) { + runnable.run(); + } + }; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java new file mode 100644 index 00000000000..1e1e387eb21 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java @@ -0,0 +1,73 @@ +package io.sentry.spring.jakarta.webflux; + +import static io.sentry.TypeCheckHint.WEBFLUX_EXCEPTION_HANDLER_EXCHANGE; +import static io.sentry.TypeCheckHint.WEBFLUX_EXCEPTION_HANDLER_REQUEST; +import static io.sentry.TypeCheckHint.WEBFLUX_EXCEPTION_HANDLER_RESPONSE; + +import io.sentry.Hint; +import io.sentry.IScopes; +import io.sentry.SentryEvent; +import io.sentry.SentryLevel; +import io.sentry.exception.ExceptionMechanismException; +import io.sentry.protocol.Mechanism; +import io.sentry.reactor.SentryReactorUtils; +import io.sentry.util.Objects; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.core.annotation.Order; +import org.springframework.web.server.ResponseStatusException; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebExceptionHandler; +import reactor.core.publisher.Mono; + +/** Handles unhandled exceptions in Spring WebFlux integration. */ +@Order( + -2) // the DefaultErrorWebExceptionHandler provided by Spring Boot for error handling is ordered +// at -1 +@ApiStatus.Experimental +public final class SentryWebExceptionHandler implements WebExceptionHandler { + public static final String MECHANISM_TYPE = "Spring6WebFluxExceptionResolver"; + private final @NotNull IScopes scopes; + + public SentryWebExceptionHandler(final @NotNull IScopes scopes) { + this.scopes = Objects.requireNonNull(scopes, "scopes are required"); + } + + @Override + public @NotNull Mono handle( + final @NotNull ServerWebExchange serverWebExchange, final @NotNull Throwable ex) { + final @Nullable IScopes requestScopes = + serverWebExchange.getAttributeOrDefault(SentryWebFilter.SENTRY_SCOPES_KEY, null); + final @NotNull IScopes scopesToUse = requestScopes != null ? requestScopes : scopes; + + return SentryReactorUtils.withSentryScopes( + Mono.just(ex) + .map( + it -> { + if (!(ex instanceof ResponseStatusException)) { + final Mechanism mechanism = new Mechanism(); + mechanism.setType(MECHANISM_TYPE); + mechanism.setHandled(false); + final Throwable throwable = + new ExceptionMechanismException(mechanism, ex, Thread.currentThread()); + final SentryEvent event = new SentryEvent(throwable); + event.setLevel(SentryLevel.FATAL); + event.setTransaction( + TransactionNameProvider.provideTransactionName(serverWebExchange)); + + final Hint hint = new Hint(); + hint.set(WEBFLUX_EXCEPTION_HANDLER_REQUEST, serverWebExchange.getRequest()); + hint.set( + WEBFLUX_EXCEPTION_HANDLER_RESPONSE, serverWebExchange.getResponse()); + hint.set(WEBFLUX_EXCEPTION_HANDLER_EXCHANGE, serverWebExchange); + + scopes.captureEvent(event, hint); + } + + return it; + }), + scopesToUse) + .flatMap(it -> Mono.error(ex)); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java new file mode 100644 index 00000000000..0ec4b44a0e5 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java @@ -0,0 +1,45 @@ +package io.sentry.spring.jakarta.webflux; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScope; +import io.sentry.IScopes; +import io.sentry.ITransaction; +import io.sentry.Sentry; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.http.server.reactive.ServerHttpRequest; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** Manages {@link IScope} in Webflux request processing. */ +@ApiStatus.Experimental +@Open +public class SentryWebFilter extends AbstractSentryWebFilter { + + private static final String TRACE_ORIGIN = "auto.spring_jakarta.webflux"; + + public SentryWebFilter(final @NotNull IScopes scopes) { + super(scopes); + } + + @Override + public Mono filter( + final @NotNull ServerWebExchange serverWebExchange, + final @NotNull WebFilterChain webFilterChain) { + @NotNull IScopes requestScopes = Sentry.forkedRootScopes("request.webflux"); + final ServerHttpRequest request = serverWebExchange.getRequest(); + final @Nullable ITransaction transaction = + maybeStartTransaction(requestScopes, request, TRACE_ORIGIN); + return webFilterChain + .filter(serverWebExchange) + .doFinally(__ -> doFinally(serverWebExchange, requestScopes, transaction)) + .doOnError(e -> doOnError(transaction, e)) + .doFirst( + () -> { + Sentry.setCurrentScopes(requestScopes); + doFirst(serverWebExchange, requestScopes); + }); + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java new file mode 100644 index 00000000000..7748d43a012 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java @@ -0,0 +1,53 @@ +package io.sentry.spring.jakarta.webflux; + +import io.sentry.IScope; +import io.sentry.IScopes; +import io.sentry.ITransaction; +import io.sentry.Sentry; +import io.sentry.reactor.SentryReactorUtils; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.server.WebFilterChain; +import reactor.core.publisher.Mono; + +/** Manages {@link IScope} in Webflux request processing. */ +@ApiStatus.Experimental +public final class SentryWebFilterWithThreadLocalAccessor extends AbstractSentryWebFilter { + + public static final String TRACE_ORIGIN = "auto.spring_jakarta.webflux"; + + public SentryWebFilterWithThreadLocalAccessor(final @NotNull IScopes scopes) { + super(scopes); + } + + @Override + public Mono filter( + final @NotNull ServerWebExchange serverWebExchange, + final @NotNull WebFilterChain webFilterChain) { + final @NotNull TransactionContainer transactionContainer = new TransactionContainer(); + return SentryReactorUtils.withSentryForkedRoots( + webFilterChain + .filter(serverWebExchange) + .doFinally( + __ -> + doFinally( + serverWebExchange, + Sentry.getCurrentScopes(), + transactionContainer.transaction)) + .doOnError(e -> doOnError(transactionContainer.transaction, e)) + .doFirst( + () -> { + doFirst(serverWebExchange, Sentry.getCurrentScopes()); + final ITransaction transaction = + maybeStartTransaction( + Sentry.getCurrentScopes(), serverWebExchange.getRequest(), TRACE_ORIGIN); + transactionContainer.transaction = transaction; + })); + } + + private static class TransactionContainer { + private volatile @Nullable ITransaction transaction; + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/TransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/TransactionNameProvider.java new file mode 100644 index 00000000000..33b3eba36be --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/TransactionNameProvider.java @@ -0,0 +1,32 @@ +package io.sentry.spring.jakarta.webflux; + +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.server.ServerWebExchange; +import org.springframework.web.util.pattern.PathPattern; + +/** + * Resolves transaction name using {@link ServerWebExchange#getRequest()} ()} and templated route + * that handled the request. To return correct transaction name, it must be used after request is + * processed by {@link + * org.springframework.web.reactive.result.method.RequestMappingInfoHandlerMapping} where {@link + * HandlerMapping#BEST_MATCHING_PATTERN_ATTRIBUTE} is set. + */ +final class TransactionNameProvider { + static @Nullable String provideTransactionName( + final @NotNull ServerWebExchange serverWebExchange) { + final PathPattern pattern = + serverWebExchange.getAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE); + + if (pattern != null) { + final String methodName = + serverWebExchange.getRequest().getMethod() != null + ? serverWebExchange.getRequest().getMethod().name() + : "unknown"; + return methodName + " " + pattern.getPatternString(); + } else { + return null; + } + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java new file mode 100644 index 00000000000..b3249570369 --- /dev/null +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java @@ -0,0 +1,9 @@ +package io.sentry.spring.jakarta.webflux.reactor; + +import io.sentry.reactor.SentryReactorUtils; + +/** + * @deprecated Please use {@link SentryReactorUtils} directly. + */ +@Deprecated +public final class ReactorUtils extends SentryReactorUtils {} diff --git a/sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer new file mode 100644 index 00000000000..69f38f69c51 --- /dev/null +++ b/sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -0,0 +1 @@ +io.sentry.spring.jakarta.SentrySpringServletContainerInitializer diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/ContextTagsEventProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/ContextTagsEventProcessorTest.kt new file mode 100644 index 00000000000..5f91887a50b --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/ContextTagsEventProcessorTest.kt @@ -0,0 +1,86 @@ +package io.sentry.spring.jakarta + +import io.sentry.SentryEvent +import io.sentry.SentryOptions +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import org.slf4j.MDC + +class ContextTagsEventProcessorTest { + class Fixture { + fun getSut( + contextTags: List = emptyList(), + mdcTags: Map = emptyMap(), + ): ContextTagsEventProcessor { + val options = SentryOptions().apply { contextTags.forEach { tag -> addContextTag(tag) } } + val sut = ContextTagsEventProcessor(options) + mdcTags.forEach { MDC.put(it.key, it.value) } + return sut + } + } + + private val fixture = Fixture() + + @BeforeTest + fun before() { + MDC.clear() + } + + @Test + fun `does not copy tags if no tags are set on options`() { + val sut = fixture.getSut() + + val result = sut.process(SentryEvent(), null) + val tags = result.tags + assertTrue(tags == null || tags.isEmpty()) + } + + @Test + fun `copies mdc tags`() { + val sut = + fixture.getSut(contextTags = listOf("user-id"), mdcTags = mapOf("user-id" to "user-id-value")) + + val result = sut.process(SentryEvent(), null) + val tags = result.tags + assertNotNull(tags) { + assertTrue(it.containsKey("user-id")) + assertEquals("user-id-value", it["user-id"]) + } + } + + @Test + fun `does not copy tags not defined in options`() { + val sut = + fixture.getSut( + contextTags = listOf("user-id"), + mdcTags = mapOf("user-id" to "user-id-value", "request-id" to "request-id-value"), + ) + + val result = sut.process(SentryEvent(), null) + val tags = result.tags + assertNotNull(tags) { + assertTrue(it.containsKey("user-id")) + assertFalse(it.containsKey("request-id")) + } + } + + @Test + fun `does not copy tag not set in MDC`() { + val sut = + fixture.getSut( + contextTags = listOf("user-id", "another-tag"), + mdcTags = mapOf("user-id" to "user-id-value"), + ) + + val result = sut.process(SentryEvent(), null) + val tags = result.tags + assertNotNull(tags) { + assertTrue(it.containsKey("user-id")) + assertFalse(it.containsKey("another-tag")) + } + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt new file mode 100644 index 00000000000..3fa5a38ac22 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt @@ -0,0 +1,239 @@ +package io.sentry.spring.jakarta + +import io.sentry.EventProcessor +import io.sentry.IScopes +import io.sentry.ITransportFactory +import io.sentry.Integration +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.transport.ITransport +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.boot.context.annotation.UserConfigurations +import org.springframework.boot.test.context.runner.ApplicationContextRunner +import org.springframework.context.annotation.Bean + +class EnableSentryTest { + private val contextRunner = + ApplicationContextRunner().withConfiguration(UserConfigurations.of(AppConfig::class.java)) + + @Test + fun `sets properties from environment on SentryOptions`() { + ApplicationContextRunner() + .withConfiguration(UserConfigurations.of(AppConfigWithDefaultSendPii::class.java)) + .run { + assertThat(it).hasSingleBean(SentryOptions::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.dsn).isEqualTo("http://key@localhost/proj") + assertThat(options.isSendDefaultPii).isTrue() + } + + ApplicationContextRunner() + .withConfiguration(UserConfigurations.of(AppConfigWithEmptyDsn::class.java)) + .run { + assertThat(it).hasSingleBean(SentryOptions::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.dsn).isEmpty() + assertThat(options.isSendDefaultPii).isFalse() + } + } + + @Test + fun `sets client name and SDK version`() { + contextRunner.run { + assertThat(it).hasSingleBean(SentryOptions::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.sentryClientName).isEqualTo("sentry.java.spring.jakarta") + assertThat(options.sdkVersion).isNotNull + assertThat(options.sdkVersion!!.name).isEqualTo("sentry.java.spring.jakarta") + assertThat(options.sdkVersion!!.version).isEqualTo(BuildConfig.VERSION_NAME) + assertThat(options.sdkVersion!!.packageSet.map { pkg -> pkg.name }) + .contains("maven:io.sentry:sentry-spring-jakarta") + assertThat(options.sdkVersion!!.integrationSet).contains("Spring6") + } + } + + @Test + fun `enables external configuration on SentryOptions`() { + contextRunner.run { + assertThat(it).hasSingleBean(SentryOptions::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.isEnableExternalConfiguration).isTrue() + } + } + + @Test + fun `creates Sentry Hub`() { + contextRunner.run { assertThat(it).hasSingleBean(IScopes::class.java) } + } + + @Test + fun `creates SentryExceptionResolver`() { + contextRunner.run { + assertThat(it).hasSingleBean(SentryExceptionResolver::class.java) + assertThat(it) + .getBean(SentryExceptionResolver::class.java) + .hasFieldOrPropertyWithValue("order", 1) + } + } + + @Test + fun `creates SentryExceptionResolver with order set in the @EnableSentry annotation`() { + ApplicationContextRunner() + .withConfiguration( + UserConfigurations.of(AppConfigWithExceptionResolverOrderIntegerMaxValue::class.java) + ) + .run { + assertThat(it).hasSingleBean(SentryExceptionResolver::class.java) + assertThat(it) + .getBean(SentryExceptionResolver::class.java) + .hasFieldOrPropertyWithValue("order", Integer.MAX_VALUE) + } + } + + @Test + fun `configures custom TracesSamplerCallback`() { + ApplicationContextRunner() + .withConfiguration( + UserConfigurations.of(AppConfigWithCustomTracesSamplerCallback::class.java) + ) + .run { + val options = it.getBean(SentryOptions::class.java) + val samplerCallback = it.getBean(SentryOptions.TracesSamplerCallback::class.java) + assertThat(options.tracesSampler).isEqualTo(samplerCallback) + } + } + + @Test + fun `configures custom TransportFactory`() { + ApplicationContextRunner() + .withConfiguration(UserConfigurations.of(AppConfigWithCustomTransportFactory::class.java)) + .run { + val options = it.getBean(SentryOptions::class.java) + val transportFactory = it.getBean(ITransportFactory::class.java) + assertThat(options.transportFactory).isEqualTo(transportFactory) + } + } + + @Test + fun `configures options with options configuration`() { + ApplicationContextRunner() + .withConfiguration(UserConfigurations.of(AppConfigWithCustomOptionsConfiguration::class.java)) + .run { + val options = it.getBean(SentryOptions::class.java) + assertThat(options.environment).isEqualTo("from-options-configuration") + } + } + + @Test + fun `configures custom before send callback`() { + ApplicationContextRunner() + .withConfiguration(UserConfigurations.of(AppConfigWithCustomBeforeSendCallback::class.java)) + .run { + val beforeSendCallback = it.getBean(SentryOptions.BeforeSendCallback::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.beforeSend).isEqualTo(beforeSendCallback) + } + } + + @Test + fun `configures custom before breadcrumb callback`() { + ApplicationContextRunner() + .withConfiguration( + UserConfigurations.of(AppConfigWithCustomBeforeBreadcrumbCallback::class.java) + ) + .run { + val beforeBreadcrumbCallback = + it.getBean(SentryOptions.BeforeBreadcrumbCallback::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.beforeBreadcrumb).isEqualTo(beforeBreadcrumbCallback) + } + } + + @Test + fun `configures custom event processors`() { + ApplicationContextRunner() + .withConfiguration(UserConfigurations.of(AppConfigWithCustomEventProcessors::class.java)) + .run { + val firstProcessor = it.getBean("firstProcessor", EventProcessor::class.java) + val secondProcessor = it.getBean("secondProcessor", EventProcessor::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.eventProcessors).contains(firstProcessor, secondProcessor) + } + } + + @Test + fun `configures custom integrations`() { + ApplicationContextRunner() + .withConfiguration(UserConfigurations.of(AppConfigWithCustomIntegrations::class.java)) + .run { + val firstIntegration = it.getBean("firstIntegration", Integration::class.java) + val secondIntegration = it.getBean("secondIntegration", Integration::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.integrations).contains(firstIntegration, secondIntegration) + } + } + + @EnableSentry(dsn = "http://key@localhost/proj") class AppConfig + + @EnableSentry(dsn = "") class AppConfigWithEmptyDsn + + @EnableSentry(dsn = "http://key@localhost/proj", sendDefaultPii = true) + class AppConfigWithDefaultSendPii + + @EnableSentry(dsn = "http://key@localhost/proj", exceptionResolverOrder = Integer.MAX_VALUE) + class AppConfigWithExceptionResolverOrderIntegerMaxValue + + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomTracesSamplerCallback { + @Bean + fun tracesSampler(): SentryOptions.TracesSamplerCallback { + return SentryOptions.TracesSamplerCallback { + return@TracesSamplerCallback 1.0 + } + } + } + + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomTransportFactory { + @Bean + fun transport() = + mock().also { + whenever(it.create(any(), any())).thenReturn(mock()) + } + } + + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomOptionsConfiguration { + @Bean + fun optionsConfiguration() = + Sentry.OptionsConfiguration { it.environment = "from-options-configuration" } + } + + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomBeforeSendCallback { + @Bean fun beforeSendCallback() = mock() + } + + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomBeforeBreadcrumbCallback { + @Bean fun beforeBreadcrumbCallback() = mock() + } + + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomEventProcessors { + @Bean fun firstProcessor() = mock() + + @Bean fun secondProcessor() = mock() + } + + @EnableSentry(dsn = "http://key@localhost/proj") + class AppConfigWithCustomIntegrations { + @Bean fun firstIntegration() = mock() + + @Bean fun secondIntegration() = mock() + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/HttpServletRequestSentryUserProviderTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/HttpServletRequestSentryUserProviderTest.kt new file mode 100644 index 00000000000..f2cce25574d --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/HttpServletRequestSentryUserProviderTest.kt @@ -0,0 +1,63 @@ +package io.sentry.spring.jakarta + +import io.sentry.SentryOptions +import java.security.Principal +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.web.context.request.RequestContextHolder +import org.springframework.web.context.request.ServletRequestAttributes + +class HttpServletRequestSentryUserProviderTest { + @Test + fun `attaches user's IP address to Sentry Event`() { + val request = MockHttpServletRequest() + request.addHeader("X-FORWARDED-FOR", "192.168.0.1,192.168.0.2") + RequestContextHolder.setRequestAttributes(ServletRequestAttributes(request)) + + val options = SentryOptions() + options.isSendDefaultPii = true + val userProvider = HttpServletRequestSentryUserProvider(options) + val result = userProvider.provideUser() + + assertNotNull(result) + assertEquals("192.168.0.1", result.ipAddress) + } + + @Test + fun `attaches username to Sentry Event`() { + val principal = mock() + whenever(principal.name).thenReturn("janesmith") + val request = MockHttpServletRequest() + request.userPrincipal = principal + RequestContextHolder.setRequestAttributes(ServletRequestAttributes(request)) + + val options = SentryOptions() + options.isSendDefaultPii = true + val userProvider = HttpServletRequestSentryUserProvider(options) + val result = userProvider.provideUser() + + assertNotNull(result) + assertEquals("janesmith", result.username) + } + + @Test + fun `when sendDefaultPii is set to false, does not attach user data Sentry Event`() { + val principal = mock() + whenever(principal.name).thenReturn("janesmith") + val request = MockHttpServletRequest() + request.userPrincipal = principal + RequestContextHolder.setRequestAttributes(ServletRequestAttributes(request)) + + val options = SentryOptions() + options.isSendDefaultPii = false + val userProvider = HttpServletRequestSentryUserProvider(options) + val result = userProvider.provideUser() + + assertNull(result) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt new file mode 100644 index 00000000000..c6edd834530 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt @@ -0,0 +1,310 @@ +package io.sentry.spring.jakarta + +import io.sentry.CheckIn +import io.sentry.CheckInStatus +import io.sentry.IScopes +import io.sentry.ISentryLifecycleToken +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.protocol.SentryId +import io.sentry.spring.jakarta.checkin.SentryCheckIn +import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration +import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.inOrder +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy +import org.springframework.context.annotation.Import +import org.springframework.context.support.PropertySourcesPlaceholderConfigurer +import org.springframework.test.context.TestPropertySource +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.util.StringValueResolver + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentryCheckInAdviceTest.Config::class) +@TestPropertySource(properties = ["my.cron.slug = mypropertycronslug"]) +class SentryCheckInAdviceTest { + + @Autowired lateinit var sampleService: SampleService + + @Autowired lateinit var sampleServiceNoSlug: SampleServiceNoSlug + + @Autowired lateinit var sampleServiceHeartbeat: SampleServiceHeartbeat + + @Autowired lateinit var sampleServiceSpringProperties: SampleServiceSpringProperties + + @Autowired lateinit var scopes: IScopes + + val lifecycleToken = mock() + + @BeforeTest + fun setup() { + reset(scopes) + whenever(scopes.options).thenReturn(SentryOptions()) + whenever(scopes.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + } + + @Test + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleService.hello() + assertEquals(1, result) + assertEquals(2, checkInCaptor.allValues.size) + + val inProgressCheckIn = checkInCaptor.firstValue + assertEquals("monitor_slug_1", inProgressCheckIn.monitorSlug) + assertEquals(CheckInStatus.IN_PROGRESS.apiName(), inProgressCheckIn.status) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_1", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + + val order = inOrder(scopes, lifecycleToken) + order.verify(scopes).forkedScopes(any()) + order.verify(scopes).makeCurrent() + order.verify(scopes, times(2)).captureCheckIn(any()) + order.verify(lifecycleToken).close() + } + + @Test + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + assertThrows { sampleService.oops() } + assertEquals(2, checkInCaptor.allValues.size) + + val inProgressCheckIn = checkInCaptor.firstValue + assertEquals("monitor_slug_1e", inProgressCheckIn.monitorSlug) + assertEquals(CheckInStatus.IN_PROGRESS.apiName(), inProgressCheckIn.status) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_1e", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + + val order = inOrder(scopes, lifecycleToken) + order.verify(scopes).forkedScopes(any()) + order.verify(scopes).makeCurrent() + order.verify(scopes, times(2)).captureCheckIn(any()) + order.verify(lifecycleToken).close() + } + + @Test + fun `when method is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceHeartbeat.hello() + assertEquals(1, result) + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_2", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + + val order = inOrder(scopes, lifecycleToken) + order.verify(scopes).forkedScopes(any()) + order.verify(scopes).makeCurrent() + order.verify(scopes).captureCheckIn(any()) + order.verify(lifecycleToken).close() + } + + @Test + fun `when method is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end with error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + assertThrows { sampleServiceHeartbeat.oops() } + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("monitor_slug_2e", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.ERROR.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + + val order = inOrder(scopes, lifecycleToken) + order.verify(scopes).forkedScopes(any()) + order.verify(scopes).makeCurrent() + order.verify(scopes).captureCheckIn(any()) + order.verify(lifecycleToken).close() + } + + @Test + fun `when method is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceNoSlug.hello() + assertEquals(1, result) + assertEquals(0, checkInCaptor.allValues.size) + + verify(scopes, never()).forkedScopes(any()) + verify(scopes, never()).makeCurrent() + verify(scopes, never()).captureCheckIn(any()) + verify(lifecycleToken, never()).close() + } + + @Test + fun `when @SentryCheckIn is passed a spring property it is resolved correctly`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceSpringProperties.hello() + assertEquals(1, result) + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("mypropertycronslug", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + + val order = inOrder(scopes, lifecycleToken) + order.verify(scopes).forkedScopes(any()) + order.verify(scopes).makeCurrent() + order.verify(scopes).captureCheckIn(any()) + order.verify(lifecycleToken).close() + } + + @Test + fun `when @SentryCheckIn is passed a spring property that does not exist, raw value is used`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceSpringProperties.helloUnresolvedProperty() + assertEquals(1, result) + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("\${my.cron.unresolved.property}", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + + val order = inOrder(scopes, lifecycleToken) + order.verify(scopes).forkedScopes(any()) + order.verify(scopes).makeCurrent() + order.verify(scopes).captureCheckIn(any()) + order.verify(lifecycleToken).close() + } + + @Test + fun `when @SentryCheckIn is passed a spring property that causes an exception, raw value is used`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(scopes.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceSpringProperties.helloExceptionProperty() + assertEquals(1, result) + assertEquals(1, checkInCaptor.allValues.size) + + val doneCheckIn = checkInCaptor.lastValue + assertEquals("\${my.cron.exception.property}", doneCheckIn.monitorSlug) + assertEquals(CheckInStatus.OK.apiName(), doneCheckIn.status) + assertNotNull(doneCheckIn.duration) + + val order = inOrder(scopes, lifecycleToken) + order.verify(scopes).forkedScopes(any()) + order.verify(scopes).makeCurrent() + order.verify(scopes).captureCheckIn(any()) + order.verify(lifecycleToken).close() + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + @Import(SentryCheckInAdviceConfiguration::class, SentryCheckInPointcutConfiguration::class) + open class Config { + + @Bean open fun sampleService() = SampleService() + + @Bean open fun sampleServiceNoSlug() = SampleServiceNoSlug() + + @Bean open fun sampleServiceHeartbeat() = SampleServiceHeartbeat() + + @Bean open fun sampleServiceSpringProperties() = SampleServiceSpringProperties() + + @Bean + open fun scopes(): IScopes { + val scopes = mock() + Sentry.setCurrentScopes(scopes) + return scopes + } + + companion object { + @Bean + @JvmStatic + fun propertySourcesPlaceholderConfigurer() = MyPropertyPlaceholderConfigurer() + } + } + + open class SampleService { + + @SentryCheckIn("monitor_slug_1") open fun hello() = 1 + + @SentryCheckIn("monitor_slug_1e") + open fun oops() { + throw RuntimeException("thrown on purpose") + } + } + + open class SampleServiceNoSlug { + + @SentryCheckIn open fun hello() = 1 + } + + open class SampleServiceHeartbeat { + + @SentryCheckIn(monitorSlug = "monitor_slug_2", heartbeat = true) open fun hello() = 1 + + @SentryCheckIn(monitorSlug = "monitor_slug_2e", heartbeat = true) + open fun oops() { + throw RuntimeException("thrown on purpose") + } + } + + open class SampleServiceSpringProperties { + + @SentryCheckIn("\${my.cron.slug}", heartbeat = true) open fun hello() = 1 + + @SentryCheckIn("\${my.cron.unresolved.property}", heartbeat = true) + open fun helloUnresolvedProperty() = 1 + + @SentryCheckIn("\${my.cron.exception.property}", heartbeat = true) + open fun helloExceptionProperty() = 1 + } + + class MyPropertyPlaceholderConfigurer : PropertySourcesPlaceholderConfigurer() { + + override fun doProcessProperties( + beanFactoryToProcess: ConfigurableListableBeanFactory, + valueResolver: StringValueResolver, + ) { + val wrappedResolver = StringValueResolver { strVal: String -> + if ("\${my.cron.exception.property}".equals(strVal)) { + throw IllegalArgumentException("Cannot resolve property: $strVal") + } else { + valueResolver.resolveStringValue(strVal) + } + } + super.doProcessProperties(beanFactoryToProcess, wrappedResolver) + } + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryExceptionResolverTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryExceptionResolverTest.kt new file mode 100644 index 00000000000..2513b5a7abb --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryExceptionResolverTest.kt @@ -0,0 +1,117 @@ +package io.sentry.spring.jakarta + +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.SentryEvent +import io.sentry.SentryLevel +import io.sentry.exception.ExceptionMechanismException +import io.sentry.spring.jakarta.tracing.TransactionNameProvider +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.any +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class SentryExceptionResolverTest { + private val scopes = mock() + private val transactionNameProvider = mock() + + private val request = mock() + private val response = mock() + + @Test + fun `when handles exception, sets wrapped exception for event`() { + val eventCaptor = argumentCaptor() + whenever(scopes.captureEvent(eventCaptor.capture(), any())).thenReturn(null) + val expectedCause = RuntimeException("test") + + SentryExceptionResolver(scopes, transactionNameProvider, 1) + .resolveException(request, response, null, expectedCause) + + assertThat(eventCaptor.firstValue.throwable).isEqualTo(expectedCause) + assertThat(eventCaptor.firstValue.throwableMechanism) + .isInstanceOf(ExceptionMechanismException::class.java) + with(eventCaptor.firstValue.throwableMechanism as ExceptionMechanismException) { + assertThat(exceptionMechanism.isHandled).isFalse + assertThat(exceptionMechanism.type).isEqualTo(SentryExceptionResolver.MECHANISM_TYPE) + assertThat(throwable).isEqualTo(expectedCause) + assertThat(thread).isEqualTo(Thread.currentThread()) + assertThat(isSnapshot).isFalse + } + } + + @Test + fun `when handles exception, sets fatal level for event`() { + val eventCaptor = argumentCaptor() + whenever(scopes.captureEvent(eventCaptor.capture(), any())).thenReturn(null) + + SentryExceptionResolver(scopes, transactionNameProvider, 1) + .resolveException(request, response, null, RuntimeException("test")) + + assertThat(eventCaptor.firstValue.level).isEqualTo(SentryLevel.FATAL) + } + + @Test + fun `when handles exception, sets transaction name for event`() { + val expectedTransactionName = "test-transaction" + whenever(transactionNameProvider.provideTransactionName(any())) + .thenReturn(expectedTransactionName) + val eventCaptor = argumentCaptor() + whenever(scopes.captureEvent(eventCaptor.capture(), any())).thenReturn(null) + + SentryExceptionResolver(scopes, transactionNameProvider, 1) + .resolveException(request, response, null, RuntimeException("test")) + + assertThat(eventCaptor.firstValue.transaction).isEqualTo(expectedTransactionName) + verify(transactionNameProvider).provideTransactionName(request) + } + + @Test + fun `when handles exception, provides spring resolver hint`() { + val hintCaptor = argumentCaptor() + whenever(scopes.captureEvent(any(), hintCaptor.capture())).thenReturn(null) + + SentryExceptionResolver(scopes, transactionNameProvider, 1) + .resolveException(request, response, null, RuntimeException("test")) + + with(hintCaptor.firstValue) { + assertThat(get("springResolver:request")).isEqualTo(request) + assertThat(get("springResolver:response")).isEqualTo(response) + } + } + + @Test + fun `when custom create event method provided, uses it to capture event`() { + val expectedEvent = SentryEvent() + val eventCaptor = argumentCaptor() + whenever(scopes.captureEvent(eventCaptor.capture(), any())).thenReturn(null) + val resolver = + object : SentryExceptionResolver(scopes, transactionNameProvider, 1) { + override fun createEvent(request: HttpServletRequest, ex: Exception) = expectedEvent + } + + resolver.resolveException(request, response, null, RuntimeException("test")) + + assertThat(eventCaptor.firstValue).isEqualTo(expectedEvent) + } + + @Test + fun `when custom create hint method provided, uses it to capture event`() { + val expectedHint = Hint() + val hintCaptor = argumentCaptor() + whenever(scopes.captureEvent(any(), hintCaptor.capture())).thenReturn(null) + val resolver = + object : SentryExceptionResolver(scopes, transactionNameProvider, 1) { + override fun createHint(request: HttpServletRequest, response: HttpServletResponse) = + expectedHint + } + + resolver.resolveException(request, response, null, RuntimeException("test")) + + assertThat(hintCaptor.firstValue).isEqualTo(expectedHint) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryInitBeanPostProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryInitBeanPostProcessorTest.kt new file mode 100644 index 00000000000..70883e19f55 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryInitBeanPostProcessorTest.kt @@ -0,0 +1,26 @@ +package io.sentry.spring.jakarta + +import io.sentry.IScopes +import kotlin.test.Test +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.springframework.context.annotation.AnnotationConfigApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration + +class SentryInitBeanPostProcessorTest { + @Test + fun closesSentryOnApplicationContextDestroy() { + val ctx = AnnotationConfigApplicationContext(TestConfig::class.java) + val scopes = ctx.getBean(IScopes::class.java) + ctx.close() + verify(scopes).close() + } + + @Configuration + open class TestConfig { + @Bean(destroyMethod = "") open fun scopes() = mock() + + @Bean open fun sentryInitBeanPostProcessor() = SentryInitBeanPostProcessor(scopes()) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessorTest.kt new file mode 100644 index 00000000000..a63f00a788f --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessorTest.kt @@ -0,0 +1,64 @@ +package io.sentry.spring.jakarta + +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.SentryEvent +import io.sentry.SentryOptions +import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider +import jakarta.servlet.http.HttpServletRequest +import java.net.URI +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.mock.web.MockServletContext +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.web.servlet.HandlerMapping + +class SentryRequestHttpServletRequestProcessorTest { + private class Fixture { + val scopes = mock() + + fun getSut( + request: HttpServletRequest, + options: SentryOptions = SentryOptions(), + ): SentryRequestHttpServletRequestProcessor { + whenever(scopes.options).thenReturn(options) + return SentryRequestHttpServletRequestProcessor(SpringMvcTransactionNameProvider(), request) + } + } + + private val fixture = Fixture() + + @Test + fun `when event does not have transaction name, sets the transaction name from the current request`() { + val request = + MockMvcRequestBuilders.get(URI.create("http://example.com?param1=xyz")) + .requestAttr(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/some-path") + .buildRequest(MockServletContext()) + val eventProcessor = fixture.getSut(request) + val event = SentryEvent() + + eventProcessor.process(event, Hint()) + + assertNotNull(event.transaction) + assertEquals("GET /some-path", event.transaction) + } + + @Test + fun `when event has transaction name set, does not overwrite transaction name with value from the current request`() { + val request = + MockMvcRequestBuilders.get(URI.create("http://example.com?param1=xyz")) + .requestAttr(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/some-path") + .buildRequest(MockServletContext()) + val eventProcessor = fixture.getSut(request) + val event = SentryEvent() + event.transaction = "some-transaction" + + eventProcessor.process(event, Hint()) + + assertNotNull(event.transaction) + assertEquals("some-transaction", event.transaction) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt new file mode 100644 index 00000000000..6c8db2fc097 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt @@ -0,0 +1,317 @@ +package io.sentry.spring.jakarta + +import io.sentry.Breadcrumb +import io.sentry.IScope +import io.sentry.IScopes +import io.sentry.ISentryLifecycleToken +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.SentryOptions +import io.sentry.SentryOptions.RequestSize.ALWAYS +import io.sentry.SentryOptions.RequestSize.MEDIUM +import io.sentry.SentryOptions.RequestSize.NONE +import io.sentry.SentryOptions.RequestSize.SMALL +import jakarta.servlet.FilterChain +import jakarta.servlet.ServletContext +import jakarta.servlet.http.HttpServletRequest +import java.net.URI +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import kotlin.test.assertTrue +import kotlin.test.fail +import org.assertj.core.api.Assertions +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.http.MediaType +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.mock.web.MockServletContext +import org.springframework.test.web.servlet.request.MockMvcRequestBuilders +import org.springframework.web.util.ContentCachingRequestWrapper + +class SentrySpringFilterTest { + private class Fixture { + val scopes = mock() + val scopesBeforeForking = mock() + val response = MockHttpServletResponse() + val lifecycleToken = mock() + val chain = mock() + lateinit var scope: IScope + lateinit var request: HttpServletRequest + + fun getSut( + request: HttpServletRequest? = null, + options: SentryOptions = SentryOptions(), + ): SentrySpringFilter { + scope = Scope(options) + whenever(scopesBeforeForking.options).thenReturn(options) + whenever(scopesBeforeForking.isEnabled).thenReturn(true) + whenever(scopes.options).thenReturn(options) + whenever(scopes.isEnabled).thenReturn(true) + whenever(scopesBeforeForking.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(any()) + this.request = + request + ?: MockHttpServletRequest().apply { + this.requestURI = "http://localhost:8080/some-uri" + this.method = "post" + } + return SentrySpringFilter(scopesBeforeForking) + } + } + + private val fixture = Fixture() + + @Test + fun `pushes scope when request gets initialized`() { + val listener = fixture.getSut() + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopesBeforeForking).forkedScopes(any()) + verify(fixture.scopes).makeCurrent() + } + + @Test + fun `adds breadcrumb when request gets initialized`() { + val listener = fixture.getSut() + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .addBreadcrumb( + check { it: Breadcrumb -> + Assertions.assertThat(it.getData("url")).isEqualTo("http://localhost:8080/some-uri") + Assertions.assertThat(it.getData("method")).isEqualTo("POST") + Assertions.assertThat(it.type).isEqualTo("http") + }, + anyOrNull(), + ) + } + + @Test + fun `pops scope when request gets destroyed`() { + val listener = fixture.getSut() + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.lifecycleToken).close() + } + + @Test + fun `pops scope when chain throws`() { + val listener = fixture.getSut() + whenever(fixture.chain.doFilter(any(), any())).thenThrow(RuntimeException()) + + try { + listener.doFilter(fixture.request, fixture.response, fixture.chain) + fail() + } catch (e: Exception) { + verify(fixture.lifecycleToken).close() + } + } + + @Test + fun `attaches basic information from HTTP request to Scope request`() { + val listener = + fixture.getSut( + request = + MockMvcRequestBuilders.get(URI.create("http://example.com?param1=xyz")) + .header("some-header", "some-header value") + .accept(MediaType.APPLICATION_JSON) + .buildRequest(MockServletContext()) + ) + + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + assertNotNull(fixture.scope.request) { + assertEquals("GET", it.method) + assertEquals( + mapOf("some-header" to "some-header value", "Accept" to "application/json"), + it.headers, + ) + assertEquals("http://example.com", it.url) + assertEquals("param1=xyz", it.queryString) + } + } + + @Test + fun `attaches header with multiple values to Scope request`() { + val listener = + fixture.getSut( + request = + MockMvcRequestBuilders.get(URI.create("http://example.com?param1=xyz")) + .header("another-header", "another value") + .header("another-header", "another value2") + .buildRequest(MockServletContext()) + ) + + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + assertNotNull(fixture.scope.request) { + assertEquals(mapOf("another-header" to "another value,another value2"), it.headers) + } + } + + @Test + fun `when sendDefaultPii is set to true, attaches filtered cookies to Scope request`() { + val sentryOptions = SentryOptions().apply { isSendDefaultPii = true } + + val listener = + fixture.getSut( + request = + MockMvcRequestBuilders.get(URI.create("http://example.com?param1=xyz")) + .header("Cookie", "name=value; JSESSIONID=123; mysessioncookiename=789") + .header("Cookie", "name2=value2; SID=456") + .buildRequest(servletContextWithCustomCookieName("mysessioncookiename")), + options = sentryOptions, + ) + + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + assertNotNull(fixture.scope.request) { + val expectedCookieString = + "name=value; JSESSIONID=[Filtered]; mysessioncookiename=[Filtered],name2=value2; SID=[Filtered]" + assertEquals(expectedCookieString, it.cookies) + assertEquals(expectedCookieString, it.headers!!["Cookie"]) + } + } + + @Test + fun `when sendDefaultPii is set to false, does not attach cookies to Scope request`() { + val sentryOptions = SentryOptions().apply { isSendDefaultPii = false } + + val listener = + fixture.getSut( + request = + MockMvcRequestBuilders.get(URI.create("http://example.com?param1=xyz")) + .header("Cookie", "name=value") + .buildRequest(MockServletContext()), + options = sentryOptions, + ) + + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + assertNotNull(fixture.scope.request) { assertNull(it.cookies) } + } + + @Test + fun `when sendDefaultPii is set to false, does not attach sensitive headers`() { + val sentryOptions = SentryOptions().apply { isSendDefaultPii = false } + + val listener = + fixture.getSut( + request = + MockMvcRequestBuilders.get(URI.create("http://example.com?param1=xyz")) + .header("some-header", "some-header value") + .header("X-FORWARDED-FOR", "192.168.0.1") + .header("authorization", "Token") + .header("Authorization", "Token") + .header("Cookie", "some cookies") + .buildRequest(MockServletContext()), + options = sentryOptions, + ) + + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + assertNotNull(fixture.scope.request) { request -> + assertNotNull(request.headers) { + assertFalse(it.containsKey("X-FORWARDED-FOR")) + assertFalse(it.containsKey("Authorization")) + assertFalse(it.containsKey("authorization")) + assertFalse(it.containsKey("Cookie")) + assertTrue(it.containsKey("some-header")) + } + } + } + + @Test + fun `caches request depending on the maxRequestBodySize value and request body length`() { + data class TestParams( + val sendDefaultPii: Boolean = true, + val maxRequestBodySize: SentryOptions.RequestSize, + val body: String, + val contentType: String = "application/json", + val expectedToBeCached: Boolean, + ) + + val params = + listOf( + TestParams(maxRequestBodySize = NONE, body = "xxx", expectedToBeCached = false), + TestParams( + maxRequestBodySize = SMALL, + body = "xxx", + expectedToBeCached = false, + sendDefaultPii = false, + ), + TestParams(maxRequestBodySize = SMALL, body = "xxx", expectedToBeCached = true), + TestParams( + maxRequestBodySize = SMALL, + body = "xxx", + contentType = "application/octet-stream", + expectedToBeCached = false, + ), + TestParams(maxRequestBodySize = SMALL, body = "x".repeat(1001), expectedToBeCached = false), + TestParams(maxRequestBodySize = MEDIUM, body = "x".repeat(1001), expectedToBeCached = true), + TestParams( + maxRequestBodySize = MEDIUM, + body = "x".repeat(10001), + expectedToBeCached = false, + ), + TestParams( + maxRequestBodySize = ALWAYS, + body = "x".repeat(10001), + expectedToBeCached = true, + ), + TestParams( + maxRequestBodySize = SMALL, + body = "xxx", + contentType = "application/x-www-form-urlencoded", + expectedToBeCached = true, + ), + ) + + params.forEach { param -> + try { + val fixture = Fixture() + val sentryOptions = + SentryOptions().apply { + maxRequestBodySize = param.maxRequestBodySize + isSendDefaultPii = param.sendDefaultPii + } + + val listener = + fixture.getSut( + request = + MockMvcRequestBuilders.post(URI.create("http://example.com?param1=xyz")) + .content(param.body) + .contentType(param.contentType) + .buildRequest(MockServletContext()), + options = sentryOptions, + ) + + listener.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.chain) + .doFilter( + check { assertEquals(param.expectedToBeCached, it is ContentCachingRequestWrapper) }, + any(), + ) + } catch (e: AssertionError) { + System.err.println("Failed to run test with params: $param") + throw e + } + } + } + + private fun servletContextWithCustomCookieName(name: String): ServletContext = + MockServletContext().also { it.sessionCookieConfig.name = name } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryTaskDecoratorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryTaskDecoratorTest.kt new file mode 100644 index 00000000000..1f0f74d25cf --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryTaskDecoratorTest.kt @@ -0,0 +1,58 @@ +package io.sentry.spring.jakarta + +import io.sentry.Sentry +import io.sentry.test.initForTest +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class SentryTaskDecoratorTest { + private val dsn = "http://key@localhost/proj" + private lateinit var executor: ExecutorService + + @BeforeTest + fun beforeTest() { + executor = Executors.newSingleThreadExecutor() + } + + @AfterTest + fun afterTest() { + Sentry.close() + executor.shutdown() + } + + @Test + fun `scopes is reset to its state within the thread after decoration is done`() { + initForTest { it.dsn = dsn } + + val sut = SentryTaskDecorator() + + val mainScopes = Sentry.getCurrentScopes() + val threadedScopes = Sentry.getCurrentScopes().forkedCurrentScope("test") + + executor.submit { Sentry.setCurrentScopes(threadedScopes) }.get() + + assertEquals(mainScopes, Sentry.getCurrentScopes()) + + val callableFuture = + executor.submit( + sut.decorate { + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) + } + ) + + callableFuture.get() + + executor + .submit { + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertEquals(threadedScopes, Sentry.getCurrentScopes()) + } + .get() + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt new file mode 100644 index 00000000000..c790f3e9997 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt @@ -0,0 +1,128 @@ +package io.sentry.spring.jakarta + +import io.sentry.IScopes +import io.sentry.SentryOptions +import io.sentry.protocol.User +import jakarta.servlet.FilterChain +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import org.mockito.kotlin.check +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse + +class SentryUserFilterTest { + class Fixture { + val scopes = mock() + val request = MockHttpServletRequest() + val response = MockHttpServletResponse() + val chain = mock() + + fun getSut( + isSendDefaultPii: Boolean = false, + userProviders: List, + ): SentryUserFilter { + val options = SentryOptions().apply { this.isSendDefaultPii = isSendDefaultPii } + whenever(scopes.options).thenReturn(options) + return SentryUserFilter(scopes, userProviders) + } + } + + private val fixture = Fixture() + + private val sampleUser = + User().apply { + username = "john.doe" + id = "user-id" + ipAddress = "192.168.0.1" + email = "john.doe@example.com" + data = mapOf("key" to "value") + } + + @Test + fun `sets provided user data on the scope`() { + val filter = fixture.getSut(userProviders = listOf(SentryUserProvider { sampleUser })) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertEquals(sampleUser, it) }) + } + + @Test + fun `when processor returns empty User, user data is not changed`() { + val filter = + fixture.getSut( + userProviders = listOf(SentryUserProvider { sampleUser }, SentryUserProvider { User() }) + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertEquals(sampleUser, it) }) + } + + @Test + fun `when processor returns null, user data is not changed`() { + val filter = + fixture.getSut( + userProviders = listOf(SentryUserProvider { sampleUser }, SentryUserProvider { null }) + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertEquals(sampleUser, it) }) + } + + @Test + fun `merges user#others with existing user#others set on SentryEvent`() { + val filter = + fixture.getSut( + userProviders = + listOf( + SentryUserProvider { User().apply { data = mapOf("key" to "value") } }, + SentryUserProvider { User().apply { data = mapOf("new-key" to "new-value") } }, + ) + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .setUser(check { assertEquals(mapOf("key" to "value", "new-key" to "new-value"), it.data) }) + } + + @Test + fun `when isSendDefaultPii is true and user is set with custom ip address, user ip is unchanged`() { + val filter = + fixture.getSut( + isSendDefaultPii = true, + userProviders = listOf(SentryUserProvider { User().apply { ipAddress = "192.168.0.1" } }), + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertEquals("192.168.0.1", it.ipAddress) }) + } + + @Test + fun `when isSendDefaultPii is true and user is set with {{auto}} ip address, user ip is set to null`() { + val filter = + fixture.getSut( + isSendDefaultPii = true, + userProviders = listOf(SentryUserProvider { User().apply { ipAddress = "{{auto}}" } }), + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes).setUser(check { assertNull(it.ipAddress) }) + } + + private fun assertEquals(user1: User, user2: User) { + assertEquals(user1.username, user2.username) + assertEquals(user1.id, user2.id) + assertEquals(user1.ipAddress, user2.ipAddress) + assertEquals(user1.email, user2.email) + assertEquals(user1.data, user2.data) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringProfilesEventProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringProfilesEventProcessorTest.kt new file mode 100644 index 00000000000..f5e642a99a4 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringProfilesEventProcessorTest.kt @@ -0,0 +1,83 @@ +package io.sentry.spring.jakarta + +import io.sentry.ITransportFactory +import io.sentry.Sentry +import io.sentry.checkEvent +import io.sentry.protocol.Spring +import io.sentry.transport.ITransport +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.boot.test.context.runner.ApplicationContextRunner +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.env.Environment + +class SpringProfilesEventProcessorTest { + private val contextRunner = + ApplicationContextRunner() + .withUserConfiguration(AppConfiguration::class.java) + .withUserConfiguration(SpringProfilesEventProcessorConfiguration::class.java) + .withUserConfiguration(MockTransportConfiguration::class.java) + + @Test + fun `when default Spring profile is active, sets active_profiles in Spring context to empty list on sent event`() { + contextRunner.run { + Sentry.captureMessage("test") + val transport = it.getBean(ITransport::class.java) + verify(transport) + .send( + checkEvent { event -> + val expected = Spring() + expected.activeProfiles = listOf().toTypedArray() + assertThat(event.contexts.spring).isEqualTo(expected) + }, + anyOrNull(), + ) + } + } + + @Test + fun `when non-default Spring profiles are active, sets active profiles in Spring context to list of profile names`() { + contextRunner.withPropertyValues("spring.profiles.active=test1,test2").run { + Sentry.captureMessage("test") + val transport = it.getBean(ITransport::class.java) + verify(transport) + .send( + checkEvent { event -> + val expected = Spring() + expected.activeProfiles = listOf("test1", "test2").toTypedArray() + assertThat(event.contexts.spring).isEqualTo(expected) + }, + anyOrNull(), + ) + } + } + + @EnableSentry(dsn = "http://key@localhost/proj") class AppConfiguration + + @Configuration(proxyBeanMethods = false) + open class SpringProfilesEventProcessorConfiguration { + @Bean + open fun springProfilesEventProcessor(environment: Environment): SpringProfilesEventProcessor = + SpringProfilesEventProcessor(environment) + } + + @Configuration(proxyBeanMethods = false) + open class MockTransportConfiguration { + private val transport = mock() + + @Bean + open fun mockTransportFactory(): ITransportFactory { + val factory = mock() + whenever(factory.create(any(), any())).thenReturn(transport) + return factory + } + + @Bean open fun sentryTransport() = transport + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringSecuritySentryUserProviderTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringSecuritySentryUserProviderTest.kt new file mode 100644 index 00000000000..80f8efc9ce2 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringSecuritySentryUserProviderTest.kt @@ -0,0 +1,56 @@ +package io.sentry.spring.jakarta + +import io.sentry.SentryOptions +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.security.core.Authentication +import org.springframework.security.core.context.SecurityContext +import org.springframework.security.core.context.SecurityContextHolder + +class SpringSecuritySentryUserProviderTest { + class Fixture { + fun getSut( + isSendDefaultPii: Boolean = true, + username: String? = null, + ): SpringSecuritySentryUserProvider { + val options = SentryOptions().apply { this.isSendDefaultPii = isSendDefaultPii } + val securityContext = mock() + if (username != null) { + val authentication = mock() + whenever(securityContext.authentication).thenReturn(authentication) + whenever(authentication.name).thenReturn("name") + } else { + whenever(securityContext.authentication).thenReturn(null) + } + SecurityContextHolder.setContext(securityContext) + return SpringSecuritySentryUserProvider(options) + } + } + + private val fixture = Fixture() + + @Test + fun `when send default pii is set to true, returns user with username set`() { + val provider = fixture.getSut(true, "name") + val user = provider.provideUser() + assertNotNull(user) { assertEquals("name", it.username) } + } + + @Test + fun `when send default pii is set to false, returns null`() { + val provider = fixture.getSut(false) + val user = provider.provideUser() + assertNull(user) + } + + @Test + fun `when send default pii is set to true and security context is not set, returns null`() { + val provider = fixture.getSut(true) + val user = provider.provideUser() + assertNull(user) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdviceTest.kt new file mode 100644 index 00000000000..f9f1c38dd16 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdviceTest.kt @@ -0,0 +1,69 @@ +package io.sentry.spring.jakarta.exception + +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.Sentry +import io.sentry.exception.ExceptionMechanismException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertTrue +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.check +import org.mockito.kotlin.mock +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy +import org.springframework.context.annotation.Import +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentryCaptureExceptionParameterAdviceTest.Config::class) +class SentryCaptureExceptionParameterAdviceTest { + @Autowired lateinit var sampleService: SampleService + + @Autowired lateinit var scopes: IScopes + + @BeforeTest + fun setup() { + reset(scopes) + } + + @Test + fun `captures exception passed to method annotated with @SentryCaptureException`() { + val exception = RuntimeException("test exception") + sampleService.methodTakingAnException(exception) + verify(scopes) + .captureException( + check { + assertTrue(it is ExceptionMechanismException) + assertEquals(exception, it.throwable) + assertEquals("SentrySpring6CaptureExceptionParameterAdvice", it.exceptionMechanism.type) + }, + any(), + ) + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + @Import(SentryCaptureExceptionParameterConfiguration::class) + open class Config { + @Bean open fun sampleService() = SampleService() + + @Bean + open fun scopes(): IScopes { + val scopes = mock() + Sentry.setCurrentScopes(scopes) + return scopes + } + } + + open class SampleService { + @SentryCaptureExceptionParameter open fun methodTakingAnException(e: Exception) = Unit + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandlerTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandlerTest.kt new file mode 100644 index 00000000000..a8039ff7660 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandlerTest.kt @@ -0,0 +1,98 @@ +package io.sentry.spring.jakarta.graphql + +import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters +import graphql.language.Document +import graphql.language.OperationDefinition +import graphql.schema.DataFetchingEnvironment +import io.sentry.IScopes +import io.sentry.graphql.ExceptionReporter +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertSame +import org.junit.jupiter.api.assertThrows +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.mock +import org.mockito.kotlin.same +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.graphql.execution.SubscriptionPublisherException +import reactor.core.publisher.Flux + +class SentrySpringSubscriptionHandlerTest { + @Test + fun `reports exception`() { + val exception = IllegalStateException("some exception") + val scopes = mock() + val exceptionReporter = mock() + val parameters = mock() + val dataFetchingEnvironment = mock() + val document = + Document.newDocument() + .definition( + OperationDefinition.newOperationDefinition() + .operation(OperationDefinition.Operation.QUERY) + .name("testQuery") + .build() + ) + .build() + whenever(dataFetchingEnvironment.document).thenReturn(document) + whenever(parameters.environment).thenReturn(dataFetchingEnvironment) + val resultObject = + SentrySpringSubscriptionHandler() + .onSubscriptionResult(Flux.error(exception), scopes, exceptionReporter, parameters) + assertThrows { (resultObject as Flux).blockFirst() } + + verify(exceptionReporter) + .captureThrowable( + same(exception), + check { + assertEquals(true, it.isSubscription) + assertSame(scopes, it.scopes) + assertEquals("query testQuery \n", it.query) + }, + anyOrNull(), + ) + } + + @Test + fun `unwraps SubscriptionPublisherException and reports cause`() { + val exception = IllegalStateException("some exception") + val wrappedException = SubscriptionPublisherException(emptyList(), exception) + val scopes = mock() + val exceptionReporter = mock() + val parameters = mock() + val dataFetchingEnvironment = mock() + val document = + Document.newDocument() + .definition( + OperationDefinition.newOperationDefinition() + .operation(OperationDefinition.Operation.QUERY) + .name("testQuery") + .build() + ) + .build() + whenever(dataFetchingEnvironment.document).thenReturn(document) + whenever(parameters.environment).thenReturn(dataFetchingEnvironment) + val resultObject = + SentrySpringSubscriptionHandler() + .onSubscriptionResult( + Flux.error(wrappedException), + scopes, + exceptionReporter, + parameters, + ) + assertThrows { (resultObject as Flux).blockFirst() } + + verify(exceptionReporter) + .captureThrowable( + same(exception), + check { + assertEquals(true, it.isSubscription) + assertSame(scopes, it.scopes) + assertEquals("query testQuery \n", it.query) + }, + anyOrNull(), + ) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt new file mode 100644 index 00000000000..6e2ea53fdfd --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt @@ -0,0 +1,537 @@ +package io.sentry.spring.jakarta.mvc + +import io.sentry.IScopes +import io.sentry.ITransportFactory +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SpanDataConvention +import io.sentry.SpanStatus +import io.sentry.checkEvent +import io.sentry.checkTransaction +import io.sentry.spring.jakarta.EnableSentry +import io.sentry.spring.jakarta.SentryExceptionResolver +import io.sentry.spring.jakarta.SentrySpringFilter +import io.sentry.spring.jakarta.SentryTaskDecorator +import io.sentry.spring.jakarta.SentryUserFilter +import io.sentry.spring.jakarta.SentryUserProvider +import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider +import io.sentry.spring.jakarta.exception.SentryCaptureExceptionParameter +import io.sentry.spring.jakarta.exception.SentryCaptureExceptionParameterConfiguration +import io.sentry.spring.jakarta.tracing.SentrySpanClientWebRequestFilter +import io.sentry.spring.jakarta.tracing.SentryTracingConfiguration +import io.sentry.spring.jakarta.tracing.SentryTracingFilter +import io.sentry.spring.jakarta.tracing.SentryTransaction +import io.sentry.transport.ITransport +import java.time.Duration +import java.util.concurrent.Callable +import java.util.concurrent.TimeUnit +import kotlin.test.BeforeTest +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.awaitility.Awaitility +import org.awaitility.kotlin.await +import org.junit.AfterClass +import org.junit.BeforeClass +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.Lazy +import org.springframework.core.Ordered +import org.springframework.core.env.Environment +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.crypto.factory.PasswordEncoderFactories +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.web.SecurityFilterChain +import org.springframework.stereotype.Service +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.reactive.function.client.ExchangeFilterFunctions +import org.springframework.web.reactive.function.client.WebClient + +@RunWith(SpringRunner::class) +@SpringBootTest(classes = [App::class], webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class SentrySpringIntegrationTest { + + companion object { + @BeforeClass + fun `configure awaitlity`() { + Awaitility.setDefaultTimeout(500, TimeUnit.MILLISECONDS) + } + + @AfterClass + fun `reset awaitility`() { + Awaitility.reset() + } + } + + @Autowired lateinit var transport: ITransport + + @Autowired lateinit var someService: SomeService + + @Autowired lateinit var anotherService: AnotherService + + @Autowired lateinit var scopes: IScopes + + @LocalServerPort var port: Int? = null + + @BeforeTest + fun `reset mocks`() { + reset(transport) + } + + @Test + fun `attaches request and user information to SentryEvents`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders() + headers["X-FORWARDED-FOR"] = listOf("169.128.0.1") + val entity = HttpEntity(headers) + + restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.request).isNotNull() + assertThat(event.request!!.url).isEqualTo("http://localhost:$port/hello") + assertThat(event.user).isNotNull() + assertThat(event.user!!.username).isEqualTo("user") + assertThat(event.user!!.ipAddress).isEqualTo("169.128.0.1") + }, + anyOrNull(), + ) + } + + @Test + fun `attaches request body to SentryEvents`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders().apply { this.contentType = MediaType.APPLICATION_JSON } + val httpEntity = HttpEntity("""{"body":"content"}""", headers) + restTemplate.exchange( + "http://localhost:$port/bodyAsParam", + HttpMethod.POST, + httpEntity, + Void::class.java, + ) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.request).isNotNull() + assertThat(event.request!!.data).isEqualTo("""{"body":"content"}""") + }, + anyOrNull(), + ) + } + + @Test + fun `attaches request body to SentryEvents on empty ControllerMethod Params`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders().apply { this.contentType = MediaType.APPLICATION_JSON } + val httpEntity = HttpEntity("""{"body":"content"}""", headers) + restTemplate.exchange( + "http://localhost:$port/body", + HttpMethod.POST, + httpEntity, + Void::class.java, + ) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.request).isNotNull() + assertThat(event.request!!.data).isEqualTo("""{"body":"content"}""") + }, + anyOrNull(), + ) + } + + @Test + fun `attaches first ip address if multiple addresses exist in a header`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders() + headers["X-FORWARDED-FOR"] = listOf("169.128.0.1, 192.168.0.1") + val entity = HttpEntity(headers) + + restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.user).isNotNull() + assertThat(event.user!!.ipAddress).isEqualTo("169.128.0.1") + }, + anyOrNull(), + ) + } + + @Test + fun `sends events for unhandled exceptions`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/throws", String::class.java) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.exceptions).isNotNull().isNotEmpty + val ex = event.exceptions!!.first() + assertThat(ex.value).isEqualTo("something went wrong") + assertThat(ex.mechanism).isNotNull() + assertThat(ex.mechanism!!.isHandled).isFalse() + assertThat(ex.mechanism!!.type).isEqualTo(SentryExceptionResolver.MECHANISM_TYPE) + }, + anyOrNull(), + ) + } + + @Test + fun `attaches transaction name to events`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/throws", String::class.java) + + verify(transport) + .send( + checkEvent { event -> assertThat(event.transaction).isEqualTo("GET /throws") }, + anyOrNull(), + ) + } + + @Test + fun `does not send events for handled exceptions`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/throws-handled", String::class.java) + + await.during(Duration.ofSeconds(2)).untilAsserted { + verify(transport, never()) + .send(checkEvent { event -> assertThat(event).isNotNull() }, anyOrNull()) + } + } + + @Test + fun `calling a method annotated with @SentryCaptureException captures exception`() { + val exception = java.lang.RuntimeException("test exception") + anotherService.aMethodThatTakesAnException(exception) + verify(transport) + .send( + checkEvent { assertThat(it.exceptions!!.first().value).isEqualTo(exception.message) }, + anyOrNull(), + ) + } + + @Test + fun `calling a method annotated with @SentryCaptureException captures exception in later param`() { + val exception = java.lang.RuntimeException("test exception") + anotherService.aMethodThatTakesAnExceptionAsLaterParam("a", "b", exception) + verify(transport) + .send( + checkEvent { assertThat(it.exceptions!!.first().value).isEqualTo(exception.message) }, + anyOrNull(), + ) + } + + @Test + fun `calling a method annotated with @SentryTransaction creates transaction`() { + someService.aMethod() + verify(transport) + .send(checkTransaction { assertThat(it.status).isEqualTo(SpanStatus.OK) }, anyOrNull()) + } + + @Test + fun `calling a method annotated with @SentryTransaction throwing exception associates Sentry event with transaction`() { + try { + someService.aMethodThrowing() + } catch (e: Exception) { + scopes.captureException(e) + } + verify(transport) + .send( + checkEvent { + assertThat(it.contexts.trace).isNotNull + assertThat(it.contexts.trace!!.operation).isEqualTo("bean") + }, + anyOrNull(), + ) + } + + @Test + fun `calling a method annotated with @SentryTransaction, where an inner span is created within transaction, throwing exception associates Sentry event with inner span`() { + try { + someService.aMethodWithInnerSpanThrowing() + } catch (e: Exception) { + scopes.captureException(e) + } + verify(transport) + .send( + checkEvent { + assertThat(it.contexts.trace).isNotNull + assertThat(it.contexts.trace!!.operation).isEqualTo("child-op") + }, + anyOrNull(), + ) + } + + @Test + fun `sets user on transaction`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/hello", String::class.java) + + // transactions are sent after response is returned + await.untilAsserted { + verify(transport) + .send( + checkTransaction { transaction -> + assertThat(transaction.user).isNotNull() + assertThat(transaction.user!!.username).isEqualTo("user") + }, + anyOrNull(), + ) + } + } + + @Test + fun `scope is applied to events triggered in async methods`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/callable", String::class.java) + + await.untilAsserted { + verify(transport) + .send( + checkEvent { event -> + assertThat(event.message!!.formatted) + .isEqualTo("this message should be in the scope of the request") + assertThat(event.request).isNotNull() + assertThat(event.request!!.url).isEqualTo("http://localhost:$port/callable") + }, + anyOrNull(), + ) + } + } + + @Test + fun `WebClient http request execution is turned into a span`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/webClient", String::class.java) + + // transactions are sent after response is returned + await.untilAsserted { + verify(transport) + .send( + checkTransaction { transaction -> + assertThat(transaction.spans).hasSize(1) + val span = transaction.spans.first() + assertThat(span.op).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET http://localhost:$port/hello") + assertThat(span.data?.get(SpanDataConvention.HTTP_STATUS_CODE_KEY)).isEqualTo(200) + assertThat(span.status).isEqualTo(SpanStatus.OK) + }, + anyOrNull(), + ) + } + } +} + +@SpringBootApplication +@EnableSentry( + dsn = "http://key@localhost/proj", + sendDefaultPii = true, + maxRequestBodySize = SentryOptions.RequestSize.MEDIUM, +) +@Import(SentryTracingConfiguration::class, SentryCaptureExceptionParameterConfiguration::class) +open class App { + + @Bean + open fun mockTransportFactory(transport: ITransport): ITransportFactory { + val factory = mock() + whenever(factory.create(any(), any())).thenReturn(transport) + return factory + } + + @Bean open fun mockTransport() = mock() + + @Bean open fun tracesSamplerCallback() = SentryOptions.TracesSamplerCallback { 1.0 } + + @Bean + open fun springSecuritySentryUserProvider(sentryOptions: SentryOptions) = + SpringSecuritySentryUserProvider(sentryOptions) + + @Bean + open fun sentryUserFilter(scopes: IScopes, @Lazy sentryUserProviders: List) = + FilterRegistrationBean().apply { + this.filter = SentryUserFilter(scopes, sentryUserProviders) + this.order = Ordered.LOWEST_PRECEDENCE + } + + @Bean + open fun sentrySpringFilter(scopes: IScopes) = + FilterRegistrationBean().apply { + this.filter = SentrySpringFilter(scopes) + this.order = Ordered.HIGHEST_PRECEDENCE + } + + @Bean + open fun sentryTracingFilter(scopes: IScopes) = + FilterRegistrationBean().apply { + this.filter = SentryTracingFilter(scopes) + this.order = Ordered.HIGHEST_PRECEDENCE + 1 // must run after SentrySpringFilter + } + + @Bean open fun sentryTaskDecorator() = SentryTaskDecorator() + + @Bean + open fun webClient(scopes: IScopes): WebClient { + return WebClient.builder() + .filter(ExchangeFilterFunctions.basicAuthentication("user", "password")) + .filter(SentrySpanClientWebRequestFilter(scopes)) + .build() + } +} + +@Service +open class AnotherService { + @SentryCaptureExceptionParameter open fun aMethodThatTakesAnException(e: Exception) {} + + @SentryCaptureExceptionParameter + open fun aMethodThatTakesAnExceptionAsLaterParam(a: String, b: String, e: Exception) {} +} + +@Service +open class SomeService { + + @SentryTransaction(operation = "bean") + open fun aMethod() { + Thread.sleep(100) + } + + @SentryTransaction(operation = "bean") + open fun aMethodThrowing() { + throw RuntimeException("oops") + } + + @SentryTransaction(operation = "bean") + open fun aMethodWithInnerSpanThrowing() { + val span = Sentry.getSpan()!!.startChild("child-op") + try { + throw RuntimeException("oops") + } catch (e: Exception) { + span.status = SpanStatus.INTERNAL_ERROR + span.throwable = e + throw e + } finally { + span.finish() + } + } +} + +@RestController +class HelloController(private val webClient: WebClient, private val env: Environment) { + + @GetMapping("/hello") + fun hello(): String { + Sentry.captureMessage("hello") + return "hello" + } + + @PostMapping("/body") + fun body() { + Sentry.captureMessage("body") + } + + @PostMapping("/bodyAsParam") + fun bodyWithReadingBodyInController(@RequestBody body: String) { + Sentry.captureMessage("body") + } + + @GetMapping("/throws") + fun throws() { + throw RuntimeException("something went wrong") + } + + @GetMapping("/throws-handled") + fun throwsHandled() { + throw CustomException("handled exception") + } + + @GetMapping("/callable") + fun callable(): Callable { + return Callable { + Sentry.captureMessage("this message should be in the scope of the request") + "from callable" + } + } + + @GetMapping("/webClient") + fun webClient(): String? { + return webClient + .get() + .uri("http://localhost:${env.getProperty("local.server.port")}/hello") + .retrieve() + .bodyToMono(String::class.java) + .block() + } +} + +class CustomException(message: String) : RuntimeException(message) + +@ControllerAdvice +class ExceptionHandlers { + + @ExceptionHandler(CustomException::class) + fun handle(e: CustomException) = ResponseEntity.badRequest().build() +} + +@Configuration +open class SecurityConfiguration { + + @Bean + open fun userDetailsService(): InMemoryUserDetailsManager { + val encoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() + val user: UserDetails = + User.builder() + .passwordEncoder { rawPassword -> encoder.encode(rawPassword) } + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(user) + } + + @Bean + @Throws(Exception::class) + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic() + + return http.build() + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanAdviceTest.kt new file mode 100644 index 00000000000..475c040175c --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanAdviceTest.kt @@ -0,0 +1,180 @@ +package io.sentry.spring.jakarta.tracing + +import io.sentry.IScopes +import io.sentry.Scope +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TransactionContext +import java.lang.RuntimeException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNull +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy +import org.springframework.context.annotation.Import +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentrySpanAdviceTest.Config::class) +class SentrySpanAdviceTest { + + @Autowired lateinit var sampleService: SampleService + + @Autowired lateinit var classAnnotatedSampleService: ClassAnnotatedSampleService + + @Autowired + lateinit var classAnnotatedWithOperationSampleService: ClassAnnotatedWithOperationSampleService + + @Autowired lateinit var scopes: IScopes + + @BeforeTest + fun setup() { + whenever(scopes.options).thenReturn(SentryOptions()) + } + + @Test + fun `when class is annotated with @SentrySpan, every method call attaches span to existing transaction`() { + val scope = Scope(SentryOptions()) + val tx = SentryTracer(TransactionContext("aTransaction", "op"), scopes) + scope.setTransaction(tx) + + whenever(scopes.span).thenReturn(tx) + val result = classAnnotatedSampleService.hello() + assertEquals(1, result) + assertEquals(1, tx.spans.size) + assertNull(tx.spans.first().description) + assertEquals("auto.function.spring_jakarta.advice", tx.spans.first().spanContext.origin) + assertEquals("ClassAnnotatedSampleService.hello", tx.spans.first().operation) + } + + @Test + fun `when class is annotated with @SentrySpan with operation set, every method call attaches span to existing transaction`() { + val scope = Scope(SentryOptions()) + val tx = SentryTracer(TransactionContext("aTransaction", "op"), scopes) + scope.setTransaction(tx) + + whenever(scopes.span).thenReturn(tx) + val result = classAnnotatedWithOperationSampleService.hello() + assertEquals(1, result) + assertEquals(1, tx.spans.size) + assertNull(tx.spans.first().description) + assertEquals("my-op", tx.spans.first().operation) + } + + @Test + fun `when method is annotated with @SentrySpan with properties set, attaches span to existing transaction`() { + val scope = Scope(SentryOptions()) + val tx = SentryTracer(TransactionContext("aTransaction", "op"), scopes) + scope.setTransaction(tx) + + whenever(scopes.span).thenReturn(tx) + val result = sampleService.methodWithSpanDescriptionSet() + assertEquals(1, result) + assertEquals(1, tx.spans.size) + assertEquals("customName", tx.spans.first().description) + assertEquals("bean", tx.spans.first().operation) + } + + @Test + fun `when method is annotated with @SentrySpan without properties set, attaches span to existing transaction and sets Span description as className dot methodName`() { + val scope = Scope(SentryOptions()) + val tx = SentryTracer(TransactionContext("aTransaction", "op"), scopes) + scope.setTransaction(tx) + + whenever(scopes.span).thenReturn(tx) + val result = sampleService.methodWithoutSpanDescriptionSet() + assertEquals(2, result) + assertEquals(1, tx.spans.size) + assertEquals("SampleService.methodWithoutSpanDescriptionSet", tx.spans.first().operation) + assertNull(tx.spans.first().description) + } + + @Test + fun `when method is annotated with @SentrySpan and returns, attached span has status OK`() { + val scope = Scope(SentryOptions()) + val tx = SentryTracer(TransactionContext("aTransaction", "op"), scopes) + scope.setTransaction(tx) + + whenever(scopes.span).thenReturn(tx) + sampleService.methodWithSpanDescriptionSet() + assertEquals(SpanStatus.OK, tx.spans.first().status) + } + + @Test + fun `when method is annotated with @SentrySpan and throws exception, attached span has throwable set and INTERNAL_ERROR status`() { + val scope = Scope(SentryOptions()) + val tx = SentryTracer(TransactionContext("aTransaction", "op"), scopes) + scope.setTransaction(tx) + + whenever(scopes.span).thenReturn(tx) + var throwable: Throwable? = null + try { + sampleService.methodThrowingException() + } catch (e: Exception) { + throwable = e + } + assertEquals(SpanStatus.INTERNAL_ERROR, tx.spans.first().status) + assertEquals(throwable, tx.spans.first().throwable) + } + + @Test + fun `when method is annotated with @SentrySpan and there is no active transaction, span is not created and method is executed`() { + whenever(scopes.span).thenReturn(null) + val result = sampleService.methodWithSpanDescriptionSet() + assertEquals(1, result) + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + @Import(SentryTracingConfiguration::class) + open class Config { + + @Bean open fun sampleService() = SampleService() + + @Bean open fun classAnnotatedSampleService() = ClassAnnotatedSampleService() + + @Bean + open fun classAnnotatedWithOperationSampleService() = ClassAnnotatedWithOperationSampleService() + + @Bean + open fun scopes(): IScopes { + val scopes = mock() + Sentry.setCurrentScopes(scopes) + return scopes + } + } + + open class SampleService { + + @SentrySpan(description = "customName", operation = "bean") + open fun methodWithSpanDescriptionSet() = 1 + + @SentrySpan open fun methodWithoutSpanDescriptionSet() = 2 + + @SentrySpan + open fun methodThrowingException() { + throw RuntimeException("ex") + } + } + + @SentrySpan + open class ClassAnnotatedSampleService { + + open fun hello() = 1 + } + + @SentrySpan(operation = "my-op") + open class ClassAnnotatedWithOperationSampleService { + + open fun hello() = 1 + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt new file mode 100644 index 00000000000..dfb8376286a --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt @@ -0,0 +1,464 @@ +package io.sentry.spring.jakarta.tracing + +import io.sentry.ILogger +import io.sentry.IScopes +import io.sentry.PropagationContext +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanId +import io.sentry.SpanStatus +import io.sentry.TraceContext +import io.sentry.TransactionContext +import io.sentry.TransactionOptions +import io.sentry.protocol.SentryId +import io.sentry.protocol.SentryTransaction +import io.sentry.protocol.TransactionNameSource +import jakarta.servlet.DispatcherType +import jakarta.servlet.FilterChain +import jakarta.servlet.http.HttpServletRequest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail +import org.assertj.core.api.Assertions.assertThat +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.springframework.http.HttpMethod +import org.springframework.mock.web.MockHttpServletRequest +import org.springframework.mock.web.MockHttpServletResponse +import org.springframework.web.context.request.async.AsyncWebRequest +import org.springframework.web.context.request.async.WebAsyncUtils +import org.springframework.web.servlet.HandlerMapping + +class SentryTracingFilterTest { + private class Fixture { + val scopes = mock() + val request = MockHttpServletRequest() + val response = MockHttpServletResponse() + val chain = mock() + val transactionNameProvider = mock() + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + tracesSampleRate = 1.0 + } + val asyncRequest = mock() + val logger = mock() + + init { + whenever(scopes.options).thenReturn(options) + } + + fun getSut( + isEnabled: Boolean = true, + status: Int = 200, + sentryTraceHeader: String? = null, + baggageHeaders: List? = null, + isAsyncSupportEnabled: Boolean = false, + ): SentryTracingFilter { + request.requestURI = "/product/12" + request.method = "POST" + request.setAttribute(HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, "/product/{id}") + whenever(transactionNameProvider.provideTransactionName(request)) + .thenReturn("POST /product/{id}") + whenever(transactionNameProvider.provideTransactionSource()) + .thenReturn(TransactionNameSource.CUSTOM) + whenever(transactionNameProvider.provideTransactionNameAndSource(request)) + .thenReturn(TransactionNameWithSource("POST /product/{id}", TransactionNameSource.CUSTOM)) + if (sentryTraceHeader != null) { + request.addHeader("sentry-trace", sentryTraceHeader) + whenever(scopes.startTransaction(any(), check { it.isBindToScope })) + .thenAnswer { SentryTracer(it.arguments[0] as TransactionContext, scopes) } + } + if (baggageHeaders != null) { + request.addHeader("baggage", baggageHeaders) + } + response.status = status + whenever( + scopes.startTransaction(any(), check { assertTrue(it.isBindToScope) }) + ) + .thenAnswer { SentryTracer(it.arguments[0] as TransactionContext, scopes) } + whenever(scopes.isEnabled).thenReturn(isEnabled) + whenever(scopes.continueTrace(any(), any())).thenAnswer { + TransactionContext.fromPropagationContext( + PropagationContext.fromHeaders( + logger, + it.arguments[0] as String?, + it.arguments[1] as List?, + ) + ) + } + return SentryTracingFilter(scopes, transactionNameProvider, isAsyncSupportEnabled) + } + } + + private val fixture = Fixture() + + @Test + fun `creates transaction around the request`() { + val filter = fixture.getSut() + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .startTransaction( + check { + assertEquals("POST /product/12", it.name) + assertEquals(TransactionNameSource.URL, it.transactionNameSource) + assertEquals("http.server", it.operation) + }, + check { + assertNotNull(it.customSamplingContext?.get("request")) + assertTrue(it.customSamplingContext?.get("request") is HttpServletRequest) + assertTrue(it.isBindToScope) + assertThat(it.origin).isEqualTo("auto.http.spring_jakarta.webmvc") + }, + ) + verify(fixture.chain).doFilter(fixture.request, fixture.response) + verify(fixture.scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("POST /product/{id}") + assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.OK) + assertThat(it.contexts.trace!!.operation).isEqualTo("http.server") + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `sets correct span status based on the response status`() { + val filter = fixture.getSut(status = 500) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.INTERNAL_ERROR) }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `does not set span status for response status that dont match predefined span statuses`() { + val filter = fixture.getSut(status = 507) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.status).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `when sentry trace is not present, transaction does not have parentSpanId set`() { + val filter = fixture.getSut() + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `when sentry trace is present, transaction has parentSpanId set`() { + val parentSpanId = SpanId() + val filter = fixture.getSut(sentryTraceHeader = "${SentryId()}-$parentSpanId-1") + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isEqualTo(parentSpanId) }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `when scopes is disabled, components are not invoked`() { + val filter = fixture.getSut(isEnabled = false) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes).isEnabled + verifyNoMoreInteractions(fixture.scopes) + verify(fixture.transactionNameProvider, never()).provideTransactionName(any()) + } + + @Test + fun `sets status to internal server error when chain throws exception`() { + val filter = fixture.getSut() + whenever(fixture.chain.doFilter(any(), any())).thenThrow(RuntimeException("error")) + + try { + filter.doFilter(fixture.request, fixture.response, fixture.chain) + fail("filter is expected to rethrow exception") + } catch (_: Exception) {} + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.status).isEqualTo(SpanStatus.INTERNAL_ERROR) }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `does not track OPTIONS request with traceOptionsRequests=false`() { + val filter = fixture.getSut() + fixture.request.method = HttpMethod.OPTIONS.name() + fixture.options.isTraceOptionsRequests = false + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes).isEnabled + verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) + verify(fixture.scopes, times(3)).options + verifyNoMoreInteractions(fixture.scopes) + verify(fixture.transactionNameProvider, never()).provideTransactionName(any()) + } + + @Test + fun `tracks OPTIONS request with traceOptionsRequests=true`() { + val filter = fixture.getSut() + fixture.request.method = HttpMethod.OPTIONS.name() + fixture.options.isTraceOptionsRequests = true + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `tracks POST request with traceOptionsRequests=false`() { + val filter = fixture.getSut() + fixture.request.method = HttpMethod.POST.name() + fixture.options.isTraceOptionsRequests = false + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `continues incoming trace even is performance is disabled`() { + val parentSpanId = SpanId() + val sentryTraceHeaderString = "2722d9f6ec019ade60c776169d9a8904-$parentSpanId-1" + val baggageHeaderStrings = + listOf( + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET" + ) + fixture.options.tracesSampleRate = null + val filter = + fixture.getSut( + sentryTraceHeader = sentryTraceHeaderString, + baggageHeaders = baggageHeaderStrings, + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes).continueTrace(eq(sentryTraceHeaderString), eq(baggageHeaderStrings)) + + verify(fixture.scopes, never()) + .captureTransaction( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `does not continue incoming trace if span origin is ignored`() { + val parentSpanId = SpanId() + val sentryTraceHeaderString = "2722d9f6ec019ade60c776169d9a8904-$parentSpanId-1" + val baggageHeaderStrings = + listOf( + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET" + ) + fixture.options.tracesSampleRate = null + fixture.options.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.webmvc")) + val filter = + fixture.getSut( + sentryTraceHeader = sentryTraceHeaderString, + baggageHeaders = baggageHeaderStrings, + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes, never()).continueTrace(any(), any()) + + verify(fixture.scopes, never()) + .captureTransaction( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `creates transaction around async request`() { + val sentryTrace = "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1" + val baggage = + listOf( + "baggage: sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d" + ) + val filter = + fixture.getSut( + sentryTraceHeader = sentryTrace, + baggageHeaders = baggage, + isAsyncSupportEnabled = true, + ) + + val asyncChain = mock() + doAnswer { + val request = it.arguments.first() as MockHttpServletRequest + whenever(fixture.asyncRequest.isAsyncStarted).thenReturn(true) + WebAsyncUtils.getAsyncManager(request).setAsyncWebRequest(fixture.asyncRequest) + } + .whenever(asyncChain) + .doFilter(any(), any()) + + filter.doFilter(fixture.request, fixture.response, asyncChain) + + verify(fixture.scopes).continueTrace(eq(sentryTrace), eq(baggage)) + verify(fixture.scopes) + .startTransaction( + check { + assertEquals("POST /product/12", it.name) + assertEquals(TransactionNameSource.URL, it.transactionNameSource) + assertEquals("http.server", it.operation) + }, + check { + assertNotNull(it.customSamplingContext?.get("request")) + assertTrue(it.customSamplingContext?.get("request") is HttpServletRequest) + assertTrue(it.isBindToScope) + assertThat(it.origin).isEqualTo("auto.http.spring_jakarta.webmvc") + }, + ) + verify(asyncChain).doFilter(fixture.request, fixture.response) + verify(fixture.scopes, never()) + .captureTransaction(any(), anyOrNull(), anyOrNull(), anyOrNull()) + + Mockito.clearInvocations(fixture.scopes) + + fixture.request.dispatcherType = DispatcherType.ASYNC + whenever(fixture.asyncRequest.isAsyncStarted).thenReturn(false) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes, never()).startTransaction(anyOrNull(), anyOrNull()) + + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("POST /product/{id}") + assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.OK) + assertThat(it.contexts.trace!!.operation).isEqualTo("http.server") + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + verify(fixture.scopes, never()).continueTrace(anyOrNull(), anyOrNull()) + } + + @Test + fun `creates and finishes transaction immediately for async request if handling disabled`() { + val sentryTrace = "f9118105af4a2d42b4124532cd1065ff-424cffc8f94feeee-1" + val baggage = + listOf( + "baggage: sentry-environment=production,sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rand=0.456789,sentry-sample_rate=0.5,sentry-sampled=true,sentry-trace_id=df71f5972f754b4c85af13ff5c07017d" + ) + val filter = + fixture.getSut( + sentryTraceHeader = sentryTrace, + baggageHeaders = baggage, + isAsyncSupportEnabled = false, + ) + + filter.doFilter(fixture.request, fixture.response, fixture.chain) + + verify(fixture.scopes) + .startTransaction( + check { + assertEquals("POST /product/12", it.name) + assertEquals(TransactionNameSource.URL, it.transactionNameSource) + assertEquals("http.server", it.operation) + }, + check { + assertNotNull(it.customSamplingContext?.get("request")) + assertTrue(it.customSamplingContext?.get("request") is HttpServletRequest) + assertTrue(it.isBindToScope) + assertThat(it.origin).isEqualTo("auto.http.spring_jakarta.webmvc") + }, + ) + verify(fixture.scopes).continueTrace(eq(sentryTrace), eq(baggage)) + verify(fixture.chain).doFilter(fixture.request, fixture.response) + + verify(fixture.scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("POST /product/{id}") + assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.OK) + assertThat(it.contexts.trace!!.operation).isEqualTo("http.server") + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTransactionAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTransactionAdviceTest.kt new file mode 100644 index 00000000000..ed9c1f9abb4 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTransactionAdviceTest.kt @@ -0,0 +1,201 @@ +package io.sentry.spring.jakarta.tracing + +import io.sentry.IScopes +import io.sentry.ISentryLifecycleToken +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TraceContext +import io.sentry.TransactionContext +import io.sentry.TransactionOptions +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertTrue +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.mock +import org.mockito.kotlin.reset +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.EnableAspectJAutoProxy +import org.springframework.context.annotation.Import +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig +import org.springframework.test.context.junit4.SpringRunner + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentryTransactionAdviceTest.Config::class) +class SentryTransactionAdviceTest { + @Autowired lateinit var sampleService: SampleService + + @Autowired lateinit var classAnnotatedSampleService: ClassAnnotatedSampleService + + @Autowired + lateinit var classAnnotatedWithOperationSampleService: ClassAnnotatedWithOperationSampleService + + @Autowired lateinit var scopes: IScopes + + val lifecycleToken = mock() + + @BeforeTest + fun setup() { + reset(scopes) + whenever( + scopes.startTransaction( + any(), + check { + assertTrue(it.isBindToScope) + assertThat(it.origin).isEqualTo("auto.function.spring_jakarta.advice") + }, + ) + ) + .thenAnswer { SentryTracer(it.arguments[0] as TransactionContext, scopes) } + whenever(scopes.options) + .thenReturn(SentryOptions().apply { dsn = "https://key@sentry.io/proj" }) + whenever(scopes.forkedScopes(any())).thenReturn(scopes) + whenever(scopes.makeCurrent()).thenReturn(lifecycleToken) + } + + @Test + fun `creates transaction around method annotated with @SentryTransaction`() { + sampleService.methodWithTransactionNameSet() + verify(scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("customName") + assertThat(it.contexts.trace!!.operation).isEqualTo("bean") + assertThat(it.status).isEqualTo(SpanStatus.OK) + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `when method annotated with @SentryTransaction throws exception, sets error status on transaction`() { + assertThrows { sampleService.methodThrowingException() } + verify(scopes) + .captureTransaction( + check { assertThat(it.status).isEqualTo(SpanStatus.INTERNAL_ERROR) }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `when @SentryTransaction has no name set, sets transaction name as className dot methodName`() { + sampleService.methodWithoutTransactionNameSet() + verify(scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("SampleService.methodWithoutTransactionNameSet") + assertThat(it.contexts.trace!!.operation).isEqualTo("op") + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `when transaction is already active, does not start new transaction`() { + whenever(scopes.options).thenReturn(SentryOptions()) + whenever(scopes.span).then { SentryTracer(TransactionContext("aTransaction", "op"), scopes) } + + sampleService.methodWithTransactionNameSet() + + verify(scopes, times(0)).captureTransaction(any(), any()) + } + + @Test + fun `creates transaction around method in class annotated with @SentryTransaction`() { + classAnnotatedSampleService.hello() + verify(scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("ClassAnnotatedSampleService.hello") + assertThat(it.contexts.trace!!.operation).isEqualTo("op") + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `creates transaction with operation set around method in class annotated with @SentryTransaction`() { + classAnnotatedWithOperationSampleService.hello() + verify(scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("ClassAnnotatedWithOperationSampleService.hello") + assertThat(it.contexts.trace!!.operation).isEqualTo("my-op") + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + + @Test + fun `pushes the scope when advice starts`() { + classAnnotatedSampleService.hello() + verify(scopes).forkedScopes(any()) + verify(scopes).makeCurrent() + } + + @Test + fun `pops the scope when advice finishes`() { + classAnnotatedSampleService.hello() + verify(lifecycleToken).close() + } + + @Configuration + @EnableAspectJAutoProxy(proxyTargetClass = true) + @Import(SentryTracingConfiguration::class) + open class Config { + @Bean open fun sampleService() = SampleService() + + @Bean open fun classAnnotatedSampleService() = ClassAnnotatedSampleService() + + @Bean + open fun classAnnotatedWithOperationSampleService() = ClassAnnotatedWithOperationSampleService() + + @Bean + open fun scopes(): IScopes { + val scopes = mock() + Sentry.setCurrentScopes(scopes) + return scopes + } + } + + open class SampleService { + @SentryTransaction(name = "customName", operation = "bean") + open fun methodWithTransactionNameSet() = Unit + + @SentryTransaction(operation = "op") open fun methodWithoutTransactionNameSet() = Unit + + @SentryTransaction(operation = "op") + open fun methodThrowingException(): Nothing = throw RuntimeException() + } + + @SentryTransaction(operation = "op") + open class ClassAnnotatedSampleService { + open fun hello() = Unit + } + + @SentryTransaction(operation = "my-op") + open class ClassAnnotatedWithOperationSampleService { + open fun hello() = Unit + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt new file mode 100644 index 00000000000..a5c16182fa7 --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt @@ -0,0 +1,58 @@ +package io.sentry.spring.jakarta.webflux + +import io.sentry.Sentry +import io.sentry.test.initForTest +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors +import kotlin.test.AfterTest +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotEquals + +class SentryScheduleHookTest { + private val dsn = "http://key@localhost/proj" + private lateinit var executor: ExecutorService + + @BeforeTest + fun beforeTest() { + executor = Executors.newSingleThreadExecutor() + } + + @AfterTest + fun afterTest() { + Sentry.close() + executor.shutdown() + } + + @Test + fun `scopes is reset to its state within the thread after hook is done`() { + initForTest { it.dsn = dsn } + + val sut = SentryScheduleHook() + + val mainScopes = Sentry.getCurrentScopes() + val threadedScopes = Sentry.getCurrentScopes().forkedCurrentScope("test") + + executor.submit { Sentry.setCurrentScopes(threadedScopes) }.get() + + assertEquals(mainScopes, Sentry.getCurrentScopes()) + + val callableFuture = + executor.submit( + sut.apply { + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertNotEquals(threadedScopes, Sentry.getCurrentScopes()) + } + ) + + callableFuture.get() + + executor + .submit { + assertNotEquals(mainScopes, Sentry.getCurrentScopes()) + assertEquals(threadedScopes, Sentry.getCurrentScopes()) + } + .get() + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt new file mode 100644 index 00000000000..b14f1b5910c --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt @@ -0,0 +1,386 @@ +package io.sentry.spring.jakarta.webflux + +import io.sentry.Breadcrumb +import io.sentry.Hint +import io.sentry.ILogger +import io.sentry.IScopes +import io.sentry.PropagationContext +import io.sentry.ScopeCallback +import io.sentry.Sentry +import io.sentry.SentryOptions +import io.sentry.SentryTracer +import io.sentry.SpanId +import io.sentry.SpanStatus +import io.sentry.TraceContext +import io.sentry.TransactionContext +import io.sentry.TransactionOptions +import io.sentry.protocol.SentryId +import io.sentry.protocol.SentryTransaction +import io.sentry.protocol.TransactionNameSource +import io.sentry.spring.jakarta.webflux.AbstractSentryWebFilter.SENTRY_SCOPES_KEY +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail +import org.assertj.core.api.Assertions.assertThat +import org.mockito.Mockito +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyNoMoreInteractions +import org.mockito.kotlin.whenever +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.server.reactive.ServerHttpRequest +import org.springframework.mock.http.server.reactive.MockServerHttpRequest +import org.springframework.mock.web.server.MockServerWebExchange +import org.springframework.web.reactive.HandlerMapping +import org.springframework.web.server.WebFilterChain +import org.springframework.web.util.pattern.PathPatternParser +import reactor.core.publisher.Mono + +class SentryWebFluxTracingFilterTest { + private class Fixture { + val scopes = mock() + lateinit var request: MockServerHttpRequest + lateinit var exchange: MockServerWebExchange + val chain = mock() + val options = + SentryOptions().apply { + dsn = "https://key@sentry.io/proj" + tracesSampleRate = 1.0 + } + val logger = mock() + + init { + whenever(scopes.options).thenReturn(options) + } + + fun getSut( + isEnabled: Boolean = true, + status: HttpStatus = HttpStatus.OK, + sentryTraceHeader: String? = null, + baggageHeaders: List? = null, + method: HttpMethod = HttpMethod.POST, + ): SentryWebFilter { + var requestBuilder = MockServerHttpRequest.method(method, "/product/{id}", 12) + if (sentryTraceHeader != null) { + requestBuilder = requestBuilder.header("sentry-trace", sentryTraceHeader) + whenever(scopes.startTransaction(any(), check { it.isBindToScope })) + .thenAnswer { SentryTracer(it.arguments[0] as TransactionContext, scopes) } + } + if (baggageHeaders != null) { + requestBuilder = requestBuilder.header("baggage", *baggageHeaders.toTypedArray()) + } + request = requestBuilder.build() + exchange = MockServerWebExchange.builder(request).build() + exchange.attributes.put(SENTRY_SCOPES_KEY, scopes) + exchange.attributes.put( + HandlerMapping.BEST_MATCHING_PATTERN_ATTRIBUTE, + PathPatternParser().parse("/product/{id}"), + ) + exchange.response.statusCode = status + whenever( + scopes.startTransaction(any(), check { assertTrue(it.isBindToScope) }) + ) + .thenAnswer { SentryTracer(it.arguments[0] as TransactionContext, scopes) } + whenever(scopes.isEnabled).thenReturn(isEnabled) + whenever(chain.filter(any())).thenReturn(Mono.create { s -> s.success() }) + whenever(scopes.continueTrace(anyOrNull(), anyOrNull())).thenAnswer { + TransactionContext.fromPropagationContext( + PropagationContext.fromHeaders( + logger, + it.arguments[0] as String?, + it.arguments[1] as List?, + ) + ) + } + return SentryWebFilter(scopes) + } + } + + private val fixture = Fixture() + + fun withMockScopes(closure: () -> Unit) = + Mockito.mockStatic(Sentry::class.java).use { + it.`when` { Sentry.getCurrentScopes() }.thenReturn(fixture.scopes) + it.`when` { Sentry.forkedRootScopes(any()) }.thenReturn(fixture.scopes) + closure.invoke() + } + + @Test + fun `creates transaction around the request`() { + val filter = fixture.getSut() + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.scopes) + .startTransaction( + check { + assertEquals("POST /product/12", it.name) + assertEquals(TransactionNameSource.URL, it.transactionNameSource) + assertEquals("http.server", it.operation) + }, + check { + assertNotNull(it.customSamplingContext?.get("request")) + assertTrue(it.customSamplingContext?.get("request") is ServerHttpRequest) + assertTrue(it.isBindToScope) + assertThat(it.origin).isEqualTo("auto.spring_jakarta.webflux") + }, + ) + verify(fixture.chain).filter(fixture.exchange) + verify(fixture.scopes) + .captureTransaction( + check { + assertThat(it.transaction).isEqualTo("POST /product/{id}") + assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.OK) + assertThat(it.contexts.trace!!.operation).isEqualTo("http.server") + assertThat(it.contexts.response!!.statusCode).isEqualTo(200) + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `sets correct span status based on the response status`() { + val filter = fixture.getSut(status = HttpStatus.INTERNAL_SERVER_ERROR) + + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.scopes) + .captureTransaction( + check { + assertThat(it.contexts.trace!!.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + assertThat(it.contexts.response!!.statusCode).isEqualTo(500) + }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `does not set span status for response status that dont match predefined span statuses`() { + val filter = fixture.getSut(status = HttpStatus.INSUFFICIENT_STORAGE) + + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.status).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `when sentry trace is not present, transaction does not have parentSpanId set`() { + val filter = fixture.getSut() + + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `when sentry trace is present, transaction has parentSpanId set`() { + val parentSpanId = SpanId() + val filter = fixture.getSut(sentryTraceHeader = "${SentryId()}-$parentSpanId-1") + + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isEqualTo(parentSpanId) }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `when scopes is disabled, components are not invoked`() { + val filter = fixture.getSut(isEnabled = false) + + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.chain).filter(fixture.exchange) + + verify(fixture.scopes, times(2)).isEnabled + verifyNoMoreInteractions(fixture.scopes) + } + } + + @Test + fun `sets status to internal server error when chain throws exception`() { + val filter = fixture.getSut() + + withMockScopes { + whenever(fixture.chain.filter(any())).thenReturn(Mono.error(RuntimeException("error"))) + + try { + filter.filter(fixture.exchange, fixture.chain).block() + fail("filter is expected to rethrow exception") + } catch (_: Exception) {} + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.status).isEqualTo(SpanStatus.INTERNAL_ERROR) }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `does not track OPTIONS request with traceOptionsRequests=false`() { + val filter = fixture.getSut(method = HttpMethod.OPTIONS) + + withMockScopes { + fixture.options.isTraceOptionsRequests = false + + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.chain).filter(fixture.exchange) + + verify(fixture.scopes, times(2)).isEnabled + verify(fixture.scopes, times(4)).options + verify(fixture.scopes).continueTrace(anyOrNull(), anyOrNull()) + verify(fixture.scopes).addBreadcrumb(any(), any()) + verify(fixture.scopes).configureScope(any()) + verifyNoMoreInteractions(fixture.scopes) + } + } + + @Test + fun `tracks OPTIONS request with traceOptionsRequests=true`() { + val filter = fixture.getSut(method = HttpMethod.OPTIONS) + + withMockScopes { + fixture.options.isTraceOptionsRequests = true + + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.chain).filter(fixture.exchange) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `tracks POST request with traceOptionsRequests=false`() { + val filter = fixture.getSut(method = HttpMethod.POST) + + withMockScopes { + fixture.options.isTraceOptionsRequests = false + + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.chain).filter(fixture.exchange) + + verify(fixture.scopes) + .captureTransaction( + check { assertThat(it.contexts.trace!!.parentSpanId).isNull() }, + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } + } + + @Test + fun `continues incoming trace even is performance is disabled`() { + val parentSpanId = SpanId() + val sentryTraceHeaderString = "2722d9f6ec019ade60c776169d9a8904-$parentSpanId-1" + val baggageHeaderStrings = + listOf( + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET" + ) + fixture.options.tracesSampleRate = null + val filter = + fixture.getSut( + sentryTraceHeader = sentryTraceHeaderString, + baggageHeaders = baggageHeaderStrings, + ) + + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.chain).filter(fixture.exchange) + + verify(fixture.scopes, never()) + .captureTransaction( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + + verify(fixture.scopes).continueTrace(eq(sentryTraceHeaderString), eq(baggageHeaderStrings)) + } + } + + @Test + fun `does not continue incoming trace is span origin is ignored`() { + val parentSpanId = SpanId() + val sentryTraceHeaderString = "2722d9f6ec019ade60c776169d9a8904-$parentSpanId-1" + val baggageHeaderStrings = + listOf( + "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET" + ) + fixture.options.tracesSampleRate = null + fixture.options.setIgnoredSpanOrigins(listOf("auto.spring_jakarta.webflux")) + val filter = + fixture.getSut( + sentryTraceHeader = sentryTraceHeaderString, + baggageHeaders = baggageHeaderStrings, + ) + + withMockScopes { + filter.filter(fixture.exchange, fixture.chain).block() + + verify(fixture.chain).filter(fixture.exchange) + + verify(fixture.scopes, never()) + .captureTransaction( + anyOrNull(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + + verify(fixture.scopes, never()).continueTrace(any(), any()) + } + } +} diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt new file mode 100644 index 00000000000..e3b3ed55a7c --- /dev/null +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt @@ -0,0 +1,196 @@ +package io.sentry.spring.jakarta.webflux + +import io.sentry.IScopes +import io.sentry.ITransportFactory +import io.sentry.ScopesAdapter +import io.sentry.Sentry +import io.sentry.checkEvent +import io.sentry.checkTransaction +import io.sentry.test.initForTest +import io.sentry.transport.ITransport +import java.time.Duration +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import org.assertj.core.api.Assertions.assertThat +import org.awaitility.kotlin.await +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.ApplicationRunner +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration +import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.context.annotation.Bean +import org.springframework.http.ResponseEntity +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.test.web.reactive.server.WebTestClient +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RestController +import reactor.core.publisher.Mono +import reactor.core.scheduler.Schedulers + +@RunWith(SpringRunner::class) +@SpringBootTest( + classes = [App::class], + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = ["spring.main.web-application-type=reactive"], +) +class SentryWebfluxIntegrationTest { + @Autowired lateinit var transport: ITransport + + @LocalServerPort var port: Int? = null + + private val testClient = WebTestClient.bindToServer().build() + + @BeforeTest + fun `reset mocks`() { + reset(transport) + } + + @Test + fun `attaches request information to SentryEvents`() { + testClient + .get() + .uri("http://localhost:$port/hello?param=value#top") + .exchange() + .expectStatus() + .isOk + + verify(transport) + .send( + checkEvent { event -> + assertNotNull(event.request) { + assertEquals("http://localhost:$port/hello", it.url) + assertEquals("GET", it.method) + assertEquals("param=value", it.queryString) + assertNull(it.fragment) + } + }, + anyOrNull(), + ) + } + + @Test + fun `sends events for unhandled exceptions`() { + testClient.get().uri("http://localhost:$port/throws").exchange().expectStatus().is5xxServerError + + verify(transport) + .send( + checkEvent { event -> + assertEquals("GET /throws", event.transaction) + assertNotNull(event.exceptions) { + val ex = it.last() + assertEquals("something went wrong", ex.value) + assertNotNull(ex.mechanism) { + assertThat(it.isHandled).isFalse() + assertThat(it.type).isEqualTo(SentryWebExceptionHandler.MECHANISM_TYPE) + } + } + }, + anyOrNull(), + ) + } + + @Test + fun `does not send events for handled exceptions`() { + testClient + .get() + .uri("http://localhost:$port/throws-handled") + .exchange() + .expectStatus() + .isBadRequest + + await.during(Duration.ofSeconds(2)).untilAsserted { + verify(transport, never()).send(checkEvent { event -> assertNotNull(event) }, anyOrNull()) + } + } + + @Test + fun `sends transaction`() { + testClient + .get() + .uri("http://localhost:$port/hello?param=value#top") + .exchange() + .expectStatus() + .isOk + + verify(transport) + .send( + checkTransaction { event -> assertEquals("GET /hello", event.transaction) }, + anyOrNull(), + ) + } +} + +@SpringBootApplication( + exclude = [ReactiveSecurityAutoConfiguration::class, SecurityAutoConfiguration::class] +) +open class App { + private val transport = mock().also { whenever(it.isHealthy).thenReturn(true) } + + @Bean + open fun mockTransportFactory(): ITransportFactory { + val factory = mock() + whenever(factory.create(any(), any())).thenReturn(transport) + return factory + } + + @Bean open fun mockTransport() = transport + + @Bean open fun scopes() = ScopesAdapter.getInstance() + + @Bean open fun sentryFilter(scopes: IScopes) = SentryWebFilter(scopes) + + @Bean open fun sentryWebExceptionHandler(scopes: IScopes) = SentryWebExceptionHandler(scopes) + + @Bean + open fun sentryScheduleHookRegistrar() = ApplicationRunner { + Schedulers.onScheduleHook("sentry", SentryScheduleHook()) + } + + @Bean + open fun sentryInitializer(transportFactory: ITransportFactory) = ApplicationRunner { + initForTest { + it.dsn = "http://key@localhost/proj" + it.setDebug(true) + it.setTransportFactory(transportFactory) + it.tracesSampleRate = 1.0 + it.isEnableBackpressureHandling = false + } + } +} + +@RestController +class HelloController { + @GetMapping("/hello") + fun hello(): Mono { + Sentry.captureMessage("hello") + return Mono.empty() + } + + @GetMapping("/throws") fun throws(): Unit = throw RuntimeException("something went wrong") + + @GetMapping("/throws-handled") + fun throwsHandled(): Unit = throw CustomException("handled exception") +} + +class CustomException(message: String) : RuntimeException(message) + +@ControllerAdvice +class ExceptionHandlers { + @ExceptionHandler(CustomException::class) + fun handle(e: CustomException) = ResponseEntity.badRequest().build() +} diff --git a/sentry-spring-boot-4-starter/.gitignore b/sentry-spring-boot-4-starter/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/sentry-spring-boot-4-starter/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sentry-spring-boot-4-starter/api/sentry-spring-boot-starter-jakarta.api b/sentry-spring-boot-4-starter/api/sentry-spring-boot-starter-jakarta.api new file mode 100644 index 00000000000..e69de29bb2d diff --git a/sentry-spring-boot-4-starter/build.gradle.kts b/sentry-spring-boot-4-starter/build.gradle.kts new file mode 100644 index 00000000000..503a66dd004 --- /dev/null +++ b/sentry-spring-boot-4-starter/build.gradle.kts @@ -0,0 +1,80 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.springframework.boot.gradle.plugin.SpringBootPlugin + +plugins { + `java-library` + id("io.sentry.javadoc") + kotlin("jvm") + jacoco + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.springboot4) apply false +} + +configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() +} + +dependencies { + api(projects.sentrySpringBoot4) + api(libs.springboot4.starter) + + annotationProcessor(platform(SpringBootPlugin.BOM_COORDINATES)) + annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure) + annotationProcessor(Config.AnnotationProcessors.springBootConfiguration) + + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +tasks.withType().configureEach { + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_SPRING_BOOT_4_STARTER_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-spring-boot-4-starter", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} diff --git a/sentry-spring-boot-4/.gitignore b/sentry-spring-boot-4/.gitignore new file mode 100644 index 00000000000..42afabfd2ab --- /dev/null +++ b/sentry-spring-boot-4/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-jakarta.api b/sentry-spring-boot-4/api/sentry-spring-boot-jakarta.api new file mode 100644 index 00000000000..b0ef970d7d3 --- /dev/null +++ b/sentry-spring-boot-4/api/sentry-spring-boot-jakarta.api @@ -0,0 +1,93 @@ +public final class io/sentry/spring/boot/jakarta/BuildConfig { + public static final field SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public class io/sentry/spring/boot/jakarta/InAppIncludesResolver : org/springframework/context/ApplicationContextAware { + public fun ()V + public fun resolveInAppIncludes ()Ljava/util/List; + public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V +} + +public class io/sentry/spring/boot/jakarta/SentryAutoConfiguration { + public fun ()V +} + +public class io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration { + public fun ()V + public fun sentryLogbackInitializer (Lio/sentry/spring/boot/jakarta/SentryProperties;)Lio/sentry/spring/boot/jakarta/SentryLogbackInitializer; +} + +public class io/sentry/spring/boot/jakarta/SentryLogbackInitializer : org/springframework/context/event/GenericApplicationListener { + public fun (Lio/sentry/spring/boot/jakarta/SentryProperties;)V + public fun onApplicationEvent (Lorg/springframework/context/ApplicationEvent;)V + public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z +} + +public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOptions { + public fun ()V + public fun getExceptionResolverOrder ()I + public fun getGraphql ()Lio/sentry/spring/boot/jakarta/SentryProperties$Graphql; + public fun getLogging ()Lio/sentry/spring/boot/jakarta/SentryProperties$Logging; + public fun getReactive ()Lio/sentry/spring/boot/jakarta/SentryProperties$Reactive; + public fun getUserFilterOrder ()Ljava/lang/Integer; + public fun isEnableAotCompatibility ()Z + public fun isKeepTransactionsOpenForAsyncResponses ()Z + public fun isUseGitCommitIdAsRelease ()Z + public fun setEnableAotCompatibility (Z)V + public fun setExceptionResolverOrder (I)V + public fun setGraphql (Lio/sentry/spring/boot/jakarta/SentryProperties$Graphql;)V + public fun setKeepTransactionsOpenForAsyncResponses (Z)V + public fun setLogging (Lio/sentry/spring/boot/jakarta/SentryProperties$Logging;)V + public fun setReactive (Lio/sentry/spring/boot/jakarta/SentryProperties$Reactive;)V + public fun setUseGitCommitIdAsRelease (Z)V + public fun setUserFilterOrder (Ljava/lang/Integer;)V +} + +public class io/sentry/spring/boot/jakarta/SentryProperties$Graphql { + public fun ()V + public fun getIgnoredErrorTypes ()Ljava/util/List; + public fun setIgnoredErrorTypes (Ljava/util/List;)V +} + +public class io/sentry/spring/boot/jakarta/SentryProperties$Logging { + public fun ()V + public fun getLoggers ()Ljava/util/List; + public fun getMinimumBreadcrumbLevel ()Lorg/slf4j/event/Level; + public fun getMinimumEventLevel ()Lorg/slf4j/event/Level; + public fun getMinimumLevel ()Lorg/slf4j/event/Level; + public fun isEnabled ()Z + public fun setEnabled (Z)V + public fun setLoggers (Ljava/util/List;)V + public fun setMinimumBreadcrumbLevel (Lorg/slf4j/event/Level;)V + public fun setMinimumEventLevel (Lorg/slf4j/event/Level;)V + public fun setMinimumLevel (Lorg/slf4j/event/Level;)V +} + +public class io/sentry/spring/boot/jakarta/SentryProperties$Reactive { + public fun ()V + public fun isThreadLocalAccessorEnabled ()Z + public fun setThreadLocalAccessorEnabled (Z)V +} + +public class io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration { + public fun ()V + public fun sentryWebExceptionHandler (Lio/sentry/IScopes;)Lio/sentry/spring/jakarta/webflux/SentryWebExceptionHandler; +} + +public class io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration { + public fun ()V + public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; + public static fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; + public fun sentryInstrumentationWebMvc (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; + public fun sentryInstrumentationWebflux (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; +} + +public class io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration { + public fun ()V + public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; + public static fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; + public fun sentryInstrumentationWebMvc (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; + public fun sentryInstrumentationWebflux (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; +} + diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts new file mode 100644 index 00000000000..fc1c8d031b1 --- /dev/null +++ b/sentry-spring-boot-4/build.gradle.kts @@ -0,0 +1,143 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile +import org.springframework.boot.gradle.plugin.SpringBootPlugin + +plugins { + `java-library` + id("io.sentry.javadoc") + kotlin("jvm") + jacoco + alias(libs.plugins.errorprone) + alias(libs.plugins.gradle.versions) + alias(libs.plugins.buildconfig) + alias(libs.plugins.springboot4) apply false +} + +configure { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() +} + +dependencies { + api(projects.sentry) + api(projects.sentrySpring7) + compileOnly(projects.sentryLogback) + compileOnly(projects.sentryApacheHttpClient5) + compileOnly(platform(SpringBootPlugin.BOM_COORDINATES)) + compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryGraphql22) + compileOnly(projects.sentryQuartz) + compileOnly(Config.Libs.springWeb) + compileOnly(Config.Libs.springWebflux) + compileOnly(libs.context.propagation) + compileOnly(libs.jetbrains.annotations) + compileOnly(libs.nopen.annotations) + compileOnly(libs.otel) + compileOnly(libs.reactor.core) + compileOnly(libs.servlet.jakarta.api) + compileOnly(libs.springboot4.starter) + compileOnly(libs.springboot4.starter.aop) + compileOnly(libs.springboot4.starter.graphql) + compileOnly(libs.springboot4.starter.quartz) + compileOnly(libs.springboot4.starter.security) + compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore) + compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) + api(projects.sentryReactor) + + annotationProcessor(platform(SpringBootPlugin.BOM_COORDINATES)) + annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure) + annotationProcessor(Config.AnnotationProcessors.springBootConfiguration) + + errorprone(libs.errorprone.core) + errorprone(libs.nopen.checker) + errorprone(libs.nullaway) + + // tests + testImplementation(projects.sentryLogback) + testImplementation(projects.sentryApacheHttpClient5) + testImplementation(projects.sentryGraphql) + testImplementation(projects.sentryGraphql22) + testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryCore) + testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryAgent) + testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) + testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryBootstrap) + testImplementation(projects.sentryQuartz) + testImplementation(projects.sentryReactor) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(platform(SpringBootPlugin.BOM_COORDINATES)) + testImplementation(libs.context.propagation) + testImplementation(libs.kotlin.test.junit) + testImplementation(libs.mockito.kotlin) + testImplementation(libs.okhttp) + testImplementation(libs.okhttp.mockwebserver) + testImplementation(libs.otel) + testImplementation(libs.otel.extension.autoconfigure.spi) + testImplementation(libs.springboot4.otel) + testImplementation(libs.springboot4.starter) + testImplementation(libs.springboot4.starter.aop) + testImplementation(libs.springboot4.starter.graphql) + testImplementation(libs.springboot4.starter.quartz) + testImplementation(libs.springboot4.starter.security) + testImplementation(libs.springboot4.starter.test) + testImplementation(libs.springboot4.starter.web) + testImplementation(libs.springboot4.starter.webflux) +} + +configure { test { java.srcDir("src/test/java") } } + +jacoco { toolVersion = libs.versions.jacoco.get() } + +tasks.jacocoTestReport { + reports { + xml.required.set(true) + html.required.set(false) + } +} + +tasks { + jacocoTestCoverageVerification { + violationRules { rule { limit { minimum = Config.QualityPlugins.Jacoco.minimumCoverage } } } + } + check { + dependsOn(jacocoTestCoverageVerification) + dependsOn(jacocoTestReport) + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.spring.boot4") + buildConfigField( + "String", + "SENTRY_SPRING_BOOT_4_SDK_NAME", + "\"${Config.Sentry.SENTRY_SPRING_BOOT_4_SDK_NAME}\"", + ) + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} + +tasks.withType().configureEach { + dependsOn(tasks.generateBuildConfig) + options.errorprone { + check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.ERROR) + option("NullAway:AnnotatedPackages", "io.sentry") + } +} + +tasks.jar { + manifest { + attributes( + "Sentry-Version-Name" to project.version, + "Sentry-SDK-Name" to Config.Sentry.SENTRY_SPRING_BOOT_4_SDK_NAME, + "Sentry-SDK-Package-Name" to "maven:io.sentry:sentry-spring-boot-4", + "Implementation-Vendor" to "Sentry", + "Implementation-Title" to project.name, + "Implementation-Version" to project.version, + ) + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/InAppIncludesResolver.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/InAppIncludesResolver.java new file mode 100644 index 00000000000..2534ad14d19 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/InAppIncludesResolver.java @@ -0,0 +1,43 @@ +package io.sentry.spring.boot.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.springframework.beans.BeansException; +import org.springframework.boot.SpringBootConfiguration; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; + +/** + * Resolves {@link SentryProperties} inAppIncludes by getting a package name from a class annotated + * with {@link SpringBootConfiguration} or another annotation meta-annotated with {@link + * SpringBootConfiguration} like {@link SpringBootApplication}. + */ +@Open +public class InAppIncludesResolver implements ApplicationContextAware { + private @Nullable ApplicationContext applicationContext; + + @NotNull + public List resolveInAppIncludes() { + if (applicationContext != null) { + Map beansWithAnnotation = + applicationContext.getBeansWithAnnotation(SpringBootConfiguration.class); + return beansWithAnnotation.values().stream() + .map(bean -> bean.getClass().getPackage().getName()) + .collect(Collectors.toList()); + } else { + return Collections.emptyList(); + } + } + + @Override + public void setApplicationContext(@NotNull ApplicationContext applicationContext) + throws BeansException { + this.applicationContext = applicationContext; + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java new file mode 100644 index 00000000000..fcaaa5f2645 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -0,0 +1,500 @@ +package io.sentry.spring.boot.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import graphql.GraphQLError; +import io.sentry.EventProcessor; +import io.sentry.IScopes; +import io.sentry.ISpanFactory; +import io.sentry.ITransportFactory; +import io.sentry.InitPriority; +import io.sentry.Integration; +import io.sentry.ScopesAdapter; +import io.sentry.Sentry; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.SentryOptions; +import io.sentry.protocol.SdkVersion; +import io.sentry.quartz.SentryJobListener; +import io.sentry.spring.boot.jakarta.graphql.SentryGraphql22AutoConfiguration; +import io.sentry.spring.boot.jakarta.graphql.SentryGraphqlAutoConfiguration; +import io.sentry.spring.jakarta.ContextTagsEventProcessor; +import io.sentry.spring.jakarta.SentryExceptionResolver; +import io.sentry.spring.jakarta.SentryRequestResolver; +import io.sentry.spring.jakarta.SentrySpringFilter; +import io.sentry.spring.jakarta.SentryUserFilter; +import io.sentry.spring.jakarta.SentryUserProvider; +import io.sentry.spring.jakarta.SentryWebConfiguration; +import io.sentry.spring.jakarta.SpringProfilesEventProcessor; +import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider; +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.opentelemetry.SentryOpenTelemetryAgentWithoutAutoInitConfiguration; +import io.sentry.spring.jakarta.opentelemetry.SentryOpenTelemetryNoAgentConfiguration; +import io.sentry.spring.jakarta.tracing.CombinedTransactionNameProvider; +import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration; +import io.sentry.spring.jakarta.tracing.SentrySpanPointcutConfiguration; +import io.sentry.spring.jakarta.tracing.SentryTracingFilter; +import io.sentry.spring.jakarta.tracing.SentryTransactionPointcutConfiguration; +import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; +import io.sentry.spring.jakarta.tracing.SpringServletTransactionNameProvider; +import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.transport.ITransportGate; +import io.sentry.transport.apache.ApacheHttpClientTransportFactory; +import jakarta.servlet.http.HttpServletRequest; +import java.util.Arrays; +import java.util.List; +import java.util.Optional; +import org.aspectj.lang.ProceedingJoinPoint; +import org.jetbrains.annotations.NotNull; +import org.quartz.core.QuartzScheduler; +import org.slf4j.MDC; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; +import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; +import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.info.GitProperties; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import org.springframework.core.env.Environment; +import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.client.RestClient; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import org.springframework.web.servlet.HandlerExceptionResolver; + +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(name = "sentry.dsn") +@Open +public class SentryAutoConfiguration { + + static { + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-spring-boot-starter-jakarta", BuildConfig.VERSION_NAME); + } + + /** Registers general purpose Sentry related beans. */ + @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties(SentryProperties.class) + @Open + static class HubConfiguration { + + @Bean + @ConditionalOnMissingBean(name = "sentryOptionsConfiguration") + @Order(Ordered.HIGHEST_PRECEDENCE) + public @NotNull Sentry.OptionsConfiguration sentryOptionsConfiguration( + final @NotNull ObjectProvider beforeSendCallback, + final @NotNull ObjectProvider + beforeSendTransactionCallback, + final @NotNull ObjectProvider + beforeSendLogsCallback, + final @NotNull ObjectProvider + beforeBreadcrumbCallback, + final @NotNull ObjectProvider tracesSamplerCallback, + final @NotNull List eventProcessors, + final @NotNull List integrations, + final @NotNull ObjectProvider transportGate, + final @NotNull ObjectProvider transportFactory, + final @NotNull InAppIncludesResolver inAppPackagesResolver) { + return options -> { + beforeSendCallback.ifAvailable(options::setBeforeSend); + beforeSendTransactionCallback.ifAvailable(options::setBeforeSendTransaction); + beforeSendLogsCallback.ifAvailable(callback -> options.getLogs().setBeforeSend(callback)); + beforeBreadcrumbCallback.ifAvailable(options::setBeforeBreadcrumb); + tracesSamplerCallback.ifAvailable(options::setTracesSampler); + eventProcessors.forEach(options::addEventProcessor); + integrations.forEach(options::addIntegration); + transportGate.ifAvailable(options::setTransportGate); + transportFactory.ifAvailable(options::setTransportFactory); + inAppPackagesResolver.resolveInAppIncludes().forEach(options::addInAppInclude); + }; + } + + @Bean + public @NotNull InAppIncludesResolver inAppPackagesResolver() { + return new InAppIncludesResolver(); + } + + @Configuration(proxyBeanMethods = false) + @Import(SentryOpenTelemetryAgentWithoutAutoInitConfiguration.class) + @Open + @ConditionalOnProperty(name = "sentry.auto-init", havingValue = "false") + @ConditionalOnClass(name = {"io.sentry.opentelemetry.agent.AgentMarker"}) + static class OpenTelemetryAgentWithoutAutoInitConfiguration {} + + @Configuration(proxyBeanMethods = false) + @Import(SentryOpenTelemetryNoAgentConfiguration.class) + @Open + @ConditionalOnClass( + name = { + "io.opentelemetry.api.OpenTelemetry", + "io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider" + }) + @ConditionalOnMissingClass("io.sentry.opentelemetry.agent.AgentMarker") + static class OpenTelemetryNoAgentConfiguration {} + + @Bean + public @NotNull IScopes sentryHub( + final @NotNull List> optionsConfigurations, + final @NotNull SentryProperties options, + final @NotNull ObjectProvider spanFactory, + final @NotNull ObjectProvider gitProperties) { + optionsConfigurations.forEach( + optionsConfiguration -> optionsConfiguration.configure(options)); + gitProperties.ifAvailable( + git -> { + if (options.getRelease() == null && options.isUseGitCommitIdAsRelease()) { + options.setRelease(git.getCommitId()); + } + }); + spanFactory.ifAvailable(options::setSpanFactory); + + options.setSentryClientName( + BuildConfig.SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME + "/" + BuildConfig.VERSION_NAME); + options.setSdkVersion(createSdkVersion(options)); + options.setInitPriority(InitPriority.LOW); + addPackageAndIntegrationInfo(); + // Spring Boot sets ignored exceptions in runtime using reflection - where the generic + // information is lost + // its technically possible to set non-throwable class to `ignoredExceptionsForType` set + // here we make sure that only classes that extend throwable are set on this field + options.getIgnoredExceptionsForType().removeIf(it -> !Throwable.class.isAssignableFrom(it)); + Sentry.init(options); + return ScopesAdapter.getInstance(); + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(MDC.class) + @Open + static class ContextTagsEventProcessorConfiguration { + + @Bean + public @NotNull ContextTagsEventProcessor contextTagsEventProcessor( + final @NotNull SentryOptions sentryOptions) { + return new ContextTagsEventProcessor(sentryOptions); + } + } + + @Configuration(proxyBeanMethods = false) + @Import(SentryGraphqlAutoConfiguration.class) + @Open + @ConditionalOnClass({ + io.sentry.graphql.SentryInstrumentation.class, + DataFetcherExceptionResolverAdapter.class, + GraphQLError.class + }) + @ConditionalOnMissingClass({ + "io.sentry.graphql22.SentryInstrumentation" // avoid duplicate bean + }) + static class GraphqlConfiguration {} + + @Configuration(proxyBeanMethods = false) + @Import(SentryGraphql22AutoConfiguration.class) + @Open + @ConditionalOnClass({ + io.sentry.graphql22.SentryInstrumentation.class, + DataFetcherExceptionResolverAdapter.class, + GraphQLError.class + }) + static class Graphql22Configuration {} + + @Configuration(proxyBeanMethods = false) + @Import(SentryQuartzConfiguration.class) + @Open + @ConditionalOnClass({ + SentryJobListener.class, + QuartzScheduler.class, + SchedulerFactoryBean.class + }) + static class QuartzConfiguration {} + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(ProceedingJoinPoint.class) + @ConditionalOnProperty( + value = "sentry.enable-aot-compatibility", + havingValue = "false", + matchIfMissing = true) + @Import(SentryCheckInAdviceConfiguration.class) + @Open + static class SentryCheckInAspectsConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(name = "sentryCheckInPointcut") + @Import(SentryCheckInPointcutConfiguration.class) + @Open + static class SentryCheckInPointcutAutoConfiguration {} + } + + /** Registers beans specific to Spring MVC. */ + @Configuration(proxyBeanMethods = false) + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) + @Import(SentryWebConfiguration.class) + @Open + static class SentryWebMvcConfiguration { + + private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE; + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(SecurityContextHolder.class) + @Open + static class SentrySecurityConfiguration { + + /** + * Configures {@link SpringSecuritySentryUserProvider} only if Spring Security is on the + * classpath. Its order is set to be higher than {@link + * SentryWebConfiguration#httpServletRequestSentryUserProvider(SentryOptions)} + * + * @param sentryOptions the Sentry options + * @return {@link SpringSecuritySentryUserProvider} + */ + @Bean + @Order(1) + public @NotNull SpringSecuritySentryUserProvider springSecuritySentryUserProvider( + final @NotNull SentryOptions sentryOptions) { + return new SpringSecuritySentryUserProvider(sentryOptions); + } + } + + /** + * Configures {@link SentryUserFilter}. By default it runs as the last filter in order to make + * sure that all potential authentication information is propagated to {@link + * HttpServletRequest#getUserPrincipal()}. If Spring Security is auto-configured, its order is + * set to run after Spring Security. + * + * @param scopes the Sentry scopes + * @param sentryProperties the Sentry properties + * @param sentryUserProvider the user provider + * @return {@link SentryUserFilter} registration bean + */ + @Bean + @ConditionalOnBean(SentryUserProvider.class) + public @NotNull FilterRegistrationBean sentryUserFilter( + final @NotNull IScopes scopes, + final @NotNull SentryProperties sentryProperties, + final @NotNull List sentryUserProvider) { + final FilterRegistrationBean filter = new FilterRegistrationBean<>(); + filter.setFilter(new SentryUserFilter(scopes, sentryUserProvider)); + filter.setOrder(resolveUserFilterOrder(sentryProperties)); + return filter; + } + + private @NotNull Integer resolveUserFilterOrder( + final @NotNull SentryProperties sentryProperties) { + return Optional.ofNullable(sentryProperties.getUserFilterOrder()) + .orElse(Ordered.LOWEST_PRECEDENCE); + } + + @Bean + public @NotNull SentryRequestResolver sentryRequestResolver(final @NotNull IScopes scopes) { + return new SentryRequestResolver(scopes); + } + + @Bean + @ConditionalOnMissingBean(name = "sentrySpringFilter") + public @NotNull FilterRegistrationBean sentrySpringFilter( + final @NotNull IScopes scopes, + final @NotNull SentryRequestResolver requestResolver, + final @NotNull TransactionNameProvider transactionNameProvider) { + FilterRegistrationBean filter = + new FilterRegistrationBean<>( + new SentrySpringFilter(scopes, requestResolver, transactionNameProvider)); + filter.setOrder(SENTRY_SPRING_FILTER_PRECEDENCE); + return filter; + } + + @Bean + @ConditionalOnMissingBean(name = "sentryTracingFilter") + public FilterRegistrationBean sentryTracingFilter( + final @NotNull IScopes scopes, + final @NotNull TransactionNameProvider transactionNameProvider, + final @NotNull SentryProperties sentryProperties) { + FilterRegistrationBean filter = + new FilterRegistrationBean<>( + new SentryTracingFilter( + scopes, + transactionNameProvider, + sentryProperties.isKeepTransactionsOpenForAsyncResponses())); + filter.setOrder(SENTRY_SPRING_FILTER_PRECEDENCE + 1); // must run after SentrySpringFilter + return filter; + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(HandlerExceptionResolver.class) + @Open + static class SentryMvcModeConfig { + + @Bean + @ConditionalOnMissingBean + public @NotNull SentryExceptionResolver sentryExceptionResolver( + final @NotNull IScopes scopes, + final @NotNull TransactionNameProvider transactionNameProvider, + final @NotNull SentryProperties options) { + return new SentryExceptionResolver( + scopes, transactionNameProvider, options.getExceptionResolverOrder()); + } + + @Bean + @ConditionalOnMissingBean(TransactionNameProvider.class) + public @NotNull TransactionNameProvider transactionNameProvider() { + return new CombinedTransactionNameProvider( + Arrays.asList( + new SpringMvcTransactionNameProvider(), + new SpringServletTransactionNameProvider())); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("org.springframework.web.servlet.HandlerExceptionResolver") + @Open + static class SentryServletModeConfig { + + @Bean + @ConditionalOnMissingBean(TransactionNameProvider.class) + public @NotNull TransactionNameProvider transactionNameProvider() { + return new SpringServletTransactionNameProvider(); + } + } + } + + @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", + havingValue = "false", + matchIfMissing = true) + @Conditional(SentryTracingCondition.class) + @ConditionalOnClass(ProceedingJoinPoint.class) + @Import(SentryAdviceConfiguration.class) + @Open + static class SentryPerformanceAspectsConfiguration { + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(name = "sentryTransactionPointcut") + @Import(SentryTransactionPointcutConfiguration.class) + @Open + static class SentryTransactionPointcutAutoConfiguration {} + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(name = "sentrySpanPointcut") + @Import(SentrySpanPointcutConfiguration.class) + @Open + static class SentrySpanPointcutAutoConfiguration {} + } + + @Configuration(proxyBeanMethods = false) + @AutoConfigureBefore(RestTemplateAutoConfiguration.class) + @ConditionalOnClass(RestTemplate.class) + @Open + static class SentryPerformanceRestTemplateConfiguration { + @Bean + public SentrySpanRestTemplateCustomizer sentrySpanRestTemplateCustomizer(IScopes scopes) { + return new SentrySpanRestTemplateCustomizer(scopes); + } + } + + @Configuration(proxyBeanMethods = false) + @AutoConfigureBefore(RestClientAutoConfiguration.class) + @ConditionalOnClass(RestClient.class) + @Open + static class SentrySpanRestClientConfiguration { + @Bean + public SentrySpanRestClientCustomizer sentrySpanRestClientCustomizer(IScopes scopes) { + return new SentrySpanRestClientCustomizer(scopes); + } + } + + @Configuration(proxyBeanMethods = false) + @AutoConfigureBefore(WebClientAutoConfiguration.class) + @ConditionalOnClass(WebClient.class) + @Open + static class SentryPerformanceWebClientConfiguration { + @Bean + public SentrySpanWebClientCustomizer sentrySpanWebClientCustomizer(IScopes scopes) { + return new SentrySpanWebClientCustomizer(scopes); + } + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingBean(ITransportFactory.class) + @ConditionalOnClass(ApacheHttpClientTransportFactory.class) + @Open + static class ApacheHttpClientTransportFactoryAutoconfiguration { + + @Bean + public @NotNull ApacheHttpClientTransportFactory apacheHttpClientTransportFactory() { + return new ApacheHttpClientTransportFactory(); + } + } + + private static @NotNull SdkVersion createSdkVersion( + final @NotNull SentryOptions sentryOptions) { + SdkVersion sdkVersion = sentryOptions.getSdkVersion(); + + final String name = BuildConfig.SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME; + final String version = BuildConfig.VERSION_NAME; + sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version); + + return sdkVersion; + } + + private static void addPackageAndIntegrationInfo() { + SentryIntegrationPackageStorage.getInstance().addIntegration("SpringBoot3"); + } + } + + static final class SentryTracingCondition extends AnyNestedCondition { + + public SentryTracingCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty(name = "sentry.traces-sample-rate") + @SuppressWarnings("UnusedNestedClass") + private static class SentryTracesSampleRateCondition {} + + @ConditionalOnBean(SentryOptions.TracesSamplerCallback.class) + @SuppressWarnings("UnusedNestedClass") + private static class SentryTracesSamplerBeanCondition {} + } + + @Configuration(proxyBeanMethods = false) + @Open + static class SpringProfilesEventProcessorConfiguration { + @Bean + public @NotNull SpringProfilesEventProcessor springProfilesEventProcessor( + final Environment environment) { + return new SpringProfilesEventProcessor(environment); + } + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration.java new file mode 100644 index 00000000000..13866d3a6a5 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration.java @@ -0,0 +1,26 @@ +package io.sentry.spring.boot.jakarta; + +import ch.qos.logback.classic.LoggerContext; +import com.jakewharton.nopen.annotation.Open; +import io.sentry.logback.SentryAppender; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** Auto-configures {@link SentryAppender}. */ +@Configuration(proxyBeanMethods = false) +@Open +@ConditionalOnClass({LoggerContext.class, SentryAppender.class}) +@ConditionalOnProperty(name = "sentry.logging.enabled", havingValue = "true", matchIfMissing = true) +@ConditionalOnBean(SentryProperties.class) +public class SentryLogbackAppenderAutoConfiguration { + + @Bean + public @NotNull SentryLogbackInitializer sentryLogbackInitializer( + final @NotNull SentryProperties sentryProperties) { + return new SentryLogbackInitializer(sentryProperties); + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackInitializer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackInitializer.java new file mode 100644 index 00000000000..be222eae1bf --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackInitializer.java @@ -0,0 +1,85 @@ +package io.sentry.spring.boot.jakarta; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.Logger; +import ch.qos.logback.classic.LoggerContext; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.Appender; +import com.jakewharton.nopen.annotation.Open; +import io.sentry.logback.SentryAppender; +import io.sentry.util.Objects; +import java.util.Iterator; +import java.util.List; +import java.util.Optional; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.LoggerFactory; +import org.springframework.context.ApplicationEvent; +import org.springframework.context.event.ContextRefreshedEvent; +import org.springframework.context.event.GenericApplicationListener; +import org.springframework.core.ResolvableType; + +/** Registers {@link SentryAppender} after Spring context gets refreshed. */ +@Open +public class SentryLogbackInitializer implements GenericApplicationListener { + private final @NotNull SentryProperties sentryProperties; + private final @NotNull List loggers; + @Nullable private SentryAppender sentryAppender; + + public SentryLogbackInitializer(final @NotNull SentryProperties sentryProperties) { + this.sentryProperties = Objects.requireNonNull(sentryProperties, "properties are required"); + loggers = sentryProperties.getLogging().getLoggers(); + } + + @Override + public boolean supportsEventType(final @NotNull ResolvableType eventType) { + return eventType.getRawClass() != null + && ContextRefreshedEvent.class.isAssignableFrom(eventType.getRawClass()); + } + + @Override + public void onApplicationEvent(final @NotNull ApplicationEvent event) { + this.loggers.forEach( + loggerName -> { + final Logger logger = (Logger) LoggerFactory.getLogger(loggerName); + if (!isSentryAppenderRegistered(logger)) { + final SentryAppender sentryAppender = getSentryAppender(); + + Optional.ofNullable(sentryProperties.getLogging().getMinimumBreadcrumbLevel()) + .map(slf4jLevel -> Level.toLevel(slf4jLevel.name())) + .ifPresent(sentryAppender::setMinimumBreadcrumbLevel); + Optional.ofNullable(sentryProperties.getLogging().getMinimumEventLevel()) + .map(slf4jLevel -> Level.toLevel(slf4jLevel.name())) + .ifPresent(sentryAppender::setMinimumEventLevel); + Optional.ofNullable(sentryProperties.getLogging().getMinimumLevel()) + .map(slf4jLevel -> Level.toLevel(slf4jLevel.name())) + .ifPresent(sentryAppender::setMinimumLevel); + + sentryAppender.start(); + logger.addAppender(sentryAppender); + } + }); + } + + @NotNull + private SentryAppender getSentryAppender() { + if (sentryAppender == null) { + sentryAppender = new SentryAppender(); + sentryAppender.setName("SENTRY_APPENDER"); + sentryAppender.setContext((LoggerContext) LoggerFactory.getILoggerFactory()); + } + return sentryAppender; + } + + private boolean isSentryAppenderRegistered(final @NotNull Logger logger) { + final Iterator> it = logger.iteratorForAppenders(); + while (it.hasNext()) { + final Appender appender = it.next(); + + if (appender.getClass().equals(SentryAppender.class)) { + return true; + } + } + return false; + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java new file mode 100644 index 00000000000..7813c2e5512 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java @@ -0,0 +1,218 @@ +package io.sentry.spring.boot.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.SentryOptions; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.slf4j.event.Level; +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** Configuration for Sentry integration. */ +@ConfigurationProperties("sentry") +@Open +public class SentryProperties extends SentryOptions { + + /** Whether to use Git commit id as a release. */ + private boolean useGitCommitIdAsRelease = true; + + /** Report all or only uncaught web exceptions. */ + private int exceptionResolverOrder = 1; + + /** + * Defines the {@link io.sentry.spring.SentryUserFilter} order. The default value is {@link + * org.springframework.core.Ordered.LOWEST_PRECEDENCE}, if Spring Security is auto-configured, its + * guaranteed to run after Spring Security filter chain. + */ + private @Nullable Integer userFilterOrder; + + @ApiStatus.Experimental private boolean keepTransactionsOpenForAsyncResponses = false; + + /** Logging framework integration properties. */ + private @NotNull Logging logging = new Logging(); + + /** Reactive framework (e.g. WebFlux) integration properties */ + private @NotNull Reactive reactive = new Reactive(); + + /** + * If set to true, this flag disables all AOP related features (e.g. {@link + * io.sentry.spring.jakarta.tracing.SentryTransaction}, {@link + * io.sentry.spring.jakarta.tracing.SentrySpan}) to successfully compile to GraalVM + */ + private boolean enableAotCompatibility = false; + + /** Graphql integration properties. */ + private @NotNull Graphql graphql = new Graphql(); + + public boolean isUseGitCommitIdAsRelease() { + return useGitCommitIdAsRelease; + } + + public void setUseGitCommitIdAsRelease(boolean useGitCommitIdAsRelease) { + this.useGitCommitIdAsRelease = useGitCommitIdAsRelease; + } + + /** + * Returns the order used for Spring SentryExceptionResolver, which determines whether all web + * exceptions are reported, or only uncaught exceptions. + * + * @return order to use for Spring SentryExceptionResolver + */ + public int getExceptionResolverOrder() { + return exceptionResolverOrder; + } + + /** + * Sets the order to use for Spring SentryExceptionResolver, which determines whether all web + * exceptions are reported, or only uncaught exceptions. + * + * @param exceptionResolverOrder order to use for Spring SentryExceptionResolver + */ + public void setExceptionResolverOrder(int exceptionResolverOrder) { + this.exceptionResolverOrder = exceptionResolverOrder; + } + + public @Nullable Integer getUserFilterOrder() { + return userFilterOrder; + } + + public void setUserFilterOrder(final @Nullable Integer userFilterOrder) { + this.userFilterOrder = userFilterOrder; + } + + public @NotNull Logging getLogging() { + return logging; + } + + public void setLogging(@NotNull Logging logging) { + this.logging = logging; + } + + public @NotNull Reactive getReactive() { + return reactive; + } + + public void setReactive(@NotNull Reactive reactive) { + this.reactive = reactive; + } + + public boolean isEnableAotCompatibility() { + return enableAotCompatibility; + } + + public void setEnableAotCompatibility(boolean enableAotCompatibility) { + this.enableAotCompatibility = enableAotCompatibility; + } + + public boolean isKeepTransactionsOpenForAsyncResponses() { + return keepTransactionsOpenForAsyncResponses; + } + + public void setKeepTransactionsOpenForAsyncResponses( + boolean keepTransactionsOpenForAsyncResponses) { + this.keepTransactionsOpenForAsyncResponses = keepTransactionsOpenForAsyncResponses; + } + + public @NotNull Graphql getGraphql() { + return graphql; + } + + public void setGraphql(@NotNull Graphql graphql) { + this.graphql = graphql; + } + + @Open + public static class Logging { + /** Enable/Disable logging auto-configuration. */ + private boolean enabled = true; + + /** Minimum logging level for recording breadcrumbs. */ + private @Nullable Level minimumBreadcrumbLevel; + + /** Minimum logging level for recording events. */ + private @Nullable Level minimumEventLevel; + + /** Minimum logging level for recording log events. */ + private @Nullable Level minimumLevel; + + /** List of loggers the SentryAppender should be added to. */ + private @NotNull List loggers = Arrays.asList(org.slf4j.Logger.ROOT_LOGGER_NAME); + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public @Nullable Level getMinimumBreadcrumbLevel() { + return minimumBreadcrumbLevel; + } + + public void setMinimumBreadcrumbLevel(@Nullable Level minimumBreadcrumbLevel) { + this.minimumBreadcrumbLevel = minimumBreadcrumbLevel; + } + + public @Nullable Level getMinimumEventLevel() { + return minimumEventLevel; + } + + public void setMinimumEventLevel(@Nullable Level minimumEventLevel) { + this.minimumEventLevel = minimumEventLevel; + } + + public @Nullable Level getMinimumLevel() { + return minimumLevel; + } + + public void setMinimumLevel(@Nullable Level minimumLevel) { + this.minimumLevel = minimumLevel; + } + + @NotNull + public List getLoggers() { + return loggers; + } + + public void setLoggers(final @NotNull List loggers) { + this.loggers = loggers; + } + } + + @Open + public static class Reactive { + /** + * Enable/Disable usage of {@link io.micrometer.context.ThreadLocalAccessor} for Scopes + * propagation + */ + private boolean threadLocalAccessorEnabled = true; + + public boolean isThreadLocalAccessorEnabled() { + return threadLocalAccessorEnabled; + } + + public void setThreadLocalAccessorEnabled(boolean threadLocalAccessorEnabled) { + this.threadLocalAccessorEnabled = threadLocalAccessorEnabled; + } + } + + @Open + public static class Graphql { + + /** List of error types the Sentry Graphql integration should ignore. */ + private @NotNull List ignoredErrorTypes = new ArrayList<>(); + + @NotNull + public List getIgnoredErrorTypes() { + return ignoredErrorTypes; + } + + public void setIgnoredErrorTypes(final @NotNull List ignoredErrorTypes) { + this.ignoredErrorTypes = ignoredErrorTypes; + } + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java new file mode 100644 index 00000000000..304fb6911e9 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java @@ -0,0 +1,30 @@ +package io.sentry.spring.boot.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.spring.jakarta.tracing.SentrySpanClientHttpRequestInterceptor; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.web.client.RestClient; + +@Open +class SentrySpanRestClientCustomizer implements RestClientCustomizer { + private final @NotNull SentrySpanClientHttpRequestInterceptor interceptor; + + public SentrySpanRestClientCustomizer(final @NotNull IScopes scopes) { + this.interceptor = new SentrySpanClientHttpRequestInterceptor(scopes, false); + } + + @Override + public void customize(final @NotNull RestClient.Builder restClientBuilder) { + restClientBuilder.requestInterceptors( + clientHttpRequestInterceptors -> { + // As the SentrySpanClientHttpRequestInterceptor is being created in this class, this + // might not work + // if somebody registers it from an outside. + if (!clientHttpRequestInterceptors.contains(interceptor)) { + clientHttpRequestInterceptors.add(interceptor); + } + }); + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java new file mode 100644 index 00000000000..4a0faa9875e --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java @@ -0,0 +1,31 @@ +package io.sentry.spring.boot.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.spring.jakarta.tracing.SentrySpanClientHttpRequestInterceptor; +import java.util.ArrayList; +import java.util.List; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.web.client.RestTemplate; + +@Open +class SentrySpanRestTemplateCustomizer implements RestTemplateCustomizer { + private final @NotNull SentrySpanClientHttpRequestInterceptor interceptor; + + public SentrySpanRestTemplateCustomizer(final @NotNull IScopes scopes) { + this.interceptor = new SentrySpanClientHttpRequestInterceptor(scopes); + } + + @Override + public void customize(final @NotNull RestTemplate restTemplate) { + final List existingInterceptors = restTemplate.getInterceptors(); + if (!existingInterceptors.contains(this.interceptor)) { + final List interceptors = new ArrayList<>(); + interceptors.add(this.interceptor); + interceptors.addAll(existingInterceptors); + restTemplate.setInterceptors(interceptors); + } + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java new file mode 100644 index 00000000000..d349ac4c6e0 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java @@ -0,0 +1,22 @@ +package io.sentry.spring.boot.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScopes; +import io.sentry.spring.jakarta.tracing.SentrySpanClientWebRequestFilter; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.web.reactive.function.client.WebClient; + +@Open +class SentrySpanWebClientCustomizer implements WebClientCustomizer { + private final @NotNull SentrySpanClientWebRequestFilter filter; + + public SentrySpanWebClientCustomizer(final @NotNull IScopes scopes) { + this.filter = new SentrySpanClientWebRequestFilter(scopes); + } + + @Override + public void customize(WebClient.Builder webClientBuilder) { + webClientBuilder.filter(this.filter); + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpringVersionChecker.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpringVersionChecker.java new file mode 100644 index 00000000000..7fb6d1ce237 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpringVersionChecker.java @@ -0,0 +1,30 @@ +package io.sentry.spring.boot.jakarta; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.boot.SpringBootVersion; +import org.springframework.boot.context.event.ApplicationContextInitializedEvent; +import org.springframework.context.ApplicationListener; + +final class SentrySpringVersionChecker + implements ApplicationListener { + + private static final Log logger = LogFactory.getLog(SentrySpringVersionChecker.class); + + @Override + public void onApplicationEvent(ApplicationContextInitializedEvent event) { + + if (!SpringBootVersion.getVersion().startsWith("3")) { + logger.warn("############################### WARNING ###############################"); + logger.warn("## ##"); + logger.warn("## !Incompatible Spring Boot Version detected! ##"); + logger.warn("## Please see the sentry docs linked below ##"); + logger.warn("## Choose your Spring Boot version and ##"); + logger.warn("## install the matching dependency ##"); + logger.warn("## ##"); + logger.warn("## https://docs.sentry.io/platforms/java/guides/spring-boot/#install ##"); + logger.warn("## ##"); + logger.warn("#######################################################################"); + } + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java new file mode 100644 index 00000000000..468f4b81071 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java @@ -0,0 +1,120 @@ +package io.sentry.spring.boot.jakarta; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IScope; +import io.sentry.IScopes; +import io.sentry.spring.jakarta.webflux.SentryScheduleHook; +import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler; +import io.sentry.spring.jakarta.webflux.SentryWebFilter; +import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.autoconfigure.condition.AllNestedConditions; +import org.springframework.boot.autoconfigure.condition.AnyNestedCondition; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; +import reactor.core.publisher.Hooks; +import reactor.core.scheduler.Schedulers; + +/** Configures Sentry integration for Spring Webflux and Project Reactor. */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) +@ConditionalOnBean(IScopes.class) +@ConditionalOnClass(Schedulers.class) +@Open +@ApiStatus.Experimental +public class SentryWebfluxAutoConfiguration { + private static final int SENTRY_SPRING_FILTER_PRECEDENCE = Ordered.HIGHEST_PRECEDENCE; + + @Configuration(proxyBeanMethods = false) + @Conditional(SentryThreadLocalAccessorCondition.class) + @Open + static class SentryWebfluxFilterThreadLocalAccessorConfiguration { + + /** + * Configures a filter that sets up Sentry {@link IScope} for each request. + * + *

Makes use of newer reactor-core and context-propagation library feature + * ThreadLocalAccessor to propagate the Sentry scopes. + */ + @Bean + @Order(SENTRY_SPRING_FILTER_PRECEDENCE) + public @NotNull SentryWebFilterWithThreadLocalAccessor sentryWebFilterWithContextPropagation( + final @NotNull IScopes scopes) { + Hooks.enableAutomaticContextPropagation(); + return new SentryWebFilterWithThreadLocalAccessor(scopes); + } + } + + @Configuration(proxyBeanMethods = false) + @Conditional(SentryLegacyFilterConfigurationCondition.class) + @Open + static class SentryWebfluxFilterConfiguration { + + /** Configures hook that sets correct scopes on the executing thread. */ + @Bean + public @NotNull ApplicationRunner sentryScheduleHookApplicationRunner() { + return args -> { + Schedulers.onScheduleHook("sentry", new SentryScheduleHook()); + }; + } + + /** Configures a filter that sets up Sentry {@link IScope} for each request. */ + @Bean + @Order(SENTRY_SPRING_FILTER_PRECEDENCE) + public @NotNull SentryWebFilter sentryWebFilter(final @NotNull IScopes scopes) { + return new SentryWebFilter(scopes); + } + } + + /** Configures exception handler that handles unhandled exceptions and sends them to Sentry. */ + @Bean + public @NotNull SentryWebExceptionHandler sentryWebExceptionHandler( + final @NotNull IScopes scopes) { + return new SentryWebExceptionHandler(scopes); + } + + static final class SentryLegacyFilterConfigurationCondition extends AnyNestedCondition { + + public SentryLegacyFilterConfigurationCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty( + name = "sentry.reactive.thread-local-accessor-enabled", + havingValue = "false") + @SuppressWarnings("UnusedNestedClass") + private static class SentryDisableThreadLocalAccessorCondition {} + + @ConditionalOnMissingClass("io.micrometer.context.ThreadLocalAccessor") + @SuppressWarnings("UnusedNestedClass") + private static class ThreadLocalAccessorClassCondition {} + } + + static final class SentryThreadLocalAccessorCondition extends AllNestedConditions { + + public SentryThreadLocalAccessorCondition() { + super(ConfigurationPhase.REGISTER_BEAN); + } + + @ConditionalOnProperty( + name = "sentry.reactive.thread-local-accessor-enabled", + havingValue = "true", + matchIfMissing = true) + @SuppressWarnings("UnusedNestedClass") + private static class SentryEnableThreadLocalAccessorCondition {} + + @ConditionalOnClass(io.micrometer.context.ThreadLocalAccessor.class) + @SuppressWarnings("UnusedNestedClass") + private static class ThreadLocalAccessorClassCondition {} + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java new file mode 100644 index 00000000000..4e5664556fa --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java @@ -0,0 +1,72 @@ +package io.sentry.spring.boot.jakarta.graphql; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.SentryGraphqlInstrumentation; +import io.sentry.graphql22.SentryInstrumentation; +import io.sentry.spring.boot.jakarta.SentryProperties; +import io.sentry.spring.jakarta.graphql.SentryDataFetcherExceptionResolverAdapter; +import io.sentry.spring.jakarta.graphql.SentryGraphqlBeanPostProcessor; +import io.sentry.spring.jakarta.graphql.SentrySpringSubscriptionHandler; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryGraphql22AutoConfiguration { + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean(name = "sentryInstrumentation") + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) + public SentryInstrumentation sentryInstrumentationWebMvc( + final @NotNull SentryProperties sentryProperties, + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + return createInstrumentation(sentryProperties, beforeSpanCallback, false); + } + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean(name = "sentryInstrumentation") + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + public SentryInstrumentation sentryInstrumentationWebflux( + final @NotNull SentryProperties sentryProperties, + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + return createInstrumentation(sentryProperties, beforeSpanCallback, true); + } + + /** + * We're not setting defaultDataFetcherExceptionHandler here on purpose and instead use the + * resolver adapter below. This way Springs handler can still forward to other resolver adapters. + */ + private SentryInstrumentation createInstrumentation( + final @NotNull SentryProperties sentryProperties, + final @NotNull ObjectProvider + beforeSpanCallback, + final boolean captureRequestBody) { + return new SentryInstrumentation( + beforeSpanCallback.getIfAvailable(), + new SentrySpringSubscriptionHandler(), + captureRequestBody, + sentryProperties.getGraphql().getIgnoredErrorTypes()); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SentryDataFetcherExceptionResolverAdapter exceptionResolverAdapter() { + return new SentryDataFetcherExceptionResolverAdapter(); + } + + @Bean + public static SentryGraphqlBeanPostProcessor graphqlBeanPostProcessor() { + return new SentryGraphqlBeanPostProcessor(); + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java new file mode 100644 index 00000000000..84f59a39f35 --- /dev/null +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java @@ -0,0 +1,72 @@ +package io.sentry.spring.boot.jakarta.graphql; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.SentryIntegrationPackageStorage; +import io.sentry.graphql.SentryGraphqlInstrumentation; +import io.sentry.graphql.SentryInstrumentation; +import io.sentry.spring.boot.jakarta.SentryProperties; +import io.sentry.spring.jakarta.graphql.SentryDataFetcherExceptionResolverAdapter; +import io.sentry.spring.jakarta.graphql.SentryGraphqlBeanPostProcessor; +import io.sentry.spring.jakarta.graphql.SentrySpringSubscriptionHandler; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.Order; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryGraphqlAutoConfiguration { + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) + public SentryInstrumentation sentryInstrumentationWebMvc( + final @NotNull SentryProperties sentryProperties, + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + return createInstrumentation(sentryProperties, beforeSpanCallback, false); + } + + @Bean(name = "sentryInstrumentation") + @ConditionalOnMissingBean + @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE) + public SentryInstrumentation sentryInstrumentationWebflux( + final @NotNull SentryProperties sentryProperties, + final @NotNull ObjectProvider + beforeSpanCallback) { + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + return createInstrumentation(sentryProperties, beforeSpanCallback, true); + } + + /** + * We're not setting defaultDataFetcherExceptionHandler here on purpose and instead use the + * resolver adapter below. This way Springs handler can still forward to other resolver adapters. + */ + private SentryInstrumentation createInstrumentation( + final @NotNull SentryProperties sentryProperties, + final @NotNull ObjectProvider + beforeSpanCallback, + final boolean captureRequestBody) { + return new SentryInstrumentation( + beforeSpanCallback.getIfAvailable(), + new SentrySpringSubscriptionHandler(), + captureRequestBody, + sentryProperties.getGraphql().getIgnoredErrorTypes()); + } + + @Bean + @Order(Ordered.HIGHEST_PRECEDENCE) + public SentryDataFetcherExceptionResolverAdapter exceptionResolverAdapter() { + return new SentryDataFetcherExceptionResolverAdapter(); + } + + @Bean + public static SentryGraphqlBeanPostProcessor graphqlBeanPostProcessor() { + return new SentryGraphqlBeanPostProcessor(); + } +} diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/native-image/io.sentry/sentry/proxy-config.json b/sentry-spring-boot-4/src/main/resources/META-INF/native-image/io.sentry/sentry/proxy-config.json new file mode 100644 index 00000000000..210c544e260 --- /dev/null +++ b/sentry-spring-boot-4/src/main/resources/META-INF/native-image/io.sentry/sentry/proxy-config.json @@ -0,0 +1,10 @@ +[ + [ + "org.springframework.boot.autoconfigure.SpringBootApplication", + "org.springframework.core.annotation.SynthesizedAnnotation" + ], + [ + "org.springframework.boot.SpringBootConfiguration", + "org.springframework.core.annotation.SynthesizedAnnotation" + ] +] diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/spring.factories b/sentry-spring-boot-4/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..4002cb6ed56 --- /dev/null +++ b/sentry-spring-boot-4/src/main/resources/META-INF/spring.factories @@ -0,0 +1 @@ +org.springframework.context.ApplicationListener=io.sentry.spring.boot.jakarta.SentrySpringVersionChecker diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 00000000000..41436fe883f --- /dev/null +++ b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1,3 @@ +io.sentry.spring.boot.jakarta.SentryAutoConfiguration +io.sentry.spring.boot.jakarta.SentryLogbackAppenderAutoConfiguration +io.sentry.spring.boot.jakarta.SentryWebfluxAutoConfiguration diff --git a/sentry-spring-boot-4/src/test/kotlin/com/acme/MainBootClass.kt b/sentry-spring-boot-4/src/test/kotlin/com/acme/MainBootClass.kt new file mode 100644 index 00000000000..101907dc8b6 --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/com/acme/MainBootClass.kt @@ -0,0 +1,5 @@ +package com.acme + +import org.springframework.boot.autoconfigure.SpringBootApplication + +@SpringBootApplication open class MainBootClass diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt new file mode 100644 index 00000000000..7b290a42880 --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -0,0 +1,1286 @@ +package io.sentry.spring.boot.jakarta + +import com.acme.MainBootClass +import io.opentelemetry.api.OpenTelemetry +import io.sentry.AsyncHttpTransportFactory +import io.sentry.Breadcrumb +import io.sentry.EventProcessor +import io.sentry.FilterString +import io.sentry.Hint +import io.sentry.IScopes +import io.sentry.ITransportFactory +import io.sentry.Integration +import io.sentry.NoOpTransportFactory +import io.sentry.SamplingContext +import io.sentry.Sentry +import io.sentry.SentryEvent +import io.sentry.SentryIntegrationPackageStorage +import io.sentry.SentryLevel +import io.sentry.SentryLogEvent +import io.sentry.SentryOptions +import io.sentry.checkEvent +import io.sentry.opentelemetry.SentryAutoConfigurationCustomizerProvider +import io.sentry.opentelemetry.agent.AgentMarker +import io.sentry.protocol.SentryTransaction +import io.sentry.protocol.User +import io.sentry.quartz.SentryJobListener +import io.sentry.spring.jakarta.ContextTagsEventProcessor +import io.sentry.spring.jakarta.HttpServletRequestSentryUserProvider +import io.sentry.spring.jakarta.SentryExceptionResolver +import io.sentry.spring.jakarta.SentryUserFilter +import io.sentry.spring.jakarta.SentryUserProvider +import io.sentry.spring.jakarta.SpringProfilesEventProcessor +import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider +import io.sentry.spring.jakarta.tracing.SentryTracingFilter +import io.sentry.spring.jakarta.tracing.SpringServletTransactionNameProvider +import io.sentry.spring.jakarta.tracing.TransactionNameProvider +import io.sentry.transport.ITransport +import io.sentry.transport.ITransportGate +import io.sentry.transport.apache.ApacheHttpClientTransportFactory +import jakarta.servlet.Filter +import java.lang.RuntimeException +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue +import org.aspectj.lang.ProceedingJoinPoint +import org.assertj.core.api.Assertions.assertThat +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.quartz.JobExecutionContext +import org.quartz.JobExecutionException +import org.quartz.JobListener +import org.quartz.Scheduler +import org.quartz.core.QuartzScheduler +import org.slf4j.MDC +import org.springframework.aop.support.NameMatchMethodPointcut +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer +import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration +import org.springframework.boot.context.annotation.UserConfigurations +import org.springframework.boot.info.GitProperties +import org.springframework.boot.test.context.FilteredClassLoader +import org.springframework.boot.test.context.assertj.ApplicationContextAssert +import org.springframework.boot.test.context.runner.WebApplicationContextRunner +import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.context.ApplicationContext +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.core.Ordered +import org.springframework.core.annotation.Order +import org.springframework.scheduling.quartz.SchedulerFactoryBean +import org.springframework.security.core.context.SecurityContextHolder +import org.springframework.web.client.RestClient +import org.springframework.web.client.RestTemplate +import org.springframework.web.reactive.function.client.WebClient +import org.springframework.web.servlet.HandlerExceptionResolver + +class SentryAutoConfigurationTest { + + private val contextRunner = + WebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + SentryAutoConfiguration::class.java, + WebMvcAutoConfiguration::class.java, + ) + ) + + @Test + fun `scopes is not created when auto-configuration dsn is not set`() { + contextRunner.run { assertThat(it).doesNotHaveBean(IScopes::class.java) } + } + + @Test + fun `scopes is created when dsn is provided`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).hasSingleBean(IScopes::class.java) + } + } + + @Test + fun `OptionsConfiguration is created if custom one with name sentryOptionsConfiguration is not provided`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).hasSingleBean(Sentry.OptionsConfiguration::class.java) + } + } + + @Test + fun `OptionsConfiguration with name sentryOptionsConfiguration is created if another one with different name is provided`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomOptionsConfigurationConfiguration::class.java) + .run { + assertThat(it).getBeans(Sentry.OptionsConfiguration::class.java).hasSize(2) + assertThat(it) + .getBean("sentryOptionsConfiguration") + .isNotNull() + .isInstanceOf(Sentry.OptionsConfiguration::class.java) + assertThat(it) + .getBean("customOptionsConfiguration") + .isNotNull() + .isInstanceOf(Sentry.OptionsConfiguration::class.java) + } + } + + @Test + fun `sentryOptionsConfiguration bean is configured before custom OptionsConfiguration`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomOptionsConfigurationConfiguration::class.java) + .run { + val options = it.getBean(SentryOptions::class.java) + assertThat(options.beforeSend).isNull() + } + } + + @Test + fun `OptionsConfiguration is not created if custom one with name sentryOptionsConfiguration is provided`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(OverridingOptionsConfigurationConfiguration::class.java) + .run { + assertThat(it).hasSingleBean(Sentry.OptionsConfiguration::class.java) + assertThat( + it.getBean(Sentry.OptionsConfiguration::class.java, "customOptionsConfiguration") + ) + .isNotNull + } + } + + @Test + fun `properties are applied to SentryOptions`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.read-timeout-millis=10", + "sentry.shutdown-timeout-millis=20", + "sentry.flush-timeout-millis=30", + "sentry.debug=true", + "sentry.diagnostic-level=INFO", + "sentry.sentry-client-name=my-client", + "sentry.max-breadcrumbs=100", + "sentry.release=1.0.3", + "sentry.environment=production", + "sentry.sample-rate=0.2", + "sentry.in-app-includes=org.springframework,com.myapp", + "sentry.in-app-excludes=org.jboss,com.microsoft", + "sentry.dist=my-dist", + "sentry.attach-threads=true", + "sentry.attach-stacktrace=true", + "sentry.server-name=host-001", + "sentry.exception-resolver-order=100", + "sentry.proxy.host=example.proxy.com", + "sentry.proxy.port=8090", + "sentry.proxy.user=proxy-user", + "sentry.proxy.pass=proxy-pass", + "sentry.traces-sample-rate=0.3", + "sentry.tags.tag1=tag1-value", + "sentry.tags.tag2=tag2-value", + "sentry.ignored-exceptions-for-type=java.lang.RuntimeException,java.lang.IllegalStateException,io.sentry.Sentry", + "sentry.trace-propagation-targets=localhost,^(http|https)://api\\..*\$", + "sentry.enabled=false", + "sentry.send-modules=false", + "sentry.ignored-checkins=slug1,slugB", + "sentry.ignored-errors=Some error,Another .*", + "sentry.ignored-transactions=transactionName1,transactionNameB", + "sentry.enable-backpressure-handling=false", + "sentry.enable-spotlight=true", + "sentry.spotlight-connection-url=http://local.sentry.io:1234", + "sentry.force-init=true", + "sentry.global-hub-mode=true", + "sentry.capture-open-telemetry-events=true", + "sentry.cron.default-checkin-margin=10", + "sentry.cron.default-max-runtime=30", + "sentry.cron.default-timezone=America/New_York", + "sentry.cron.default-failure-issue-threshold=40", + "sentry.cron.default-recovery-threshold=50", + "sentry.logs.enabled=true", + ) + .run { + val options = it.getBean(SentryProperties::class.java) + assertThat(options.readTimeoutMillis).isEqualTo(10) + assertThat(options.shutdownTimeoutMillis).isEqualTo(20) + assertThat(options.flushTimeoutMillis).isEqualTo(30) + assertThat(options.isDebug).isTrue() + assertThat(options.diagnosticLevel).isEqualTo(SentryLevel.INFO) + assertThat(options.maxBreadcrumbs).isEqualTo(100) + assertThat(options.release).isEqualTo("1.0.3") + assertThat(options.environment).isEqualTo("production") + assertThat(options.sampleRate).isEqualTo(0.2) + assertThat(options.inAppIncludes).containsOnly("org.springframework", "com.myapp") + assertThat(options.inAppExcludes).containsOnly("com.microsoft", "org.jboss") + assertThat(options.dist).isEqualTo("my-dist") + assertThat(options.isAttachThreads).isEqualTo(true) + assertThat(options.isAttachStacktrace).isEqualTo(true) + assertThat(options.serverName).isEqualTo("host-001") + assertThat(options.exceptionResolverOrder).isEqualTo(100) + assertThat(options.proxy).isNotNull + assertThat(options.proxy!!.host).isEqualTo("example.proxy.com") + assertThat(options.proxy!!.port).isEqualTo("8090") + assertThat(options.proxy!!.user).isEqualTo("proxy-user") + assertThat(options.proxy!!.pass).isEqualTo("proxy-pass") + assertThat(options.tracesSampleRate).isEqualTo(0.3) + assertThat(options.tags) + .containsEntry("tag1", "tag1-value") + .containsEntry("tag2", "tag2-value") + assertThat(options.ignoredExceptionsForType) + .containsOnly(RuntimeException::class.java, IllegalStateException::class.java) + assertThat(options.tracePropagationTargets) + .containsOnly("localhost", "^(http|https)://api\\..*\$") + assertThat(options.isEnabled).isEqualTo(false) + assertThat(options.isSendModules).isEqualTo(false) + assertThat(options.ignoredCheckIns) + .containsOnly(FilterString("slug1"), FilterString("slugB")) + assertThat(options.ignoredErrors) + .containsOnly(FilterString("Some error"), FilterString("Another .*")) + assertThat(options.ignoredTransactions) + .containsOnly(FilterString("transactionName1"), FilterString("transactionNameB")) + assertThat(options.isEnableBackpressureHandling).isEqualTo(false) + assertThat(options.isForceInit).isEqualTo(true) + assertThat(options.isGlobalHubMode).isEqualTo(true) + assertThat(options.isCaptureOpenTelemetryEvents).isEqualTo(true) + assertThat(options.isEnableSpotlight).isEqualTo(true) + assertThat(options.spotlightConnectionUrl).isEqualTo("http://local.sentry.io:1234") + assertThat(options.cron).isNotNull + assertThat(options.cron!!.defaultCheckinMargin).isEqualTo(10L) + assertThat(options.cron!!.defaultMaxRuntime).isEqualTo(30L) + assertThat(options.cron!!.defaultTimezone).isEqualTo("America/New_York") + assertThat(options.cron!!.defaultFailureIssueThreshold).isEqualTo(40L) + assertThat(options.cron!!.defaultRecoveryThreshold).isEqualTo(50L) + assertThat(options.logs.isEnabled).isEqualTo(true) + } + } + + @Test + fun `when tracePropagationTargets are not set, default is returned`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + val options = it.getBean(SentryProperties::class.java) + assertThat(options.tracePropagationTargets).isNotNull().containsOnly(".*") + } + } + + @Test + fun `when tracePropagationTargets property is set to empty list, empty list is returned`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.trace-propagation-targets=", + ) + .run { + val options = it.getBean(SentryProperties::class.java) + assertThat(options.tracePropagationTargets).isNotNull().isEmpty() + } + } + + @Test + fun `when traces sample rate is set to null and tracing is enabled, traces sample rate should be set to 0`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + val options = it.getBean(SentryProperties::class.java) + assertThat(options.tracesSampleRate).isNull() + } + } + + @Test + fun `when traces sample rate is set to a value and tracing is enabled, traces sample rate should not be overwritten`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=0.3") + .run { + val options = it.getBean(SentryProperties::class.java) + assertThat(options.tracesSampleRate).isNotNull().isEqualTo(0.3) + } + } + + @Test + fun `sets sentryClientName property on SentryOptions`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it.getBean(SentryOptions::class.java).sentryClientName) + .isEqualTo("sentry.java.spring-boot.jakarta/${BuildConfig.VERSION_NAME}") + } + } + + @Test + fun `sets SDK version on sent events`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(MockTransportConfiguration::class.java) + .run { + Sentry.captureMessage("Some message") + val transport = it.getBean(ITransport::class.java) + verify(transport) + .send( + checkEvent { event -> + assertThat(event.sdk).isNotNull + val sdk = event.sdk!! + assertThat(sdk.version).isEqualTo(BuildConfig.VERSION_NAME) + assertThat(sdk.name).isEqualTo(BuildConfig.SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME) + assertThat(sdk.packageSet).anyMatch { pkg -> + pkg.name == "maven:io.sentry:sentry-spring-boot-starter-jakarta" && + pkg.version == BuildConfig.VERSION_NAME + } + assertTrue(sdk.integrationSet.contains("SpringBoot3")) + }, + anyOrNull(), + ) + } + } + + @Test + fun `registers beforeSendCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomBeforeSendCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).beforeSend) + .isInstanceOf(CustomBeforeSendCallback::class.java) + } + } + + @Test + fun `registers beforeSendTransactionCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomBeforeSendTransactionCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).beforeSendTransaction) + .isInstanceOf(CustomBeforeSendTransactionCallback::class.java) + } + } + + @Test + fun `registers logs beforeSendCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomBeforeSendLogsCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).logs.beforeSend) + .isInstanceOf(CustomBeforeSendLogsCallback::class.java) + } + } + + @Test + fun `registers beforeBreadcrumbCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomBeforeBreadcrumbCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).beforeBreadcrumb) + .isInstanceOf(CustomBeforeBreadcrumbCallback::class.java) + } + } + + @Test + fun `registers event processor on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomEventProcessorConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).eventProcessors).anyMatch { processor -> + processor.javaClass == CustomEventProcessor::class.java + } + } + } + + @Test + fun `registers transport gate on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomTransportGateConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).transportGate) + .isInstanceOf(CustomTransportGate::class.java) + } + } + + @Test + fun `registers custom integration on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomIntegration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).integrations).anyMatch { integration -> + integration.javaClass == CustomIntegration::class.java + } + } + } + + @Test + fun `sets release on SentryEvents if Git integration is configured`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration( + MockTransportConfiguration::class.java, + MockGitPropertiesConfiguration::class.java, + ) + .run { + Sentry.captureMessage("Some message") + val transport = it.getBean(ITransport::class.java) + verify(transport) + .send( + checkEvent { event -> assertThat(event.release).isEqualTo("git-commit-id") }, + anyOrNull(), + ) + } + } + + @Test + fun `sets custom release on SentryEvents if release property is set and Git integration is configured`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.release=my-release") + .withUserConfiguration( + MockTransportConfiguration::class.java, + MockGitPropertiesConfiguration::class.java, + ) + .run { + Sentry.captureMessage("Some message") + val transport = it.getBean(ITransport::class.java) + + verify(transport) + .send( + checkEvent { event -> assertThat(event.release).isEqualTo("my-release") }, + anyOrNull(), + ) + } + } + + @Test + fun `sets inAppIncludes on SentryOptions from a class annotated with @SpringBootApplication`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(MainBootClass::class.java) + .run { + assertThat(it.getBean(SentryProperties::class.java).inAppIncludes).containsOnly("com.acme") + } + } + + @Test + fun `when custom SentryUserProvider bean is configured, it's added after HttpServletRequestSentryUserProvider`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true") + .withConfiguration(UserConfigurations.of(SentryUserProviderConfiguration::class.java)) + .run { + val userProviders = it.getSentryUserProviders() + assertEquals(3, userProviders.size) + assertTrue(userProviders[0] is HttpServletRequestSentryUserProvider) + assertTrue(userProviders[1] is SpringSecuritySentryUserProvider) + assertTrue(userProviders[2] is CustomSentryUserProvider) + } + } + + @Test + fun `when custom SentryUserProvider bean with higher order is configured, it's added before HttpServletRequestSentryUserProvider`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true") + .withConfiguration( + UserConfigurations.of(SentryHighestOrderUserProviderConfiguration::class.java) + ) + .run { + val userProviders = it.getSentryUserProviders() + assertEquals(3, userProviders.size) + assertTrue(userProviders[0] is CustomSentryUserProvider) + assertTrue(userProviders[1] is HttpServletRequestSentryUserProvider) + assertTrue(userProviders[2] is SpringSecuritySentryUserProvider) + } + } + + @Test + fun `when Spring Security is not on the classpath, SpringSecuritySentryUserProvider is not configured`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true") + .withClassLoader(FilteredClassLoader(SecurityContextHolder::class.java)) + .run { ctx -> + val userProviders = ctx.getSentryUserProviders() + assertTrue(userProviders.isNotEmpty()) + userProviders.forEach { assertFalse(it is SpringSecuritySentryUserProvider) } + } + } + + @Test + fun `when Spring MVC is not on the classpath, SentryExceptionResolver is not configured`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true") + .withClassLoader(FilteredClassLoader(HandlerExceptionResolver::class.java)) + .run { assertThat(it).doesNotHaveBean(SentryExceptionResolver::class.java) } + } + + @Test + fun `when Spring MVC is not on the classpath, fallback TransactionNameProvider is configured`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.send-default-pii=true") + .withClassLoader(FilteredClassLoader(HandlerExceptionResolver::class.java)) + .run { + assertThat(it.getBean(TransactionNameProvider::class.java)) + .isInstanceOf(SpringServletTransactionNameProvider::class.java) + } + } + + @Test + fun `when tracing is enabled, creates tracing filter`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .run { assertThat(it).hasBean("sentryTracingFilter") } + } + + @Test + fun `when traces sample rate is set, creates tracing filter`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=0.2") + .run { assertThat(it).hasBean("sentryTracingFilter") } + } + + @Test + fun `when traces sample rate is set to 0, creates tracing filter`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=0.0") + .run { assertThat(it).hasBean("sentryTracingFilter") } + } + + @Test + fun `when custom traces sampler callback is registered, creates tracing filter`() { + contextRunner + .withUserConfiguration(CustomTracesSamplerCallbackConfiguration::class.java) + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .run { assertThat(it).hasBean("sentryTracingFilter") } + } + + @Test + fun `creates tracing filter`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).hasBean("sentryTracingFilter") + } + } + + @Test + fun `when tracing is enabled and sentryTracingFilter already exists, does not create tracing filter`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withUserConfiguration(CustomSentryTracingFilter::class.java) + .run { + assertThat(it).hasBean("sentryTracingFilter") + val filter = it.getBean("sentryTracingFilter") + + if (filter is FilterRegistrationBean<*>) { + assertThat(filter.filter).isNotInstanceOf(SentryTracingFilter::class.java) + } else { + assertThat(filter).isNotInstanceOf(SentryTracingFilter::class.java) + } + } + } + + @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") + .run { assertThat(it).hasSentryTransactionBeans() } + } + + @Test + fun `when traces sample rate is set to 0, creates AOP beans to support @SentryTransaction`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=0.0") + .run { assertThat(it).hasSentryTransactionBeans() } + } + + @Test + fun `when custom traces sampler callback is registered, creates AOP beans to support @SentryTransaction`() { + contextRunner + .withUserConfiguration(CustomTracesSamplerCallbackConfiguration::class.java) + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .run { assertThat(it).hasSentryTransactionBeans() } + } + + @Test + fun `when tracing is disabled, does not create AOP beans to support @SentryTransaction`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).doesNotHaveSentryTransactionBeans() + } + } + + @Test + fun `when Spring AOP is not on the classpath, does not create AOP beans to support @SentryTransaction`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withClassLoader(FilteredClassLoader(ProceedingJoinPoint::class.java)) + .run { assertThat(it).doesNotHaveSentryTransactionBeans() } + } + + @Test + fun `when tracing is enabled and custom sentryTransactionPointcut is provided, sentryTransactionPointcut bean is not created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withUserConfiguration(CustomSentryPerformancePointcutConfiguration::class.java) + .run { + assertThat(it).hasBean("sentryTransactionPointcut") + val pointcut = it.getBean("sentryTransactionPointcut") + assertThat(pointcut).isInstanceOf(NameMatchMethodPointcut::class.java) + } + } + + @Test + fun `when tracing is enabled creates AOP beans to support @SentrySpan`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .run { assertThat(it).hasSentrySpanBeans() } + } + + @Test + fun `when traces sample rate is set to 0, creates AOP beans to support @SentrySpan`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=0.0") + .run { assertThat(it).hasSentrySpanBeans() } + } + + @Test + fun `when custom traces sampler callback is registered, creates AOP beans to support @SentrySpan`() { + contextRunner + .withUserConfiguration(CustomTracesSamplerCallbackConfiguration::class.java) + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .run { assertThat(it).hasSentrySpanBeans() } + } + + @Test + fun `when tracing is disabled, does not create AOP beans to support @Span`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).doesNotHaveSentrySpanBeans() + } + } + + @Test + fun `when Spring AOP is not on the classpath, does not create AOP beans to support @SentrySpan`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withClassLoader(FilteredClassLoader(ProceedingJoinPoint::class.java)) + .run { assertThat(it).doesNotHaveSentrySpanBeans() } + } + + @Test + fun `when tracing is enabled and custom sentrySpanPointcut is provided, sentrySpanPointcut bean is not created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withUserConfiguration(CustomSentryPerformancePointcutConfiguration::class.java) + .run { + assertThat(it).hasBean("sentrySpanPointcut") + val pointcut = it.getBean("sentrySpanPointcut") + assertThat(pointcut).isInstanceOf(NameMatchMethodPointcut::class.java) + } + } + + @Test + fun `when tracing is enabled and RestTemplate is on the classpath, SentrySpanRestTemplateCustomizer bean is created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .run { assertThat(it).hasSingleBean(SentrySpanRestTemplateCustomizer::class.java) } + } + + @Test + fun `when tracing is enabled and RestTemplate is not on the classpath, SentrySpanRestTemplateCustomizer bean is not created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withClassLoader(FilteredClassLoader(RestTemplate::class.java)) + .run { assertThat(it).doesNotHaveBean(SentrySpanRestTemplateCustomizer::class.java) } + } + + @Test + fun `when tracing is enabled and RestClient is on the classpath, SentrySpanRestClientCustomizer bean is created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .run { assertThat(it).hasSingleBean(SentrySpanRestClientCustomizer::class.java) } + } + + @Test + fun `when tracing is enabled and RestClient is not on the classpath, SentrySpanRestClientCustomizer bean is not created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withClassLoader(FilteredClassLoader(RestClient::class.java)) + .run { assertThat(it).doesNotHaveBean(SentrySpanRestClientCustomizer::class.java) } + } + + @Test + fun `when tracing is enabled and WebClient is on the classpath, SentrySpanWebClientCustomizer bean is created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .run { assertThat(it).hasSingleBean(SentrySpanWebClientCustomizer::class.java) } + } + + @Test + fun `when tracing is enabled and WebClient is not on the classpath, SentrySpanWebClientCustomizer bean is not created`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.traces-sample-rate=1.0") + .withClassLoader(FilteredClassLoader(WebClient::class.java)) + .run { assertThat(it).doesNotHaveBean(SentrySpanWebClientCustomizer::class.java) } + } + + @Test + fun `registers tracesSamplerCallback on SentryOptions`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(CustomTracesSamplerCallbackConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).tracesSampler) + .isInstanceOf(CustomTracesSamplerCallback::class.java) + } + } + + @Test + fun `when sentry-apache-http-client-5 is on the classpath, creates apache transport factory`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it.getBean(SentryOptions::class.java).transportFactory) + .isInstanceOf(ApacheHttpClientTransportFactory::class.java) + } + } + + @Test + fun `when sentry-apache-http-client-5 is not on the classpath, does not create apache transport factory`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(ApacheHttpClientTransportFactory::class.java)) + .run { + assertThat(it.getBean(SentryOptions::class.java).transportFactory) + .isInstanceOf(AsyncHttpTransportFactory::class.java) + } + } + + @Test + fun `when sentry-apache-http-client-5 is on the classpath and custom transport factory bean is set, does not create apache transport factory`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withUserConfiguration(MockTransportConfiguration::class.java) + .run { + assertThat(it.getBean(SentryOptions::class.java).transportFactory) + .isNotInstanceOf(ApacheHttpClientTransportFactory::class.java) + .isNotInstanceOf(NoOpTransportFactory::class.java) + } + } + + @Test + fun `when MDC is on the classpath, creates ContextTagsEventProcessor`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).hasSingleBean(ContextTagsEventProcessor::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.eventProcessors).anyMatch { processor -> + processor.javaClass == ContextTagsEventProcessor::class.java + } + } + } + + @Test + fun `when MDC is not on the classpath, does not create ContextTagsEventProcessor`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(MDC::class.java)) + .run { + assertThat(it).doesNotHaveBean(ContextTagsEventProcessor::class.java) + val options = it.getBean(SentryOptions::class.java) + assertThat(options.eventProcessors).noneMatch { processor -> + processor.javaClass == ContextTagsEventProcessor::class.java + } + } + } + + @Test + fun `when AgentMarker is on the classpath and auto init off, runs SentryOpenTelemetryAgentWithoutAutoInitConfiguration`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.auto-init=false") + .run { + assertTrue( + SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + ) + } + } + + @Test + fun `when AgentMarker is on the classpath and auto init on, does not run SentryOpenTelemetryAgentWithoutAutoInitConfiguration`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertFalse( + SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + ) + } + } + + @Test + fun `when AgentMarker is not on the classpath and auto init off, does not run SentryOpenTelemetryAgentWithoutAutoInitConfiguration`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.auto-init=false") + .withClassLoader(FilteredClassLoader(AgentMarker::class.java)) + .run { + assertFalse( + SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + ) + } + } + + @Test + fun `when AgentMarker is not on the classpath but OpenTelemetry is, runs SpringBoot3OpenTelemetryNoAgent`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(AgentMarker::class.java)) + .withUserConfiguration(OtelBeanConfig::class.java) + .run { + assertTrue( + SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("SpringBoot3OpenTelemetryNoAgent") + ) + } + } + + @Test + fun `when AgentMarker and OpenTelemetry are not on the classpath, does not run SpringBoot3OpenTelemetryNoAgent`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(AgentMarker::class.java, OpenTelemetry::class.java)) + .run { + assertFalse( + SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("SpringBoot3OpenTelemetryNoAgent") + ) + } + } + + @Test + fun `when AgentMarker and SentryAutoConfigurationCustomizerProvider are not on the classpath, does not run SpringBoot3OpenTelemetryNoAgent`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader( + FilteredClassLoader( + AgentMarker::class.java, + SentryAutoConfigurationCustomizerProvider::class.java, + ) + ) + .withUserConfiguration(OtelBeanConfig::class.java) + .run { + assertFalse( + SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("SpringBoot3OpenTelemetryNoAgent") + ) + } + } + + @Test + fun `when AgentMarker is not on the classpath and auto init on, does not run SentryOpenTelemetryAgentWithoutAutoInitConfiguration`() { + SentryIntegrationPackageStorage.getInstance().clearStorage() + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(AgentMarker::class.java)) + .run { + assertFalse( + SentryIntegrationPackageStorage.getInstance() + .integrations + .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + ) + } + } + + @Test + fun `creates quartz config`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-automatic-checkins=true", + ) + .run { assertThat(it).hasSingleBean(SchedulerFactoryBeanCustomizer::class.java) } + } + + @Test + fun `does not create quartz config if quartz lib missing`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-automatic-checkins=true", + ) + .withClassLoader(FilteredClassLoader(QuartzScheduler::class.java)) + .run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) } + } + + @Test + fun `does not create quartz config if spring-quartz lib missing`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-automatic-checkins=true", + ) + .withClassLoader(FilteredClassLoader(SchedulerFactoryBean::class.java)) + .run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) } + } + + @Test + fun `does not create quartz config if sentry-quartz lib missing`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-automatic-checkins=true", + ) + .withClassLoader(FilteredClassLoader(SentryJobListener::class.java)) + .run { assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) } + } + + @Test + fun `does not create any graphql config if no sentry-graphql lib on classpath`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader( + FilteredClassLoader( + io.sentry.graphql.SentryInstrumentation::class.java, + io.sentry.graphql22.SentryInstrumentation::class.java, + ) + ) + .run { + assertThat(it).doesNotHaveBean(io.sentry.graphql.SentryInstrumentation::class.java) + assertThat(it).doesNotHaveBean(io.sentry.graphql22.SentryInstrumentation::class.java) + } + } + + @Test + fun `sentry-graphql22 configuration takes precedence over sentry-graphql if both on classpath`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).hasSingleBean(io.sentry.graphql22.SentryInstrumentation::class.java) + assertThat(it).doesNotHaveBean(io.sentry.graphql.SentryInstrumentation::class.java) + } + } + + @Test + fun `sentry graphql configuration is created if graphql22 not on classpath`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(io.sentry.graphql22.SentryInstrumentation::class.java)) + .run { + assertThat(it).hasSingleBean(io.sentry.graphql.SentryInstrumentation::class.java) + assertThat(it).doesNotHaveBean(io.sentry.graphql22.SentryInstrumentation::class.java) + } + } + + @Test + fun `sentry graphql22 configuration is created if graphql not on classpath`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(io.sentry.graphql.SentryInstrumentation::class.java)) + .run { + assertThat(it).doesNotHaveBean(io.sentry.graphql.SentryInstrumentation::class.java) + assertThat(it).hasSingleBean(io.sentry.graphql22.SentryInstrumentation::class.java) + } + } + + @Test + fun `Sentry quartz job listener is added`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-automatic-checkins=true", + ) + .withUserConfiguration(QuartzAutoConfiguration::class.java) + .run { + val jobListeners = it.getBean(Scheduler::class.java).listenerManager.jobListeners + assertThat(jobListeners).hasSize(1) + assertThat(jobListeners[0]) + .matches({ it.name == "sentry-job-listener" }, "is sentry job listener") + } + } + + @Test + fun `user defined SchedulerFactoryBeanCustomizer overrides Sentry Customizer`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.enable-automatic-checkins=true", + ) + .withUserConfiguration( + QuartzAutoConfiguration::class.java, + CustomSchedulerFactoryBeanCustomizerConfiguration::class.java, + ) + .run { + val jobListeners = it.getBean(Scheduler::class.java).listenerManager.jobListeners + assertThat(jobListeners).hasSize(1) + assertThat(jobListeners[0]) + .matches({ it.name == "custom-job-listener" }, "is custom job listener") + } + } + + @Test + fun `registers SpringProfilesEventProcessor on SentryOptions`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it.getBean(SentryOptions::class.java).eventProcessors).anyMatch { processor -> + processor.javaClass == SpringProfilesEventProcessor::class.java + } + } + } + + @Configuration(proxyBeanMethods = false) + open class CustomSchedulerFactoryBeanCustomizerConfiguration { + class MyJobListener : JobListener { + override fun getName() = "custom-job-listener" + + override fun jobToBeExecuted(context: JobExecutionContext?) { + // do nothing + } + + override fun jobExecutionVetoed(context: JobExecutionContext?) { + // do nothing + } + + override fun jobWasExecuted( + context: JobExecutionContext?, + jobException: JobExecutionException?, + ) { + // do nothing + } + } + + @Bean + open fun mySchedulerFactoryBeanCustomizer(): SchedulerFactoryBeanCustomizer { + return SchedulerFactoryBeanCustomizer { schedulerFactoryBean -> + schedulerFactoryBean.setGlobalJobListeners(MyJobListener()) + } + } + } + + @Configuration(proxyBeanMethods = false) + open class CustomOptionsConfigurationConfiguration { + + @Bean + open fun customOptionsConfiguration() = + Sentry.OptionsConfiguration { it.setBeforeSend(null) } + + @Bean open fun beforeSendCallback() = CustomBeforeSendCallback() + } + + @Configuration(proxyBeanMethods = false) + open class OverridingOptionsConfigurationConfiguration { + + @Bean open fun sentryOptionsConfiguration() = Sentry.OptionsConfiguration {} + } + + @Configuration(proxyBeanMethods = false) + open class MockTransportConfiguration { + + private val transport = mock() + + @Bean + open fun mockTransportFactory(): ITransportFactory { + val factory = mock() + whenever(factory.create(any(), any())).thenReturn(transport) + return factory + } + + @Bean open fun sentryTransport() = transport + } + + @Configuration(proxyBeanMethods = false) + open class CustomBeforeSendCallbackConfiguration { + + @Bean open fun beforeSendCallback() = CustomBeforeSendCallback() + } + + class CustomBeforeSendCallback : SentryOptions.BeforeSendCallback { + override fun execute(event: SentryEvent, hint: Hint): SentryEvent? = null + } + + @Configuration(proxyBeanMethods = false) + open class CustomBeforeSendLogsCallbackConfiguration { + + @Bean open fun beforeSendCallback() = CustomBeforeSendLogsCallback() + } + + class CustomBeforeSendLogsCallback : SentryOptions.Logs.BeforeSendLogCallback { + override fun execute(event: SentryLogEvent): SentryLogEvent? = null + } + + @Configuration(proxyBeanMethods = false) + open class CustomBeforeSendTransactionCallbackConfiguration { + + @Bean open fun beforeSendTransactionCallback() = CustomBeforeSendTransactionCallback() + } + + class CustomBeforeSendTransactionCallback : SentryOptions.BeforeSendTransactionCallback { + override fun execute(event: SentryTransaction, hint: Hint): SentryTransaction? = null + } + + @Configuration(proxyBeanMethods = false) + open class CustomBeforeBreadcrumbCallbackConfiguration { + + @Bean open fun beforeBreadcrumbCallback() = CustomBeforeBreadcrumbCallback() + } + + class CustomBeforeBreadcrumbCallback : SentryOptions.BeforeBreadcrumbCallback { + override fun execute(breadcrumb: Breadcrumb, hint: Hint): Breadcrumb? = null + } + + @Configuration(proxyBeanMethods = false) + open class CustomEventProcessorConfiguration { + + @Bean open fun customEventProcessor() = CustomEventProcessor() + } + + class CustomEventProcessor : EventProcessor { + override fun process(event: SentryEvent, hint: Hint) = null + } + + @Configuration(proxyBeanMethods = false) + open class CustomIntegrationConfiguration { + + @Bean open fun customIntegration() = CustomIntegration() + } + + class CustomIntegration : Integration { + override fun register(scopes: IScopes, options: SentryOptions) {} + } + + @Configuration(proxyBeanMethods = false) + open class CustomTransportGateConfiguration { + + @Bean open fun customTransportGate() = CustomTransportGate() + } + + class CustomTransportGate : ITransportGate { + override fun isConnected() = true + } + + @Configuration(proxyBeanMethods = false) + open class MockGitPropertiesConfiguration { + + @Bean + open fun gitProperties(): GitProperties { + val git = mock() + whenever(git.commitId).thenReturn("git-commit-id") + return git + } + } + + @Configuration + open class SentryUserProviderConfiguration { + + @Bean open fun userProvider() = CustomSentryUserProvider() + } + + @Configuration + open class SentryHighestOrderUserProviderConfiguration { + + @Bean @Order(Ordered.HIGHEST_PRECEDENCE) open fun userProvider() = CustomSentryUserProvider() + } + + @Configuration + open class CustomSentryTracingFilter { + + @Bean open fun sentryTracingFilter() = mock() + } + + @Configuration + open class CustomSentryPerformancePointcutConfiguration { + + @Bean open fun sentryTransactionPointcut() = NameMatchMethodPointcut() + + @Bean open fun sentrySpanPointcut() = NameMatchMethodPointcut() + } + + @Configuration + open class CustomTracesSamplerCallbackConfiguration { + + @Bean open fun tracingSamplerCallback() = CustomTracesSamplerCallback() + } + + /** this should be taken care of by the otel spring starter in a real application */ + @Configuration + open class OtelBeanConfig { + + @Bean open fun openTelemetry() = OpenTelemetry.noop() + } + + class CustomTracesSamplerCallback : SentryOptions.TracesSamplerCallback { + override fun sample(samplingContext: SamplingContext) = 1.0 + } + + open class CustomSentryUserProvider : SentryUserProvider { + override fun provideUser(): User? { + val user = User() + user.username = "john.smith" + return user + } + } + + private fun ApplicationContextAssert.hasSentryTransactionBeans(): + ApplicationContextAssert { + this.hasBean("sentryTransactionPointcut") + this.hasBean("sentryTransactionAdvice") + this.hasBean("sentryTransactionAdvisor") + return this + } + + private fun ApplicationContextAssert + .doesNotHaveSentryTransactionBeans(): ApplicationContextAssert { + this.doesNotHaveBean("sentryTransactionPointcut") + this.doesNotHaveBean("sentryTransactionAdvice") + this.doesNotHaveBean("sentryTransactionAdvisor") + return this + } + + private fun ApplicationContextAssert.hasSentrySpanBeans(): + ApplicationContextAssert { + this.hasBean("sentrySpanPointcut") + this.hasBean("sentrySpanAdvice") + this.hasBean("sentrySpanAdvisor") + return this + } + + private fun ApplicationContextAssert.doesNotHaveSentrySpanBeans(): + ApplicationContextAssert { + this.doesNotHaveBean("sentrySpanPointcut") + this.doesNotHaveBean("sentrySpanAdvice") + this.doesNotHaveBean("sentrySpanAdvisor") + return this + } + + private fun ApplicationContextAssert + .hasSentryExceptionParameterAdviceBeans(): ApplicationContextAssert { + this.hasBean("sentryCaptureExceptionParameterPointcut") + this.hasBean("sentryCaptureExceptionParameterAdvice") + this.hasBean("sentryCaptureExceptionParameterAdvisor") + return this + } + + private fun ApplicationContextAssert + .doesNotHaveSentryExceptionParameterAdviceBeans(): ApplicationContextAssert { + this.doesNotHaveBean("sentryCaptureExceptionParameterPointcut") + this.doesNotHaveBean("sentryCaptureExceptionParameterAdvice") + this.doesNotHaveBean("sentryCaptureExceptionParameterAdvisor") + return this + } + + private fun ApplicationContext.getSentryUserProviders(): List { + val userFilter = + this.getBean("sentryUserFilter", FilterRegistrationBean::class.java).filter + as SentryUserFilter + return userFilter.sentryUserProviders + } +} diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfigurationTest.kt new file mode 100644 index 00000000000..77712ac70c6 --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfigurationTest.kt @@ -0,0 +1,142 @@ +package io.sentry.spring.boot.jakarta + +import ch.qos.logback.classic.Level +import ch.qos.logback.classic.Logger +import ch.qos.logback.classic.LoggerContext +import ch.qos.logback.classic.spi.ILoggingEvent +import ch.qos.logback.core.Appender +import io.sentry.logback.SentryAppender +import kotlin.test.BeforeTest +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.slf4j.LoggerFactory +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.test.context.FilteredClassLoader +import org.springframework.boot.test.context.runner.ApplicationContextRunner + +class SentryLogbackAppenderAutoConfigurationTest { + + private val contextRunner = + ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + SentryLogbackAppenderAutoConfiguration::class.java, + SentryAutoConfiguration::class.java, + ) + ) + + private val rootLogger = LoggerFactory.getLogger(org.slf4j.Logger.ROOT_LOGGER_NAME) as Logger + + @BeforeTest + fun `reset Logback context`() { + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + loggerContext.reset() + } + + @Test + fun `does not configure SentryAppender when auto-configuration dsn is not set`() { + contextRunner.run { assertThat(rootLogger.getAppenders(SentryAppender::class.java)).isEmpty() } + } + + @Test + fun `configures SentryAppender`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(rootLogger.getAppenders(SentryAppender::class.java)).hasSize(1) + } + } + + @Test + fun `configures SentryAppender for configured loggers`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.logging.loggers[0]=foo.bar", + "sentry.logging.loggers[1]=baz", + ) + .run { + val fooBarLogger = LoggerFactory.getLogger("foo.bar") as Logger + val bazLogger = LoggerFactory.getLogger("baz") as Logger + + assertThat(rootLogger.getAppenders(SentryAppender::class.java)).hasSize(0) + assertThat(fooBarLogger.getAppenders(SentryAppender::class.java)).hasSize(1) + assertThat(bazLogger.getAppenders(SentryAppender::class.java)).hasSize(1) + } + } + + @Test + fun `configures SentryAppender for none of the loggers if so configured`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.logging.loggers=") + .run { + val fooBarLogger = LoggerFactory.getLogger("foo.bar") as Logger + val bazLogger = LoggerFactory.getLogger("baz") as Logger + + assertThat(rootLogger.getAppenders(SentryAppender::class.java)).hasSize(0) + assertThat(fooBarLogger.getAppenders(SentryAppender::class.java)).hasSize(0) + assertThat(bazLogger.getAppenders(SentryAppender::class.java)).hasSize(0) + } + } + + @Test + fun `sets SentryAppender properties`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.logging.minimum-event-level=info", + "sentry.logging.minimum-breadcrumb-level=debug", + "sentry.logging.minimum-level=error", + ) + .run { + val appenders = rootLogger.getAppenders(SentryAppender::class.java) + assertThat(appenders).hasSize(1) + val sentryAppender = appenders[0] as SentryAppender + + assertThat(sentryAppender.minimumBreadcrumbLevel).isEqualTo(Level.DEBUG) + assertThat(sentryAppender.minimumEventLevel).isEqualTo(Level.INFO) + assertThat(sentryAppender.minimumLevel).isEqualTo(Level.ERROR) + } + } + + @Test + fun `does not configure SentryAppender when logging is disabled`() { + contextRunner.withPropertyValues("sentry.logging.enabled=false").run { + assertThat(rootLogger.getAppenders(SentryAppender::class.java)).isEmpty() + } + } + + @Test + fun `does not configure SentryAppender when appender is already configured`() { + val loggerContext = LoggerFactory.getILoggerFactory() as LoggerContext + val sentryAppender = SentryAppender() + sentryAppender.name = "customAppender" + sentryAppender.context = loggerContext + sentryAppender.start() + rootLogger.addAppender(sentryAppender) + + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + val appenders = rootLogger.getAppenders(SentryAppender::class.java) + assertThat(appenders).hasSize(1) + assertThat(appenders.first().name).isEqualTo("customAppender") + } + } + + @Test + fun `does not configure SentryAppender when logback is not on the classpath`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(LoggerContext::class.java)) + .run { assertThat(rootLogger.getAppenders(SentryAppender::class.java)).isEmpty() } + } + + @Test + fun `does not configure SentryAppender when sentry-logback module is not on the classpath`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(SentryAppender::class.java)) + .run { assertThat(rootLogger.getAppenders(SentryAppender::class.java)).isEmpty() } + } +} + +fun Logger.getAppenders(clazz: Class): List> { + return this.iteratorForAppenders().asSequence().toList().filter { it.javaClass == clazz } +} diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt new file mode 100644 index 00000000000..a0ce38e7c3d --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt @@ -0,0 +1,386 @@ +package io.sentry.spring.boot.jakarta + +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.IScopes +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TracesSamplingDecision +import io.sentry.TransactionContext +import io.sentry.mockServerRequestTimeoutMillis +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.apache.hc.client5.http.impl.classic.HttpClients +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.assertNull +import org.mockito.Mockito.doAnswer +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.whenever +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpStatus +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory +import org.springframework.web.client.RestClient +import org.springframework.web.client.toEntity + +class SentrySpanRestClientCustomizerTest { + class Fixture { + val sentryOptions = SentryOptions() + val scopes = mock() + val restClientBuilder = RestClient.builder() + var mockServer = MockWebServer() + val transaction: SentryTracer + internal val customizer = SentrySpanRestClientCustomizer(scopes) + val url = mockServer.url("/test/123").toString() + val scope = Scope(sentryOptions) + + init { + whenever(scopes.options).thenReturn(sentryOptions) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(any()) + transaction = + SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) + } + + fun getSut( + isTransactionActive: Boolean, + status: HttpStatus = HttpStatus.OK, + socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, + includeMockServerInTracingOrigins: Boolean = true, + ): RestClient.Builder { + customizer.customize(restClientBuilder) + + if (includeMockServerInTracingOrigins) { + sentryOptions.setTracePropagationTargets(listOf(mockServer.hostName)) + } else { + sentryOptions.setTracePropagationTargets(listOf("other-api")) + } + + sentryOptions.dsn = "https://key@sentry.io/proj" + sentryOptions.isTraceSampling = true + + mockServer.enqueue( + MockResponse().setBody("OK").setSocketPolicy(socketPolicy).setResponseCode(status.value()) + ) + + if (isTransactionActive) { + whenever(scopes.span).thenReturn(transaction) + } + + return restClientBuilder.apply { + val httpClient = + HttpClients.custom() + .disableAutomaticRetries() // Required to not make another request automatically + .build() + val requestFactory = HttpComponentsClientHttpRequestFactory(httpClient) + requestFactory.setConnectTimeout(Duration.ofSeconds(2)) + requestFactory.setConnectionRequestTimeout(Duration.ofSeconds(2)) + it.requestFactory(requestFactory) + } + } + } + + private val fixture = Fixture() + + @Test + fun `when transaction is active, creates span around RestClient HTTP call`() { + val result = + fixture + .getSut(isTransactionActive = true) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + + assertThat(result.body).isEqualTo("OK") + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET ${fixture.url}") + assertThat(span.status).isEqualTo(SpanStatus.OK) + + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertThat(recordedRequest.headers["sentry-trace"]!!) + .startsWith(fixture.transaction.spanContext.traceId.toString()) + .endsWith("-1") + .doesNotContain(fixture.transaction.spanContext.spanId.toString()) + assertThat(recordedRequest.headers["baggage"]!!) + .contains(fixture.transaction.spanContext.traceId.toString()) + } + + @Test + fun `when there is an active span, existing baggage headers are merged with sentry baggage into single header`() { + val sut = fixture.getSut(isTransactionActive = true) + val headers = HttpHeaders() + headers.add("baggage", "thirdPartyBaggage=someValue") + headers.add( + "baggage", + "secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue", + ) + + sut + .build() + .get() + .uri(fixture.url) + .httpRequest { it.headers.addAll(headers) } + .retrieve() + .toEntity(String::class.java) + + val recorderRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + + val baggageHeaderValues = recorderRequest.headers.values(BaggageHeader.BAGGAGE_HEADER) + assertEquals(baggageHeaderValues.size, 1) + assertTrue( + baggageHeaderValues[0].startsWith( + "thirdPartyBaggage=someValue,secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + ) + ) + assertTrue(baggageHeaderValues[0].contains("sentry-public_key=key")) + assertTrue(baggageHeaderValues[0].contains("sentry-transaction=aTransaction")) + assertTrue(baggageHeaderValues[0].contains("sentry-trace_id")) + } + + @Test + fun `when transaction is active and server is not listed in tracing origins, does not add sentry trace header to the request`() { + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = false) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertThat(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]).isNull() + } + + @Test + fun `when transaction is active and server is listed in tracing origins, adds sentry trace header to the request`() { + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertThat(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]).isNotNull() + } + + @Test + fun `when transaction is active and response code is not 2xx, creates span with error status around RestClient HTTP call`() { + try { + fixture + .getSut(isTransactionActive = true, status = HttpStatus.INTERNAL_SERVER_ERROR) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + } catch (e: Throwable) {} + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET ${fixture.url}") + assertThat(span.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + } + + @Test + fun `when transaction is active and throws IO exception, creates span with error status around RestClient HTTP call`() { + try { + val sut = + fixture + .getSut(isTransactionActive = true, socketPolicy = SocketPolicy.DISCONNECT_AT_START) + .build() + sut.get().uri(fixture.url).retrieve().toEntity(String::class.java) + } catch (t: Throwable) { + println(t) + } + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET ${fixture.url}") + assertThat(span.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + } + + @Test + fun `when transaction is not active, does not create span around RestClient HTTP call`() { + val result = + fixture + .getSut(isTransactionActive = false) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + + assertThat(result.body).isEqualTo("OK") + assertThat(fixture.transaction.spans).isEmpty() + } + + @Test + fun `when transaction is not active, adds tracing headers from scope`() { + val sut = fixture.getSut(isTransactionActive = false) + val headers = HttpHeaders() + headers.add("baggage", "thirdPartyBaggage=someValue") + headers.add( + "baggage", + "secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue", + ) + + sut + .build() + .get() + .uri(fixture.url) + .httpRequest { it.headers.addAll(headers) } + .retrieve() + .toEntity(String::class.java) + + val recorderRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + + val baggageHeaderValues = recorderRequest.headers.values(BaggageHeader.BAGGAGE_HEADER) + assertEquals(baggageHeaderValues.size, 1) + assertTrue( + baggageHeaderValues[0].startsWith( + "thirdPartyBaggage=someValue,secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + ) + ) + assertTrue(baggageHeaderValues[0].contains("sentry-public_key=key")) + assertTrue(baggageHeaderValues[0].contains("sentry-trace_id")) + } + + @Test + fun `does not add sentry-trace header if span origin is ignored`() { + fixture.sentryOptions.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.restclient")) + val sut = fixture.getSut(isTransactionActive = false) + val headers = HttpHeaders() + + sut + .build() + .get() + .uri(fixture.url) + .httpRequest { it.headers.addAll(headers) } + .retrieve() + .toEntity(String::class.java) + + val recorderRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when transaction is active adds breadcrumb when http calls succeeds`() { + fixture + .getSut(isTransactionActive = true) + .build() + .post() + .uri(fixture.url) + .body("content") + .retrieve() + .toEntity(String::class.java) + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("POST", it.data["method"]) + assertEquals(7, it.data["request_body_size"]) + }, + anyOrNull(), + ) + } + + @SuppressWarnings("SwallowedException") + @Test + fun `when transaction is active adds breadcrumb when http calls results in exception`() { + try { + fixture + .getSut(isTransactionActive = true, status = HttpStatus.INTERNAL_SERVER_ERROR) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + } catch (e: Throwable) {} + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("GET", it.data["method"]) + }, + anyOrNull(), + ) + } + + @Test + fun `when transaction is not active adds breadcrumb when http calls succeeds`() { + fixture + .getSut(isTransactionActive = false) + .build() + .post() + .uri(fixture.url) + .body("content") + .retrieve() + .toEntity(String::class.java) + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("POST", it.data["method"]) + assertEquals(7, it.data["request_body_size"]) + }, + anyOrNull(), + ) + } + + @SuppressWarnings("SwallowedException") + @Test + fun `when transaction is not active adds breadcrumb when http calls results in exception`() { + try { + fixture + .getSut(isTransactionActive = false, status = HttpStatus.INTERNAL_SERVER_ERROR) + .build() + .get() + .uri(fixture.url) + .retrieve() + .toEntity(String::class.java) + } catch (e: Throwable) {} + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("GET", it.data["method"]) + }, + anyOrNull(), + ) + } +} diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt new file mode 100644 index 00000000000..f7a7f55c5db --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt @@ -0,0 +1,331 @@ +package io.sentry.spring.boot.jakarta + +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.IScopes +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TracesSamplingDecision +import io.sentry.TransactionContext +import io.sentry.mockServerRequestTimeoutMillis +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.SocketPolicy +import org.assertj.core.api.Assertions.assertThat +import org.junit.Assert.assertNull +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.web.client.RestTemplate + +class SentrySpanRestTemplateCustomizerTest { + class Fixture { + val sentryOptions = SentryOptions() + val scopes = mock() + val restTemplate = + RestTemplateBuilder() + .setConnectTimeout(Duration.ofSeconds(2)) + .setReadTimeout(Duration.ofSeconds(2)) + .build() + var mockServer = MockWebServer() + val transaction: SentryTracer + internal val customizer = SentrySpanRestTemplateCustomizer(scopes) + val url = mockServer.url("/test/123").toString() + val scope = Scope(sentryOptions) + + init { + whenever(scopes.options).thenReturn(sentryOptions) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(any()) + transaction = + SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) + } + + fun getSut( + isTransactionActive: Boolean, + status: HttpStatus = HttpStatus.OK, + socketPolicy: SocketPolicy = SocketPolicy.KEEP_OPEN, + includeMockServerInTracingOrigins: Boolean = true, + ): RestTemplate { + customizer.customize(restTemplate) + + if (includeMockServerInTracingOrigins) { + sentryOptions.setTracePropagationTargets(listOf(mockServer.hostName)) + } else { + sentryOptions.setTracePropagationTargets(listOf("other-api")) + } + + sentryOptions.dsn = "https://key@sentry.io/proj" + sentryOptions.isTraceSampling = true + + mockServer.enqueue( + MockResponse().setBody("OK").setSocketPolicy(socketPolicy).setResponseCode(status.value()) + ) + + if (isTransactionActive) { + whenever(scopes.span).thenReturn(transaction) + } + + return restTemplate + } + } + + private val fixture = Fixture() + + @Test + fun `when transaction is active, creates span around RestTemplate HTTP call`() { + val result = + fixture.getSut(isTransactionActive = true).getForObject(fixture.url, String::class.java) + + assertThat(result).isEqualTo("OK") + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET ${fixture.url}") + assertThat(span.status).isEqualTo(SpanStatus.OK) + + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertThat(recordedRequest.headers["sentry-trace"]!!) + .startsWith(fixture.transaction.spanContext.traceId.toString()) + .endsWith("-1") + .doesNotContain(fixture.transaction.spanContext.spanId.toString()) + assertThat(recordedRequest.headers["baggage"]!!) + .contains(fixture.transaction.spanContext.traceId.toString()) + } + + @Test + fun `when there is an active span, existing baggage headers are merged with sentry baggage into single header`() { + val sut = fixture.getSut(isTransactionActive = true) + val headers = HttpHeaders() + headers.add("baggage", "thirdPartyBaggage=someValue") + headers.add( + "baggage", + "secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue", + ) + + val requestEntity = HttpEntity(headers) + + sut.exchange(fixture.url, HttpMethod.GET, requestEntity, String::class.java) + + val recorderRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + + val baggageHeaderValues = recorderRequest.headers.values(BaggageHeader.BAGGAGE_HEADER) + assertEquals(baggageHeaderValues.size, 1) + assertTrue( + baggageHeaderValues[0].startsWith( + "thirdPartyBaggage=someValue,secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + ) + ) + assertTrue(baggageHeaderValues[0].contains("sentry-public_key=key")) + assertTrue(baggageHeaderValues[0].contains("sentry-transaction=aTransaction")) + assertTrue(baggageHeaderValues[0].contains("sentry-trace_id")) + } + + @Test + fun `when transaction is active and server is not listed in tracing origins, does not add sentry trace header to the request`() { + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = false) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertThat(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]).isNull() + } + + @Test + fun `when transaction is active and server is listed in tracing origins, adds sentry trace header to the request`() { + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = true) + .getForObject(fixture.url, String::class.java) + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertThat(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]).isNotNull() + } + + @Test + fun `when transaction is active and response code is not 2xx, creates span with error status around RestTemplate HTTP call`() { + try { + fixture + .getSut(isTransactionActive = true, status = HttpStatus.INTERNAL_SERVER_ERROR) + .getForObject(fixture.url, String::class.java) + } catch (e: Throwable) {} + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET ${fixture.url}") + assertThat(span.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + } + + @Test + fun `when transaction is active and throws IO exception, creates span with error status around RestTemplate HTTP call`() { + try { + fixture + .getSut(isTransactionActive = true, socketPolicy = SocketPolicy.DISCONNECT_AT_START) + .getForObject(fixture.url, String::class.java) + } catch (e: Throwable) {} + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET ${fixture.url}") + assertThat(span.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + } + + @Test + fun `when transaction is not active, does not create span around RestTemplate HTTP call`() { + val result = + fixture.getSut(isTransactionActive = false).getForObject(fixture.url, String::class.java) + + assertThat(result).isEqualTo("OK") + assertThat(fixture.transaction.spans).isEmpty() + } + + @Test + fun `when transaction is not active, adds tracing headers from scope`() { + val sut = fixture.getSut(isTransactionActive = false) + val headers = HttpHeaders() + headers.add("baggage", "thirdPartyBaggage=someValue") + headers.add( + "baggage", + "secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue", + ) + + val requestEntity = HttpEntity(headers) + + sut.exchange(fixture.url, HttpMethod.GET, requestEntity, String::class.java) + + val recorderRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + + val baggageHeaderValues = recorderRequest.headers.values(BaggageHeader.BAGGAGE_HEADER) + assertEquals(baggageHeaderValues.size, 1) + assertTrue( + baggageHeaderValues[0].startsWith( + "thirdPartyBaggage=someValue,secondThirdPartyBaggage=secondValue; property;propertyKey=propertyValue,anotherThirdPartyBaggage=anotherValue" + ) + ) + assertTrue(baggageHeaderValues[0].contains("sentry-public_key=key")) + assertTrue(baggageHeaderValues[0].contains("sentry-trace_id")) + } + + @Test + fun `does not add sentry-trace header when span origin is ignored`() { + fixture.sentryOptions.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.resttemplate")) + val sut = fixture.getSut(isTransactionActive = false) + val headers = HttpHeaders() + val requestEntity = HttpEntity(headers) + + sut.exchange(fixture.url, HttpMethod.GET, requestEntity, String::class.java) + + val recorderRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recorderRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recorderRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `avoids duplicate registration`() { + val restTemplate = fixture.getSut(isTransactionActive = true) + + fixture.customizer.customize(restTemplate) + assertThat(restTemplate.interceptors).hasSize(1) + fixture.customizer.customize(restTemplate) + assertThat(restTemplate.interceptors).hasSize(1) + } + + @Test + fun `when transaction is active adds breadcrumb when http calls succeeds`() { + fixture + .getSut(isTransactionActive = true) + .postForObject(fixture.url, "content", String::class.java) + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("POST", it.data["method"]) + assertEquals(7, it.data["request_body_size"]) + }, + anyOrNull(), + ) + } + + @SuppressWarnings("SwallowedException") + @Test + fun `when transaction is active adds breadcrumb when http calls results in exception`() { + try { + fixture + .getSut(isTransactionActive = true, status = HttpStatus.INTERNAL_SERVER_ERROR) + .getForObject(fixture.url, String::class.java) + } catch (e: Throwable) {} + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("GET", it.data["method"]) + }, + anyOrNull(), + ) + } + + @Test + fun `when transaction is not active adds breadcrumb when http calls succeeds`() { + fixture + .getSut(isTransactionActive = false) + .postForObject(fixture.url, "content", String::class.java) + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("POST", it.data["method"]) + assertEquals(7, it.data["request_body_size"]) + }, + anyOrNull(), + ) + } + + @SuppressWarnings("SwallowedException") + @Test + fun `when transaction is not active adds breadcrumb when http calls results in exception`() { + try { + fixture + .getSut(isTransactionActive = false, status = HttpStatus.INTERNAL_SERVER_ERROR) + .getForObject(fixture.url, String::class.java) + } catch (e: Throwable) {} + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(fixture.url, it.data["url"]) + assertEquals("GET", it.data["method"]) + }, + anyOrNull(), + ) + } +} diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt new file mode 100644 index 00000000000..83321f30e78 --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt @@ -0,0 +1,371 @@ +package io.sentry.spring.boot.jakarta + +import io.sentry.BaggageHeader +import io.sentry.Breadcrumb +import io.sentry.IScope +import io.sentry.IScopes +import io.sentry.Scope +import io.sentry.ScopeCallback +import io.sentry.Sentry.OptionsConfiguration +import io.sentry.SentryOptions +import io.sentry.SentryTraceHeader +import io.sentry.SentryTracer +import io.sentry.SpanStatus +import io.sentry.TracesSamplingDecision +import io.sentry.TransactionContext +import io.sentry.mockServerRequestTimeoutMillis +import java.util.concurrent.TimeUnit +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull +import okhttp3.mockwebserver.Dispatcher +import okhttp3.mockwebserver.MockResponse +import okhttp3.mockwebserver.MockWebServer +import okhttp3.mockwebserver.RecordedRequest +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.check +import org.mockito.kotlin.doAnswer +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.web.reactive.function.BodyInserters +import org.springframework.web.reactive.function.client.WebClient + +class SentrySpanWebClientCustomizerTest { + class Fixture { + lateinit var sentryOptions: SentryOptions + lateinit var scope: IScope + val scopes = mock() + var mockServer = MockWebServer() + lateinit var transaction: SentryTracer + private val customizer = SentrySpanWebClientCustomizer(scopes) + + fun getSut( + isTransactionActive: Boolean, + status: HttpStatus = HttpStatus.OK, + throwIOException: Boolean = false, + includeMockServerInTracingOrigins: Boolean = true, + optionsConfiguration: OptionsConfiguration? = null, + ): WebClient { + sentryOptions = + SentryOptions().also { + optionsConfiguration?.configure(it) + if (includeMockServerInTracingOrigins) { + it.setTracePropagationTargets(listOf(mockServer.hostName)) + } else { + it.setTracePropagationTargets(listOf("other-api")) + } + it.dsn = "http://key@localhost/proj" + } + scope = Scope(sentryOptions) + whenever(scopes.options).thenReturn(sentryOptions) + doAnswer { (it.arguments[0] as ScopeCallback).run(scope) } + .whenever(scopes) + .configureScope(any()) + transaction = + SentryTracer(TransactionContext("aTransaction", "op", TracesSamplingDecision(true)), scopes) + val webClientBuilder = WebClient.builder() + customizer.customize(webClientBuilder) + val webClient = webClientBuilder.build() + + if (isTransactionActive) { + val scope = Scope(sentryOptions) + scope.transaction = transaction + whenever(scopes.span).thenReturn(transaction) + } + + val dispatcher: Dispatcher = + object : Dispatcher() { + @Throws(InterruptedException::class) + override fun dispatch(request: RecordedRequest): MockResponse { + if (isTransactionActive && includeMockServerInTracingOrigins) { + assertThat(request.headers["sentry-trace"]!!) + .startsWith(transaction.spanContext.traceId.toString()) + .endsWith("-1") + .doesNotContain(transaction.spanContext.spanId.toString()) + return if (throwIOException) { + MockResponse() + .setResponseCode(500) + .addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + } else { + MockResponse() + .setResponseCode(status.value()) + .setBody("OK") + .addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + } + } else { + return MockResponse() + .setResponseCode(status.value()) + .setBody("OK") + .addHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE) + } + } + } + mockServer.dispatcher = dispatcher + return webClient + } + } + + private val fixture = Fixture() + + @BeforeEach + fun setUp() { + fixture.mockServer.start() + } + + @AfterEach + fun tearDown() { + fixture.mockServer.shutdown() + } + + @Test + fun `when transaction is active, creates span around WebClient HTTP call`() { + val uri = fixture.mockServer.url("/test/123").toUri() + val result = + fixture + .getSut(isTransactionActive = true) + .get() + .uri(uri) + .retrieve() + .bodyToMono(String::class.java) + .block() + assertThat(result).isEqualTo("OK") + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET $uri") + assertThat(span.status).isEqualTo(SpanStatus.OK) + } + + @Test + fun `when transaction is active and server is not listed in tracing origins, does not add sentry trace header to the request`() { + fixture + .getSut(isTransactionActive = true, includeMockServerInTracingOrigins = false) + .get() + .uri(fixture.mockServer.url("/test/123").toUri()) + .retrieve() + .bodyToMono(String::class.java) + .block() + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when no transaction is active and server is not listed in tracing origins, does not add sentry trace header to the request`() { + fixture + .getSut(isTransactionActive = false, includeMockServerInTracingOrigins = false) + .get() + .uri(fixture.mockServer.url("/test/123").toUri()) + .retrieve() + .bodyToMono(String::class.java) + .block() + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when no transaction is active, adds sentry trace header to the request from scope`() { + fixture + .getSut(isTransactionActive = false, includeMockServerInTracingOrigins = true) + .get() + .uri(fixture.mockServer.url("/test/123").toUri()) + .retrieve() + .bodyToMono(String::class.java) + .block() + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `does not add sentry-trace header when span origin is ignored`() { + val sut = + fixture.getSut(isTransactionActive = false, includeMockServerInTracingOrigins = true) { + options -> + options.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.webclient")) + } + sut + .get() + .uri(fixture.mockServer.url("/test/123").toUri()) + .retrieve() + .bodyToMono(String::class.java) + .block() + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNull(recordedRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when transaction is active and server is listed in tracing origins, adds sentry trace header to the request`() { + fixture + .getSut(isTransactionActive = true) + .get() + .uri(fixture.mockServer.url("/test/123").toUri()) + .retrieve() + .bodyToMono(String::class.java) + .block() + val recordedRequest = + fixture.mockServer.takeRequest(mockServerRequestTimeoutMillis, TimeUnit.MILLISECONDS)!! + assertNotNull(recordedRequest.headers[SentryTraceHeader.SENTRY_TRACE_HEADER]) + assertNotNull(recordedRequest.headers[BaggageHeader.BAGGAGE_HEADER]) + } + + @Test + fun `when transaction is active and response code is not 2xx, creates span with error status around WebClient HTTP call`() { + val uri = fixture.mockServer.url("/test/123").toUri() + try { + fixture + .getSut(isTransactionActive = true, status = HttpStatus.INTERNAL_SERVER_ERROR) + .get() + .uri(uri) + .retrieve() + .bodyToMono(String::class.java) + .block() + } catch (e: Throwable) {} + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET $uri") + assertThat(span.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + } + + @Test + fun `when transaction is active and throws IO exception, creates span with error status around WebClient HTTP call`() { + val uri = fixture.mockServer.url("/test/123").toUri() + try { + fixture + .getSut(isTransactionActive = true, throwIOException = true) + .get() + .uri(uri) + .retrieve() + .bodyToMono(String::class.java) + .block() + } catch (e: Throwable) {} + assertThat(fixture.transaction.spans).hasSize(1) + val span = fixture.transaction.spans.first() + assertThat(span.operation).isEqualTo("http.client") + assertThat(span.description).isEqualTo("GET $uri") + assertThat(span.status).isEqualTo(SpanStatus.INTERNAL_ERROR) + } + + @Test + fun `when transaction is not active, does not create span around WebClient HTTP call`() { + val uri = fixture.mockServer.url("/test/123").toUri() + val result = + fixture + .getSut(isTransactionActive = false) + .get() + .uri(uri) + .accept(MediaType.APPLICATION_JSON) + .exchangeToMono { response -> response.bodyToMono(String::class.java) } + .block() + + assertThat(result).isEqualTo("OK") + assertThat(fixture.transaction.spans).isEmpty() + } + + @Test + fun `when transaction is active adds breadcrumb when http calls succeeds`() { + val uri = fixture.mockServer.url("/test/123").toUri() + fixture + .getSut(isTransactionActive = true) + .post() + .uri(uri) + .body(BodyInserters.fromValue("content")) + .retrieve() + .bodyToMono(String::class.java) + .block() + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(uri.toString(), it.data["url"]) + assertEquals("POST", it.data["method"]) + }, + anyOrNull(), + ) + } + + @SuppressWarnings("SwallowedException") + @Test + fun `when transaction is active adds breadcrumb when http calls results in exception`() { + val uri = fixture.mockServer.url("/test/123").toUri() + try { + fixture + .getSut(isTransactionActive = true, status = HttpStatus.INTERNAL_SERVER_ERROR) + .get() + .uri(uri) + .retrieve() + .bodyToMono(String::class.java) + .block() + } catch (e: Throwable) {} + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(uri.toString(), it.data["url"]) + assertEquals("GET", it.data["method"]) + }, + anyOrNull(), + ) + } + + @Test + fun `when transaction is not active adds breadcrumb when http calls succeeds`() { + val uri = fixture.mockServer.url("/test/123").toUri() + fixture + .getSut(isTransactionActive = false) + .post() + .uri(uri) + .body(BodyInserters.fromValue("content")) + .retrieve() + .bodyToMono(String::class.java) + .block() + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(uri.toString(), it.data["url"]) + assertEquals("POST", it.data["method"]) + }, + anyOrNull(), + ) + } + + @SuppressWarnings("SwallowedException") + @Test + fun `when transaction is not active adds breadcrumb when http calls results in exception`() { + val uri = fixture.mockServer.url("/test/123").toUri() + try { + fixture + .getSut(isTransactionActive = false, status = HttpStatus.INTERNAL_SERVER_ERROR) + .get() + .uri(uri) + .retrieve() + .bodyToMono(String::class.java) + .block() + } catch (e: Throwable) {} + verify(fixture.scopes) + .addBreadcrumb( + check { + assertEquals("http", it.type) + assertEquals(uri.toString(), it.data["url"]) + assertEquals("GET", it.data["method"]) + }, + anyOrNull(), + ) + } +} diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfigurationTest.kt new file mode 100644 index 00000000000..fd47317d1d2 --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfigurationTest.kt @@ -0,0 +1,96 @@ +package io.sentry.spring.boot.jakarta + +import io.micrometer.context.ThreadLocalAccessor +import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler +import io.sentry.spring.jakarta.webflux.SentryWebFilter +import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.springframework.boot.autoconfigure.AutoConfigurations +import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration +import org.springframework.boot.test.context.FilteredClassLoader +import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner +import reactor.core.scheduler.Schedulers + +class SentryWebfluxAutoConfigurationTest { + private val contextRunner = + ReactiveWebApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of( + WebFluxAutoConfiguration::class.java, + SentryWebfluxAutoConfiguration::class.java, + SentryAutoConfiguration::class.java, + ) + ) + + @Test + fun `configures sentryWebFilter`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).hasSingleBean(SentryWebFilterWithThreadLocalAccessor::class.java) + assertThat(it).doesNotHaveBean(SentryWebFilter::class.java) + } + } + + @Test + fun `configures exception handler`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { + assertThat(it).hasSingleBean(SentryWebExceptionHandler::class.java) + } + } + + @Test + fun `does not run when reactor is not on the classpath`() { + contextRunner + .withPropertyValues("sentry.dsn=http://key@localhost/proj") + .withClassLoader(FilteredClassLoader(Schedulers::class.java)) + .run { + assertThat(it).doesNotHaveBean(SentryWebExceptionHandler::class.java) + assertThat(it).doesNotHaveBean(SentryWebFilter::class.java) + } + } + + @Test + fun `does not run when dsn is not configured`() { + contextRunner.withClassLoader(FilteredClassLoader(Schedulers::class.java)).run { + assertThat(it).doesNotHaveBean(SentryWebExceptionHandler::class.java) + assertThat(it).doesNotHaveBean(SentryWebFilter::class.java) + } + } + + @Test + fun `configures web filter with ThreadLocalAccessor support if available and enabled`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.reactive.thread-local-accessor-enabled=true", + ) + .run { + assertThat(it).hasSingleBean(SentryWebFilterWithThreadLocalAccessor::class.java) + assertThat(it).doesNotHaveBean(SentryWebFilter::class.java) + } + } + + @Test + fun `does not configure web filter with ThreadLocalAccessor support if disabled`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.reactive.thread-local-accessor-enabled=false", + ) + .run { + assertThat(it).doesNotHaveBean(SentryWebFilterWithThreadLocalAccessor::class.java) + assertThat(it).hasSingleBean(SentryWebFilter::class.java) + } + } + + @Test + fun `does not configure web filter with ThreadLocalAccessor support if not available`() { + contextRunner + .withPropertyValues( + "sentry.dsn=http://key@localhost/proj", + "sentry.reactive.thread-local-accessor-enabled=true", + ) + .withClassLoader(FilteredClassLoader(ThreadLocalAccessor::class.java)) + .run { assertThat(it).doesNotHaveBean(SentryWebFilterWithThreadLocalAccessor::class.java) } + } +} diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt new file mode 100644 index 00000000000..89d378d2087 --- /dev/null +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt @@ -0,0 +1,350 @@ +package io.sentry.spring.boot.jakarta.it + +import io.sentry.DefaultSpanFactory +import io.sentry.IScopes +import io.sentry.ITransportFactory +import io.sentry.Sentry +import io.sentry.SentryOpenTelemetryMode +import io.sentry.SentryOptions +import io.sentry.checkEvent +import io.sentry.checkTransaction +import io.sentry.spring.jakarta.tracing.SentrySpan +import io.sentry.transport.ITransport +import kotlin.test.BeforeTest +import kotlin.test.Test +import org.assertj.core.api.Assertions.assertThat +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.reset +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever +import org.slf4j.LoggerFactory +import org.springframework.beans.factory.annotation.Autowired +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.test.context.SpringBootTest +import org.springframework.boot.test.mock.mockito.SpyBean +import org.springframework.boot.test.web.client.TestRestTemplate +import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.http.HttpEntity +import org.springframework.http.HttpHeaders +import org.springframework.http.HttpMethod +import org.springframework.http.HttpStatus +import org.springframework.http.MediaType +import org.springframework.http.ResponseEntity +import org.springframework.security.config.annotation.web.builders.HttpSecurity +import org.springframework.security.core.userdetails.User +import org.springframework.security.core.userdetails.UserDetails +import org.springframework.security.crypto.factory.PasswordEncoderFactories +import org.springframework.security.crypto.password.PasswordEncoder +import org.springframework.security.provisioning.InMemoryUserDetailsManager +import org.springframework.security.web.SecurityFilterChain +import org.springframework.stereotype.Service +import org.springframework.test.context.junit4.SpringRunner +import org.springframework.web.bind.annotation.ControllerAdvice +import org.springframework.web.bind.annotation.ExceptionHandler +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RestController + +@RunWith(SpringRunner::class) +@SpringBootTest( + classes = [App::class], + webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, + properties = + [ + "sentry.dsn=http://key@localhost/proj", + "sentry.send-default-pii=true", + "sentry.traces-sample-rate=1.0", + "sentry.max-request-body-size=medium", + "sentry.enable-backpressure-handling=false", + ], +) +class SentrySpringIntegrationTest { + + @Autowired lateinit var transport: ITransport + + @SpyBean lateinit var scopes: IScopes + + @LocalServerPort var port: Int? = null + + @BeforeTest + fun reset() { + reset(transport) + } + + @Test + fun `attaches request and user information to SentryEvents`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders() + headers["X-FORWARDED-FOR"] = listOf("169.128.0.1") + val entity = HttpEntity(headers) + + restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.request).isNotNull() + assertThat(event.request!!.url).isEqualTo("http://localhost:$port/hello") + assertThat(event.user).isNotNull() + assertThat(event.user!!.username).isEqualTo("user") + assertThat(event.user!!.ipAddress).isEqualTo("169.128.0.1") + }, + anyOrNull(), + ) + } + + @Test + fun `attaches request body to SentryEvents`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders().apply { this.contentType = MediaType.APPLICATION_JSON } + val httpEntity = HttpEntity("""{"body":"content"}""", headers) + restTemplate.exchange( + "http://localhost:$port/bodyAsParam", + HttpMethod.POST, + httpEntity, + Void::class.java, + ) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.request).isNotNull() + assertThat(event.request!!.data).isEqualTo("""{"body":"content"}""") + }, + anyOrNull(), + ) + } + + @Test + fun `attaches request body to SentryEvents on empty ControllerMethod Params`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders().apply { this.contentType = MediaType.APPLICATION_JSON } + val httpEntity = HttpEntity("""{"body":"content"}""", headers) + restTemplate.exchange( + "http://localhost:$port/body", + HttpMethod.POST, + httpEntity, + Void::class.java, + ) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.request).isNotNull() + assertThat(event.request!!.data).isEqualTo("""{"body":"content"}""") + }, + anyOrNull(), + ) + } + + @Test + fun `attaches first ip address if multiple addresses exist in a header`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + val headers = HttpHeaders() + headers["X-FORWARDED-FOR"] = listOf("169.128.0.1, 192.168.0.1") + val entity = HttpEntity(headers) + + restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.user).isNotNull() + assertThat(event.user!!.ipAddress).isEqualTo("169.128.0.1") + }, + anyOrNull(), + ) + } + + @Test + fun `sends events for unhandled exceptions`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/throws", String::class.java) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.exceptions).isNotNull().isNotEmpty + val ex = event.exceptions!!.first() + assertThat(ex.value).isEqualTo("something went wrong") + assertThat(ex.mechanism).isNotNull() + assertThat(ex.mechanism!!.isHandled).isFalse() + }, + anyOrNull(), + ) + } + + @Test + fun `sends events for error logs`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/logging", String::class.java) + + verify(transport) + .send( + checkEvent { event -> + assertThat(event.message).isNotNull() + assertThat(event.message!!.message).isEqualTo("event from logger") + }, + anyOrNull(), + ) + } + + @Test + fun `attaches span context to events triggered within transaction`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/performance", String::class.java) + + verify(transport) + .send(checkEvent { event -> assertThat(event.contexts.trace).isNotNull() }, anyOrNull()) + } + + @Test + fun `tracing filter does not overwrite resposne status code`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + val response = + restTemplate.getForEntity("http://localhost:$port/performance", String::class.java) + assertThat(response.statusCode).isEqualTo(HttpStatus.INTERNAL_SERVER_ERROR) + } + + @Test + fun `does not send events for handled exceptions`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/throws-handled", String::class.java) + + verify(scopes, never()).captureEvent(any()) + } + + @Test + fun `sets user on transaction`() { + val restTemplate = TestRestTemplate().withBasicAuth("user", "password") + + restTemplate.getForEntity("http://localhost:$port/performance", String::class.java) + + verify(transport) + .send( + checkTransaction { transaction -> + assertThat(transaction.transaction).isEqualTo("GET /performance") + assertThat(transaction.user).isNotNull() + assertThat(transaction.user!!.username).isEqualTo("user") + }, + anyOrNull(), + ) + } +} + +@SpringBootApplication +open class App { + private val transport = mock().also { whenever(it.isHealthy).thenReturn(true) } + + @Bean + open fun mockTransportFactory(): ITransportFactory { + val factory = mock() + whenever(factory.create(any(), any())).thenReturn(transport) + return factory + } + + @Bean open fun mockTransport() = transport + + @Bean + open fun optionsCallback() = + Sentry.OptionsConfiguration { options -> + // due to OTel being on the classpath we need to set the default again + options.spanFactory = DefaultSpanFactory() + options.openTelemetryMode = SentryOpenTelemetryMode.OFF + } +} + +@RestController +class HelloController(private val helloService: HelloService) { + private val logger = LoggerFactory.getLogger(HelloController::class.java) + + @GetMapping("/hello") + fun hello() { + Sentry.captureMessage("hello") + } + + @GetMapping("/throws") + fun throws() { + throw RuntimeException("something went wrong") + } + + @GetMapping("/throws-handled") + fun throwsHandled() { + throw CustomException("handled exception") + } + + @GetMapping("/performance") + fun performance() { + helloService.throws() + } + + @GetMapping("/logging") + fun logging() { + logger.error("event from logger") + } + + @PostMapping("/body") + fun body() { + Sentry.captureMessage("body") + } + + @PostMapping("/bodyAsParam") + fun bodyWithReadingBodyInController(@RequestBody body: String) { + Sentry.captureMessage("body") + } +} + +@Service +open class HelloService { + + @SentrySpan + open fun throws() { + throw RuntimeException("something went wrong") + } +} + +@Configuration +open class SecurityConfiguration { + + @Bean + open fun userDetailsService(): InMemoryUserDetailsManager { + val encoder: PasswordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder() + val user: UserDetails = + User.builder() + .passwordEncoder { rawPassword -> encoder.encode(rawPassword) } + .username("user") + .password("password") + .roles("USER") + .build() + return InMemoryUserDetailsManager(user) + } + + @Bean + @Throws(Exception::class) + open fun filterChain(http: HttpSecurity): SecurityFilterChain { + http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic() + + return http.build() + } +} + +class CustomException(message: String) : RuntimeException(message) + +@ControllerAdvice +class ExceptionHandlers { + + @ExceptionHandler(CustomException::class) + fun handle(e: CustomException) = ResponseEntity.badRequest().build() +} diff --git a/sentry-spring-boot-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/sentry-spring-boot-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000000..1f0955d450f --- /dev/null +++ b/sentry-spring-boot-4/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline diff --git a/settings.gradle.kts b/settings.gradle.kts index d4aa2a95c32..f1213f38d9b 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -43,10 +43,13 @@ include( "sentry-apache-http-client-5", "sentry-spring", "sentry-spring-jakarta", + "sentry-spring-7", "sentry-spring-boot", "sentry-spring-boot-jakarta", "sentry-spring-boot-starter", "sentry-spring-boot-starter-jakarta", + "sentry-spring-boot-4", + "sentry-spring-boot-4-starter", "sentry-bom", "sentry-openfeign", "sentry-graphql", @@ -73,6 +76,7 @@ include( "sentry-samples:sentry-samples-servlet", "sentry-samples:sentry-samples-spring", "sentry-samples:sentry-samples-spring-jakarta", + "sentry-samples:sentry-samples-spring-7", "sentry-samples:sentry-samples-spring-boot", "sentry-samples:sentry-samples-spring-boot-opentelemetry", "sentry-samples:sentry-samples-spring-boot-opentelemetry-noagent", @@ -81,6 +85,10 @@ include( "sentry-samples:sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "sentry-samples:sentry-samples-spring-boot-webflux", "sentry-samples:sentry-samples-spring-boot-webflux-jakarta", + "sentry-samples:sentry-samples-spring-boot-4", + "sentry-samples:sentry-samples-spring-boot-4-opentelemetry", + "sentry-samples:sentry-samples-spring-boot-4-opentelemetry-noagent", + "sentry-samples:sentry-samples-spring-boot-4-webflux", "sentry-samples:sentry-samples-netflix-dgs", "sentry-android-integration-tests:sentry-uitest-android-critical", "sentry-android-integration-tests:sentry-uitest-android-benchmark", diff --git a/test/system-test-runner.py b/test/system-test-runner.py index fda5042c819..053846dddf6 100644 --- a/test/system-test-runner.py +++ b/test/system-test-runner.py @@ -586,6 +586,11 @@ def get_available_modules(self) -> List[ModuleConfig]: ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry-noagent", "false", "true", "false"), ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "true", "false"), ModuleConfig("sentry-samples-spring-boot-jakarta-opentelemetry", "true", "false", "false"), + ModuleConfig("sentry-samples-spring-boot-4-webflux", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-4", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-4-opentelemetry-noagent", "false", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-4-opentelemetry", "true", "true", "false"), + ModuleConfig("sentry-samples-spring-boot-4-opentelemetry", "true", "false", "false"), ModuleConfig("sentry-samples-console", "false", "true", "false"), ModuleConfig("sentry-samples-console-opentelemetry-noagent", "false", "true", "false"), ] From ddeada2829143233baed2495a6c338c1071d157c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 8 Aug 2025 12:02:25 +0200 Subject: [PATCH 02/10] Fix Spring 7 and Spring Boot 4 modules (#4602) --- gradle/libs.versions.toml | 6 ++++++ ...ntry-spring-jakarta.api => sentry-spring-7.api} | 14 +++++++------- sentry-spring-7/build.gradle.kts | 6 +++--- .../sentry/spring/jakarta/SentryHubRegistrar.java | 5 +++-- .../sentry/spring/jakarta/SentrySpringFilter.java | 2 +- .../jakarta/checkin/SentryQuartzConfiguration.java | 2 +- .../SentrySchedulerFactoryBeanCustomizer.java | 2 +- .../jakarta/webflux/SentryRequestResolver.java | 2 +- .../io/sentry/spring/jakarta/EnableSentryTest.kt | 1 + .../jakarta/mvc/SentrySpringIntegrationTest.kt | 13 ++++++++----- .../webflux/SentryWebfluxIntegrationTest.kt | 6 +++--- ...akarta.api => sentry-spring-boot-4-starter.api} | 0 ...g-boot-jakarta.api => sentry-spring-boot-4.api} | 10 +++++----- sentry-spring-boot-4/build.gradle.kts | 2 ++ .../boot/jakarta/SentryAutoConfiguration.java | 11 ++++++----- .../jakarta/SentrySpanRestClientCustomizer.java | 2 +- .../jakarta/SentrySpanRestTemplateCustomizer.java | 2 +- .../jakarta/SentrySpanWebClientCustomizer.java | 2 +- 18 files changed, 51 insertions(+), 37 deletions(-) rename sentry-spring-7/api/{sentry-spring-jakarta.api => sentry-spring-7.api} (98%) rename sentry-spring-boot-4-starter/api/{sentry-spring-boot-starter-jakarta.api => sentry-spring-boot-4-starter.api} (100%) rename sentry-spring-boot-4/api/{sentry-spring-boot-jakarta.api => sentry-spring-boot-4.api} (97%) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 62a952dd6a0..80fe6ae520d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -12,6 +12,7 @@ jacoco = "0.8.7" jackson = "2.18.3" jetbrainsCompose = "1.6.11" kotlin = "1.9.24" +kotlinSpring7 = "2.2.0" kotlin-compatible-version = "1.6" ktorClient = "3.0.0" logback = "1.2.9" @@ -96,6 +97,7 @@ jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", ver jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jetbrains-annotations = { module = "org.jetbrains:annotations", version = "23.0.0" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +kotlin-test-junit-spring7 = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinSpring7" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" } ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClient" } @@ -162,6 +164,8 @@ springboot4-starter-websocket = { module = "org.springframework.boot:spring-boot springboot4-starter-webflux = { module = "org.springframework.boot:spring-boot-starter-webflux", version.ref = "springboot4" } springboot4-starter-aop = { module = "org.springframework.boot:spring-boot-starter-aop", version.ref = "springboot4" } springboot4-starter-security = { module = "org.springframework.boot:spring-boot-starter-security", version.ref = "springboot4" } +springboot4-starter-restclient = { module = "org.springframework.boot:spring-boot-starter-restclient", version.ref = "springboot4" } +springboot4-starter-webclient = { module = "org.springframework.boot:spring-boot-starter-webclient", version.ref = "springboot4" } springboot4-starter-jdbc = { module = "org.springframework.boot:spring-boot-starter-jdbc", version.ref = "springboot4" } springboot4-starter-actuator = { module = "org.springframework.boot:spring-boot-starter-actuator", version.ref = "springboot4" } timber = { module = "com.jakewharton.timber:timber", version = "4.7.1" } @@ -177,12 +181,14 @@ androidx-test-orchestrator = { module = "androidx.test:orchestrator", version = androidx-test-rules = { module = "androidx.test:rules", version.ref = "androidxTestCore" } androidx-test-runner = { module = "androidx.test:runner", version = "1.6.2" } awaitility-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "4.1.1" } +awaitility-kotlin-spring7 = { module = "org.awaitility:awaitility-kotlin", version = "4.3.0" } awaitility3-kotlin = { module = "org.awaitility:awaitility-kotlin", version = "3.1.6" } hsqldb = { module = "org.hsqldb:hsqldb", version = "2.6.1" } javafaker = { module = "com.github.javafaker:javafaker", version = "1.0.2" } kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" } leakcanary-instrumentation = { module = "com.squareup.leakcanary:leakcanary-android-instrumentation", version = "2.14" } mockito-kotlin = { module = "org.mockito.kotlin:mockito-kotlin", version = "4.1.0" } +mockito-kotlin-spring7 = { module = "org.mockito.kotlin:mockito-kotlin", version = "6.0.0" } mockito-inline = { module = "org.mockito:mockito-inline", version = "4.8.0" } msgpack = { module = "org.msgpack:msgpack-core", version = "0.9.8" } okhttp-mockwebserver = { module = "com.squareup.okhttp3:mockwebserver", version.ref = "okhttp" } diff --git a/sentry-spring-7/api/sentry-spring-jakarta.api b/sentry-spring-7/api/sentry-spring-7.api similarity index 98% rename from sentry-spring-7/api/sentry-spring-jakarta.api rename to sentry-spring-7/api/sentry-spring-7.api index 3c1db200cbb..22e110446e8 100644 --- a/sentry-spring-7/api/sentry-spring-jakarta.api +++ b/sentry-spring-7/api/sentry-spring-7.api @@ -1,8 +1,3 @@ -public final class io/sentry/spring/jakarta/BuildConfig { - public static final field SENTRY_SPRING_JAKARTA_SDK_NAME Ljava/lang/String; - public static final field VERSION_NAME Ljava/lang/String; -} - public final class io/sentry/spring/jakarta/ContextTagsEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/SentryOptions;)V public fun getOrder ()Ljava/lang/Long; @@ -124,10 +119,10 @@ public class io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration public class io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration { public fun ()V - public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; + public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer; } -public final class io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer { +public final class io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer { public fun ()V public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V } @@ -361,3 +356,8 @@ public final class io/sentry/spring/jakarta/webflux/reactor/ReactorUtils : io/se public fun ()V } +public final class io/sentry/spring7/BuildConfig { + public static final field SENTRY_SPRING_7_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + diff --git a/sentry-spring-7/build.gradle.kts b/sentry-spring-7/build.gradle.kts index 483549330a7..c61e75a7ad5 100644 --- a/sentry-spring-7/build.gradle.kts +++ b/sentry-spring-7/build.gradle.kts @@ -58,11 +58,11 @@ dependencies { testImplementation(projects.sentryTestSupport) testImplementation(projects.sentryGraphql) testImplementation(kotlin(Config.kotlinStdLib)) - testImplementation(libs.awaitility.kotlin) + testImplementation(libs.awaitility.kotlin.spring7) testImplementation(libs.context.propagation) testImplementation(libs.graphql.java24) - testImplementation(libs.kotlin.test.junit) - testImplementation(libs.mockito.kotlin) + testImplementation(libs.kotlin.test.junit.spring7) + testImplementation(libs.mockito.kotlin.spring7) testImplementation(libs.mockito.inline) testImplementation(libs.springboot4.starter.aop) testImplementation(libs.springboot4.starter.graphql) diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java index cca74ae2a2d..f0c9d952176 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java @@ -7,6 +7,7 @@ import io.sentry.SentryOptions; import io.sentry.protocol.SdkVersion; import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; +import io.sentry.spring7.BuildConfig; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -50,7 +51,7 @@ private void registerSentryOptions( } builder.addPropertyValue("dsn", annotationAttributes.getString("dsn")); builder.addPropertyValue("enableExternalConfiguration", true); - builder.addPropertyValue("sentryClientName", BuildConfig.SENTRY_SPRING_JAKARTA_SDK_NAME); + builder.addPropertyValue("sentryClientName", BuildConfig.SENTRY_SPRING_7_SDK_NAME); builder.addPropertyValue("sdkVersion", createSdkVersion()); builder.addPropertyValue("initPriority", InitPriority.LOW); addPackageAndIntegrationInfo(); @@ -90,7 +91,7 @@ private void registerSentryExceptionResolver( final SentryOptions defaultOptions = new SentryOptions(); SdkVersion sdkVersion = defaultOptions.getSdkVersion(); - final String name = BuildConfig.SENTRY_SPRING_JAKARTA_SDK_NAME; + final String name = BuildConfig.SENTRY_SPRING_7_SDK_NAME; final String version = BuildConfig.VERSION_NAME; sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java index 34dd5f0c469..c3ffbf41bad 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java @@ -111,7 +111,7 @@ private void configureScope( final @NotNull IScopes scopes, final @NotNull HttpServletRequest request) { if (scopes.getOptions().isSendDefaultPii() && qualifiesForCaching(request, scopes.getOptions().getMaxRequestBodySize())) { - return new ContentCachingRequestWrapper(request); + return new ContentCachingRequestWrapper(request, 0); } return request; } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java index e4e9104eb05..0ef7f4a9a4c 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java @@ -2,7 +2,7 @@ import com.jakewharton.nopen.annotation.Open; import org.jetbrains.annotations.ApiStatus; -import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.boot.quartz.autoconfigure.SchedulerFactoryBeanCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.Ordered; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java index e37f5ac4674..756624bf1eb 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -2,7 +2,7 @@ import io.sentry.quartz.SentryJobListener; import org.jetbrains.annotations.ApiStatus; -import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.boot.quartz.autoconfigure.SchedulerFactoryBeanCustomizer; import org.springframework.scheduling.quartz.SchedulerFactoryBean; @ApiStatus.Experimental diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java index d58291ade6e..447b0a2b8fb 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java @@ -49,7 +49,7 @@ public SentryRequestResolver(final @NotNull IScopes scopes) { @NotNull Map resolveHeadersMap(final HttpHeaders request) { final Map headersMap = new HashMap<>(); - for (Map.Entry> entry : request.entrySet()) { + for (Map.Entry> entry : request.headerSet()) { // do not copy personal information identifiable headers String headerName = entry.getKey(); if (scopes.getOptions().isSendDefaultPii() diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt index 3fa5a38ac22..d1b289261d4 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt @@ -6,6 +6,7 @@ import io.sentry.ITransportFactory import io.sentry.Integration import io.sentry.Sentry import io.sentry.SentryOptions +import io.sentry.spring7.BuildConfig import io.sentry.transport.ITransport import kotlin.test.Test import org.assertj.core.api.Assertions.assertThat diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt index 6e2ea53fdfd..418b6fd0a39 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt @@ -43,8 +43,8 @@ import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.boot.web.server.test.LocalServerPort +import org.springframework.boot.web.server.test.client.TestRestTemplate import org.springframework.boot.web.servlet.FilterRegistrationBean import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -110,7 +110,7 @@ class SentrySpringIntegrationTest { fun `attaches request and user information to SentryEvents`() { val restTemplate = TestRestTemplate().withBasicAuth("user", "password") val headers = HttpHeaders() - headers["X-FORWARDED-FOR"] = listOf("169.128.0.1") + headers.put("X-FORWARDED-FOR", listOf("169.128.0.1")) val entity = HttpEntity(headers) restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) @@ -176,7 +176,7 @@ class SentrySpringIntegrationTest { fun `attaches first ip address if multiple addresses exist in a header`() { val restTemplate = TestRestTemplate().withBasicAuth("user", "password") val headers = HttpHeaders() - headers["X-FORWARDED-FOR"] = listOf("169.128.0.1, 192.168.0.1") + headers.put("X-FORWARDED-FOR", listOf("169.128.0.1, 192.168.0.1")) val entity = HttpEntity(headers) restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) @@ -530,7 +530,10 @@ open class SecurityConfiguration { @Bean @Throws(Exception::class) open fun filterChain(http: HttpSecurity): SecurityFilterChain { - http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic() + http + .csrf { it.disable() } + .authorizeHttpRequests { it.anyRequest().authenticated() } + .httpBasic {} return http.build() } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt index e3b3ed55a7c..bc07e5dbbf9 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt @@ -27,10 +27,10 @@ import org.mockito.kotlin.whenever import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.ApplicationRunner import org.springframework.boot.autoconfigure.SpringBootApplication -import org.springframework.boot.autoconfigure.security.reactive.ReactiveSecurityAutoConfiguration -import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration +import org.springframework.boot.security.autoconfigure.reactive.ReactiveSecurityAutoConfiguration +import org.springframework.boot.security.autoconfigure.servlet.SecurityAutoConfiguration import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.boot.web.server.test.LocalServerPort import org.springframework.context.annotation.Bean import org.springframework.http.ResponseEntity import org.springframework.test.context.junit4.SpringRunner diff --git a/sentry-spring-boot-4-starter/api/sentry-spring-boot-starter-jakarta.api b/sentry-spring-boot-4-starter/api/sentry-spring-boot-4-starter.api similarity index 100% rename from sentry-spring-boot-4-starter/api/sentry-spring-boot-starter-jakarta.api rename to sentry-spring-boot-4-starter/api/sentry-spring-boot-4-starter.api diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-jakarta.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api similarity index 97% rename from sentry-spring-boot-4/api/sentry-spring-boot-jakarta.api rename to sentry-spring-boot-4/api/sentry-spring-boot-4.api index b0ef970d7d3..d213413d836 100644 --- a/sentry-spring-boot-4/api/sentry-spring-boot-jakarta.api +++ b/sentry-spring-boot-4/api/sentry-spring-boot-4.api @@ -1,8 +1,3 @@ -public final class io/sentry/spring/boot/jakarta/BuildConfig { - public static final field SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME Ljava/lang/String; - public static final field VERSION_NAME Ljava/lang/String; -} - public class io/sentry/spring/boot/jakarta/InAppIncludesResolver : org/springframework/context/ApplicationContextAware { public fun ()V public fun resolveInAppIncludes ()Ljava/util/List; @@ -91,3 +86,8 @@ public class io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguratio public fun sentryInstrumentationWebflux (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; } +public final class io/sentry/spring/boot4/BuildConfig { + public static final field SENTRY_SPRING_BOOT_4_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts index fc1c8d031b1..16f9e46a40f 100644 --- a/sentry-spring-boot-4/build.gradle.kts +++ b/sentry-spring-boot-4/build.gradle.kts @@ -45,6 +45,8 @@ dependencies { compileOnly(libs.springboot4.starter.graphql) compileOnly(libs.springboot4.starter.quartz) compileOnly(libs.springboot4.starter.security) + compileOnly(libs.springboot4.starter.restclient) + compileOnly(libs.springboot4.starter.webclient) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryAgentcustomization) api(projects.sentryReactor) diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index fcaaa5f2645..e6861fb035a 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -16,6 +16,7 @@ import io.sentry.quartz.SentryJobListener; import io.sentry.spring.boot.jakarta.graphql.SentryGraphql22AutoConfiguration; import io.sentry.spring.boot.jakarta.graphql.SentryGraphqlAutoConfiguration; +import io.sentry.spring.boot4.BuildConfig; import io.sentry.spring.jakarta.ContextTagsEventProcessor; import io.sentry.spring.jakarta.SentryExceptionResolver; import io.sentry.spring.jakarta.SentryRequestResolver; @@ -59,12 +60,12 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication; -import org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration; -import org.springframework.boot.autoconfigure.web.client.RestTemplateAutoConfiguration; -import org.springframework.boot.autoconfigure.web.reactive.function.client.WebClientAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.info.GitProperties; +import org.springframework.boot.restclient.autoconfigure.RestClientAutoConfiguration; +import org.springframework.boot.restclient.autoconfigure.RestTemplateAutoConfiguration; import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.boot.webclient.autoconfigure.WebClientAutoConfiguration; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -167,7 +168,7 @@ static class OpenTelemetryNoAgentConfiguration {} spanFactory.ifAvailable(options::setSpanFactory); options.setSentryClientName( - BuildConfig.SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME + "/" + BuildConfig.VERSION_NAME); + BuildConfig.SENTRY_SPRING_BOOT_4_SDK_NAME + "/" + BuildConfig.VERSION_NAME); options.setSdkVersion(createSdkVersion(options)); options.setInitPriority(InitPriority.LOW); addPackageAndIntegrationInfo(); @@ -461,7 +462,7 @@ static class ApacheHttpClientTransportFactoryAutoconfiguration { final @NotNull SentryOptions sentryOptions) { SdkVersion sdkVersion = sentryOptions.getSdkVersion(); - final String name = BuildConfig.SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME; + final String name = BuildConfig.SENTRY_SPRING_BOOT_4_SDK_NAME; final String version = BuildConfig.VERSION_NAME; sdkVersion = SdkVersion.updateSdkVersion(sdkVersion, name, version); diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java index 304fb6911e9..cf4a20413c4 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java @@ -4,7 +4,7 @@ import io.sentry.IScopes; import io.sentry.spring.jakarta.tracing.SentrySpanClientHttpRequestInterceptor; import org.jetbrains.annotations.NotNull; -import org.springframework.boot.web.client.RestClientCustomizer; +import org.springframework.boot.restclient.RestClientCustomizer; import org.springframework.web.client.RestClient; @Open diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java index 4a0faa9875e..34b3f86be46 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java @@ -6,7 +6,7 @@ import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.NotNull; -import org.springframework.boot.web.client.RestTemplateCustomizer; +import org.springframework.boot.restclient.RestTemplateCustomizer; import org.springframework.http.client.ClientHttpRequestInterceptor; import org.springframework.web.client.RestTemplate; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java index d349ac4c6e0..ffb0dd50b75 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java @@ -4,7 +4,7 @@ import io.sentry.IScopes; import io.sentry.spring.jakarta.tracing.SentrySpanClientWebRequestFilter; import org.jetbrains.annotations.NotNull; -import org.springframework.boot.web.reactive.function.client.WebClientCustomizer; +import org.springframework.boot.webclient.WebClientCustomizer; import org.springframework.web.reactive.function.client.WebClient; @Open From 59cb440e254e040366c5f61f91f23b021ad2db6b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 11 Aug 2025 14:12:27 +0200 Subject: [PATCH 03/10] Attempt to fix kotlin 2.2 issue (#4603) * Fix Spring 7 and Spring Boot 4 modules * Attempt to fix kotlin 2.2 issue * Second attempt to fix Kotlin 2.2 (#4610) * Second attempt to fix Kotlin 2.2 * Format code * Third attempt at fixing Kotlin 2.2 compat (#4613) --------- Co-authored-by: Sentry Github Bot * Address compose-related issues * Fix tests for Spring 7 and Spring Boot 4 (#4614) * Second attempt to fix Kotlin 2.2 * Format code * Third attempt at fixing Kotlin 2.2 compat * Fix tests for Spring 7 and Spring Boot 4 * Move Spring 7 and Spring Boot 4 packages (#4615) * Move Spring 7 and Spring Boot 4 packages * Fix class not found due to OTel not supporting spring boot 4 yet (#4616) * Format code * some fixes * change kotlin 1.8 to 1.9 and some cleanup * ignore warnings about api level that is not relevant * fix optional dependencies in SentryAutoConfiguration * Update trace origin * Remove duplicate e2e test config * Update Strings for Spring 7 and Spring Boot 4 * Disable Spring Boot 4 agentless e2e tests for now --------- Co-authored-by: Sentry Github Bot --------- Co-authored-by: Sentry Github Bot Co-authored-by: markushi --- .github/workflows/system-tests-backend.yml | 9 +- build.gradle.kts | 5 +- buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/java/Config.kt | 1 + gradle/libs.versions.toml | 10 +- sentry-android-core/build.gradle.kts | 4 +- sentry-android-fragment/build.gradle.kts | 8 +- .../build.gradle.kts | 4 +- .../build.gradle.kts | 5 +- .../sentry-uitest-android/build.gradle.kts | 5 +- sentry-android-navigation/build.gradle.kts | 9 +- sentry-android-ndk/build.gradle.kts | 4 +- sentry-android-replay/build.gradle.kts | 10 +- .../java/io/sentry/android/replay/Windows.kt | 3 + .../android/replay/capture/CaptureStrategy.kt | 2 + sentry-android-sqlite/build.gradle.kts | 9 +- sentry-android-timber/build.gradle.kts | 9 +- sentry-android/build.gradle.kts | 2 +- sentry-apache-http-client-5/build.gradle.kts | 7 +- sentry-apollo-3/build.gradle.kts | 7 +- sentry-apollo-4/api/sentry-apollo-4.api | 1 + sentry-apollo-4/build.gradle.kts | 7 +- sentry-apollo/build.gradle.kts | 7 +- sentry-compose/build.gradle.kts | 26 ++- sentry-graphql-22/build.gradle.kts | 7 +- sentry-graphql-core/build.gradle.kts | 7 +- sentry-graphql/build.gradle.kts | 7 +- sentry-jdbc/build.gradle.kts | 4 +- sentry-jul/build.gradle.kts | 4 +- sentry-kotlin-extensions/build.gradle.kts | 7 +- sentry-ktor-client/build.gradle.kts | 6 +- sentry-log4j2/build.gradle.kts | 4 +- sentry-logback/build.gradle.kts | 4 +- sentry-okhttp/build.gradle.kts | 6 +- sentry-openfeign/build.gradle.kts | 4 +- .../build.gradle.kts | 4 +- .../build.gradle.kts | 4 +- .../build.gradle.kts | 4 +- sentry-quartz/build.gradle.kts | 7 +- sentry-reactor/build.gradle.kts | 7 +- .../sentry-samples-android/build.gradle.kts | 5 +- .../build.gradle.kts | 10 +- .../sentry-samples-console/build.gradle.kts | 10 +- .../build.gradle.kts | 4 +- .../build.gradle.kts | 8 +- .../sentry-samples-spring-7/build.gradle.kts | 13 +- .../jakarta => spring7}/AppConfig.java | 6 +- .../jakarta => spring7}/AppInitializer.java | 4 +- .../SecurityConfiguration.java | 9 +- .../jakarta => spring7}/SentryConfig.java | 6 +- .../jakarta => spring7}/WebConfig.java | 6 +- .../jakarta => spring7}/web/Person.java | 2 +- .../web/PersonController.java | 2 +- .../web/PersonService.java | 4 +- .../build.gradle.kts | 16 +- .../spring/boot4}/CustomEventProcessor.java | 2 +- .../{boot/jakarta => boot4}/CustomJob.java | 6 +- .../DistributedTracingController.java | 2 +- .../sentry/samples/spring/boot4}/Person.java | 2 +- .../jakarta => boot4}/PersonController.java | 2 +- .../samples/spring/boot4}/PersonService.java | 4 +- .../spring/boot4}/SecurityConfiguration.java | 9 +- .../SentryDemoApplication.java | 6 +- .../io/sentry/samples/spring/boot4}/Todo.java | 2 +- .../samples/spring/boot4}/TodoController.java | 2 +- .../boot4}/graphql/AssigneeController.java | 2 +- .../boot4}/graphql/GreetingController.java | 2 +- .../boot4}/graphql/ProjectController.java | 2 +- .../graphql/TaskCreatorController.java | 2 +- .../spring/boot4}/quartz/SampleJob.java | 2 +- .../build.gradle.kts | 19 ++- .../spring/boot4}/CustomEventProcessor.java | 2 +- .../{boot/jakarta => boot4}/CustomJob.java | 6 +- .../DistributedTracingController.java | 2 +- .../sentry/samples/spring/boot4}/Person.java | 2 +- .../jakarta => boot4}/PersonController.java | 2 +- .../samples/spring/boot4}/PersonService.java | 4 +- .../spring/boot4}/SecurityConfiguration.java | 9 +- .../SentryDemoApplication.java | 6 +- .../io/sentry/samples/spring/boot4}/Todo.java | 2 +- .../samples/spring/boot4}/TodoController.java | 2 +- .../graphql/AssigneeController.java | 2 +- .../boot4}/graphql/GreetingController.java | 2 +- .../graphql/ProjectController.java | 2 +- .../graphql/TaskCreatorController.java | 2 +- .../spring/boot4}/quartz/SampleJob.java | 2 +- .../build.gradle.kts | 14 +- .../DistributedTracingController.java | 2 +- .../sentry/samples/spring/boot4}/Person.java | 2 +- .../jakarta => boot4}/PersonController.java | 2 +- .../jakarta => boot4}/PersonService.java | 2 +- .../SentryDemoApplication.java | 2 +- .../spring/{boot/jakarta => boot4}/Todo.java | 2 +- .../jakarta => boot4}/TodoController.java | 2 +- .../graphql/GreetingController.java | 2 +- .../build.gradle.kts | 19 ++- .../samples/spring/boot/jakarta/Person.java | 24 --- .../spring/boot4}/CustomEventProcessor.java | 2 +- .../{boot/jakarta => boot4}/CustomJob.java | 6 +- .../DistributedTracingController.java | 2 +- .../sentry/samples/spring/boot4/Person.java | 24 +++ .../jakarta => boot4}/PersonController.java | 2 +- .../jakarta => boot4}/PersonService.java | 4 +- .../SecurityConfiguration.java | 9 +- .../SentryDemoApplication.java | 6 +- .../io/sentry/samples/spring/boot4}/Todo.java | 2 +- .../jakarta => boot4}/TodoController.java | 2 +- .../boot4}/graphql/AssigneeController.java | 2 +- .../boot4}/graphql/GreetingController.java | 2 +- .../boot4}/graphql/ProjectController.java | 2 +- .../graphql/TaskCreatorController.java | 2 +- .../spring/boot4}/quartz/SampleJob.java | 2 +- .../build.gradle.kts | 10 +- .../build.gradle.kts | 10 +- .../build.gradle.kts | 10 +- .../build.gradle.kts | 10 +- .../build.gradle.kts | 10 +- .../build.gradle.kts | 8 +- .../build.gradle.kts | 8 +- .../build.gradle.kts | 10 +- .../build.gradle.kts | 8 +- .../sentry-samples-spring/build.gradle.kts | 8 +- sentry-servlet-jakarta/build.gradle.kts | 7 +- ...yRequestHttpServletRequestProcessorTest.kt | 2 +- sentry-servlet/build.gradle.kts | 7 +- sentry-spring-7/api/sentry-spring-7.api | 154 +++++++++--------- sentry-spring-7/build.gradle.kts | 23 ++- .../ContextTagsEventProcessor.java | 2 +- .../jakarta => spring7}/EnableSentry.java | 2 +- .../HttpServletRequestSentryUserProvider.java | 2 +- .../RequestPayloadExtractor.java | 2 +- .../SentryExceptionResolver.java | 6 +- .../SentryHubRegistrar.java | 9 +- .../SentryInitBeanPostProcessor.java | 2 +- ...tryRequestHttpServletRequestProcessor.java | 4 +- .../SentryRequestResolver.java | 2 +- .../SentrySpringFilter.java | 6 +- ...ntrySpringServletContainerInitializer.java | 2 +- .../SentryTaskDecorator.java | 2 +- .../jakarta => spring7}/SentryUserFilter.java | 2 +- .../SentryUserProvider.java | 2 +- .../SentryWebConfiguration.java | 2 +- .../SpringProfilesEventProcessor.java | 2 +- .../SpringSecuritySentryUserProvider.java | 2 +- .../checkin/SentryCheckIn.java | 2 +- .../checkin/SentryCheckInAdvice.java | 2 +- .../SentryCheckInAdviceConfiguration.java | 2 +- .../SentryCheckInPointcutConfiguration.java | 2 +- .../checkin/SentryQuartzConfiguration.java | 2 +- .../SentrySchedulerFactoryBeanCustomizer.java | 2 +- .../SentryCaptureExceptionParameter.java | 2 +- ...SentryCaptureExceptionParameterAdvice.java | 4 +- ...aptureExceptionParameterConfiguration.java | 2 +- ...ceptionParameterPointcutConfiguration.java | 2 +- ...ExceptionParameterAdviceConfiguration.java | 2 +- .../graphql/SentryBatchLoaderRegistry.java | 2 +- ...ryDataFetcherExceptionResolverAdapter.java | 2 +- .../graphql/SentryDgsSubscriptionHandler.java | 4 +- .../graphql/SentryGraphql22Configuration.java | 6 +- .../SentryGraphqlBeanPostProcessor.java | 2 +- .../graphql/SentryGraphqlConfiguration.java | 6 +- .../SentrySpringSubscriptionHandler.java | 2 +- ...etryAgentWithoutAutoInitConfiguration.java | 4 +- ...ntryOpenTelemetryNoAgentConfiguration.java | 4 +- .../CombinedTransactionNameProvider.java | 2 +- .../tracing/SentryAdviceConfiguration.java | 2 +- .../tracing/SentrySpan.java | 2 +- .../tracing/SentrySpanAdvice.java | 4 +- ...entrySpanClientHttpRequestInterceptor.java | 6 +- .../SentrySpanClientWebRequestFilter.java | 4 +- .../SentrySpanPointcutConfiguration.java | 2 +- .../tracing/SentryTracingConfiguration.java | 2 +- .../tracing/SentryTracingFilter.java | 4 +- .../tracing/SentryTransaction.java | 2 +- .../tracing/SentryTransactionAdvice.java | 4 +- ...entryTransactionPointcutConfiguration.java | 2 +- .../SpringMvcTransactionNameProvider.java | 2 +- .../SpringServletTransactionNameProvider.java | 2 +- .../tracing/TransactionNameProvider.java | 2 +- .../tracing/TransactionNameWithSource.java | 2 +- .../webflux/AbstractSentryWebFilter.java | 2 +- .../webflux/SentryRequestResolver.java | 2 +- .../webflux/SentryScheduleHook.java | 2 +- .../webflux/SentryWebExceptionHandler.java | 4 +- .../webflux/SentryWebFilter.java | 4 +- ...entryWebFilterWithThreadLocalAccessor.java | 4 +- .../webflux/TransactionNameProvider.java | 2 +- .../webflux/reactor/ReactorUtils.java | 2 +- ...akarta.servlet.ServletContainerInitializer | 2 +- .../ContextTagsEventProcessorTest.kt | 2 +- .../jakarta => spring7}/EnableSentryTest.kt | 11 +- ...ttpServletRequestSentryUserProviderTest.kt | 2 +- .../SentryCheckInAdviceTest.kt | 8 +- .../SentryExceptionResolverTest.kt | 4 +- .../SentryInitBeanPostProcessorTest.kt | 2 +- ...yRequestHttpServletRequestProcessorTest.kt | 4 +- .../SentrySpringFilterTest.kt | 2 +- .../SentryTaskDecoratorTest.kt | 2 +- .../SentryUserFilterTest.kt | 2 +- .../SpringProfilesEventProcessorTest.kt | 2 +- .../SpringSecuritySentryUserProviderTest.kt | 2 +- ...ntryCaptureExceptionParameterAdviceTest.kt | 4 +- .../SentrySpringSubscriptionHandlerTest.kt | 2 +- .../mvc/SentrySpringIntegrationTest.kt | 28 ++-- .../tracing/SentrySpanAdviceTest.kt | 4 +- .../tracing/SentryTracingFilterTest.kt | 10 +- .../tracing/SentryTransactionAdviceTest.kt | 4 +- .../webflux/SentryScheduleHookTest.kt | 2 +- .../webflux/SentryWebFluxTracingFilterTest.kt | 8 +- .../webflux/SentryWebfluxIntegrationTest.kt | 2 +- sentry-spring-boot-4-starter/build.gradle.kts | 7 +- .../api/sentry-spring-boot-4.api | 66 ++++---- sentry-spring-boot-4/build.gradle.kts | 30 +++- .../InAppIncludesResolver.java | 2 +- .../SentryAutoConfiguration.java | 107 ++++++------ ...entryLogbackAppenderAutoConfiguration.java | 2 +- .../SentryLogbackInitializer.java | 2 +- .../jakarta => boot4}/SentryProperties.java | 6 +- .../SentrySpanRestClientCustomizer.java | 4 +- .../SentrySpanRestTemplateCustomizer.java | 4 +- .../SentrySpanWebClientCustomizer.java | 4 +- .../SentrySpringVersionChecker.java | 4 +- .../SentryWebfluxAutoConfiguration.java | 10 +- .../SentryGraphql22AutoConfiguration.java | 14 +- .../SentryGraphqlAutoConfiguration.java | 14 +- .../main/resources/META-INF/spring.factories | 2 +- ...ot.autoconfigure.AutoConfiguration.imports | 6 +- .../SentryAutoConfigurationTest.kt | 56 +++---- ...tryLogbackAppenderAutoConfigurationTest.kt | 2 +- .../SentrySpanRestClientCustomizerTest.kt | 4 +- .../SentrySpanRestTemplateCustomizerTest.kt | 10 +- .../SentrySpanWebClientCustomizerTest.kt | 4 +- .../SentryWebfluxAutoConfigurationTest.kt | 10 +- .../it/SentrySpringIntegrationTest.kt | 21 ++- sentry-spring-boot-jakarta/build.gradle.kts | 7 +- .../build.gradle.kts | 7 +- sentry-spring-boot-starter/build.gradle.kts | 7 +- sentry-spring-boot/build.gradle.kts | 7 +- sentry-spring-jakarta/build.gradle.kts | 11 +- sentry-spring/build.gradle.kts | 7 +- sentry-system-test-support/build.gradle.kts | 6 +- sentry-test-support/build.gradle.kts | 6 +- sentry/build.gradle.kts | 4 +- 243 files changed, 878 insertions(+), 744 deletions(-) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/AppConfig.java (76%) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/AppInitializer.java (93%) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/SecurityConfiguration.java (88%) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/SentryConfig.java (87%) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/WebConfig.java (86%) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/web/Person.java (91%) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/web/PersonController.java (96%) rename sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/{spring/jakarta => spring7}/web/PersonService.java (90%) rename sentry-samples/{sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/CustomEventProcessor.java (95%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/CustomJob.java (80%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/DistributedTracingController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/Person.java (90%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/PersonController.java (98%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/PersonService.java (90%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/SecurityConfiguration.java (87%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/SentryDemoApplication.java (93%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/Todo.java (89%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/TodoController.java (98%) rename sentry-samples/{sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/graphql/AssigneeController.java (95%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/graphql/GreetingController.java (89%) rename sentry-samples/{sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/graphql/ProjectController.java (98%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/graphql/TaskCreatorController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4}/quartz/SampleJob.java (89%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/CustomEventProcessor.java (95%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/CustomJob.java (79%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/DistributedTracingController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/Person.java (90%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/PersonController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/PersonService.java (90%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/SecurityConfiguration.java (87%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/SentryDemoApplication.java (93%) rename sentry-samples/{sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/Todo.java (89%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/TodoController.java (98%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/graphql/AssigneeController.java (95%) rename sentry-samples/{sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/graphql/GreetingController.java (89%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/graphql/ProjectController.java (98%) rename sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/graphql/TaskCreatorController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4}/quartz/SampleJob.java (89%) rename sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/DistributedTracingController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4}/Person.java (90%) rename sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/PersonController.java (96%) rename sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/PersonService.java (91%) rename sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/SentryDemoApplication.java (91%) rename sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/Todo.java (89%) rename sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/TodoController.java (94%) rename sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/graphql/GreetingController.java (91%) delete mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4}/CustomEventProcessor.java (95%) rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/CustomJob.java (79%) rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/DistributedTracingController.java (97%) create mode 100644 sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/Person.java rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/PersonController.java (97%) rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/PersonService.java (90%) rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/SecurityConfiguration.java (87%) rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/SentryDemoApplication.java (93%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4}/Todo.java (89%) rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/TodoController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4}/graphql/AssigneeController.java (95%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4}/graphql/GreetingController.java (89%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4}/graphql/ProjectController.java (98%) rename sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/{boot/jakarta => boot4}/graphql/TaskCreatorController.java (97%) rename sentry-samples/{sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta => sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4}/quartz/SampleJob.java (89%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/ContextTagsEventProcessor.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/EnableSentry.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/HttpServletRequestSentryUserProvider.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/RequestPayloadExtractor.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryExceptionResolver.java (95%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryHubRegistrar.java (94%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryInitBeanPostProcessor.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryRequestHttpServletRequestProcessor.java (93%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryRequestResolver.java (99%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentrySpringFilter.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentrySpringServletContainerInitializer.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryTaskDecorator.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryUserFilter.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryUserProvider.java (90%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SentryWebConfiguration.java (95%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SpringProfilesEventProcessor.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/SpringSecuritySentryUserProvider.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/checkin/SentryCheckIn.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/checkin/SentryCheckInAdvice.java (99%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/checkin/SentryCheckInAdviceConfiguration.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/checkin/SentryCheckInPointcutConfiguration.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/checkin/SentryQuartzConfiguration.java (94%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/checkin/SentrySchedulerFactoryBeanCustomizer.java (92%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/exception/SentryCaptureExceptionParameter.java (91%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/exception/SentryCaptureExceptionParameterAdvice.java (95%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/exception/SentryCaptureExceptionParameterConfiguration.java (92%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/exception/SentryCaptureExceptionParameterPointcutConfiguration.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/exception/SentryExceptionParameterAdviceConfiguration.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/graphql/SentryBatchLoaderRegistry.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/graphql/SentryDataFetcherExceptionResolverAdapter.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/graphql/SentryDgsSubscriptionHandler.java (94%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/graphql/SentryGraphql22Configuration.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/graphql/SentryGraphqlBeanPostProcessor.java (94%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/graphql/SentryGraphqlConfiguration.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/graphql/SentrySpringSubscriptionHandler.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java (89%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java (92%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/CombinedTransactionNameProvider.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentryAdviceConfiguration.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentrySpan.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentrySpanAdvice.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentrySpanClientHttpRequestInterceptor.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentrySpanClientWebRequestFilter.java (97%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentrySpanPointcutConfiguration.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentryTracingConfiguration.java (92%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentryTracingFilter.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentryTransaction.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentryTransactionAdvice.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SentryTransactionPointcutConfiguration.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SpringMvcTransactionNameProvider.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/SpringServletTransactionNameProvider.java (94%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/TransactionNameProvider.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/tracing/TransactionNameWithSource.java (95%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/AbstractSentryWebFilter.java (99%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/SentryRequestResolver.java (98%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/SentryScheduleHook.java (95%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/SentryWebExceptionHandler.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/SentryWebFilter.java (92%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/SentryWebFilterWithThreadLocalAccessor.java (94%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/TransactionNameProvider.java (96%) rename sentry-spring-7/src/main/java/io/sentry/{spring/jakarta => spring7}/webflux/reactor/ReactorUtils.java (79%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/ContextTagsEventProcessorTest.kt (98%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/EnableSentryTest.kt (97%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/HttpServletRequestSentryUserProviderTest.kt (98%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SentryCheckInAdviceTest.kt (97%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SentryExceptionResolverTest.kt (97%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SentryInitBeanPostProcessorTest.kt (95%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SentryRequestHttpServletRequestProcessorTest.kt (95%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SentrySpringFilterTest.kt (99%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SentryTaskDecoratorTest.kt (97%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SentryUserFilterTest.kt (99%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SpringProfilesEventProcessorTest.kt (98%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/SpringSecuritySentryUserProviderTest.kt (98%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/exception/SentryCaptureExceptionParameterAdviceTest.kt (95%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/graphql/SentrySpringSubscriptionHandlerTest.kt (98%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/mvc/SentrySpringIntegrationTest.kt (95%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/tracing/SentrySpanAdviceTest.kt (97%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/tracing/SentryTracingFilterTest.kt (98%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/tracing/SentryTransactionAdviceTest.kt (99%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/webflux/SentryScheduleHookTest.kt (97%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/webflux/SentryWebFluxTracingFilterTest.kt (97%) rename sentry-spring-7/src/test/kotlin/io/sentry/{spring/jakarta => spring7}/webflux/SentryWebfluxIntegrationTest.kt (99%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/InAppIncludesResolver.java (97%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentryAutoConfiguration.java (85%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentryLogbackAppenderAutoConfiguration.java (96%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentryLogbackInitializer.java (98%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentryProperties.java (97%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentrySpanRestClientCustomizer.java (89%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentrySpanRestTemplateCustomizer.java (90%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentrySpanWebClientCustomizer.java (85%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentrySpringVersionChecker.java (93%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/SentryWebfluxAutoConfiguration.java (93%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/graphql/SentryGraphql22AutoConfiguration.java (88%) rename sentry-spring-boot-4/src/main/java/io/sentry/spring/{boot/jakarta => boot4}/graphql/SentryGraphqlAutoConfiguration.java (88%) rename sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/{boot/jakarta => boot4}/SentryAutoConfigurationTest.kt (96%) rename sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/{boot/jakarta => boot4}/SentryLogbackAppenderAutoConfigurationTest.kt (99%) rename sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/{boot/jakarta => boot4}/SentrySpanRestClientCustomizerTest.kt (99%) rename sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/{boot/jakarta => boot4}/SentrySpanRestTemplateCustomizerTest.kt (98%) rename sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/{boot/jakarta => boot4}/SentrySpanWebClientCustomizerTest.kt (99%) rename sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/{boot/jakarta => boot4}/SentryWebfluxAutoConfigurationTest.kt (90%) rename sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/{boot/jakarta => boot4}/it/SentrySpringIntegrationTest.kt (94%) diff --git a/.github/workflows/system-tests-backend.yml b/.github/workflows/system-tests-backend.yml index 5df5f3ec1d8..68ccf467018 100644 --- a/.github/workflows/system-tests-backend.yml +++ b/.github/workflows/system-tests-backend.yml @@ -54,15 +54,12 @@ jobs: - sample: "sentry-samples-console" agent: "false" agent-auto-init: "true" - - sample: "sentry-samples-spring-boot-jakarta" - agent: "false" - agent-auto-init: "true" - sample: "sentry-samples-spring-boot-4-webflux" agent: "false" agent-auto-init: "true" - - sample: "sentry-samples-spring-boot-4-opentelemetry-noagent" - agent: "false" - agent-auto-init: "true" +# - sample: "sentry-samples-spring-boot-4-opentelemetry-noagent" +# agent: "false" +# agent-auto-init: "true" - sample: "sentry-samples-spring-boot-4-opentelemetry" agent: "true" agent-auto-init: "true" diff --git a/build.gradle.kts b/build.gradle.kts index 820624fa1d4..17b57291eb2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -18,11 +18,14 @@ plugins { alias(libs.plugins.kover) apply false alias(libs.plugins.vanniktech.maven.publish) apply false alias(libs.plugins.kotlin.android) apply false + alias(libs.plugins.kotlin.multiplatform) apply false + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.kotlin.spring) apply false alias(libs.plugins.buildconfig) apply false // dokka is required by gradle-maven-publish-plugin. alias(libs.plugins.dokka) apply false alias(libs.plugins.dokka.javadoc) apply false - alias(libs.plugins.compose.compiler) apply false + alias(libs.plugins.kotlin.compose) apply false alias(libs.plugins.errorprone) apply false alias(libs.plugins.gradle.versions) apply false alias(libs.plugins.spring.dependency.management) apply false diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 5d8cbb335fc..451e5827ed9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -9,5 +9,5 @@ repositories { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index a892af8e920..b620979232f 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -4,6 +4,7 @@ import java.math.BigDecimal object Config { val AGP = System.getenv("VERSION_AGP") ?: "8.6.0" val kotlinStdLib = "stdlib-jdk8" + val kotlinTestJunit = "test-junit" object BuildPlugins { val androidGradle = "com.android.tools.build:gradle:$AGP" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 80fe6ae520d..94fab957381 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,9 +11,9 @@ feign = "11.6" jacoco = "0.8.7" jackson = "2.18.3" jetbrainsCompose = "1.6.11" -kotlin = "1.9.24" +kotlin = "2.2.0" kotlinSpring7 = "2.2.0" -kotlin-compatible-version = "1.6" +kotlin-compatible-version = "1.9" ktorClient = "3.0.0" logback = "1.2.9" log4j2 = "2.20.0" @@ -41,11 +41,15 @@ spotless = "7.0.4" [plugins] kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-spring = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlin" } +kotlin-spring7 = { id = "org.jetbrains.kotlin.plugin.spring", version.ref = "kotlinSpring7" } +kotlin-jvm-spring7 = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlinSpring7" } +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" } buildconfig = { id = "com.github.gmazzo.buildconfig", version = "5.6.5" } dokka = { id = "org.jetbrains.dokka", version = "2.0.0" } dokka-javadoc = { id = "org.jetbrains.dokka-javadoc", version = "2.0.0" } binary-compatibility-validator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.13.0" } -compose-compiler = { id = "org.jetbrains.compose", version.ref = "jetbrainsCompose" } errorprone = { id = "net.ltgt.errorprone", version = "3.0.1" } gradle-versions = { id = "com.github.ben-manes.versions", version = "0.42.0" } spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } diff --git a/sentry-android-core/build.gradle.kts b/sentry-android-core/build.gradle.kts index 01e89ff27c6..802a8bfb118 100644 --- a/sentry-android-core/build.gradle.kts +++ b/sentry-android-core/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.config.KotlinCompilerVersion plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.errorprone) @@ -34,7 +34,7 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } testOptions { animationsDisabled = true diff --git a/sentry-android-fragment/build.gradle.kts b/sentry-android-fragment/build.gradle.kts index 455f09af5d8..7a4178b0652 100644 --- a/sentry-android-fragment/build.gradle.kts +++ b/sentry-android-fragment/build.gradle.kts @@ -2,7 +2,7 @@ import io.gitlab.arturbosch.detekt.Detekt plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) @@ -25,7 +25,11 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + } testOptions { animationsDisabled = true diff --git a/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts index f4ac7d99068..e6480d8b37d 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-benchmark/build.gradle.kts @@ -3,7 +3,7 @@ import net.ltgt.gradle.errorprone.errorprone plugins { id("com.android.application") - kotlin("android") + alias(libs.plugins.kotlin.android) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) alias(libs.plugins.detekt) @@ -76,7 +76,7 @@ android { } } - kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } lint { warningsAsErrors = true diff --git a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts index 9af3fdaf8f3..4b0cd68ca90 100644 --- a/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android-critical/build.gradle.kts @@ -2,7 +2,8 @@ import io.gitlab.arturbosch.detekt.Detekt plugins { id("com.android.application") - kotlin("android") + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) } android { @@ -30,7 +31,7 @@ android { proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } - kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = libs.versions.composeCompiler.get() } androidComponents.beforeVariants { diff --git a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts index ed57263f6f8..0c32cbad941 100644 --- a/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts +++ b/sentry-android-integration-tests/sentry-uitest-android/build.gradle.kts @@ -3,7 +3,8 @@ import net.ltgt.gradle.errorprone.errorprone plugins { id("com.android.application") - kotlin("android") + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) alias(libs.plugins.detekt) @@ -68,7 +69,7 @@ android { } } - kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } lint { warningsAsErrors = true diff --git a/sentry-android-navigation/build.gradle.kts b/sentry-android-navigation/build.gradle.kts index 7fd5683adbf..7f5d1017ec3 100644 --- a/sentry-android-navigation/build.gradle.kts +++ b/sentry-android-navigation/build.gradle.kts @@ -2,7 +2,7 @@ import io.gitlab.arturbosch.detekt.Detekt plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) @@ -25,9 +25,10 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } testOptions { diff --git a/sentry-android-ndk/build.gradle.kts b/sentry-android-ndk/build.gradle.kts index 8b01c7beaef..413fd3a7b77 100644 --- a/sentry-android-ndk/build.gradle.kts +++ b/sentry-android-ndk/build.gradle.kts @@ -2,7 +2,7 @@ import org.jetbrains.kotlin.config.KotlinCompilerVersion plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) @@ -28,7 +28,7 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } testOptions { animationsDisabled = true diff --git a/sentry-android-replay/build.gradle.kts b/sentry-android-replay/build.gradle.kts index 9cb46cc007b..30afc911910 100644 --- a/sentry-android-replay/build.gradle.kts +++ b/sentry-android-replay/build.gradle.kts @@ -4,7 +4,8 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) @@ -37,9 +38,10 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } testOptions { diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt index 183a8c8d81f..9b9d8f0157b 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/Windows.kt @@ -124,6 +124,7 @@ internal class RootViewsSpy private constructor() : Closeable { private val delegatingViewList: ArrayList = object : ArrayList() { + @Suppress("NewApi") override fun addAll(elements: Collection): Boolean { listeners.forEach { listener -> elements.forEach { element -> listener.onRootViewsChanged(element, true) } @@ -131,11 +132,13 @@ internal class RootViewsSpy private constructor() : Closeable { return super.addAll(elements) } + @Suppress("NewApi") override fun add(element: View): Boolean { listeners.forEach { it.onRootViewsChanged(element, true) } return super.add(element) } + @Suppress("NewApi") override fun removeAt(index: Int): View { val removedView = super.removeAt(index) listeners.forEach { it.onRootViewsChanged(removedView, false) } diff --git a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt index 2ae12c03c3a..8e078161c15 100644 --- a/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt +++ b/sentry-android-replay/src/main/java/io/sentry/android/replay/capture/CaptureStrategy.kt @@ -125,6 +125,7 @@ internal interface CaptureStrategy { ) } + @Suppress("NewApi") private fun buildReplay( options: SentryOptions, video: File, @@ -256,6 +257,7 @@ internal interface CaptureStrategy { scopes?.captureReplay(replay, hint.apply { replayRecording = recording }) } + @Suppress("NewApi") fun setSegmentId(segmentId: Int) { replay.segmentId = segmentId recording.payload?.forEach { diff --git a/sentry-android-sqlite/build.gradle.kts b/sentry-android-sqlite/build.gradle.kts index 2c0908bd4fa..58e19ee54f8 100644 --- a/sentry-android-sqlite/build.gradle.kts +++ b/sentry-android-sqlite/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.config.KotlinCompilerVersion plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) @@ -26,9 +26,10 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } testOptions { diff --git a/sentry-android-timber/build.gradle.kts b/sentry-android-timber/build.gradle.kts index d9b3b897637..11be26e8936 100644 --- a/sentry-android-timber/build.gradle.kts +++ b/sentry-android-timber/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.config.KotlinCompilerVersion plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) jacoco alias(libs.plugins.jacoco.android) alias(libs.plugins.gradle.versions) @@ -33,9 +33,10 @@ android { getByName("release") { consumerProguardFiles("proguard-rules.pro") } } - kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } testOptions { diff --git a/sentry-android/build.gradle.kts b/sentry-android/build.gradle.kts index 378358c3665..a2d92829b29 100644 --- a/sentry-android/build.gradle.kts +++ b/sentry-android/build.gradle.kts @@ -1,6 +1,6 @@ plugins { id("com.android.library") - kotlin("android") + alias(libs.plugins.kotlin.android) alias(libs.plugins.gradle.versions) } diff --git a/sentry-apache-http-client-5/build.gradle.kts b/sentry-apache-http-client-5/build.gradle.kts index bb71bac9a2e..4c9aba6e31b 100644 --- a/sentry-apache-http-client-5/build.gradle.kts +++ b/sentry-apache-http-client-5/build.gradle.kts @@ -4,15 +4,16 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-apollo-3/build.gradle.kts b/sentry-apollo-3/build.gradle.kts index 299f275bf59..d9971397a2f 100644 --- a/sentry-apollo-3/build.gradle.kts +++ b/sentry-apollo-3/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-apollo-4/api/sentry-apollo-4.api b/sentry-apollo-4/api/sentry-apollo-4.api index ec7f6ff0512..22964621304 100644 --- a/sentry-apollo-4/api/sentry-apollo-4.api +++ b/sentry-apollo-4/api/sentry-apollo-4.api @@ -20,6 +20,7 @@ public final class io/sentry/apollo4/SentryApollo4HttpInterceptor : com/apollogr public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;Z)V public fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;)V public synthetic fun (Lio/sentry/IScopes;Lio/sentry/apollo4/SentryApollo4HttpInterceptor$BeforeSpanCallback;ZLjava/util/List;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun dispose ()V public fun intercept (Lcom/apollographql/apollo/api/http/HttpRequest;Lcom/apollographql/apollo/network/http/HttpInterceptorChain;Lkotlin/coroutines/Continuation;)Ljava/lang/Object; } diff --git a/sentry-apollo-4/build.gradle.kts b/sentry-apollo-4/build.gradle.kts index f07fc61885a..931a646eb52 100644 --- a/sentry-apollo-4/build.gradle.kts +++ b/sentry-apollo-4/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -17,8 +17,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-apollo/build.gradle.kts b/sentry-apollo/build.gradle.kts index 8fd32f26580..ce6ceb08bb6 100644 --- a/sentry-apollo/build.gradle.kts +++ b/sentry-apollo/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-compose/build.gradle.kts b/sentry-compose/build.gradle.kts index 9624bd018b9..533037a5253 100644 --- a/sentry-compose/build.gradle.kts +++ b/sentry-compose/build.gradle.kts @@ -1,10 +1,12 @@ import io.gitlab.arturbosch.detekt.Detekt import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion plugins { - kotlin("multiplatform") + alias(libs.plugins.kotlin.multiplatform) + alias(libs.plugins.kotlin.compose) id("com.android.library") - id("org.jetbrains.compose") alias(libs.plugins.kover) alias(libs.plugins.gradle.versions) alias(libs.plugins.detekt) @@ -17,13 +19,23 @@ kotlin { explicitApi() androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + apiVersion.set(KotlinVersion.KOTLIN_1_9) + languageVersion.set(KotlinVersion.KOTLIN_1_9) + } publishLibraryVariants("release") - compilations.all { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } } jvm("desktop") { - compilations.all { kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() } + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + apiVersion.set(KotlinVersion.KOTLIN_1_9) + languageVersion.set(KotlinVersion.KOTLIN_1_9) + } } + coreLibrariesVersion = "1.8" + sourceSets.all { // Allow all experimental APIs, since MPP projects are themselves experimental languageSettings.apply { @@ -34,9 +46,9 @@ kotlin { sourceSets { val commonMain by getting { - dependencies { - compileOnly(compose.runtime) - compileOnly(compose.ui) + compilerOptions { + apiVersion.set(KotlinVersion.KOTLIN_1_9) + languageVersion.set(KotlinVersion.KOTLIN_1_9) } } val androidMain by getting { diff --git a/sentry-graphql-22/build.gradle.kts b/sentry-graphql-22/build.gradle.kts index e11b38ac916..a8256ca8a27 100644 --- a/sentry-graphql-22/build.gradle.kts +++ b/sentry-graphql-22/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-graphql-core/build.gradle.kts b/sentry-graphql-core/build.gradle.kts index dbda965fc4f..cb8c9f49493 100644 --- a/sentry-graphql-core/build.gradle.kts +++ b/sentry-graphql-core/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-graphql/build.gradle.kts b/sentry-graphql/build.gradle.kts index 08d29e38a7f..46bef6e4b9d 100644 --- a/sentry-graphql/build.gradle.kts +++ b/sentry-graphql/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-jdbc/build.gradle.kts b/sentry-jdbc/build.gradle.kts index 6ffcbefbdf3..0415fd8ccff 100644 --- a/sentry-jdbc/build.gradle.kts +++ b/sentry-jdbc/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,7 +12,7 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-jul/build.gradle.kts b/sentry-jul/build.gradle.kts index 2035c6f4db8..13bee6418d6 100644 --- a/sentry-jul/build.gradle.kts +++ b/sentry-jul/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,7 +12,7 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-kotlin-extensions/build.gradle.kts b/sentry-kotlin-extensions/build.gradle.kts index ebdf8320444..e5e6c89a9d0 100644 --- a/sentry-kotlin-extensions/build.gradle.kts +++ b/sentry-kotlin-extensions/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-ktor-client/build.gradle.kts b/sentry-ktor-client/build.gradle.kts index 4369812e806..1c989d012fe 100644 --- a/sentry-ktor-client/build.gradle.kts +++ b/sentry-ktor-client/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco id("io.sentry.javadoc") alias(libs.plugins.errorprone) @@ -13,7 +13,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } kotlin { explicitApi() } diff --git a/sentry-log4j2/build.gradle.kts b/sentry-log4j2/build.gradle.kts index 10fbc165d41..68ebd90b1e8 100644 --- a/sentry-log4j2/build.gradle.kts +++ b/sentry-log4j2/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,7 +12,7 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-logback/build.gradle.kts b/sentry-logback/build.gradle.kts index 55915b8d314..385209e8c49 100644 --- a/sentry-logback/build.gradle.kts +++ b/sentry-logback/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,7 +12,7 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-okhttp/build.gradle.kts b/sentry-okhttp/build.gradle.kts index 3f0f596c3aa..783b578e3c1 100644 --- a/sentry-okhttp/build.gradle.kts +++ b/sentry-okhttp/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco id("io.sentry.javadoc") alias(libs.plugins.errorprone) @@ -13,7 +13,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } kotlin { explicitApi() } diff --git a/sentry-openfeign/build.gradle.kts b/sentry-openfeign/build.gradle.kts index a2f9646fe5a..40119987f72 100644 --- a/sentry-openfeign/build.gradle.kts +++ b/sentry-openfeign/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,7 +12,7 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts index 85746e4064e..b4a84300efd 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-agentcustomization/build.gradle.kts @@ -4,14 +4,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts index 825bda09170..64db4096bb9 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-bootstrap/build.gradle.kts @@ -4,14 +4,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts index cafdf06563d..2ab3d4988d5 100644 --- a/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts +++ b/sentry-opentelemetry/sentry-opentelemetry-core/build.gradle.kts @@ -4,14 +4,14 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-quartz/build.gradle.kts b/sentry-quartz/build.gradle.kts index 07e3e25626a..f81254f110f 100644 --- a/sentry-quartz/build.gradle.kts +++ b/sentry-quartz/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-reactor/build.gradle.kts b/sentry-reactor/build.gradle.kts index 22d500b0cc5..9e8b6e74be9 100644 --- a/sentry-reactor/build.gradle.kts +++ b/sentry-reactor/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -17,8 +17,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-samples/sentry-samples-android/build.gradle.kts b/sentry-samples/sentry-samples-android/build.gradle.kts index 48f8c3d833e..56f270d235b 100644 --- a/sentry-samples/sentry-samples-android/build.gradle.kts +++ b/sentry-samples/sentry-samples-android/build.gradle.kts @@ -5,7 +5,8 @@ import org.gradle.internal.extensions.stdlib.capitalized plugins { id("com.android.application") - kotlin("android") + alias(libs.plugins.kotlin.android) + alias(libs.plugins.kotlin.compose) } android { @@ -89,7 +90,7 @@ android { } } - kotlinOptions { jvmTarget = JavaVersion.VERSION_1_8.toString() } + kotlin { compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } androidComponents.beforeVariants { it.enable = !Config.Android.shouldSkipDebugVariant(it.buildType) diff --git a/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts index 8821c25626b..74f95e193cf 100644 --- a/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-console-opentelemetry-noagent/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java application - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.versions) id("com.github.johnrengelman.shadow") version "8.1.1" } @@ -22,13 +22,13 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-console/build.gradle.kts b/sentry-samples/sentry-samples-console/build.gradle.kts index b9d754db204..3e4a2b889f3 100644 --- a/sentry-samples/sentry-samples-console/build.gradle.kts +++ b/sentry-samples/sentry-samples-console/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java application - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.versions) id("com.github.johnrengelman.shadow") version "8.1.1" } @@ -22,13 +22,13 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-ktor-client/build.gradle.kts b/sentry-samples/sentry-samples-ktor-client/build.gradle.kts index 01fe56067f2..08a08b01c88 100644 --- a/sentry-samples/sentry-samples-ktor-client/build.gradle.kts +++ b/sentry-samples/sentry-samples-ktor-client/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) application alias(libs.plugins.gradle.versions) } @@ -12,7 +12,7 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { diff --git a/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts b/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts index d79812593a9..ade18a0cbc1 100644 --- a/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts +++ b/sentry-samples/sentry-samples-netflix-dgs/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot2) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -35,8 +35,8 @@ dependencies { tasks.withType().configureEach { useJUnitPlatform() } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } } diff --git a/sentry-samples/sentry-samples-spring-7/build.gradle.kts b/sentry-samples/sentry-samples-spring-7/build.gradle.kts index bf5eecea94b..f1565124743 100644 --- a/sentry-samples/sentry-samples-spring-7/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-7/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { alias(libs.plugins.springboot4) apply false alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) id("war") alias(libs.plugins.gretty) @@ -45,8 +45,13 @@ dependencies { tasks.withType().configureEach { useJUnitPlatform() } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + explicitApi() + // skip metadata version check, as Spring 7 / Spring Boot 4 is + // compiled against a newer version of Kotlin + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-Xskip-metadata-version-check") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } } diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppConfig.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/AppConfig.java similarity index 76% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppConfig.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/AppConfig.java index 72ecb14e2f4..d2e1442ed18 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppConfig.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/AppConfig.java @@ -1,8 +1,8 @@ -package io.sentry.samples.spring.jakarta; +package io.sentry.samples.spring7; import io.sentry.IScopes; -import io.sentry.spring.jakarta.SentryUserFilter; -import io.sentry.spring.jakarta.SentryUserProvider; +import io.sentry.spring7.SentryUserFilter; +import io.sentry.spring7.SentryUserProvider; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppInitializer.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/AppInitializer.java similarity index 93% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppInitializer.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/AppInitializer.java index 374a416c3ce..3b8ff72c136 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/AppInitializer.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/AppInitializer.java @@ -1,6 +1,6 @@ -package io.sentry.samples.spring.jakarta; +package io.sentry.samples.spring7; -import io.sentry.spring.jakarta.tracing.SentryTracingFilter; +import io.sentry.spring7.tracing.SentryTracingFilter; import jakarta.servlet.Filter; import org.springframework.web.filter.DelegatingFilterProxy; import org.springframework.web.filter.RequestContextFilter; diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/SecurityConfiguration.java similarity index 88% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SecurityConfiguration.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/SecurityConfiguration.java index d6c8a315583..0f593a1b7c1 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SecurityConfiguration.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/SecurityConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.jakarta; +package io.sentry.samples.spring7; import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Bean; @@ -20,9 +20,10 @@ public class SecurityConfiguration { @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) @Bean public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { - http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); - - return http.build(); + return http.csrf((csrf) -> csrf.disable()) + .authorizeHttpRequests((r) -> r.anyRequest().authenticated()) + .httpBasic((h) -> {}) + .build(); } @Bean diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/SentryConfig.java similarity index 87% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/SentryConfig.java index 443299048f3..ef327a48758 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/SentryConfig.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/SentryConfig.java @@ -1,9 +1,9 @@ -package io.sentry.samples.spring.jakarta; +package io.sentry.samples.spring7; import io.sentry.SentryOptions; import io.sentry.SentryOptions.TracesSamplerCallback; -import io.sentry.spring.jakarta.EnableSentry; -import io.sentry.spring.jakarta.tracing.SentryTracingConfiguration; +import io.sentry.spring7.EnableSentry; +import io.sentry.spring7.tracing.SentryTracingConfiguration; import jakarta.servlet.http.HttpServletRequest; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Import; diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/WebConfig.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/WebConfig.java similarity index 86% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/WebConfig.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/WebConfig.java index 73d425b2868..e8dce9b35c4 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/WebConfig.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/WebConfig.java @@ -1,7 +1,7 @@ -package io.sentry.samples.spring.jakarta; +package io.sentry.samples.spring7; import io.sentry.IScopes; -import io.sentry.spring.jakarta.tracing.SentrySpanClientHttpRequestInterceptor; +import io.sentry.spring7.tracing.SentrySpanClientHttpRequestInterceptor; import java.util.Collections; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; @@ -12,7 +12,7 @@ @Configuration @EnableAspectJAutoProxy(proxyTargetClass = true) -@ComponentScan("io.sentry.samples.spring.jakarta") +@ComponentScan("io.sentry.samples.spring7") @EnableWebMvc public class WebConfig { diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/Person.java similarity index 91% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/Person.java index ee1f4f0827e..784291f1c0d 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/Person.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/Person.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.jakarta.web; +package io.sentry.samples.spring7.web; public class Person { private final String firstName; diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonController.java similarity index 96% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonController.java index c5ee953810c..5baf24acc16 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonController.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.jakarta.web; +package io.sentry.samples.spring7.web; import io.sentry.Sentry; import org.slf4j.Logger; diff --git a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonService.java b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonService.java similarity index 90% rename from sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonService.java rename to sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonService.java index e8dcb8c1e30..fbe5b67ba4c 100644 --- a/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring/jakarta/web/PersonService.java +++ b/sentry-samples/sentry-samples-spring-7/src/main/java/io/sentry/samples/spring7/web/PersonService.java @@ -1,6 +1,6 @@ -package io.sentry.samples.spring.jakarta.web; +package io.sentry.samples.spring7.web; -import io.sentry.spring.jakarta.tracing.SentrySpan; +import io.sentry.spring7.tracing.SentrySpan; import java.util.Map; import org.springframework.stereotype.Service; import org.springframework.web.client.RestTemplate; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts index 05964eb76dc..9c01f148d10 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot4) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -24,10 +24,14 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + explicitApi() + // skip metadata version check, as Spring 7 / Spring Boot 4 is + // compiled against a newer version of Kotlin + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-Xskip-metadata-version-check") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } } @@ -42,6 +46,8 @@ dependencies { implementation(libs.springboot4.starter.web) implementation(libs.springboot4.starter.webflux) implementation(libs.springboot4.starter.websocket) + implementation(libs.springboot4.starter.webclient) + implementation(libs.springboot4.starter.restclient) implementation(Config.Libs.aspectj) implementation(Config.Libs.kotlinReflect) implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java similarity index 95% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java index 51451a5d77e..723d9683d31 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.EventProcessor; import io.sentry.Hint; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java similarity index 80% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java index cac83e6d797..7d0175d0b91 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java @@ -1,7 +1,7 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; -import io.sentry.spring.jakarta.checkin.SentryCheckIn; -import io.sentry.spring.jakarta.tracing.SentryTransaction; +import io.sentry.spring7.checkin.SentryCheckIn; +import io.sentry.spring7.tracing.SentryTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java index cfff0be4702..790ac3c8418 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.opentelemetry.instrumentation.annotations.WithSpan; import java.nio.charset.Charset; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/Person.java similarity index 90% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/Person.java index 2c8bdc33c62..a12881fb346 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/Person.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; public class Person { private final String firstName; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java similarity index 98% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index b3f22fd7fd4..f3f03b39e1f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonService.java similarity index 90% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonService.java index 50cdc9dd4e7..de2e684c920 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/PersonService.java @@ -1,8 +1,8 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.ISpan; import io.sentry.Sentry; -import io.sentry.spring.jakarta.tracing.SentrySpan; +import io.sentry.spring7.tracing.SentrySpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java similarity index 87% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java index e5987c8f4a2..d12a40fb51d 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Bean; @@ -18,9 +18,10 @@ public class SecurityConfiguration { @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) @Bean public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { - http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); - - return http.build(); + return http.csrf((csrf) -> csrf.disable()) + .authorizeHttpRequests((r) -> r.anyRequest().authenticated()) + .httpBasic((h) -> {}) + .build(); } @Bean diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java similarity index 93% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 7f412eaa0d6..61e726defcb 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -1,16 +1,16 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; import io.opentelemetry.api.OpenTelemetry; import io.opentelemetry.api.trace.Tracer; -import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import io.sentry.samples.spring.boot4.quartz.SampleJob; import java.util.Collections; import org.quartz.JobDetail; import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/Todo.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/Todo.java index 5fc4164d1b0..ae3d128d6b9 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/Todo.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; public class Todo { private final Long id; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/TodoController.java similarity index 98% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/TodoController.java index 0fa450a879c..d4fda10d0bd 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/TodoController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java similarity index 95% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java index 6fdf96506c8..d43cde143d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.util.HashMap; import java.util.Map; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java index bfc383c9122..4770a75a255 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java similarity index 98% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java index 63790bca628..2e1725ea644 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.nio.file.NoSuchFileException; import java.time.Duration; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java index cb6677c0c37..f8824abd07f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.util.HashMap; import java.util.Map; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java index d0f0973c864..c3b4ffd422f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.quartz; +package io.sentry.samples.spring.boot4.quartz; import org.quartz.Job; import org.quartz.JobExecutionContext; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts index 23a73cfd991..b9ac158296c 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.tasks.run.BootRun plugins { alias(libs.plugins.springboot4) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -25,13 +25,14 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() -} - -tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + explicitApi() + // skip metadata version check, as Spring 7 / Spring Boot 4 is + // compiled against a newer version of Kotlin + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-Xskip-metadata-version-check") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } } @@ -46,6 +47,8 @@ dependencies { implementation(libs.springboot4.starter.web) implementation(libs.springboot4.starter.webflux) implementation(libs.springboot4.starter.websocket) + implementation(libs.springboot4.starter.webclient) + implementation(libs.springboot4.starter.restclient) implementation(Config.Libs.aspectj) implementation(Config.Libs.kotlinReflect) implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java similarity index 95% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java index 51451a5d77e..723d9683d31 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.EventProcessor; import io.sentry.Hint; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java similarity index 79% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java index 4cd609d67ce..b96d2a6c431 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java @@ -1,7 +1,7 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; -import io.sentry.spring.jakarta.checkin.SentryCheckIn; -import io.sentry.spring.jakarta.tracing.SentryTransaction; +import io.sentry.spring7.checkin.SentryCheckIn; +import io.sentry.spring7.tracing.SentryTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java index d67059abb68..9018e4c2184 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import java.nio.charset.Charset; import org.slf4j.Logger; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/Person.java similarity index 90% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/Person.java index 2c8bdc33c62..a12881fb346 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/Person.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; public class Person { private final String firstName; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index 1584a9e823b..9b727447ffd 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonService.java similarity index 90% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonService.java index 50cdc9dd4e7..de2e684c920 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/PersonService.java @@ -1,8 +1,8 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.ISpan; import io.sentry.Sentry; -import io.sentry.spring.jakarta.tracing.SentrySpan; +import io.sentry.spring7.tracing.SentrySpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java similarity index 87% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java index e5987c8f4a2..d12a40fb51d 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Bean; @@ -18,9 +18,10 @@ public class SecurityConfiguration { @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) @Bean public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { - http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); - - return http.build(); + return http.csrf((csrf) -> csrf.disable()) + .authorizeHttpRequests((r) -> r.anyRequest().authenticated()) + .httpBasic((h) -> {}) + .build(); } @Bean diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java similarity index 93% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index a6eb46f4c74..aa5ebce68cd 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -1,16 +1,16 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; import io.opentelemetry.api.GlobalOpenTelemetry; import io.opentelemetry.api.trace.Tracer; -import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import io.sentry.samples.spring.boot4.quartz.SampleJob; import java.util.Collections; import org.quartz.JobDetail; import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/Todo.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/Todo.java index 5fc4164d1b0..ae3d128d6b9 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/Todo.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; public class Todo { private final Long id; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/TodoController.java similarity index 98% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/TodoController.java index 0fa450a879c..d4fda10d0bd 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/TodoController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.opentelemetry.api.trace.Span; import io.opentelemetry.api.trace.Tracer; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java similarity index 95% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java index 6fdf96506c8..d43cde143d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.util.HashMap; import java.util.Map; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java index bfc383c9122..4770a75a255 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java similarity index 98% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java index 63790bca628..2e1725ea644 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.nio.file.NoSuchFileException; import java.time.Duration; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java index cb6677c0c37..f8824abd07f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.util.HashMap; import java.util.Map; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java rename to sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java index d0f0973c864..c3b4ffd422f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java +++ b/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.quartz; +package io.sentry.samples.spring.boot4.quartz; import org.quartz.Job; import org.quartz.JobExecutionContext; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts index 1d68ad1a83e..e9e0af50e70 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot4) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -29,6 +29,7 @@ dependencies { implementation(libs.springboot4.starter.actuator) implementation(libs.springboot4.starter.graphql) implementation(libs.springboot4.starter.webflux) + implementation(libs.springboot4.starter.webclient) testImplementation(kotlin(Config.kotlinStdLib)) testImplementation(projects.sentrySystemTestSupport) @@ -45,9 +46,14 @@ dependencies { configure { test { java.srcDir("src/test/java") } } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + explicitApi() + // skip metadata version check, as Spring 7 / Spring Boot 4 is + // compiled against a newer version of Kotlin + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-Xskip-metadata-version-check") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } } diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java index 38409509905..dd087100791 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import java.nio.charset.Charset; import org.slf4j.Logger; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/Person.java similarity index 90% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/Person.java index 2c8bdc33c62..a12881fb346 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/Person.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; public class Person { private final String firstName; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java similarity index 96% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index a7b7752806d..b2563200c83 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.Sentry; import org.slf4j.Logger; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonService.java similarity index 91% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonService.java index 75b0e38b125..a45b9fa85c3 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/PersonService.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.Sentry; import java.time.Duration; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java similarity index 91% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 926298bb97b..72980871730 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/Todo.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/Todo.java index 5fc4164d1b0..ae3d128d6b9 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/Todo.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; public class Todo { private final Long id; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/TodoController.java similarity index 94% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/TodoController.java index de29c206ecb..352a83e4c93 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/TodoController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; diff --git a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java similarity index 91% rename from sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java rename to sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java index 421631ca7a5..73b5f04502b 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4-webflux/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; diff --git a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts index 95aa93c1ed0..9b127e1136f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-4/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot4) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -24,13 +24,14 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() -} - -tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + explicitApi() + // skip metadata version check, as Spring 7 / Spring Boot 4 is + // compiled against a newer version of Kotlin + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict", "-Xskip-metadata-version-check") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } } @@ -45,6 +46,8 @@ dependencies { implementation(libs.springboot4.starter.web) implementation(libs.springboot4.starter.webflux) implementation(libs.springboot4.starter.websocket) + implementation(libs.springboot4.starter.restclient) + implementation(libs.springboot4.starter.webclient) implementation(Config.Libs.aspectj) implementation(Config.Libs.kotlinReflect) implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java deleted file mode 100644 index 2c8bdc33c62..00000000000 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/Person.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.sentry.samples.spring.boot.jakarta; - -public class Person { - private final String firstName; - private final String lastName; - - public Person(String firstName, String lastName) { - this.firstName = firstName; - this.lastName = lastName; - } - - public String getFirstName() { - return firstName; - } - - public String getLastName() { - return lastName; - } - - @Override - public String toString() { - return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; - } -} diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java similarity index 95% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java index 51451a5d77e..723d9683d31 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomEventProcessor.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CustomEventProcessor.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.EventProcessor; import io.sentry.Hint; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java similarity index 79% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java index 4cd609d67ce..b96d2a6c431 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/CustomJob.java @@ -1,7 +1,7 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; -import io.sentry.spring.jakarta.checkin.SentryCheckIn; -import io.sentry.spring.jakarta.tracing.SentryTransaction; +import io.sentry.spring7.checkin.SentryCheckIn; +import io.sentry.spring7.tracing.SentryTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java index d67059abb68..9018e4c2184 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/DistributedTracingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/DistributedTracingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import java.nio.charset.Charset; import org.slf4j.Logger; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/Person.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/Person.java new file mode 100644 index 00000000000..a12881fb346 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/Person.java @@ -0,0 +1,24 @@ +package io.sentry.samples.spring.boot4; + +public class Person { + private final String firstName; + private final String lastName; + + public Person(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + @Override + public String toString() { + return "Person{" + "firstName='" + firstName + '\'' + ", lastName='" + lastName + '\'' + '}'; + } +} diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java index 94a4b9b8520..305850ec18b 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.ISpan; import io.sentry.Sentry; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonService.java similarity index 90% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonService.java index 50cdc9dd4e7..de2e684c920 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/PersonService.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/PersonService.java @@ -1,8 +1,8 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.ISpan; import io.sentry.Sentry; -import io.sentry.spring.jakarta.tracing.SentrySpan; +import io.sentry.spring7.tracing.SentrySpan; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jdbc.core.JdbcTemplate; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java similarity index 87% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java index e5987c8f4a2..d12a40fb51d 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SecurityConfiguration.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SecurityConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import org.jetbrains.annotations.NotNull; import org.springframework.context.annotation.Bean; @@ -18,9 +18,10 @@ public class SecurityConfiguration { @SuppressWarnings({"lgtm[java/spring-disabled-csrf-protection]", "removal"}) @Bean public SecurityFilterChain filterChain(final @NotNull HttpSecurity http) throws Exception { - http.csrf().disable().authorizeHttpRequests().anyRequest().authenticated().and().httpBasic(); - - return http.build(); + return http.csrf((csrf) -> csrf.disable()) + .authorizeHttpRequests((r) -> r.anyRequest().authenticated()) + .httpBasic((h) -> {}) + .build(); } @Bean diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java similarity index 93% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java index 8050cb8e74c..71463a9a819 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/SentryDemoApplication.java @@ -1,14 +1,14 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import static io.sentry.quartz.SentryJobListener.SENTRY_SLUG_KEY; -import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import io.sentry.samples.spring.boot4.quartz.SampleJob; import java.util.Collections; import org.quartz.JobDetail; import org.quartz.SimpleTrigger; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.boot.web.client.RestTemplateBuilder; +import org.springframework.boot.restclient.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.scheduling.quartz.CronTriggerFactoryBean; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/Todo.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/Todo.java index 5fc4164d1b0..ae3d128d6b9 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/Todo.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/Todo.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; public class Todo { private final Long id; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoController.java index 987d516936b..0f71cca0419 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/TodoController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/TodoController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta; +package io.sentry.samples.spring.boot4; import io.sentry.reactor.SentryReactorUtils; import org.springframework.web.bind.annotation.GetMapping; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java similarity index 95% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java index 6fdf96506c8..d43cde143d1 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/AssigneeController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/AssigneeController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.util.HashMap; import java.util.Map; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java index bfc383c9122..4770a75a255 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/GreetingController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/GreetingController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import org.springframework.graphql.data.method.annotation.Argument; import org.springframework.graphql.data.method.annotation.QueryMapping; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java similarity index 98% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java index 63790bca628..2e1725ea644 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry-noagent/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/ProjectController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/ProjectController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.nio.file.NoSuchFileException; import java.time.Duration; diff --git a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java similarity index 97% rename from sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java index 434a4822084..9ee0ef2c7a4 100644 --- a/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot/jakarta/graphql/TaskCreatorController.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/graphql/TaskCreatorController.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.graphql; +package io.sentry.samples.spring.boot4.graphql; import java.util.HashMap; import java.util.Map; diff --git a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java similarity index 89% rename from sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java rename to sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java index d0f0973c864..c3b4ffd422f 100644 --- a/sentry-samples/sentry-samples-spring-boot-4-opentelemetry/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java +++ b/sentry-samples/sentry-samples-spring-boot-4/src/main/java/io/sentry/samples/spring/boot4/quartz/SampleJob.java @@ -1,4 +1,4 @@ -package io.sentry.samples.spring.boot.jakarta.quartz; +package io.sentry.samples.spring.boot4.quartz; import org.quartz.Job; import org.quartz.JobExecutionContext; diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts index c40b9e0f468..5a341f82892 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry-noagent/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot3) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -24,10 +24,10 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts index 307f6e6803f..40eb4c04c2b 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta-opentelemetry/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.tasks.run.BootRun plugins { alias(libs.plugins.springboot3) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -25,13 +25,13 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts index 009088ce53c..b3593ed46bb 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot3) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -24,13 +24,13 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts index 47f1b42629d..2223458dc76 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry-noagent/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot2) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -24,13 +24,13 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts index 774ab356e69..a704a6d6004 100644 --- a/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-opentelemetry/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.tasks.run.BootRun plugins { alias(libs.plugins.springboot2) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -25,10 +25,10 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts index 144dcf57773..7be7d68eb94 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-webflux-jakarta/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot3) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -45,9 +45,9 @@ dependencies { configure { test { java.srcDir("src/test/java") } } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts index dae155e5c22..58662e614d6 100644 --- a/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-webflux/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot2) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -44,9 +44,9 @@ dependencies { configure { test { java.srcDir("src/test/java") } } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts index b024b2618f4..e5344e38c9a 100644 --- a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { alias(libs.plugins.springboot2) alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) } @@ -24,13 +24,13 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index 2ecc26c4045..c48c97907bd 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { alias(libs.plugins.springboot3) apply false alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) id("war") alias(libs.plugins.gretty) @@ -45,8 +45,8 @@ dependencies { tasks.withType().configureEach { useJUnitPlatform() } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring/build.gradle.kts b/sentry-samples/sentry-samples-spring/build.gradle.kts index 170f58d19e9..986493af9bc 100644 --- a/sentry-samples/sentry-samples-spring/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin.BOM_COORDINATES plugins { alias(libs.plugins.springboot2) apply false alias(libs.plugins.spring.dependency.management) - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.kotlin.spring) id("war") alias(libs.plugins.gretty) @@ -58,8 +58,8 @@ tasks.withType().configureEach { } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } } diff --git a/sentry-servlet-jakarta/build.gradle.kts b/sentry-servlet-jakarta/build.gradle.kts index 0dab0cdc406..ec079b6d65f 100644 --- a/sentry-servlet-jakarta/build.gradle.kts +++ b/sentry-servlet-jakarta/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-servlet-jakarta/src/test/kotlin/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessorTest.kt b/sentry-servlet-jakarta/src/test/kotlin/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessorTest.kt index 9fd1ae6a21e..3e420aa1dfb 100644 --- a/sentry-servlet-jakarta/src/test/kotlin/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessorTest.kt +++ b/sentry-servlet-jakarta/src/test/kotlin/io/sentry/servlet/jakarta/SentryRequestHttpServletRequestProcessorTest.kt @@ -145,7 +145,7 @@ fun toRequestUrl(uri: URI): StringBuffer? { url.append(':').append(port) } - if (uri?.isNotBlank()) { + if (uri.isNotBlank()) { url.append(uri) } return url diff --git a/sentry-servlet/build.gradle.kts b/sentry-servlet/build.gradle.kts index 0af2cb30718..ceaa160695a 100644 --- a/sentry-servlet/build.gradle.kts +++ b/sentry-servlet/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,8 +12,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-spring-7/api/sentry-spring-7.api b/sentry-spring-7/api/sentry-spring-7.api index 22e110446e8..cd17eab315e 100644 --- a/sentry-spring-7/api/sentry-spring-7.api +++ b/sentry-spring-7/api/sentry-spring-7.api @@ -1,163 +1,168 @@ -public final class io/sentry/spring/jakarta/ContextTagsEventProcessor : io/sentry/EventProcessor { +public final class io/sentry/spring7/BuildConfig { + public static final field SENTRY_SPRING_7_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/spring7/ContextTagsEventProcessor : io/sentry/EventProcessor { public fun (Lio/sentry/SentryOptions;)V public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } -public abstract interface annotation class io/sentry/spring/jakarta/EnableSentry : java/lang/annotation/Annotation { +public abstract interface annotation class io/sentry/spring7/EnableSentry : java/lang/annotation/Annotation { public abstract fun dsn ()Ljava/lang/String; public abstract fun exceptionResolverOrder ()I public abstract fun maxRequestBodySize ()Lio/sentry/SentryOptions$RequestSize; public abstract fun sendDefaultPii ()Z } -public final class io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider : io/sentry/spring/jakarta/SentryUserProvider { +public final class io/sentry/spring7/HttpServletRequestSentryUserProvider : io/sentry/spring7/SentryUserProvider { public fun (Lio/sentry/SentryOptions;)V public fun provideUser ()Lio/sentry/protocol/User; } -public class io/sentry/spring/jakarta/SentryExceptionResolver : org/springframework/core/Ordered, org/springframework/web/servlet/HandlerExceptionResolver { +public class io/sentry/spring7/SentryExceptionResolver : org/springframework/core/Ordered, org/springframework/web/servlet/HandlerExceptionResolver { public static final field MECHANISM_TYPE Ljava/lang/String; - public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;I)V + public fun (Lio/sentry/IScopes;Lio/sentry/spring7/tracing/TransactionNameProvider;I)V protected fun createEvent (Ljakarta/servlet/http/HttpServletRequest;Ljava/lang/Exception;)Lio/sentry/SentryEvent; protected fun createHint (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;)Lio/sentry/Hint; public fun getOrder ()I public fun resolveException (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljava/lang/Object;Ljava/lang/Exception;)Lorg/springframework/web/servlet/ModelAndView; } -public class io/sentry/spring/jakarta/SentryHubRegistrar : org/springframework/context/annotation/ImportBeanDefinitionRegistrar { +public class io/sentry/spring7/SentryHubRegistrar : org/springframework/context/annotation/ImportBeanDefinitionRegistrar { public fun ()V public fun registerBeanDefinitions (Lorg/springframework/core/type/AnnotationMetadata;Lorg/springframework/beans/factory/support/BeanDefinitionRegistry;)V } -public class io/sentry/spring/jakarta/SentryInitBeanPostProcessor : org/springframework/beans/factory/DisposableBean, org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/context/ApplicationContextAware { +public class io/sentry/spring7/SentryInitBeanPostProcessor : org/springframework/beans/factory/DisposableBean, org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/context/ApplicationContextAware { public fun ()V public fun destroy ()V public fun postProcessAfterInitialization (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V } -public class io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor { - public fun (Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V +public class io/sentry/spring7/SentryRequestHttpServletRequestProcessor : io/sentry/EventProcessor { + public fun (Lio/sentry/spring7/tracing/TransactionNameProvider;Ljakarta/servlet/http/HttpServletRequest;)V public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; } -public class io/sentry/spring/jakarta/SentryRequestResolver { +public class io/sentry/spring7/SentryRequestResolver { protected static final field staticLock Lio/sentry/util/AutoClosableReentrantLock; public fun (Lio/sentry/IScopes;)V public fun resolveSentryRequest (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/protocol/Request; } -public class io/sentry/spring/jakarta/SentrySpringFilter : org/springframework/web/filter/OncePerRequestFilter { +public class io/sentry/spring7/SentrySpringFilter : org/springframework/web/filter/OncePerRequestFilter { public fun ()V public fun (Lio/sentry/IScopes;)V - public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/SentryRequestResolver;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;)V + public fun (Lio/sentry/IScopes;Lio/sentry/spring7/SentryRequestResolver;Lio/sentry/spring7/tracing/TransactionNameProvider;)V protected fun doFilterInternal (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljakarta/servlet/FilterChain;)V } -public class io/sentry/spring/jakarta/SentrySpringServletContainerInitializer : jakarta/servlet/ServletContainerInitializer { +public class io/sentry/spring7/SentrySpringServletContainerInitializer : jakarta/servlet/ServletContainerInitializer { public fun ()V public fun onStartup (Ljava/util/Set;Ljakarta/servlet/ServletContext;)V } -public final class io/sentry/spring/jakarta/SentryTaskDecorator : org/springframework/core/task/TaskDecorator { +public final class io/sentry/spring7/SentryTaskDecorator : org/springframework/core/task/TaskDecorator { public fun ()V public fun decorate (Ljava/lang/Runnable;)Ljava/lang/Runnable; } -public class io/sentry/spring/jakarta/SentryUserFilter : org/springframework/web/filter/OncePerRequestFilter { +public class io/sentry/spring7/SentryUserFilter : org/springframework/web/filter/OncePerRequestFilter { public fun (Lio/sentry/IScopes;Ljava/util/List;)V protected fun doFilterInternal (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljakarta/servlet/FilterChain;)V public fun getSentryUserProviders ()Ljava/util/List; } -public abstract interface class io/sentry/spring/jakarta/SentryUserProvider { +public abstract interface class io/sentry/spring7/SentryUserProvider { public abstract fun provideUser ()Lio/sentry/protocol/User; } -public class io/sentry/spring/jakarta/SentryWebConfiguration { +public class io/sentry/spring7/SentryWebConfiguration { public fun ()V - public fun httpServletRequestSentryUserProvider (Lio/sentry/SentryOptions;)Lio/sentry/spring/jakarta/HttpServletRequestSentryUserProvider; + public fun httpServletRequestSentryUserProvider (Lio/sentry/SentryOptions;)Lio/sentry/spring7/HttpServletRequestSentryUserProvider; } -public final class io/sentry/spring/jakarta/SpringProfilesEventProcessor : io/sentry/EventProcessor { +public final class io/sentry/spring7/SpringProfilesEventProcessor : io/sentry/EventProcessor { public fun (Lorg/springframework/core/env/Environment;)V public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent; public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } -public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : io/sentry/spring/jakarta/SentryUserProvider { +public final class io/sentry/spring7/SpringSecuritySentryUserProvider : io/sentry/spring7/SentryUserProvider { public fun (Lio/sentry/SentryOptions;)V public fun provideUser ()Lio/sentry/protocol/User; } -public abstract interface annotation class io/sentry/spring/jakarta/checkin/SentryCheckIn : java/lang/annotation/Annotation { +public abstract interface annotation class io/sentry/spring7/checkin/SentryCheckIn : java/lang/annotation/Annotation { public abstract fun heartbeat ()Z public abstract fun monitorSlug ()Ljava/lang/String; public abstract fun value ()Ljava/lang/String; } -public class io/sentry/spring/jakarta/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor, org/springframework/context/EmbeddedValueResolverAware { +public class io/sentry/spring7/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor, org/springframework/context/EmbeddedValueResolverAware { public fun ()V public fun (Lio/sentry/IScopes;)V public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; public fun setEmbeddedValueResolver (Lorg/springframework/util/StringValueResolver;)V } -public class io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration { +public class io/sentry/spring7/checkin/SentryCheckInAdviceConfiguration { public fun ()V public fun sentryCheckInAdvice ()Lorg/aopalliance/aop/Advice; public fun sentryCheckInAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; } -public class io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration { +public class io/sentry/spring7/checkin/SentryCheckInPointcutConfiguration { public fun ()V public fun sentryCheckInPointcut ()Lorg/springframework/aop/Pointcut; } -public class io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration { +public class io/sentry/spring7/checkin/SentryQuartzConfiguration { public fun ()V public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer; } -public final class io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer { +public final class io/sentry/spring7/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer { public fun ()V 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 abstract interface annotation class io/sentry/spring7/exception/SentryCaptureExceptionParameter : java/lang/annotation/Annotation { } -public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice : org/aopalliance/intercept/MethodInterceptor { +public class io/sentry/spring7/exception/SentryCaptureExceptionParameterAdvice : org/aopalliance/intercept/MethodInterceptor { public fun ()V public fun (Lio/sentry/IScopes;)V public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; } -public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration { +public class io/sentry/spring7/exception/SentryCaptureExceptionParameterConfiguration { public fun ()V } -public class io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration { +public class io/sentry/spring7/exception/SentryCaptureExceptionParameterPointcutConfiguration { public fun ()V public fun sentryCaptureExceptionParameterPointcut ()Lorg/springframework/aop/Pointcut; } -public class io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration { +public class io/sentry/spring7/exception/SentryExceptionParameterAdviceConfiguration { public fun ()V public fun sentryCaptureExceptionParameterAdvice ()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 final class io/sentry/spring7/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; public fun registerDataLoaders (Lorg/dataloader/DataLoaderRegistry;Lgraphql/GraphQLContext;)V } -public final class io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry$SentryRegistrationSpec : org/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec { +public final class io/sentry/spring7/graphql/SentryBatchLoaderRegistry$SentryRegistrationSpec : org/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec { public fun (Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec;Ljava/lang/Class;Ljava/lang/Class;)V public fun (Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec;Ljava/lang/String;)V public fun registerBatchLoader (Ljava/util/function/BiFunction;)V @@ -167,62 +172,62 @@ public final class io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry$Se public fun withOptions (Lorg/dataloader/DataLoaderOptions;)Lorg/springframework/graphql/execution/BatchLoaderRegistry$RegistrationSpec; } -public final class io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter : org/springframework/graphql/execution/DataFetcherExceptionResolverAdapter { +public final class io/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter : org/springframework/graphql/execution/DataFetcherExceptionResolverAdapter { public fun ()V public fun isThreadLocalContextAware ()Z } -public final class io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { +public final class io/sentry/spring7/graphql/SentryDgsSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { public fun ()V public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; } -public class io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration { +public class io/sentry/spring7/graphql/SentryGraphql22Configuration { public fun ()V - public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; - public fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; + public fun exceptionResolverAdapter ()Lio/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter; + public fun graphqlBeanPostProcessor ()Lio/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor; public fun sentryInstrumentationWebMvc (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; public fun sentryInstrumentationWebflux (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; } -public final class io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor : org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/core/PriorityOrdered { +public final class io/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor : org/springframework/beans/factory/config/BeanPostProcessor, org/springframework/core/PriorityOrdered { public fun ()V public fun getOrder ()I public fun postProcessAfterInitialization (Ljava/lang/Object;Ljava/lang/String;)Ljava/lang/Object; } -public class io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration { +public class io/sentry/spring7/graphql/SentryGraphqlConfiguration { public fun ()V - public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; - public fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; + public fun exceptionResolverAdapter ()Lio/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter; + public fun graphqlBeanPostProcessor ()Lio/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor; public fun sentryInstrumentationWebMvc (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; public fun sentryInstrumentationWebflux (Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; } -public final class io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { +public final class io/sentry/spring7/graphql/SentrySpringSubscriptionHandler : io/sentry/graphql/SentrySubscriptionHandler { public fun ()V public fun onSubscriptionResult (Ljava/lang/Object;Lio/sentry/IScopes;Lio/sentry/graphql/ExceptionReporter;Lgraphql/execution/instrumentation/parameters/InstrumentationFieldFetchParameters;)Ljava/lang/Object; } -public class io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration { +public class io/sentry/spring7/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration { public fun ()V public fun sentryOpenTelemetryOptionsConfiguration ()Lio/sentry/Sentry$OptionsConfiguration; } -public class io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration { +public class io/sentry/spring7/opentelemetry/SentryOpenTelemetryNoAgentConfiguration { public fun ()V public static fun openTelemetrySpanFactory (Lio/opentelemetry/api/OpenTelemetry;)Lio/sentry/ISpanFactory; public fun sentryOpenTelemetryOptionsConfiguration ()Lio/sentry/Sentry$OptionsConfiguration; } -public final class io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider { +public final class io/sentry/spring7/tracing/CombinedTransactionNameProvider : io/sentry/spring7/tracing/TransactionNameProvider { public fun (Ljava/util/List;)V public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; - public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring/jakarta/tracing/TransactionNameWithSource; + public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring7/tracing/TransactionNameWithSource; public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; } -public class io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration { +public class io/sentry/spring7/tracing/SentryAdviceConfiguration { public fun ()V public fun sentrySpanAdvice ()Lorg/aopalliance/aop/Advice; public fun sentrySpanAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; @@ -230,89 +235,89 @@ public class io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration { public fun sentryTransactionAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; } -public abstract interface annotation class io/sentry/spring/jakarta/tracing/SentrySpan : java/lang/annotation/Annotation { +public abstract interface annotation class io/sentry/spring7/tracing/SentrySpan : java/lang/annotation/Annotation { public abstract fun description ()Ljava/lang/String; public abstract fun operation ()Ljava/lang/String; public abstract fun value ()Ljava/lang/String; } -public class io/sentry/spring/jakarta/tracing/SentrySpanAdvice : org/aopalliance/intercept/MethodInterceptor { +public class io/sentry/spring7/tracing/SentrySpanAdvice : org/aopalliance/intercept/MethodInterceptor { public fun ()V public fun (Lio/sentry/IScopes;)V public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; } -public class io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor : org/springframework/http/client/ClientHttpRequestInterceptor { +public class io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor : org/springframework/http/client/ClientHttpRequestInterceptor { public fun (Lio/sentry/IScopes;)V public fun (Lio/sentry/IScopes;Z)V public fun intercept (Lorg/springframework/http/HttpRequest;[BLorg/springframework/http/client/ClientHttpRequestExecution;)Lorg/springframework/http/client/ClientHttpResponse; } -public class io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter : org/springframework/web/reactive/function/client/ExchangeFilterFunction { +public class io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter : org/springframework/web/reactive/function/client/ExchangeFilterFunction { public fun (Lio/sentry/IScopes;)V public fun filter (Lorg/springframework/web/reactive/function/client/ClientRequest;Lorg/springframework/web/reactive/function/client/ExchangeFunction;)Lreactor/core/publisher/Mono; } -public class io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration { +public class io/sentry/spring7/tracing/SentrySpanPointcutConfiguration { public fun ()V public fun sentrySpanPointcut ()Lorg/springframework/aop/Pointcut; } -public class io/sentry/spring/jakarta/tracing/SentryTracingConfiguration { +public class io/sentry/spring7/tracing/SentryTracingConfiguration { public fun ()V } -public class io/sentry/spring/jakarta/tracing/SentryTracingFilter : org/springframework/web/filter/OncePerRequestFilter { +public class io/sentry/spring7/tracing/SentryTracingFilter : org/springframework/web/filter/OncePerRequestFilter { public fun ()V public fun (Lio/sentry/IScopes;)V - public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;)V - public fun (Lio/sentry/IScopes;Lio/sentry/spring/jakarta/tracing/TransactionNameProvider;Z)V + public fun (Lio/sentry/IScopes;Lio/sentry/spring7/tracing/TransactionNameProvider;)V + public fun (Lio/sentry/IScopes;Lio/sentry/spring7/tracing/TransactionNameProvider;Z)V protected fun doFilterInternal (Ljakarta/servlet/http/HttpServletRequest;Ljakarta/servlet/http/HttpServletResponse;Ljakarta/servlet/FilterChain;)V protected fun shouldNotFilterAsyncDispatch ()Z } -public abstract interface annotation class io/sentry/spring/jakarta/tracing/SentryTransaction : java/lang/annotation/Annotation { +public abstract interface annotation class io/sentry/spring7/tracing/SentryTransaction : java/lang/annotation/Annotation { public abstract fun name ()Ljava/lang/String; public abstract fun operation ()Ljava/lang/String; public abstract fun value ()Ljava/lang/String; } -public class io/sentry/spring/jakarta/tracing/SentryTransactionAdvice : org/aopalliance/intercept/MethodInterceptor { +public class io/sentry/spring7/tracing/SentryTransactionAdvice : org/aopalliance/intercept/MethodInterceptor { public fun ()V public fun (Lio/sentry/IScopes;)V public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; } -public class io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration { +public class io/sentry/spring7/tracing/SentryTransactionPointcutConfiguration { public fun ()V public fun sentryTransactionPointcut ()Lorg/springframework/aop/Pointcut; } -public final class io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider { +public final class io/sentry/spring7/tracing/SpringMvcTransactionNameProvider : io/sentry/spring7/tracing/TransactionNameProvider { public fun ()V public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; } -public final class io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider : io/sentry/spring/jakarta/tracing/TransactionNameProvider { +public final class io/sentry/spring7/tracing/SpringServletTransactionNameProvider : io/sentry/spring7/tracing/TransactionNameProvider { public fun ()V public fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; } -public abstract interface class io/sentry/spring/jakarta/tracing/TransactionNameProvider { +public abstract interface class io/sentry/spring7/tracing/TransactionNameProvider { public abstract fun provideTransactionName (Ljakarta/servlet/http/HttpServletRequest;)Ljava/lang/String; - public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring/jakarta/tracing/TransactionNameWithSource; + public fun provideTransactionNameAndSource (Ljakarta/servlet/http/HttpServletRequest;)Lio/sentry/spring7/tracing/TransactionNameWithSource; public fun provideTransactionSource ()Lio/sentry/protocol/TransactionNameSource; } -public final class io/sentry/spring/jakarta/tracing/TransactionNameWithSource { +public final class io/sentry/spring7/tracing/TransactionNameWithSource { public fun (Ljava/lang/String;Lio/sentry/protocol/TransactionNameSource;)V public fun getTransactionName ()Ljava/lang/String; public fun getTransactionNameSource ()Lio/sentry/protocol/TransactionNameSource; } -public abstract class io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter : org/springframework/web/server/WebFilter { +public abstract class io/sentry/spring7/webflux/AbstractSentryWebFilter : org/springframework/web/server/WebFilter { public static final field SENTRY_HUB_KEY Ljava/lang/String; public static final field SENTRY_SCOPES_KEY Ljava/lang/String; public fun (Lio/sentry/IScopes;)V @@ -324,40 +329,35 @@ public abstract class io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter : protected fun startTransaction (Lio/sentry/IScopes;Lorg/springframework/http/server/reactive/ServerHttpRequest;Lio/sentry/TransactionContext;Ljava/lang/String;)Lio/sentry/ITransaction; } -public class io/sentry/spring/jakarta/webflux/SentryRequestResolver { +public class io/sentry/spring7/webflux/SentryRequestResolver { public fun (Lio/sentry/IScopes;)V public fun resolveSentryRequest (Lorg/springframework/http/server/reactive/ServerHttpRequest;)Lio/sentry/protocol/Request; } -public final class io/sentry/spring/jakarta/webflux/SentryScheduleHook : java/util/function/Function { +public final class io/sentry/spring7/webflux/SentryScheduleHook : java/util/function/Function { public fun ()V public synthetic fun apply (Ljava/lang/Object;)Ljava/lang/Object; public fun apply (Ljava/lang/Runnable;)Ljava/lang/Runnable; } -public final class io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler : org/springframework/web/server/WebExceptionHandler { +public final class io/sentry/spring7/webflux/SentryWebExceptionHandler : org/springframework/web/server/WebExceptionHandler { public static final field MECHANISM_TYPE Ljava/lang/String; public fun (Lio/sentry/IScopes;)V public fun handle (Lorg/springframework/web/server/ServerWebExchange;Ljava/lang/Throwable;)Lreactor/core/publisher/Mono; } -public class io/sentry/spring/jakarta/webflux/SentryWebFilter : io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter { +public class io/sentry/spring7/webflux/SentryWebFilter : io/sentry/spring7/webflux/AbstractSentryWebFilter { public fun (Lio/sentry/IScopes;)V public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono; } -public final class io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor : io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter { +public final class io/sentry/spring7/webflux/SentryWebFilterWithThreadLocalAccessor : io/sentry/spring7/webflux/AbstractSentryWebFilter { public static final field TRACE_ORIGIN Ljava/lang/String; public fun (Lio/sentry/IScopes;)V public fun filter (Lorg/springframework/web/server/ServerWebExchange;Lorg/springframework/web/server/WebFilterChain;)Lreactor/core/publisher/Mono; } -public final class io/sentry/spring/jakarta/webflux/reactor/ReactorUtils : io/sentry/reactor/SentryReactorUtils { +public final class io/sentry/spring7/webflux/reactor/ReactorUtils : io/sentry/reactor/SentryReactorUtils { public fun ()V } -public final class io/sentry/spring7/BuildConfig { - public static final field SENTRY_SPRING_7_SDK_NAME Ljava/lang/String; - public static final field VERSION_NAME Ljava/lang/String; -} - diff --git a/sentry-spring-7/build.gradle.kts b/sentry-spring-7/build.gradle.kts index c61e75a7ad5..45a70b33a6b 100644 --- a/sentry-spring-7/build.gradle.kts +++ b/sentry-spring-7/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -19,10 +19,10 @@ configure { } tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - languageVersion = libs.versions.kotlin.compatible.version.get() - freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_2_1) + freeCompilerArgs.add("-Xjsr305=strict") } } @@ -61,7 +61,7 @@ dependencies { testImplementation(libs.awaitility.kotlin.spring7) testImplementation(libs.context.propagation) testImplementation(libs.graphql.java24) - testImplementation(libs.kotlin.test.junit.spring7) + testImplementation(libs.kotlin.test.junit) testImplementation(libs.mockito.kotlin.spring7) testImplementation(libs.mockito.inline) testImplementation(libs.springboot4.starter.aop) @@ -70,6 +70,8 @@ dependencies { testImplementation(libs.springboot4.starter.test) testImplementation(libs.springboot4.starter.web) testImplementation(libs.springboot4.starter.webflux) + testImplementation(libs.springboot4.starter.restclient) + testImplementation(libs.springboot4.starter.webclient) testImplementation(projects.sentryReactor) } @@ -125,3 +127,12 @@ tasks.jar { ) } } + +kotlin { + explicitApi() + compilerOptions { + // skip metadata version check, as Spring 7 / Spring Boot 4 is + // compiled against a newer version of Kotlin + freeCompilerArgs.add("-Xskip-metadata-version-check") + } +} diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/ContextTagsEventProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/ContextTagsEventProcessor.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/ContextTagsEventProcessor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/ContextTagsEventProcessor.java index 94f49d83190..89fdef8d1b2 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/ContextTagsEventProcessor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/ContextTagsEventProcessor.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.EventProcessor; import io.sentry.Hint; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/EnableSentry.java b/sentry-spring-7/src/main/java/io/sentry/spring7/EnableSentry.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/EnableSentry.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/EnableSentry.java index e8cd91f3f44..f65e91d3016 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/EnableSentry.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/EnableSentry.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.SentryOptions; import java.lang.annotation.ElementType; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/HttpServletRequestSentryUserProvider.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/HttpServletRequestSentryUserProvider.java index 6174da0dc5f..54ad7602ae0 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/HttpServletRequestSentryUserProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/HttpServletRequestSentryUserProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.SentryOptions; import io.sentry.protocol.User; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/RequestPayloadExtractor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/RequestPayloadExtractor.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/RequestPayloadExtractor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/RequestPayloadExtractor.java index 2eb41462f8a..60058cd4a98 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/RequestPayloadExtractor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/RequestPayloadExtractor.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.SentryLevel; import io.sentry.SentryOptions; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryExceptionResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryExceptionResolver.java similarity index 95% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryExceptionResolver.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryExceptionResolver.java index efa98ef581b..f7110aa0f21 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryExceptionResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryExceptionResolver.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import static io.sentry.TypeCheckHint.SPRING_RESOLVER_REQUEST; import static io.sentry.TypeCheckHint.SPRING_RESOLVER_RESPONSE; @@ -10,7 +10,7 @@ import io.sentry.SentryLevel; import io.sentry.exception.ExceptionMechanismException; import io.sentry.protocol.Mechanism; -import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.spring7.tracing.TransactionNameProvider; import io.sentry.util.Objects; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; @@ -27,7 +27,7 @@ */ @Open public class SentryExceptionResolver implements HandlerExceptionResolver, Ordered { - public static final String MECHANISM_TYPE = "Spring6ExceptionResolver"; + public static final String MECHANISM_TYPE = "Spring7ExceptionResolver"; private final @NotNull IScopes scopes; private final @NotNull TransactionNameProvider transactionNameProvider; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryHubRegistrar.java similarity index 94% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryHubRegistrar.java index f0c9d952176..72e69b18447 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryHubRegistrar.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryHubRegistrar.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import com.jakewharton.nopen.annotation.Open; import io.sentry.InitPriority; @@ -6,8 +6,7 @@ import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryOptions; import io.sentry.protocol.SdkVersion; -import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; -import io.sentry.spring7.BuildConfig; +import io.sentry.spring7.tracing.SpringMvcTransactionNameProvider; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -23,7 +22,7 @@ public class SentryHubRegistrar implements ImportBeanDefinitionRegistrar { static { SentryIntegrationPackageStorage.getInstance() - .addPackage("maven:io.sentry:sentry-spring-jakarta", BuildConfig.VERSION_NAME); + .addPackage("maven:io.sentry:sentry-spring-7", BuildConfig.VERSION_NAME); } @Override @@ -99,6 +98,6 @@ private void registerSentryExceptionResolver( } private static void addPackageAndIntegrationInfo() { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7"); } } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryInitBeanPostProcessor.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryInitBeanPostProcessor.java index d33dfca8d8a..4b778d68649 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryInitBeanPostProcessor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryInitBeanPostProcessor.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import com.jakewharton.nopen.annotation.Open; import io.sentry.EventProcessor; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestHttpServletRequestProcessor.java similarity index 93% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestHttpServletRequestProcessor.java index 91b27ddeac7..2412083812d 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestHttpServletRequestProcessor.java @@ -1,10 +1,10 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import com.jakewharton.nopen.annotation.Open; import io.sentry.EventProcessor; import io.sentry.Hint; import io.sentry.SentryEvent; -import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.spring7.tracing.TransactionNameProvider; import io.sentry.util.Objects; import jakarta.servlet.http.HttpServletRequest; import org.jetbrains.annotations.NotNull; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java similarity index 99% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java index 4bb2ad312bb..aba0ae808d1 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryRequestResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryRequestResolver.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentrySpringFilter.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentrySpringFilter.java index c3ffbf41bad..c709cebbca7 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentrySpringFilter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import static io.sentry.SentryOptions.RequestSize.*; import static io.sentry.TypeCheckHint.SPRING_REQUEST_FILTER_REQUEST; @@ -15,8 +15,8 @@ import io.sentry.SentryLevel; import io.sentry.SentryOptions; import io.sentry.SentryOptions.RequestSize; -import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; -import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.spring7.tracing.SpringMvcTransactionNameProvider; +import io.sentry.spring7.tracing.TransactionNameProvider; import io.sentry.util.Objects; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringServletContainerInitializer.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentrySpringServletContainerInitializer.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringServletContainerInitializer.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentrySpringServletContainerInitializer.java index 9a71bc6910b..b060eed4182 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentrySpringServletContainerInitializer.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentrySpringServletContainerInitializer.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import static io.sentry.util.ClassLoaderUtils.classLoaderOrDefault; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryTaskDecorator.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryTaskDecorator.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryTaskDecorator.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryTaskDecorator.java index ba757952606..087608c1c37 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryTaskDecorator.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryTaskDecorator.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.IScopes; import io.sentry.ISentryLifecycleToken; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java index 31cc73a3468..b7e226929a5 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserFilter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScope; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserProvider.java similarity index 90% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserProvider.java index 9a87dbc78a5..0c6c4033a2b 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryUserProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryUserProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.protocol.User; import org.jetbrains.annotations.Nullable; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryWebConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryWebConfiguration.java similarity index 95% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryWebConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SentryWebConfiguration.java index 3f4d356505e..f53840a17bd 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SentryWebConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SentryWebConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import com.jakewharton.nopen.annotation.Open; import io.sentry.SentryOptions; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringProfilesEventProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SpringProfilesEventProcessor.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringProfilesEventProcessor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SpringProfilesEventProcessor.java index 48957e88507..100bdd6a38b 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringProfilesEventProcessor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SpringProfilesEventProcessor.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.EventProcessor; import io.sentry.Hint; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringSecuritySentryUserProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/SpringSecuritySentryUserProvider.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringSecuritySentryUserProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/SpringSecuritySentryUserProvider.java index d36bc4bf2b0..164a43c5bd2 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/SpringSecuritySentryUserProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/SpringSecuritySentryUserProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta; +package io.sentry.spring7; import io.sentry.SentryOptions; import io.sentry.protocol.User; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckIn.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckIn.java index a2f53da955c..70de3aaad4e 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckIn.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.checkin; +package io.sentry.spring7.checkin; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java similarity index 99% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java index dd22f4dc5dc..f5f33c252a3 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdvice.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.checkin; +package io.sentry.spring7.checkin; import com.jakewharton.nopen.annotation.Open; import io.sentry.CheckIn; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdviceConfiguration.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdviceConfiguration.java index 1f948581f18..4ef53629ddd 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInAdviceConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.checkin; +package io.sentry.spring7.checkin; import com.jakewharton.nopen.annotation.Open; import org.aopalliance.aop.Advice; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInPointcutConfiguration.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInPointcutConfiguration.java index d5c7b8e97e6..40d1260a46e 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryCheckInPointcutConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.checkin; +package io.sentry.spring7.checkin; import com.jakewharton.nopen.annotation.Open; import org.jetbrains.annotations.ApiStatus; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryQuartzConfiguration.java similarity index 94% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryQuartzConfiguration.java index 0ef7f4a9a4c..0d26fbeca6a 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentryQuartzConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.checkin; +package io.sentry.spring7.checkin; import com.jakewharton.nopen.annotation.Open; import org.jetbrains.annotations.ApiStatus; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentrySchedulerFactoryBeanCustomizer.java similarity index 92% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentrySchedulerFactoryBeanCustomizer.java index 756624bf1eb..9d65add5483 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.checkin; +package io.sentry.spring7.checkin; import io.sentry.quartz.SentryJobListener; import org.jetbrains.annotations.ApiStatus; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameter.java similarity index 91% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameter.java index 4911419c0b9..3d9e47c3a5f 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.exception; +package io.sentry.spring7.exception; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterAdvice.java similarity index 95% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterAdvice.java index c6537f853c2..cb5cba62a4d 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdvice.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterAdvice.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.exception; +package io.sentry.spring7.exception; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; @@ -21,7 +21,7 @@ @ApiStatus.Internal @Open public class SentryCaptureExceptionParameterAdvice implements MethodInterceptor { - private static final String MECHANISM_TYPE = "SentrySpring6CaptureExceptionParameterAdvice"; + private static final String MECHANISM_TYPE = "SentrySpring7CaptureExceptionParameterAdvice"; private final @NotNull IScopes scopes; public SentryCaptureExceptionParameterAdvice() { diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterConfiguration.java similarity index 92% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterConfiguration.java index 0d4f4274be1..fac7ce3f2e4 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.exception; +package io.sentry.spring7.exception; import com.jakewharton.nopen.annotation.Open; import org.springframework.context.annotation.Configuration; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterPointcutConfiguration.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterPointcutConfiguration.java index 3744182a306..5397babb1e3 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterPointcutConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryCaptureExceptionParameterPointcutConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.exception; +package io.sentry.spring7.exception; import com.jakewharton.nopen.annotation.Open; import org.jetbrains.annotations.NotNull; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryExceptionParameterAdviceConfiguration.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryExceptionParameterAdviceConfiguration.java index aa75510c0a2..b84ba3db373 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/exception/SentryExceptionParameterAdviceConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/exception/SentryExceptionParameterAdviceConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.exception; +package io.sentry.spring7.exception; import com.jakewharton.nopen.annotation.Open; import org.aopalliance.aop.Advice; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryBatchLoaderRegistry.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryBatchLoaderRegistry.java index a75aa281349..591863d906b 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryBatchLoaderRegistry.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryBatchLoaderRegistry.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql; +package io.sentry.spring7.graphql; import static io.sentry.graphql.SentryGraphqlInstrumentation.SENTRY_SCOPES_CONTEXT_KEY; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter.java index 3f34931147c..864b0ae7fd5 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql; +package io.sentry.spring7.graphql; import graphql.GraphQLError; import graphql.execution.DataFetcherExceptionHandlerResult; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler.java b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryDgsSubscriptionHandler.java similarity index 94% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryDgsSubscriptionHandler.java index a7a6cccd3e3..eeb684321e1 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryDgsSubscriptionHandler.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryDgsSubscriptionHandler.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql; +package io.sentry.spring7.graphql; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import io.sentry.IScopes; @@ -11,7 +11,7 @@ public final class SentryDgsSubscriptionHandler implements SentrySubscriptionHandler { public SentryDgsSubscriptionHandler() { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6NetflixDGSGrahQL"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7NetflixDGSGrahQL"); } @Override diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphql22Configuration.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphql22Configuration.java index be94017201e..3b35d2a2c90 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphql22Configuration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphql22Configuration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql; +package io.sentry.spring7.graphql; import com.jakewharton.nopen.annotation.Open; import io.sentry.SentryIntegrationPackageStorage; @@ -23,7 +23,7 @@ public class SentryGraphql22Configuration { public SentryInstrumentation sentryInstrumentationWebMvc( final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebMVC"); return createInstrumentation(beforeSpanCallback, false); } @@ -33,7 +33,7 @@ public SentryInstrumentation sentryInstrumentationWebMvc( public SentryInstrumentation sentryInstrumentationWebflux( final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebFlux"); return createInstrumentation(beforeSpanCallback, true); } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor.java similarity index 94% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor.java index 5eb4374bb1f..2ea390fd2dd 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql; +package io.sentry.spring7.graphql; import org.jetbrains.annotations.ApiStatus; import org.springframework.beans.BeansException; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphqlConfiguration.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphqlConfiguration.java index 611e6bf5279..1c8cbd292ad 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentryGraphqlConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentryGraphqlConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql; +package io.sentry.spring7.graphql; import com.jakewharton.nopen.annotation.Open; import io.sentry.SentryIntegrationPackageStorage; @@ -23,7 +23,7 @@ public class SentryGraphqlConfiguration { public SentryInstrumentation sentryInstrumentationWebMvc( final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebMVC"); return createInstrumentation(beforeSpanCallback, false); } @@ -33,7 +33,7 @@ public SentryInstrumentation sentryInstrumentationWebMvc( public SentryInstrumentation sentryInstrumentationWebflux( final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebFlux"); return createInstrumentation(beforeSpanCallback, true); } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler.java b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentrySpringSubscriptionHandler.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentrySpringSubscriptionHandler.java index eec86f5e8b7..e0b091d494e 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandler.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/graphql/SentrySpringSubscriptionHandler.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql; +package io.sentry.spring7.graphql; import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters; import io.sentry.IScopes; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java similarity index 89% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java index bac77cfafc1..e447d647a3c 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/opentelemetry/SentryOpenTelemetryAgentWithoutAutoInitConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.opentelemetry; +package io.sentry.spring7.opentelemetry; import com.jakewharton.nopen.annotation.Open; import io.sentry.Sentry; @@ -20,7 +20,7 @@ public class SentryOpenTelemetryAgentWithoutAutoInitConfiguration { sentryOpenTelemetryOptionsConfiguration() { return options -> { SentryIntegrationPackageStorage.getInstance() - .addIntegration("SpringBoot3OpenTelemetryAgentWithoutAutoInit"); + .addIntegration("SpringBoot4OpenTelemetryAgentWithoutAutoInit"); options.setOpenTelemetryMode(SentryOpenTelemetryMode.AGENT); }; } diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java similarity index 92% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java index 3e3ef9e2afd..e218dc9ead0 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/opentelemetry/SentryOpenTelemetryNoAgentConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.opentelemetry; +package io.sentry.spring7.opentelemetry; import com.jakewharton.nopen.annotation.Open; import io.opentelemetry.api.OpenTelemetry; @@ -30,7 +30,7 @@ public static ISpanFactory openTelemetrySpanFactory(OpenTelemetry openTelemetry) sentryOpenTelemetryOptionsConfiguration() { return options -> { SentryIntegrationPackageStorage.getInstance() - .addIntegration("SpringBoot3OpenTelemetryNoAgent"); + .addIntegration("SpringBoot4OpenTelemetryNoAgent"); SentryAutoConfigurationCustomizerProvider.skipInit = true; options.setOpenTelemetryMode(SentryOpenTelemetryMode.AGENTLESS_SPRING); }; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/CombinedTransactionNameProvider.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/CombinedTransactionNameProvider.java index 1e142e958dc..a211ee6c759 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/CombinedTransactionNameProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/CombinedTransactionNameProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import io.sentry.protocol.TransactionNameSource; import jakarta.servlet.http.HttpServletRequest; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryAdviceConfiguration.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryAdviceConfiguration.java index c3e6e1ef6af..ec4d45a08ea 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryAdviceConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryAdviceConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import com.jakewharton.nopen.annotation.Open; import org.aopalliance.aop.Advice; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpan.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpan.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpan.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpan.java index 846cf53283a..b19902684b0 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpan.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpan.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import io.sentry.protocol.SentryTransaction; import java.lang.annotation.ElementType; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanAdvice.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanAdvice.java index 668c8d1b0b8..6c9a3287edc 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanAdvice.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanAdvice.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; @@ -22,7 +22,7 @@ */ @Open public class SentrySpanAdvice implements MethodInterceptor { - private static final String TRACE_ORIGIN = "auto.function.spring_jakarta.advice"; + private static final String TRACE_ORIGIN = "auto.function.spring7.advice"; private final @NotNull IScopes scopes; public SentrySpanAdvice() { diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java index f50c93976e5..901321f0213 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientHttpRequestInterceptor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientHttpRequestInterceptor.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import static io.sentry.TypeCheckHint.SPRING_REQUEST_INTERCEPTOR_REQUEST; import static io.sentry.TypeCheckHint.SPRING_REQUEST_INTERCEPTOR_REQUEST_BODY; @@ -28,8 +28,8 @@ @Open public class SentrySpanClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { - private static final String TRACE_ORIGIN_REST_TEMPLATE = "auto.http.spring_jakarta.resttemplate"; - private static final String TRACE_ORIGIN_REST_CLIENT = "auto.http.spring_jakarta.restclient"; + private static final String TRACE_ORIGIN_REST_TEMPLATE = "auto.http.spring7.resttemplate"; + private static final String TRACE_ORIGIN_REST_CLIENT = "auto.http.spring7.restclient"; private final @NotNull IScopes scopes; private final @NotNull String traceOrigin; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java similarity index 97% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java index 1189532c0c4..6726302a83e 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanClientWebRequestFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanClientWebRequestFilter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import static io.sentry.TypeCheckHint.SPRING_EXCHANGE_FILTER_REQUEST; import static io.sentry.TypeCheckHint.SPRING_EXCHANGE_FILTER_RESPONSE; @@ -26,7 +26,7 @@ @Open public class SentrySpanClientWebRequestFilter implements ExchangeFilterFunction { - private static final String TRACE_ORIGIN = "auto.http.spring_jakarta.webclient"; + private static final String TRACE_ORIGIN = "auto.http.spring7.webclient"; private final @NotNull IScopes scopes; public SentrySpanClientWebRequestFilter(final @NotNull IScopes scopes) { diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanPointcutConfiguration.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanPointcutConfiguration.java index 95544c7992d..df3cb7a2798 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentrySpanPointcutConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentrySpanPointcutConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import com.jakewharton.nopen.annotation.Open; import org.jetbrains.annotations.NotNull; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTracingConfiguration.java similarity index 92% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTracingConfiguration.java index ffd3d1d0d79..d91baf1c102 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTracingConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import com.jakewharton.nopen.annotation.Open; import org.springframework.context.annotation.Configuration; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTracingFilter.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTracingFilter.java index d059338c0dc..60f302c4cc4 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTracingFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTracingFilter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import com.jakewharton.nopen.annotation.Open; import io.sentry.BaggageHeader; @@ -36,7 +36,7 @@ public class SentryTracingFilter extends OncePerRequestFilter { /** Operation used by {@link SentryTransaction} created in {@link SentryTracingFilter}. */ private static final String TRANSACTION_OP = "http.server"; - private static final String TRACE_ORIGIN = "auto.http.spring_jakarta.webmvc"; + private static final String TRACE_ORIGIN = "auto.http.spring7.webmvc"; private static final String TRANSACTION_ATTR = "sentry.transaction"; private final @NotNull TransactionNameProvider transactionNameProvider; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransaction.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransaction.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransaction.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransaction.java index 01d2e9e146b..d1f0708895b 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransaction.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransaction.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionAdvice.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransactionAdvice.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionAdvice.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransactionAdvice.java index 95618f76fd7..b16a6ab00ef 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionAdvice.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransactionAdvice.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; @@ -27,7 +27,7 @@ @ApiStatus.Internal @Open public class SentryTransactionAdvice implements MethodInterceptor { - private static final String TRACE_ORIGIN = "auto.function.spring_jakarta.advice"; + private static final String TRACE_ORIGIN = "auto.function.spring7.advice"; private final @NotNull IScopes scopesBeforeForking; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransactionPointcutConfiguration.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransactionPointcutConfiguration.java index 6f2d886b750..c759452a5c1 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SentryTransactionPointcutConfiguration.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SentryTransactionPointcutConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import com.jakewharton.nopen.annotation.Open; import org.jetbrains.annotations.NotNull; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SpringMvcTransactionNameProvider.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SpringMvcTransactionNameProvider.java index 0dc8dd8a8c9..59402b23e9e 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringMvcTransactionNameProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SpringMvcTransactionNameProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import io.sentry.protocol.TransactionNameSource; import jakarta.servlet.http.HttpServletRequest; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SpringServletTransactionNameProvider.java similarity index 94% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SpringServletTransactionNameProvider.java index e0beda65a0b..44c5f2d7918 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/SpringServletTransactionNameProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/SpringServletTransactionNameProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import io.sentry.protocol.TransactionNameSource; import jakarta.servlet.http.HttpServletRequest; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/TransactionNameProvider.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/TransactionNameProvider.java index 7c0eac8152f..7e63c655b38 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/TransactionNameProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import io.sentry.protocol.TransactionNameSource; import jakarta.servlet.http.HttpServletRequest; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameWithSource.java b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/TransactionNameWithSource.java similarity index 95% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameWithSource.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/tracing/TransactionNameWithSource.java index f913b895cf7..19ea4afb54b 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/tracing/TransactionNameWithSource.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/tracing/TransactionNameWithSource.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing; +package io.sentry.spring7.tracing; import io.sentry.protocol.TransactionNameSource; import org.jetbrains.annotations.ApiStatus; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/AbstractSentryWebFilter.java similarity index 99% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/AbstractSentryWebFilter.java index 57b7b86e40f..0b41974a69d 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/AbstractSentryWebFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/AbstractSentryWebFilter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.spring7.webflux; import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_REQUEST; import static io.sentry.TypeCheckHint.WEBFLUX_FILTER_RESPONSE; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java similarity index 98% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java index 447b0a2b8fb..3d6857cb648 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryRequestResolver.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryRequestResolver.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.spring7.webflux; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryScheduleHook.java similarity index 95% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryScheduleHook.java index 57a74732ea8..625a1d4bd14 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryScheduleHook.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryScheduleHook.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.spring7.webflux; import io.sentry.IScopes; import io.sentry.ISentryLifecycleToken; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebExceptionHandler.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebExceptionHandler.java index 1e1e387eb21..19e6fa0fc4e 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebExceptionHandler.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebExceptionHandler.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.spring7.webflux; import static io.sentry.TypeCheckHint.WEBFLUX_EXCEPTION_HANDLER_EXCHANGE; import static io.sentry.TypeCheckHint.WEBFLUX_EXCEPTION_HANDLER_REQUEST; @@ -27,7 +27,7 @@ // at -1 @ApiStatus.Experimental public final class SentryWebExceptionHandler implements WebExceptionHandler { - public static final String MECHANISM_TYPE = "Spring6WebFluxExceptionResolver"; + public static final String MECHANISM_TYPE = "Spring7WebFluxExceptionResolver"; private final @NotNull IScopes scopes; public SentryWebExceptionHandler(final @NotNull IScopes scopes) { diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebFilter.java similarity index 92% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebFilter.java index 0ec4b44a0e5..508ddbaf7ce 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilter.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebFilter.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.spring7.webflux; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScope; @@ -18,7 +18,7 @@ @Open public class SentryWebFilter extends AbstractSentryWebFilter { - private static final String TRACE_ORIGIN = "auto.spring_jakarta.webflux"; + private static final String TRACE_ORIGIN = "auto.spring7.webflux"; public SentryWebFilter(final @NotNull IScopes scopes) { super(scopes); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebFilterWithThreadLocalAccessor.java similarity index 94% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebFilterWithThreadLocalAccessor.java index 7748d43a012..ff6c61353a9 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/SentryWebFilterWithThreadLocalAccessor.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/SentryWebFilterWithThreadLocalAccessor.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.spring7.webflux; import io.sentry.IScope; import io.sentry.IScopes; @@ -16,7 +16,7 @@ @ApiStatus.Experimental public final class SentryWebFilterWithThreadLocalAccessor extends AbstractSentryWebFilter { - public static final String TRACE_ORIGIN = "auto.spring_jakarta.webflux"; + public static final String TRACE_ORIGIN = "auto.spring7.webflux"; public SentryWebFilterWithThreadLocalAccessor(final @NotNull IScopes scopes) { super(scopes); diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/TransactionNameProvider.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/TransactionNameProvider.java similarity index 96% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/TransactionNameProvider.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/TransactionNameProvider.java index 33b3eba36be..0691c570da0 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/TransactionNameProvider.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/TransactionNameProvider.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux; +package io.sentry.spring7.webflux; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/reactor/ReactorUtils.java similarity index 79% rename from sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java rename to sentry-spring-7/src/main/java/io/sentry/spring7/webflux/reactor/ReactorUtils.java index b3249570369..c23faf62f57 100644 --- a/sentry-spring-7/src/main/java/io/sentry/spring/jakarta/webflux/reactor/ReactorUtils.java +++ b/sentry-spring-7/src/main/java/io/sentry/spring7/webflux/reactor/ReactorUtils.java @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux.reactor; +package io.sentry.spring7.webflux.reactor; import io.sentry.reactor.SentryReactorUtils; diff --git a/sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer b/sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer index 69f38f69c51..0101bfd4ec2 100644 --- a/sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer +++ b/sentry-spring-7/src/main/resources/META-INF/services/jakarta.servlet.ServletContainerInitializer @@ -1 +1 @@ -io.sentry.spring.jakarta.SentrySpringServletContainerInitializer +io.sentry.spring7.SentrySpringServletContainerInitializer diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/ContextTagsEventProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/ContextTagsEventProcessorTest.kt similarity index 98% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/ContextTagsEventProcessorTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/ContextTagsEventProcessorTest.kt index 5f91887a50b..ddf19c41e6a 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/ContextTagsEventProcessorTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/ContextTagsEventProcessorTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.SentryEvent import io.sentry.SentryOptions diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/EnableSentryTest.kt similarity index 97% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/EnableSentryTest.kt index d1b289261d4..d6588ad4013 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/EnableSentryTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/EnableSentryTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.EventProcessor import io.sentry.IScopes @@ -6,7 +6,6 @@ import io.sentry.ITransportFactory import io.sentry.Integration import io.sentry.Sentry import io.sentry.SentryOptions -import io.sentry.spring7.BuildConfig import io.sentry.transport.ITransport import kotlin.test.Test import org.assertj.core.api.Assertions.assertThat @@ -47,13 +46,13 @@ class EnableSentryTest { contextRunner.run { assertThat(it).hasSingleBean(SentryOptions::class.java) val options = it.getBean(SentryOptions::class.java) - assertThat(options.sentryClientName).isEqualTo("sentry.java.spring.jakarta") + assertThat(options.sentryClientName).isEqualTo("sentry.java.spring-7") assertThat(options.sdkVersion).isNotNull - assertThat(options.sdkVersion!!.name).isEqualTo("sentry.java.spring.jakarta") + assertThat(options.sdkVersion!!.name).isEqualTo("sentry.java.spring-7") assertThat(options.sdkVersion!!.version).isEqualTo(BuildConfig.VERSION_NAME) assertThat(options.sdkVersion!!.packageSet.map { pkg -> pkg.name }) - .contains("maven:io.sentry:sentry-spring-jakarta") - assertThat(options.sdkVersion!!.integrationSet).contains("Spring6") + .contains("maven:io.sentry:sentry-spring-7") + assertThat(options.sdkVersion!!.integrationSet).contains("Spring7") } } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/HttpServletRequestSentryUserProviderTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/HttpServletRequestSentryUserProviderTest.kt similarity index 98% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/HttpServletRequestSentryUserProviderTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/HttpServletRequestSentryUserProviderTest.kt index f2cce25574d..5254270a05c 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/HttpServletRequestSentryUserProviderTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/HttpServletRequestSentryUserProviderTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.SentryOptions import java.security.Principal diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryCheckInAdviceTest.kt similarity index 97% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryCheckInAdviceTest.kt index c6edd834530..e15affdbdd5 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryCheckInAdviceTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.CheckIn import io.sentry.CheckInStatus @@ -7,9 +7,9 @@ import io.sentry.ISentryLifecycleToken import io.sentry.Sentry import io.sentry.SentryOptions import io.sentry.protocol.SentryId -import io.sentry.spring.jakarta.checkin.SentryCheckIn -import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration -import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration +import io.sentry.spring7.checkin.SentryCheckIn +import io.sentry.spring7.checkin.SentryCheckInAdviceConfiguration +import io.sentry.spring7.checkin.SentryCheckInPointcutConfiguration import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryExceptionResolverTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryExceptionResolverTest.kt similarity index 97% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryExceptionResolverTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryExceptionResolverTest.kt index 2513b5a7abb..94f5210a66f 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryExceptionResolverTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryExceptionResolverTest.kt @@ -1,11 +1,11 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.Hint import io.sentry.IScopes import io.sentry.SentryEvent import io.sentry.SentryLevel import io.sentry.exception.ExceptionMechanismException -import io.sentry.spring.jakarta.tracing.TransactionNameProvider +import io.sentry.spring7.tracing.TransactionNameProvider import jakarta.servlet.http.HttpServletRequest import jakarta.servlet.http.HttpServletResponse import kotlin.test.Test diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryInitBeanPostProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryInitBeanPostProcessorTest.kt similarity index 95% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryInitBeanPostProcessorTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryInitBeanPostProcessorTest.kt index 70883e19f55..0beba9abcf7 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryInitBeanPostProcessorTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryInitBeanPostProcessorTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.IScopes import kotlin.test.Test diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryRequestHttpServletRequestProcessorTest.kt similarity index 95% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessorTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryRequestHttpServletRequestProcessorTest.kt index a63f00a788f..4c2276275f7 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryRequestHttpServletRequestProcessorTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryRequestHttpServletRequestProcessorTest.kt @@ -1,10 +1,10 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.Hint import io.sentry.IScopes import io.sentry.SentryEvent import io.sentry.SentryOptions -import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider +import io.sentry.spring7.tracing.SpringMvcTransactionNameProvider import jakarta.servlet.http.HttpServletRequest import java.net.URI import kotlin.test.Test diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentrySpringFilterTest.kt similarity index 99% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentrySpringFilterTest.kt index 6c8db2fc097..639b1642a51 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentrySpringFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentrySpringFilterTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.Breadcrumb import io.sentry.IScope diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryTaskDecoratorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryTaskDecoratorTest.kt similarity index 97% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryTaskDecoratorTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryTaskDecoratorTest.kt index 1f0f74d25cf..d589537d59c 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryTaskDecoratorTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryTaskDecoratorTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.Sentry import io.sentry.test.initForTest diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt similarity index 99% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt index c790f3e9997..6284e8241ae 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SentryUserFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SentryUserFilterTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.IScopes import io.sentry.SentryOptions diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringProfilesEventProcessorTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SpringProfilesEventProcessorTest.kt similarity index 98% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringProfilesEventProcessorTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SpringProfilesEventProcessorTest.kt index f5e642a99a4..07e122a688d 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringProfilesEventProcessorTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SpringProfilesEventProcessorTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.ITransportFactory import io.sentry.Sentry diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringSecuritySentryUserProviderTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SpringSecuritySentryUserProviderTest.kt similarity index 98% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringSecuritySentryUserProviderTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/SpringSecuritySentryUserProviderTest.kt index 80f8efc9ce2..6330405999c 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/SpringSecuritySentryUserProviderTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/SpringSecuritySentryUserProviderTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta +package io.sentry.spring7 import io.sentry.SentryOptions import kotlin.test.Test diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/exception/SentryCaptureExceptionParameterAdviceTest.kt similarity index 95% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdviceTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/exception/SentryCaptureExceptionParameterAdviceTest.kt index f9f1c38dd16..854d99c905d 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/exception/SentryCaptureExceptionParameterAdviceTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/exception/SentryCaptureExceptionParameterAdviceTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.exception +package io.sentry.spring7.exception import io.sentry.Hint import io.sentry.IScopes @@ -43,7 +43,7 @@ class SentryCaptureExceptionParameterAdviceTest { check { assertTrue(it is ExceptionMechanismException) assertEquals(exception, it.throwable) - assertEquals("SentrySpring6CaptureExceptionParameterAdvice", it.exceptionMechanism.type) + assertEquals("SentrySpring7CaptureExceptionParameterAdvice", it.exceptionMechanism.type) }, any(), ) diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandlerTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/graphql/SentrySpringSubscriptionHandlerTest.kt similarity index 98% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandlerTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/graphql/SentrySpringSubscriptionHandlerTest.kt index a8039ff7660..c216b4595d2 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/graphql/SentrySpringSubscriptionHandlerTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/graphql/SentrySpringSubscriptionHandlerTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.graphql +package io.sentry.spring7.graphql import graphql.execution.instrumentation.parameters.InstrumentationFieldFetchParameters import graphql.language.Document diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/mvc/SentrySpringIntegrationTest.kt similarity index 95% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/mvc/SentrySpringIntegrationTest.kt index 418b6fd0a39..c9a05dbdad6 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/mvc/SentrySpringIntegrationTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/mvc/SentrySpringIntegrationTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.mvc +package io.sentry.spring7.mvc import io.sentry.IScopes import io.sentry.ITransportFactory @@ -8,19 +8,19 @@ import io.sentry.SpanDataConvention import io.sentry.SpanStatus import io.sentry.checkEvent import io.sentry.checkTransaction -import io.sentry.spring.jakarta.EnableSentry -import io.sentry.spring.jakarta.SentryExceptionResolver -import io.sentry.spring.jakarta.SentrySpringFilter -import io.sentry.spring.jakarta.SentryTaskDecorator -import io.sentry.spring.jakarta.SentryUserFilter -import io.sentry.spring.jakarta.SentryUserProvider -import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider -import io.sentry.spring.jakarta.exception.SentryCaptureExceptionParameter -import io.sentry.spring.jakarta.exception.SentryCaptureExceptionParameterConfiguration -import io.sentry.spring.jakarta.tracing.SentrySpanClientWebRequestFilter -import io.sentry.spring.jakarta.tracing.SentryTracingConfiguration -import io.sentry.spring.jakarta.tracing.SentryTracingFilter -import io.sentry.spring.jakarta.tracing.SentryTransaction +import io.sentry.spring7.EnableSentry +import io.sentry.spring7.SentryExceptionResolver +import io.sentry.spring7.SentrySpringFilter +import io.sentry.spring7.SentryTaskDecorator +import io.sentry.spring7.SentryUserFilter +import io.sentry.spring7.SentryUserProvider +import io.sentry.spring7.SpringSecuritySentryUserProvider +import io.sentry.spring7.exception.SentryCaptureExceptionParameter +import io.sentry.spring7.exception.SentryCaptureExceptionParameterConfiguration +import io.sentry.spring7.tracing.SentrySpanClientWebRequestFilter +import io.sentry.spring7.tracing.SentryTracingConfiguration +import io.sentry.spring7.tracing.SentryTracingFilter +import io.sentry.spring7.tracing.SentryTransaction import io.sentry.transport.ITransport import java.time.Duration import java.util.concurrent.Callable diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanAdviceTest.kt similarity index 97% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanAdviceTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanAdviceTest.kt index 475c040175c..35de0b6dd4d 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentrySpanAdviceTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentrySpanAdviceTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing +package io.sentry.spring7.tracing import io.sentry.IScopes import io.sentry.Scope @@ -52,7 +52,7 @@ class SentrySpanAdviceTest { assertEquals(1, result) assertEquals(1, tx.spans.size) assertNull(tx.spans.first().description) - assertEquals("auto.function.spring_jakarta.advice", tx.spans.first().spanContext.origin) + assertEquals("auto.function.spring7.advice", tx.spans.first().spanContext.origin) assertEquals("ClassAnnotatedSampleService.hello", tx.spans.first().operation) } diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTracingFilterTest.kt similarity index 98% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTracingFilterTest.kt index dfb8376286a..f63b5d9631c 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTracingFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTracingFilterTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing +package io.sentry.spring7.tracing import io.sentry.ILogger import io.sentry.IScopes @@ -122,7 +122,7 @@ class SentryTracingFilterTest { assertNotNull(it.customSamplingContext?.get("request")) assertTrue(it.customSamplingContext?.get("request") is HttpServletRequest) assertTrue(it.isBindToScope) - assertThat(it.origin).isEqualTo("auto.http.spring_jakarta.webmvc") + assertThat(it.origin).isEqualTo("auto.http.spring7.webmvc") }, ) verify(fixture.chain).doFilter(fixture.request, fixture.response) @@ -325,7 +325,7 @@ class SentryTracingFilterTest { "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET" ) fixture.options.tracesSampleRate = null - fixture.options.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.webmvc")) + fixture.options.setIgnoredSpanOrigins(listOf("auto.http.spring7.webmvc")) val filter = fixture.getSut( sentryTraceHeader = sentryTraceHeaderString, @@ -384,7 +384,7 @@ class SentryTracingFilterTest { assertNotNull(it.customSamplingContext?.get("request")) assertTrue(it.customSamplingContext?.get("request") is HttpServletRequest) assertTrue(it.isBindToScope) - assertThat(it.origin).isEqualTo("auto.http.spring_jakarta.webmvc") + assertThat(it.origin).isEqualTo("auto.http.spring7.webmvc") }, ) verify(asyncChain).doFilter(fixture.request, fixture.response) @@ -443,7 +443,7 @@ class SentryTracingFilterTest { assertNotNull(it.customSamplingContext?.get("request")) assertTrue(it.customSamplingContext?.get("request") is HttpServletRequest) assertTrue(it.isBindToScope) - assertThat(it.origin).isEqualTo("auto.http.spring_jakarta.webmvc") + assertThat(it.origin).isEqualTo("auto.http.spring7.webmvc") }, ) verify(fixture.scopes).continueTrace(eq(sentryTrace), eq(baggage)) diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTransactionAdviceTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTransactionAdviceTest.kt similarity index 99% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTransactionAdviceTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTransactionAdviceTest.kt index ed9c1f9abb4..1c2359d4489 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/tracing/SentryTransactionAdviceTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/tracing/SentryTransactionAdviceTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.tracing +package io.sentry.spring7.tracing import io.sentry.IScopes import io.sentry.ISentryLifecycleToken @@ -53,7 +53,7 @@ class SentryTransactionAdviceTest { any(), check { assertTrue(it.isBindToScope) - assertThat(it.origin).isEqualTo("auto.function.spring_jakarta.advice") + assertThat(it.origin).isEqualTo("auto.function.spring7.advice") }, ) ) diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryScheduleHookTest.kt similarity index 97% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryScheduleHookTest.kt index a5c16182fa7..32b56e2e27c 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryScheduleHookTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryScheduleHookTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux +package io.sentry.spring7.webflux import io.sentry.Sentry import io.sentry.test.initForTest diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt similarity index 97% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt index b14f1b5910c..495f45ac650 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebFluxTracingFilterTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebFluxTracingFilterTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux +package io.sentry.spring7.webflux import io.sentry.Breadcrumb import io.sentry.Hint @@ -17,7 +17,7 @@ import io.sentry.TransactionOptions import io.sentry.protocol.SentryId import io.sentry.protocol.SentryTransaction import io.sentry.protocol.TransactionNameSource -import io.sentry.spring.jakarta.webflux.AbstractSentryWebFilter.SENTRY_SCOPES_KEY +import io.sentry.spring7.webflux.AbstractSentryWebFilter.SENTRY_SCOPES_KEY import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull @@ -131,7 +131,7 @@ class SentryWebFluxTracingFilterTest { assertNotNull(it.customSamplingContext?.get("request")) assertTrue(it.customSamplingContext?.get("request") is ServerHttpRequest) assertTrue(it.isBindToScope) - assertThat(it.origin).isEqualTo("auto.spring_jakarta.webflux") + assertThat(it.origin).isEqualTo("auto.spring7.webflux") }, ) verify(fixture.chain).filter(fixture.exchange) @@ -360,7 +360,7 @@ class SentryWebFluxTracingFilterTest { "sentry-public_key=502f25099c204a2fbf4cb16edc5975d1,sentry-sample_rate=1,sentry-trace_id=2722d9f6ec019ade60c776169d9a8904,sentry-transaction=HTTP%20GET" ) fixture.options.tracesSampleRate = null - fixture.options.setIgnoredSpanOrigins(listOf("auto.spring_jakarta.webflux")) + fixture.options.setIgnoredSpanOrigins(listOf("auto.spring7.webflux")) val filter = fixture.getSut( sentryTraceHeader = sentryTraceHeaderString, diff --git a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebfluxIntegrationTest.kt similarity index 99% rename from sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt rename to sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebfluxIntegrationTest.kt index bc07e5dbbf9..2d44b40c27d 100644 --- a/sentry-spring-7/src/test/kotlin/io/sentry/spring/jakarta/webflux/SentryWebfluxIntegrationTest.kt +++ b/sentry-spring-7/src/test/kotlin/io/sentry/spring7/webflux/SentryWebfluxIntegrationTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.jakarta.webflux +package io.sentry.spring7.webflux import io.sentry.IScopes import io.sentry.ITransportFactory diff --git a/sentry-spring-boot-4-starter/build.gradle.kts b/sentry-spring-boot-4-starter/build.gradle.kts index 503a66dd004..2c8eab0ba66 100644 --- a/sentry-spring-boot-4-starter/build.gradle.kts +++ b/sentry-spring-boot-4-starter/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -18,8 +18,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-spring-boot-4/api/sentry-spring-boot-4.api b/sentry-spring-boot-4/api/sentry-spring-boot-4.api index d213413d836..4eb01c46ded 100644 --- a/sentry-spring-boot-4/api/sentry-spring-boot-4.api +++ b/sentry-spring-boot-4/api/sentry-spring-boot-4.api @@ -1,51 +1,56 @@ -public class io/sentry/spring/boot/jakarta/InAppIncludesResolver : org/springframework/context/ApplicationContextAware { +public final class io/sentry/spring/boot4/BuildConfig { + public static final field SENTRY_SPRING_BOOT_4_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public class io/sentry/spring/boot4/InAppIncludesResolver : org/springframework/context/ApplicationContextAware { public fun ()V public fun resolveInAppIncludes ()Ljava/util/List; public fun setApplicationContext (Lorg/springframework/context/ApplicationContext;)V } -public class io/sentry/spring/boot/jakarta/SentryAutoConfiguration { +public class io/sentry/spring/boot4/SentryAutoConfiguration { public fun ()V } -public class io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration { +public class io/sentry/spring/boot4/SentryLogbackAppenderAutoConfiguration { public fun ()V - public fun sentryLogbackInitializer (Lio/sentry/spring/boot/jakarta/SentryProperties;)Lio/sentry/spring/boot/jakarta/SentryLogbackInitializer; + public fun sentryLogbackInitializer (Lio/sentry/spring/boot4/SentryProperties;)Lio/sentry/spring/boot4/SentryLogbackInitializer; } -public class io/sentry/spring/boot/jakarta/SentryLogbackInitializer : org/springframework/context/event/GenericApplicationListener { - public fun (Lio/sentry/spring/boot/jakarta/SentryProperties;)V +public class io/sentry/spring/boot4/SentryLogbackInitializer : org/springframework/context/event/GenericApplicationListener { + public fun (Lio/sentry/spring/boot4/SentryProperties;)V public fun onApplicationEvent (Lorg/springframework/context/ApplicationEvent;)V public fun supportsEventType (Lorg/springframework/core/ResolvableType;)Z } -public class io/sentry/spring/boot/jakarta/SentryProperties : io/sentry/SentryOptions { +public class io/sentry/spring/boot4/SentryProperties : io/sentry/SentryOptions { public fun ()V public fun getExceptionResolverOrder ()I - public fun getGraphql ()Lio/sentry/spring/boot/jakarta/SentryProperties$Graphql; - public fun getLogging ()Lio/sentry/spring/boot/jakarta/SentryProperties$Logging; - public fun getReactive ()Lio/sentry/spring/boot/jakarta/SentryProperties$Reactive; + public fun getGraphql ()Lio/sentry/spring/boot4/SentryProperties$Graphql; + public fun getLogging ()Lio/sentry/spring/boot4/SentryProperties$Logging; + public fun getReactive ()Lio/sentry/spring/boot4/SentryProperties$Reactive; public fun getUserFilterOrder ()Ljava/lang/Integer; public fun isEnableAotCompatibility ()Z public fun isKeepTransactionsOpenForAsyncResponses ()Z public fun isUseGitCommitIdAsRelease ()Z public fun setEnableAotCompatibility (Z)V public fun setExceptionResolverOrder (I)V - public fun setGraphql (Lio/sentry/spring/boot/jakarta/SentryProperties$Graphql;)V + public fun setGraphql (Lio/sentry/spring/boot4/SentryProperties$Graphql;)V public fun setKeepTransactionsOpenForAsyncResponses (Z)V - public fun setLogging (Lio/sentry/spring/boot/jakarta/SentryProperties$Logging;)V - public fun setReactive (Lio/sentry/spring/boot/jakarta/SentryProperties$Reactive;)V + public fun setLogging (Lio/sentry/spring/boot4/SentryProperties$Logging;)V + public fun setReactive (Lio/sentry/spring/boot4/SentryProperties$Reactive;)V public fun setUseGitCommitIdAsRelease (Z)V public fun setUserFilterOrder (Ljava/lang/Integer;)V } -public class io/sentry/spring/boot/jakarta/SentryProperties$Graphql { +public class io/sentry/spring/boot4/SentryProperties$Graphql { public fun ()V public fun getIgnoredErrorTypes ()Ljava/util/List; public fun setIgnoredErrorTypes (Ljava/util/List;)V } -public class io/sentry/spring/boot/jakarta/SentryProperties$Logging { +public class io/sentry/spring/boot4/SentryProperties$Logging { public fun ()V public fun getLoggers ()Ljava/util/List; public fun getMinimumBreadcrumbLevel ()Lorg/slf4j/event/Level; @@ -59,35 +64,30 @@ public class io/sentry/spring/boot/jakarta/SentryProperties$Logging { public fun setMinimumLevel (Lorg/slf4j/event/Level;)V } -public class io/sentry/spring/boot/jakarta/SentryProperties$Reactive { +public class io/sentry/spring/boot4/SentryProperties$Reactive { public fun ()V public fun isThreadLocalAccessorEnabled ()Z public fun setThreadLocalAccessorEnabled (Z)V } -public class io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration { +public class io/sentry/spring/boot4/SentryWebfluxAutoConfiguration { public fun ()V - public fun sentryWebExceptionHandler (Lio/sentry/IScopes;)Lio/sentry/spring/jakarta/webflux/SentryWebExceptionHandler; + public fun sentryWebExceptionHandler (Lio/sentry/IScopes;)Lio/sentry/spring7/webflux/SentryWebExceptionHandler; } -public class io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration { +public class io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration { public fun ()V - public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; - public static fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; - public fun sentryInstrumentationWebMvc (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; - public fun sentryInstrumentationWebflux (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; + public fun exceptionResolverAdapter ()Lio/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter; + public static fun graphqlBeanPostProcessor ()Lio/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor; + public fun sentryInstrumentationWebMvc (Lio/sentry/spring/boot4/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; + public fun sentryInstrumentationWebflux (Lio/sentry/spring/boot4/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql22/SentryInstrumentation; } -public class io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration { +public class io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration { public fun ()V - public fun exceptionResolverAdapter ()Lio/sentry/spring/jakarta/graphql/SentryDataFetcherExceptionResolverAdapter; - public static fun graphqlBeanPostProcessor ()Lio/sentry/spring/jakarta/graphql/SentryGraphqlBeanPostProcessor; - public fun sentryInstrumentationWebMvc (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; - public fun sentryInstrumentationWebflux (Lio/sentry/spring/boot/jakarta/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; -} - -public final class io/sentry/spring/boot4/BuildConfig { - public static final field SENTRY_SPRING_BOOT_4_SDK_NAME Ljava/lang/String; - public static final field VERSION_NAME Ljava/lang/String; + public fun exceptionResolverAdapter ()Lio/sentry/spring7/graphql/SentryDataFetcherExceptionResolverAdapter; + public static fun graphqlBeanPostProcessor ()Lio/sentry/spring7/graphql/SentryGraphqlBeanPostProcessor; + public fun sentryInstrumentationWebMvc (Lio/sentry/spring/boot4/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; + public fun sentryInstrumentationWebflux (Lio/sentry/spring/boot4/SentryProperties;Lorg/springframework/beans/factory/ObjectProvider;)Lio/sentry/graphql/SentryInstrumentation; } diff --git a/sentry-spring-boot-4/build.gradle.kts b/sentry-spring-boot-4/build.gradle.kts index 16f9e46a40f..980fa7416e3 100644 --- a/sentry-spring-boot-4/build.gradle.kts +++ b/sentry-spring-boot-4/build.gradle.kts @@ -5,8 +5,9 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") jacoco + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.spring) alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) alias(libs.plugins.buildconfig) @@ -19,8 +20,11 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions { + jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) + languageVersion.set(org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9) + freeCompilerArgs.add("-Xjsr305=strict") + } } dependencies { @@ -72,6 +76,7 @@ dependencies { testImplementation(projects.sentryReactor) testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.Libs.kotlinReflect) testImplementation(platform(SpringBootPlugin.BOM_COORDINATES)) testImplementation(libs.context.propagation) testImplementation(libs.kotlin.test.junit) @@ -80,7 +85,13 @@ dependencies { testImplementation(libs.okhttp.mockwebserver) testImplementation(libs.otel) testImplementation(libs.otel.extension.autoconfigure.spi) - testImplementation(libs.springboot4.otel) + /** + * Adding a version of opentelemetry-spring-boot-starter that doesn't support Spring Boot 4 causes + * java.lang.IllegalArgumentException: Could not find class + * [org.springframework.boot.autoconfigure.web.client.RestClientAutoConfiguration] + * https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/14363 + */ + // testImplementation(libs.springboot4.otel) testImplementation(libs.springboot4.starter) testImplementation(libs.springboot4.starter.aop) testImplementation(libs.springboot4.starter.graphql) @@ -89,6 +100,8 @@ dependencies { testImplementation(libs.springboot4.starter.test) testImplementation(libs.springboot4.starter.web) testImplementation(libs.springboot4.starter.webflux) + testImplementation(libs.springboot4.starter.restclient) + testImplementation(libs.springboot4.starter.webclient) } configure { test { java.srcDir("src/test/java") } } @@ -143,3 +156,12 @@ tasks.jar { ) } } + +kotlin { + explicitApi() + compilerOptions { + // skip metadata version check, as Spring 7 / Spring Boot 4 is + // compiled against a newer version of Kotlin + freeCompilerArgs.add("-Xskip-metadata-version-check") + } +} diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/InAppIncludesResolver.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/InAppIncludesResolver.java similarity index 97% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/InAppIncludesResolver.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/InAppIncludesResolver.java index 2534ad14d19..ec65a149198 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/InAppIncludesResolver.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/InAppIncludesResolver.java @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import com.jakewharton.nopen.annotation.Open; import java.util.Collections; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java similarity index 85% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java index e6861fb035a..618895e51fe 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryAutoConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import com.jakewharton.nopen.annotation.Open; import graphql.GraphQLError; @@ -14,33 +14,32 @@ import io.sentry.SentryOptions; import io.sentry.protocol.SdkVersion; import io.sentry.quartz.SentryJobListener; -import io.sentry.spring.boot.jakarta.graphql.SentryGraphql22AutoConfiguration; -import io.sentry.spring.boot.jakarta.graphql.SentryGraphqlAutoConfiguration; -import io.sentry.spring.boot4.BuildConfig; -import io.sentry.spring.jakarta.ContextTagsEventProcessor; -import io.sentry.spring.jakarta.SentryExceptionResolver; -import io.sentry.spring.jakarta.SentryRequestResolver; -import io.sentry.spring.jakarta.SentrySpringFilter; -import io.sentry.spring.jakarta.SentryUserFilter; -import io.sentry.spring.jakarta.SentryUserProvider; -import io.sentry.spring.jakarta.SentryWebConfiguration; -import io.sentry.spring.jakarta.SpringProfilesEventProcessor; -import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider; -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.opentelemetry.SentryOpenTelemetryAgentWithoutAutoInitConfiguration; -import io.sentry.spring.jakarta.opentelemetry.SentryOpenTelemetryNoAgentConfiguration; -import io.sentry.spring.jakarta.tracing.CombinedTransactionNameProvider; -import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration; -import io.sentry.spring.jakarta.tracing.SentrySpanPointcutConfiguration; -import io.sentry.spring.jakarta.tracing.SentryTracingFilter; -import io.sentry.spring.jakarta.tracing.SentryTransactionPointcutConfiguration; -import io.sentry.spring.jakarta.tracing.SpringMvcTransactionNameProvider; -import io.sentry.spring.jakarta.tracing.SpringServletTransactionNameProvider; -import io.sentry.spring.jakarta.tracing.TransactionNameProvider; +import io.sentry.spring.boot4.graphql.SentryGraphql22AutoConfiguration; +import io.sentry.spring.boot4.graphql.SentryGraphqlAutoConfiguration; +import io.sentry.spring7.ContextTagsEventProcessor; +import io.sentry.spring7.SentryExceptionResolver; +import io.sentry.spring7.SentryRequestResolver; +import io.sentry.spring7.SentrySpringFilter; +import io.sentry.spring7.SentryUserFilter; +import io.sentry.spring7.SentryUserProvider; +import io.sentry.spring7.SentryWebConfiguration; +import io.sentry.spring7.SpringProfilesEventProcessor; +import io.sentry.spring7.SpringSecuritySentryUserProvider; +import io.sentry.spring7.checkin.SentryCheckInAdviceConfiguration; +import io.sentry.spring7.checkin.SentryCheckInPointcutConfiguration; +import io.sentry.spring7.checkin.SentryQuartzConfiguration; +import io.sentry.spring7.exception.SentryCaptureExceptionParameterPointcutConfiguration; +import io.sentry.spring7.exception.SentryExceptionParameterAdviceConfiguration; +import io.sentry.spring7.opentelemetry.SentryOpenTelemetryAgentWithoutAutoInitConfiguration; +import io.sentry.spring7.opentelemetry.SentryOpenTelemetryNoAgentConfiguration; +import io.sentry.spring7.tracing.CombinedTransactionNameProvider; +import io.sentry.spring7.tracing.SentryAdviceConfiguration; +import io.sentry.spring7.tracing.SentrySpanPointcutConfiguration; +import io.sentry.spring7.tracing.SentryTracingFilter; +import io.sentry.spring7.tracing.SentryTransactionPointcutConfiguration; +import io.sentry.spring7.tracing.SpringMvcTransactionNameProvider; +import io.sentry.spring7.tracing.SpringServletTransactionNameProvider; +import io.sentry.spring7.tracing.TransactionNameProvider; import io.sentry.transport.ITransportGate; import io.sentry.transport.apache.ApacheHttpClientTransportFactory; import jakarta.servlet.http.HttpServletRequest; @@ -88,7 +87,7 @@ public class SentryAutoConfiguration { static { SentryIntegrationPackageStorage.getInstance() - .addPackage("maven:io.sentry:sentry-spring-boot-starter-jakarta", BuildConfig.VERSION_NAME); + .addPackage("maven:io.sentry:sentry-spring-boot-4-starter", BuildConfig.VERSION_NAME); } /** Registers general purpose Sentry related beans. */ @@ -414,35 +413,47 @@ static class SentrySpanPointcutAutoConfiguration {} } @Configuration(proxyBeanMethods = false) - @AutoConfigureBefore(RestTemplateAutoConfiguration.class) - @ConditionalOnClass(RestTemplate.class) + @ConditionalOnClass({RestTemplate.class, RestTemplateAutoConfiguration.class}) @Open - static class SentryPerformanceRestTemplateConfiguration { - @Bean - public SentrySpanRestTemplateCustomizer sentrySpanRestTemplateCustomizer(IScopes scopes) { - return new SentrySpanRestTemplateCustomizer(scopes); + static class SentryPerformanceRestTemplateConfigurationWrapper { + @Configuration(proxyBeanMethods = false) + @AutoConfigureBefore(RestTemplateAutoConfiguration.class) + @Open + static class SentryPerformanceRestTemplateConfiguration { + @Bean + public SentrySpanRestTemplateCustomizer sentrySpanRestTemplateCustomizer(IScopes scopes) { + return new SentrySpanRestTemplateCustomizer(scopes); + } } } @Configuration(proxyBeanMethods = false) - @AutoConfigureBefore(RestClientAutoConfiguration.class) - @ConditionalOnClass(RestClient.class) + @ConditionalOnClass({RestClient.class, RestClientAutoConfiguration.class}) @Open - static class SentrySpanRestClientConfiguration { - @Bean - public SentrySpanRestClientCustomizer sentrySpanRestClientCustomizer(IScopes scopes) { - return new SentrySpanRestClientCustomizer(scopes); + static class SentrySpanRestClientConfigurationWrapper { + @Configuration(proxyBeanMethods = false) + @AutoConfigureBefore(RestClientAutoConfiguration.class) + @Open + static class SentrySpanRestClientConfiguration { + @Bean + public SentrySpanRestClientCustomizer sentrySpanRestClientCustomizer(IScopes scopes) { + return new SentrySpanRestClientCustomizer(scopes); + } } } @Configuration(proxyBeanMethods = false) - @AutoConfigureBefore(WebClientAutoConfiguration.class) - @ConditionalOnClass(WebClient.class) + @ConditionalOnClass({WebClient.class, WebClientAutoConfiguration.class}) @Open - static class SentryPerformanceWebClientConfiguration { - @Bean - public SentrySpanWebClientCustomizer sentrySpanWebClientCustomizer(IScopes scopes) { - return new SentrySpanWebClientCustomizer(scopes); + static class SentryPerformanceWebClientConfigurationWrapper { + @Configuration(proxyBeanMethods = false) + @AutoConfigureBefore(WebClientAutoConfiguration.class) + @Open + static class SentryPerformanceWebClientConfiguration { + @Bean + public SentrySpanWebClientCustomizer sentrySpanWebClientCustomizer(IScopes scopes) { + return new SentrySpanWebClientCustomizer(scopes); + } } } @@ -470,7 +481,7 @@ static class ApacheHttpClientTransportFactoryAutoconfiguration { } private static void addPackageAndIntegrationInfo() { - SentryIntegrationPackageStorage.getInstance().addIntegration("SpringBoot3"); + SentryIntegrationPackageStorage.getInstance().addIntegration("SpringBoot4"); } } diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryLogbackAppenderAutoConfiguration.java similarity index 96% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryLogbackAppenderAutoConfiguration.java index 13866d3a6a5..55398d679b5 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryLogbackAppenderAutoConfiguration.java @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import ch.qos.logback.classic.LoggerContext; import com.jakewharton.nopen.annotation.Open; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackInitializer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryLogbackInitializer.java similarity index 98% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackInitializer.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryLogbackInitializer.java index be222eae1bf..58de0bc4b26 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryLogbackInitializer.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryLogbackInitializer.java @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import ch.qos.logback.classic.Level; import ch.qos.logback.classic.Logger; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java similarity index 97% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java index 7813c2e5512..edb8d44cdd3 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryProperties.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryProperties.java @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import com.jakewharton.nopen.annotation.Open; import io.sentry.SentryOptions; @@ -39,8 +39,8 @@ public class SentryProperties extends SentryOptions { /** * If set to true, this flag disables all AOP related features (e.g. {@link - * io.sentry.spring.jakarta.tracing.SentryTransaction}, {@link - * io.sentry.spring.jakarta.tracing.SentrySpan}) to successfully compile to GraalVM + * io.sentry.spring7.tracing.SentryTransaction}, {@link io.sentry.spring7.tracing.SentrySpan}) to + * successfully compile to GraalVM */ private boolean enableAotCompatibility = false; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanRestClientCustomizer.java similarity index 89% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanRestClientCustomizer.java index cf4a20413c4..4611a3be51a 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizer.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanRestClientCustomizer.java @@ -1,8 +1,8 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; -import io.sentry.spring.jakarta.tracing.SentrySpanClientHttpRequestInterceptor; +import io.sentry.spring7.tracing.SentrySpanClientHttpRequestInterceptor; import org.jetbrains.annotations.NotNull; import org.springframework.boot.restclient.RestClientCustomizer; import org.springframework.web.client.RestClient; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizer.java similarity index 90% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizer.java index 34b3f86be46..11a0b5e095a 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizer.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizer.java @@ -1,8 +1,8 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; -import io.sentry.spring.jakarta.tracing.SentrySpanClientHttpRequestInterceptor; +import io.sentry.spring7.tracing.SentrySpanClientHttpRequestInterceptor; import java.util.ArrayList; import java.util.List; import org.jetbrains.annotations.NotNull; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanWebClientCustomizer.java similarity index 85% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanWebClientCustomizer.java index ffb0dd50b75..8180d5522bb 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizer.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpanWebClientCustomizer.java @@ -1,8 +1,8 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScopes; -import io.sentry.spring.jakarta.tracing.SentrySpanClientWebRequestFilter; +import io.sentry.spring7.tracing.SentrySpanClientWebRequestFilter; import org.jetbrains.annotations.NotNull; import org.springframework.boot.webclient.WebClientCustomizer; import org.springframework.web.reactive.function.client.WebClient; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpringVersionChecker.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpringVersionChecker.java similarity index 93% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpringVersionChecker.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpringVersionChecker.java index 7fb6d1ce237..973330e3b1e 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentrySpringVersionChecker.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentrySpringVersionChecker.java @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -14,7 +14,7 @@ final class SentrySpringVersionChecker @Override public void onApplicationEvent(ApplicationContextInitializedEvent event) { - if (!SpringBootVersion.getVersion().startsWith("3")) { + if (!SpringBootVersion.getVersion().startsWith("4")) { logger.warn("############################### WARNING ###############################"); logger.warn("## ##"); logger.warn("## !Incompatible Spring Boot Version detected! ##"); diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryWebfluxAutoConfiguration.java similarity index 93% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryWebfluxAutoConfiguration.java index 468f4b81071..aead2605218 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/SentryWebfluxAutoConfiguration.java @@ -1,12 +1,12 @@ -package io.sentry.spring.boot.jakarta; +package io.sentry.spring.boot4; import com.jakewharton.nopen.annotation.Open; import io.sentry.IScope; import io.sentry.IScopes; -import io.sentry.spring.jakarta.webflux.SentryScheduleHook; -import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler; -import io.sentry.spring.jakarta.webflux.SentryWebFilter; -import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor; +import io.sentry.spring7.webflux.SentryScheduleHook; +import io.sentry.spring7.webflux.SentryWebExceptionHandler; +import io.sentry.spring7.webflux.SentryWebFilter; +import io.sentry.spring7.webflux.SentryWebFilterWithThreadLocalAccessor; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.springframework.boot.ApplicationRunner; diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration.java similarity index 88% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration.java index 4e5664556fa..b9ae6beff32 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphql22AutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphql22AutoConfiguration.java @@ -1,13 +1,13 @@ -package io.sentry.spring.boot.jakarta.graphql; +package io.sentry.spring.boot4.graphql; import com.jakewharton.nopen.annotation.Open; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.graphql.SentryGraphqlInstrumentation; import io.sentry.graphql22.SentryInstrumentation; -import io.sentry.spring.boot.jakarta.SentryProperties; -import io.sentry.spring.jakarta.graphql.SentryDataFetcherExceptionResolverAdapter; -import io.sentry.spring.jakarta.graphql.SentryGraphqlBeanPostProcessor; -import io.sentry.spring.jakarta.graphql.SentrySpringSubscriptionHandler; +import io.sentry.spring.boot4.SentryProperties; +import io.sentry.spring7.graphql.SentryDataFetcherExceptionResolverAdapter; +import io.sentry.spring7.graphql.SentryGraphqlBeanPostProcessor; +import io.sentry.spring7.graphql.SentrySpringSubscriptionHandler; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -28,7 +28,7 @@ public SentryInstrumentation sentryInstrumentationWebMvc( final @NotNull SentryProperties sentryProperties, final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebMVC"); return createInstrumentation(sentryProperties, beforeSpanCallback, false); } @@ -39,7 +39,7 @@ public SentryInstrumentation sentryInstrumentationWebflux( final @NotNull SentryProperties sentryProperties, final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebFlux"); return createInstrumentation(sentryProperties, beforeSpanCallback, true); } diff --git a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration.java similarity index 88% rename from sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java rename to sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration.java index 84f59a39f35..742dc544c0d 100644 --- a/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot/jakarta/graphql/SentryGraphqlAutoConfiguration.java +++ b/sentry-spring-boot-4/src/main/java/io/sentry/spring/boot4/graphql/SentryGraphqlAutoConfiguration.java @@ -1,13 +1,13 @@ -package io.sentry.spring.boot.jakarta.graphql; +package io.sentry.spring.boot4.graphql; import com.jakewharton.nopen.annotation.Open; import io.sentry.SentryIntegrationPackageStorage; import io.sentry.graphql.SentryGraphqlInstrumentation; import io.sentry.graphql.SentryInstrumentation; -import io.sentry.spring.boot.jakarta.SentryProperties; -import io.sentry.spring.jakarta.graphql.SentryDataFetcherExceptionResolverAdapter; -import io.sentry.spring.jakarta.graphql.SentryGraphqlBeanPostProcessor; -import io.sentry.spring.jakarta.graphql.SentrySpringSubscriptionHandler; +import io.sentry.spring.boot4.SentryProperties; +import io.sentry.spring7.graphql.SentryDataFetcherExceptionResolverAdapter; +import io.sentry.spring7.graphql.SentryGraphqlBeanPostProcessor; +import io.sentry.spring7.graphql.SentrySpringSubscriptionHandler; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -28,7 +28,7 @@ public SentryInstrumentation sentryInstrumentationWebMvc( final @NotNull SentryProperties sentryProperties, final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebMVC"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebMVC"); return createInstrumentation(sentryProperties, beforeSpanCallback, false); } @@ -39,7 +39,7 @@ public SentryInstrumentation sentryInstrumentationWebflux( final @NotNull SentryProperties sentryProperties, final @NotNull ObjectProvider beforeSpanCallback) { - SentryIntegrationPackageStorage.getInstance().addIntegration("Spring6GrahQLWebFlux"); + SentryIntegrationPackageStorage.getInstance().addIntegration("Spring7GrahQLWebFlux"); return createInstrumentation(sentryProperties, beforeSpanCallback, true); } diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/spring.factories b/sentry-spring-boot-4/src/main/resources/META-INF/spring.factories index 4002cb6ed56..a1a27108bfc 100644 --- a/sentry-spring-boot-4/src/main/resources/META-INF/spring.factories +++ b/sentry-spring-boot-4/src/main/resources/META-INF/spring.factories @@ -1 +1 @@ -org.springframework.context.ApplicationListener=io.sentry.spring.boot.jakarta.SentrySpringVersionChecker +org.springframework.context.ApplicationListener=io.sentry.spring.boot4.SentrySpringVersionChecker diff --git a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 41436fe883f..4697c6b6a9d 100644 --- a/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/sentry-spring-boot-4/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,3 @@ -io.sentry.spring.boot.jakarta.SentryAutoConfiguration -io.sentry.spring.boot.jakarta.SentryLogbackAppenderAutoConfiguration -io.sentry.spring.boot.jakarta.SentryWebfluxAutoConfiguration +io.sentry.spring.boot4.SentryAutoConfiguration +io.sentry.spring.boot4.SentryLogbackAppenderAutoConfiguration +io.sentry.spring.boot4.SentryWebfluxAutoConfiguration diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt similarity index 96% rename from sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt rename to sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt index 7b290a42880..9aed094779b 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryAutoConfigurationTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta +package io.sentry.spring.boot4 import com.acme.MainBootClass import io.opentelemetry.api.OpenTelemetry @@ -24,16 +24,16 @@ import io.sentry.opentelemetry.agent.AgentMarker import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User import io.sentry.quartz.SentryJobListener -import io.sentry.spring.jakarta.ContextTagsEventProcessor -import io.sentry.spring.jakarta.HttpServletRequestSentryUserProvider -import io.sentry.spring.jakarta.SentryExceptionResolver -import io.sentry.spring.jakarta.SentryUserFilter -import io.sentry.spring.jakarta.SentryUserProvider -import io.sentry.spring.jakarta.SpringProfilesEventProcessor -import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider -import io.sentry.spring.jakarta.tracing.SentryTracingFilter -import io.sentry.spring.jakarta.tracing.SpringServletTransactionNameProvider -import io.sentry.spring.jakarta.tracing.TransactionNameProvider +import io.sentry.spring7.ContextTagsEventProcessor +import io.sentry.spring7.HttpServletRequestSentryUserProvider +import io.sentry.spring7.SentryExceptionResolver +import io.sentry.spring7.SentryUserFilter +import io.sentry.spring7.SentryUserProvider +import io.sentry.spring7.SpringProfilesEventProcessor +import io.sentry.spring7.SpringSecuritySentryUserProvider +import io.sentry.spring7.tracing.SentryTracingFilter +import io.sentry.spring7.tracing.SpringServletTransactionNameProvider +import io.sentry.spring7.tracing.TransactionNameProvider import io.sentry.transport.ITransport import io.sentry.transport.ITransportGate import io.sentry.transport.apache.ApacheHttpClientTransportFactory @@ -58,15 +58,15 @@ import org.quartz.core.QuartzScheduler import org.slf4j.MDC import org.springframework.aop.support.NameMatchMethodPointcut import org.springframework.boot.autoconfigure.AutoConfigurations -import org.springframework.boot.autoconfigure.quartz.QuartzAutoConfiguration -import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer -import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration import org.springframework.boot.context.annotation.UserConfigurations import org.springframework.boot.info.GitProperties +import org.springframework.boot.quartz.autoconfigure.QuartzAutoConfiguration +import org.springframework.boot.quartz.autoconfigure.SchedulerFactoryBeanCustomizer import org.springframework.boot.test.context.FilteredClassLoader import org.springframework.boot.test.context.assertj.ApplicationContextAssert import org.springframework.boot.test.context.runner.WebApplicationContextRunner import org.springframework.boot.web.servlet.FilterRegistrationBean +import org.springframework.boot.webmvc.autoconfigure.WebMvcAutoConfiguration import org.springframework.context.ApplicationContext import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -299,7 +299,7 @@ class SentryAutoConfigurationTest { fun `sets sentryClientName property on SentryOptions`() { contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj").run { assertThat(it.getBean(SentryOptions::class.java).sentryClientName) - .isEqualTo("sentry.java.spring-boot.jakarta/${BuildConfig.VERSION_NAME}") + .isEqualTo("sentry.java.spring-boot-4/${BuildConfig.VERSION_NAME}") } } @@ -317,12 +317,12 @@ class SentryAutoConfigurationTest { assertThat(event.sdk).isNotNull val sdk = event.sdk!! assertThat(sdk.version).isEqualTo(BuildConfig.VERSION_NAME) - assertThat(sdk.name).isEqualTo(BuildConfig.SENTRY_SPRING_BOOT_JAKARTA_SDK_NAME) + assertThat(sdk.name).isEqualTo(BuildConfig.SENTRY_SPRING_BOOT_4_SDK_NAME) assertThat(sdk.packageSet).anyMatch { pkg -> - pkg.name == "maven:io.sentry:sentry-spring-boot-starter-jakarta" && + pkg.name == "maven:io.sentry:sentry-spring-boot-4-starter" && pkg.version == BuildConfig.VERSION_NAME } - assertTrue(sdk.integrationSet.contains("SpringBoot3")) + assertTrue(sdk.integrationSet.contains("SpringBoot4")) }, anyOrNull(), ) @@ -805,7 +805,7 @@ class SentryAutoConfigurationTest { assertTrue( SentryIntegrationPackageStorage.getInstance() .integrations - .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + .contains("SpringBoot4OpenTelemetryAgentWithoutAutoInit") ) } } @@ -817,7 +817,7 @@ class SentryAutoConfigurationTest { assertFalse( SentryIntegrationPackageStorage.getInstance() .integrations - .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + .contains("SpringBoot4OpenTelemetryAgentWithoutAutoInit") ) } } @@ -832,13 +832,13 @@ class SentryAutoConfigurationTest { assertFalse( SentryIntegrationPackageStorage.getInstance() .integrations - .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + .contains("SpringBoot4OpenTelemetryAgentWithoutAutoInit") ) } } @Test - fun `when AgentMarker is not on the classpath but OpenTelemetry is, runs SpringBoot3OpenTelemetryNoAgent`() { + fun `when AgentMarker is not on the classpath but OpenTelemetry is, runs SpringBoot4OpenTelemetryNoAgent`() { SentryIntegrationPackageStorage.getInstance().clearStorage() contextRunner .withPropertyValues("sentry.dsn=http://key@localhost/proj") @@ -848,13 +848,13 @@ class SentryAutoConfigurationTest { assertTrue( SentryIntegrationPackageStorage.getInstance() .integrations - .contains("SpringBoot3OpenTelemetryNoAgent") + .contains("SpringBoot4OpenTelemetryNoAgent") ) } } @Test - fun `when AgentMarker and OpenTelemetry are not on the classpath, does not run SpringBoot3OpenTelemetryNoAgent`() { + fun `when AgentMarker and OpenTelemetry are not on the classpath, does not run SpringBoot4OpenTelemetryNoAgent`() { SentryIntegrationPackageStorage.getInstance().clearStorage() contextRunner .withPropertyValues("sentry.dsn=http://key@localhost/proj") @@ -863,13 +863,13 @@ class SentryAutoConfigurationTest { assertFalse( SentryIntegrationPackageStorage.getInstance() .integrations - .contains("SpringBoot3OpenTelemetryNoAgent") + .contains("SpringBoot4OpenTelemetryNoAgent") ) } } @Test - fun `when AgentMarker and SentryAutoConfigurationCustomizerProvider are not on the classpath, does not run SpringBoot3OpenTelemetryNoAgent`() { + fun `when AgentMarker and SentryAutoConfigurationCustomizerProvider are not on the classpath, does not run SpringBoot4OpenTelemetryNoAgent`() { SentryIntegrationPackageStorage.getInstance().clearStorage() contextRunner .withPropertyValues("sentry.dsn=http://key@localhost/proj") @@ -884,7 +884,7 @@ class SentryAutoConfigurationTest { assertFalse( SentryIntegrationPackageStorage.getInstance() .integrations - .contains("SpringBoot3OpenTelemetryNoAgent") + .contains("SpringBoot4OpenTelemetryNoAgent") ) } } @@ -899,7 +899,7 @@ class SentryAutoConfigurationTest { assertFalse( SentryIntegrationPackageStorage.getInstance() .integrations - .contains("SpringBoot3OpenTelemetryAgentWithoutAutoInit") + .contains("SpringBoot4OpenTelemetryAgentWithoutAutoInit") ) } } diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryLogbackAppenderAutoConfigurationTest.kt similarity index 99% rename from sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfigurationTest.kt rename to sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryLogbackAppenderAutoConfigurationTest.kt index 77712ac70c6..17e903d9a34 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryLogbackAppenderAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryLogbackAppenderAutoConfigurationTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta +package io.sentry.spring.boot4 import ch.qos.logback.classic.Level import ch.qos.logback.classic.Logger diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt similarity index 99% rename from sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt rename to sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt index a0ce38e7c3d..fb803a3100f 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestClientCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestClientCustomizerTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta +package io.sentry.spring.boot4 import io.sentry.BaggageHeader import io.sentry.Breadcrumb @@ -276,7 +276,7 @@ class SentrySpanRestClientCustomizerTest { @Test fun `does not add sentry-trace header if span origin is ignored`() { - fixture.sentryOptions.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.restclient")) + fixture.sentryOptions.setIgnoredSpanOrigins(listOf("auto.http.spring7.restclient")) val sut = fixture.getSut(isTransactionActive = false) val headers = HttpHeaders() diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt similarity index 98% rename from sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt rename to sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt index f7a7f55c5db..c6bd6d2ccaf 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanRestTemplateCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanRestTemplateCustomizerTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta +package io.sentry.spring.boot4 import io.sentry.BaggageHeader import io.sentry.Breadcrumb @@ -30,7 +30,7 @@ import org.mockito.kotlin.doAnswer import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever -import org.springframework.boot.web.client.RestTemplateBuilder +import org.springframework.boot.restclient.RestTemplateBuilder import org.springframework.http.HttpEntity import org.springframework.http.HttpHeaders import org.springframework.http.HttpMethod @@ -43,8 +43,8 @@ class SentrySpanRestTemplateCustomizerTest { val scopes = mock() val restTemplate = RestTemplateBuilder() - .setConnectTimeout(Duration.ofSeconds(2)) - .setReadTimeout(Duration.ofSeconds(2)) + .connectTimeout(Duration.ofSeconds(2)) + .readTimeout(Duration.ofSeconds(2)) .build() var mockServer = MockWebServer() val transaction: SentryTracer @@ -234,7 +234,7 @@ class SentrySpanRestTemplateCustomizerTest { @Test fun `does not add sentry-trace header when span origin is ignored`() { - fixture.sentryOptions.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.resttemplate")) + fixture.sentryOptions.setIgnoredSpanOrigins(listOf("auto.http.spring7.resttemplate")) val sut = fixture.getSut(isTransactionActive = false) val headers = HttpHeaders() val requestEntity = HttpEntity(headers) diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanWebClientCustomizerTest.kt similarity index 99% rename from sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt rename to sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanWebClientCustomizerTest.kt index 83321f30e78..b593dd261dc 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentrySpanWebClientCustomizerTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentrySpanWebClientCustomizerTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta +package io.sentry.spring.boot4 import io.sentry.BaggageHeader import io.sentry.Breadcrumb @@ -194,7 +194,7 @@ class SentrySpanWebClientCustomizerTest { val sut = fixture.getSut(isTransactionActive = false, includeMockServerInTracingOrigins = true) { options -> - options.setIgnoredSpanOrigins(listOf("auto.http.spring_jakarta.webclient")) + options.setIgnoredSpanOrigins(listOf("auto.http.spring7.webclient")) } sut .get() diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfigurationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryWebfluxAutoConfigurationTest.kt similarity index 90% rename from sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfigurationTest.kt rename to sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryWebfluxAutoConfigurationTest.kt index fd47317d1d2..fbb3aedf375 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryWebfluxAutoConfigurationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/SentryWebfluxAutoConfigurationTest.kt @@ -1,15 +1,15 @@ -package io.sentry.spring.boot.jakarta +package io.sentry.spring.boot4 import io.micrometer.context.ThreadLocalAccessor -import io.sentry.spring.jakarta.webflux.SentryWebExceptionHandler -import io.sentry.spring.jakarta.webflux.SentryWebFilter -import io.sentry.spring.jakarta.webflux.SentryWebFilterWithThreadLocalAccessor +import io.sentry.spring7.webflux.SentryWebExceptionHandler +import io.sentry.spring7.webflux.SentryWebFilter +import io.sentry.spring7.webflux.SentryWebFilterWithThreadLocalAccessor import kotlin.test.Test import org.assertj.core.api.Assertions.assertThat import org.springframework.boot.autoconfigure.AutoConfigurations -import org.springframework.boot.autoconfigure.web.reactive.WebFluxAutoConfiguration import org.springframework.boot.test.context.FilteredClassLoader import org.springframework.boot.test.context.runner.ReactiveWebApplicationContextRunner +import org.springframework.boot.webflux.autoconfigure.WebFluxAutoConfiguration import reactor.core.scheduler.Schedulers class SentryWebfluxAutoConfigurationTest { diff --git a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/it/SentrySpringIntegrationTest.kt similarity index 94% rename from sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt rename to sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/it/SentrySpringIntegrationTest.kt index 89d378d2087..998dfaaf867 100644 --- a/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot/jakarta/it/SentrySpringIntegrationTest.kt +++ b/sentry-spring-boot-4/src/test/kotlin/io/sentry/spring/boot4/it/SentrySpringIntegrationTest.kt @@ -1,4 +1,4 @@ -package io.sentry.spring.boot.jakarta.it +package io.sentry.spring.boot4.it import io.sentry.DefaultSpanFactory import io.sentry.IScopes @@ -8,7 +8,7 @@ import io.sentry.SentryOpenTelemetryMode import io.sentry.SentryOptions import io.sentry.checkEvent import io.sentry.checkTransaction -import io.sentry.spring.jakarta.tracing.SentrySpan +import io.sentry.spring7.tracing.SentrySpan import io.sentry.transport.ITransport import kotlin.test.BeforeTest import kotlin.test.Test @@ -25,9 +25,8 @@ import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.test.context.SpringBootTest -import org.springframework.boot.test.mock.mockito.SpyBean -import org.springframework.boot.test.web.client.TestRestTemplate -import org.springframework.boot.test.web.server.LocalServerPort +import org.springframework.boot.web.server.test.LocalServerPort +import org.springframework.boot.web.server.test.client.TestRestTemplate import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration import org.springframework.http.HttpEntity @@ -44,6 +43,7 @@ import org.springframework.security.crypto.password.PasswordEncoder import org.springframework.security.provisioning.InMemoryUserDetailsManager import org.springframework.security.web.SecurityFilterChain import org.springframework.stereotype.Service +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean import org.springframework.test.context.junit4.SpringRunner import org.springframework.web.bind.annotation.ControllerAdvice import org.springframework.web.bind.annotation.ExceptionHandler @@ -69,7 +69,7 @@ class SentrySpringIntegrationTest { @Autowired lateinit var transport: ITransport - @SpyBean lateinit var scopes: IScopes + @MockitoSpyBean lateinit var scopes: IScopes @LocalServerPort var port: Int? = null @@ -82,7 +82,7 @@ class SentrySpringIntegrationTest { fun `attaches request and user information to SentryEvents`() { val restTemplate = TestRestTemplate().withBasicAuth("user", "password") val headers = HttpHeaders() - headers["X-FORWARDED-FOR"] = listOf("169.128.0.1") + headers.put("X-FORWARDED-FOR", listOf("169.128.0.1")) val entity = HttpEntity(headers) restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) @@ -148,7 +148,7 @@ class SentrySpringIntegrationTest { fun `attaches first ip address if multiple addresses exist in a header`() { val restTemplate = TestRestTemplate().withBasicAuth("user", "password") val headers = HttpHeaders() - headers["X-FORWARDED-FOR"] = listOf("169.128.0.1, 192.168.0.1") + headers.put("X-FORWARDED-FOR", listOf("169.128.0.1, 192.168.0.1")) val entity = HttpEntity(headers) restTemplate.exchange("http://localhost:$port/hello", HttpMethod.GET, entity, Void::class.java) @@ -334,7 +334,10 @@ open class SecurityConfiguration { @Bean @Throws(Exception::class) open fun filterChain(http: HttpSecurity): SecurityFilterChain { - http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic() + http + .csrf { it.disable() } + .authorizeHttpRequests { it.anyRequest().authenticated() } + .httpBasic {} return http.build() } diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 0976ff93083..2dad5d25dd1 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -19,8 +19,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-spring-boot-starter-jakarta/build.gradle.kts b/sentry-spring-boot-starter-jakarta/build.gradle.kts index e671425de3c..60ac812b013 100644 --- a/sentry-spring-boot-starter-jakarta/build.gradle.kts +++ b/sentry-spring-boot-starter-jakarta/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -18,8 +18,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-spring-boot-starter/build.gradle.kts b/sentry-spring-boot-starter/build.gradle.kts index 06a49453019..a8b22a50f09 100644 --- a/sentry-spring-boot-starter/build.gradle.kts +++ b/sentry-spring-boot-starter/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -13,8 +13,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 8e5159dfb8e..7a486ef0968 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -14,8 +14,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index e4a902d35c0..f1920e24510 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -19,10 +19,11 @@ configure { } tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - languageVersion = libs.versions.kotlin.compatible.version.get() - freeCompilerArgs = listOf("-Xjsr305=strict") + kotlin { + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") } } diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index be395e65dfb..57c0b9d9f31 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -5,7 +5,7 @@ import org.springframework.boot.gradle.plugin.SpringBootPlugin plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -14,8 +14,9 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() - kotlinOptions.languageVersion = libs.versions.kotlin.compatible.version.get() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-system-test-support/build.gradle.kts b/sentry-system-test-support/build.gradle.kts index 4828ac6052d..2c015326c92 100644 --- a/sentry-system-test-support/build.gradle.kts +++ b/sentry-system-test-support/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -14,7 +14,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_17.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry-test-support/build.gradle.kts b/sentry-test-support/build.gradle.kts index a6de28d5029..29b2083a0a9 100644 --- a/sentry-test-support/build.gradle.kts +++ b/sentry-test-support/build.gradle.kts @@ -1,7 +1,7 @@ plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -13,7 +13,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 + compilerOptions.languageVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 + compilerOptions.apiVersion = org.jetbrains.kotlin.gradle.dsl.KotlinVersion.KOTLIN_1_9 } dependencies { diff --git a/sentry/build.gradle.kts b/sentry/build.gradle.kts index bad0ea56e50..56df9121710 100644 --- a/sentry/build.gradle.kts +++ b/sentry/build.gradle.kts @@ -4,7 +4,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { `java-library` id("io.sentry.javadoc") - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) jacoco alias(libs.plugins.errorprone) alias(libs.plugins.gradle.versions) @@ -12,7 +12,7 @@ plugins { } tasks.withType().configureEach { - kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8 } dependencies { From 975f6c77096f589efbce33486da0e3d656386eb8 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 11 Aug 2025 14:23:10 +0200 Subject: [PATCH 04/10] changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a38f544c17..0cb91d105bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## Unreleased +### Features + +- Add support for Spring Boot 4 and Spring 7 ([#4601](https://github.com/getsentry/sentry-java/pull/4601)) + - NOTE: Our `sentry-opentelemetry-agentless-spring` is not working yet for Spring Boot 4. Please use `sentry-opentelemetry-agent` until OpenTelemetry has support for Spring Boot 4. + ### Improvements - Session Replay: Use main thread looper to schedule replay capture ([#4542](https://github.com/getsentry/sentry-java/pull/4542)) From 1ae3d929136a94ec724287599b51b9879eec0570 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 Aug 2025 16:19:54 +0200 Subject: [PATCH 05/10] fix ci --- sentry-samples/sentry-samples-jul/build.gradle.kts | 8 ++++---- sentry-samples/sentry-samples-log4j2/build.gradle.kts | 8 ++++---- sentry-samples/sentry-samples-logback/build.gradle.kts | 8 ++++---- .../sentry-samples-spring-boot/build.gradle.kts | 4 ---- 4 files changed, 12 insertions(+), 16 deletions(-) diff --git a/sentry-samples/sentry-samples-jul/build.gradle.kts b/sentry-samples/sentry-samples-jul/build.gradle.kts index 377baa612f5..8b5f5057054 100644 --- a/sentry-samples/sentry-samples-jul/build.gradle.kts +++ b/sentry-samples/sentry-samples-jul/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java application - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.versions) id("com.github.johnrengelman.shadow") version "8.1.1" } @@ -22,9 +22,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-log4j2/build.gradle.kts b/sentry-samples/sentry-samples-log4j2/build.gradle.kts index 75b2a991fa2..dede2d9cb29 100644 --- a/sentry-samples/sentry-samples-log4j2/build.gradle.kts +++ b/sentry-samples/sentry-samples-log4j2/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java application - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.versions) id("com.github.johnrengelman.shadow") version "8.1.1" } @@ -22,9 +22,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-logback/build.gradle.kts b/sentry-samples/sentry-samples-logback/build.gradle.kts index 618ced977e3..ee6949c6c6b 100644 --- a/sentry-samples/sentry-samples-logback/build.gradle.kts +++ b/sentry-samples/sentry-samples-logback/build.gradle.kts @@ -3,7 +3,7 @@ import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { java application - kotlin("jvm") + alias(libs.plugins.kotlin.jvm) alias(libs.plugins.gradle.versions) id("com.github.johnrengelman.shadow") version "8.1.1" } @@ -22,9 +22,9 @@ configure { } tasks.withType().configureEach { - kotlinOptions { - freeCompilerArgs = listOf("-Xjsr305=strict") - jvmTarget = JavaVersion.VERSION_17.toString() + kotlin { + compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") + compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 } } diff --git a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts index e5344e38c9a..3958417edd4 100644 --- a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts @@ -23,10 +23,6 @@ configure { targetCompatibility = JavaVersion.VERSION_17 } -tasks.withType().configureEach { - compilerOptions.jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 -} - tasks.withType().configureEach { kotlin { compilerOptions.freeCompilerArgs = listOf("-Xjsr305=strict") From fe36a4ef9f67a9c2c06e53e836f0f52824dbae4c Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 14 Aug 2025 10:13:17 +0200 Subject: [PATCH 06/10] add ignored span origins for Spring 7 and Spring Boot 4 --- sentry/src/main/java/io/sentry/util/SpanUtils.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/sentry/src/main/java/io/sentry/util/SpanUtils.java b/sentry/src/main/java/io/sentry/util/SpanUtils.java index 20ae53ba7c2..c93c5f3214f 100644 --- a/sentry/src/main/java/io/sentry/util/SpanUtils.java +++ b/sentry/src/main/java/io/sentry/util/SpanUtils.java @@ -24,15 +24,20 @@ public final class SpanUtils { if (SentryOpenTelemetryMode.AGENT == mode || SentryOpenTelemetryMode.AGENTLESS_SPRING == mode) { origins.add("auto.http.spring_jakarta.webmvc"); origins.add("auto.http.spring.webmvc"); + origins.add("auto.http.spring7.webmvc"); origins.add("auto.spring_jakarta.webflux"); origins.add("auto.spring.webflux"); + origins.add("auto.spring7.webflux"); origins.add("auto.db.jdbc"); origins.add("auto.http.spring_jakarta.webclient"); origins.add("auto.http.spring.webclient"); + origins.add("auto.http.spring4.webclient"); origins.add("auto.http.spring_jakarta.restclient"); origins.add("auto.http.spring.restclient"); + origins.add("auto.http.spring7.restclient"); origins.add("auto.http.spring_jakarta.resttemplate"); origins.add("auto.http.spring.resttemplate"); + origins.add("auto.http.spring7.resttemplate"); origins.add("auto.http.openfeign"); origins.add("auto.http.ktor-client"); } From d3164a3f64baa5c88ad95595c5978f369911d4e7 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 14 Aug 2025 11:18:44 +0200 Subject: [PATCH 07/10] move changelog --- CHANGELOG.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9ecb8d5bd15..312db3a2b2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- Add support for Spring Boot 4 and Spring 7 ([#4601](https://github.com/getsentry/sentry-java/pull/4601)) + - NOTE: Our `sentry-opentelemetry-agentless-spring` is not working yet for Spring Boot 4. Please use `sentry-opentelemetry-agent` until OpenTelemetry has support for Spring Boot 4. - Add onDiscard to enable users to track the type and amount of data discarded before reaching Sentry ([#4612](https://github.com/getsentry/sentry-java/pull/4612)) - Stub for setting the callback on `Sentry.init`: ```java @@ -29,11 +31,6 @@ - Add a `isEnableSystemEventBreadcrumbsExtras` option to disable reporting system events extras for breadcrumbs ([#4625](https://github.com/getsentry/sentry-java/pull/4625)) -### Features - -- Add support for Spring Boot 4 and Spring 7 ([#4601](https://github.com/getsentry/sentry-java/pull/4601)) - - NOTE: Our `sentry-opentelemetry-agentless-spring` is not working yet for Spring Boot 4. Please use `sentry-opentelemetry-agent` until OpenTelemetry has support for Spring Boot 4. - ### Improvements - Session Replay: Use main thread looper to schedule replay capture ([#4542](https://github.com/getsentry/sentry-java/pull/4542)) From 65ed9c6c85ac05d5eaf4fcfa552f8f118c76ce1b Mon Sep 17 00:00:00 2001 From: Alex Alderman Webb Date: Mon, 18 Aug 2025 08:30:41 +0200 Subject: [PATCH 08/10] Example how to fix version mismatch (#4642) --- gradle/libs.versions.toml | 1 + .../sentry-samples-spring-jakarta/build.gradle.kts | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b74a9a6bc03..62e486bd084 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -100,6 +100,7 @@ graphql-java24 = { module = "com.graphql-java:graphql-java", version = "24.0" } jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", version.ref = "jackson" } jackson-kotlin = { module = "com.fasterxml.jackson.module:jackson-module-kotlin", version.ref = "jackson" } jetbrains-annotations = { module = "org.jetbrains:annotations", version = "23.0.0" } +kotlin-bom = { module = "org.jetbrains.kotlin:kotlin-bom", version.ref = "kotlin" } kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } kotlin-test-junit-spring7 = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlinSpring7" } kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "coroutines" } diff --git a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts index 83513e53333..591d49a84d4 100644 --- a/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-jakarta/build.gradle.kts @@ -1,4 +1,3 @@ -import org.jetbrains.kotlin.config.KotlinCompilerVersion import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.springframework.boot.gradle.plugin.SpringBootPlugin @@ -27,7 +26,12 @@ java.targetCompatibility = JavaVersion.VERSION_17 repositories { mavenCentral() } -dependencyManagement { imports { mavenBom(SpringBootPlugin.BOM_COORDINATES) } } +dependencyManagement { + imports { + mavenBom(SpringBootPlugin.BOM_COORDINATES) + mavenBom(libs.kotlin.bom.get().toString()) + } +} dependencies { implementation(Config.Libs.springWeb) @@ -36,7 +40,7 @@ dependencies { implementation(Config.Libs.springSecurityWeb) implementation(Config.Libs.springSecurityConfig) implementation(Config.Libs.kotlinReflect) - implementation(kotlin(Config.kotlinStdLib, KotlinCompilerVersion.VERSION)) + implementation(kotlin(Config.kotlinStdLib)) implementation(projects.sentrySpringJakarta) implementation(projects.sentryLogback) implementation(libs.jackson.databind) From a1438d01b3afb6879bcf08c86c9c09f14eef74c1 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 18 Aug 2025 08:33:43 +0200 Subject: [PATCH 09/10] Fix ignored span origin --- sentry/src/main/java/io/sentry/util/SpanUtils.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/util/SpanUtils.java b/sentry/src/main/java/io/sentry/util/SpanUtils.java index c93c5f3214f..cad4d483656 100644 --- a/sentry/src/main/java/io/sentry/util/SpanUtils.java +++ b/sentry/src/main/java/io/sentry/util/SpanUtils.java @@ -31,7 +31,7 @@ public final class SpanUtils { origins.add("auto.db.jdbc"); origins.add("auto.http.spring_jakarta.webclient"); origins.add("auto.http.spring.webclient"); - origins.add("auto.http.spring4.webclient"); + origins.add("auto.http.spring7.webclient"); origins.add("auto.http.spring_jakarta.restclient"); origins.add("auto.http.spring.restclient"); origins.add("auto.http.spring7.restclient"); From 55352638094bac9d73a78c1082943eb35a7d3fbd Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 26 Aug 2025 11:27:23 +0200 Subject: [PATCH 10/10] move changelog --- CHANGELOG.md | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cf2bd9edd6..563997f9f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Features + +- Add support for Spring Boot 4 and Spring 7 ([#4601](https://github.com/getsentry/sentry-java/pull/4601)) + - NOTE: Our `sentry-opentelemetry-agentless-spring` is not working yet for Spring Boot 4. Please use `sentry-opentelemetry-agent` until OpenTelemetry has support for Spring Boot 4. + ## 8.20.0 ### Fixes @@ -9,8 +16,6 @@ ### Features -- Add support for Spring Boot 4 and Spring 7 ([#4601](https://github.com/getsentry/sentry-java/pull/4601)) - - NOTE: Our `sentry-opentelemetry-agentless-spring` is not working yet for Spring Boot 4. Please use `sentry-opentelemetry-agent` until OpenTelemetry has support for Spring Boot 4. - Add onDiscard to enable users to track the type and amount of data discarded before reaching Sentry ([#4612](https://github.com/getsentry/sentry-java/pull/4612)) - Stub for setting the callback on `Sentry.init`: ```java