Skip to content

Commit 32212cd

Browse files
authored
Report active Spring profiles in envelope item context (#4147)
* Report active Spring profiles in trace context * formatting etc * formatting * remove duplicate test * changelog * ./gradlew apiDump * move main class and tests to the spring and spring-jakarta modules * ./gradlew apiDump * delete duplicate test in spring-boot-jakarta * use a new Context for Spring, add tests * changelog * Update CHANGELOG.md * Update CHANGELOG.md
1 parent 4c80d9e commit 32212cd

File tree

24 files changed

+616
-0
lines changed

24 files changed

+616
-0
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
### Features
66

77
- The `ignoredErrors` option is now configurable via the manifest property `io.sentry.traces.ignored-errors` ([#4178](https://github.com/getsentry/sentry-java/pull/4178))
8+
- A list of active Spring profiles is attached to payloads sent to Sentry (errors, traces, etc.) and displayed in the UI when using our Spring or Spring Boot integrations ([#4147](https://github.com/getsentry/sentry-java/pull/4147))
9+
- This consists of an empty list when only the default profile is active
810

911
### Fixes
1012

sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import io.sentry.spring.jakarta.SentryUserFilter;
2424
import io.sentry.spring.jakarta.SentryUserProvider;
2525
import io.sentry.spring.jakarta.SentryWebConfiguration;
26+
import io.sentry.spring.jakarta.SpringProfilesEventProcessor;
2627
import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider;
2728
import io.sentry.spring.jakarta.checkin.SentryCheckInAdviceConfiguration;
2829
import io.sentry.spring.jakarta.checkin.SentryCheckInPointcutConfiguration;
@@ -68,6 +69,7 @@
6869
import org.springframework.context.annotation.Import;
6970
import org.springframework.core.Ordered;
7071
import org.springframework.core.annotation.Order;
72+
import org.springframework.core.env.Environment;
7173
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
7274
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
7375
import org.springframework.security.core.context.SecurityContextHolder;
@@ -470,4 +472,14 @@ private static class SentryTracesSampleRateCondition {}
470472
@SuppressWarnings("UnusedNestedClass")
471473
private static class SentryTracesSamplerBeanCondition {}
472474
}
475+
476+
@Configuration(proxyBeanMethods = false)
477+
@Open
478+
static class SpringProfilesEventProcessorConfiguration {
479+
@Bean
480+
public @NotNull SpringProfilesEventProcessor springProfilesEventProcessor(
481+
final Environment environment) {
482+
return new SpringProfilesEventProcessor(environment);
483+
}
484+
}
473485
}

sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import io.sentry.spring.jakarta.HttpServletRequestSentryUserProvider
2828
import io.sentry.spring.jakarta.SentryExceptionResolver
2929
import io.sentry.spring.jakarta.SentryUserFilter
3030
import io.sentry.spring.jakarta.SentryUserProvider
31+
import io.sentry.spring.jakarta.SpringProfilesEventProcessor
3132
import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider
3233
import io.sentry.spring.jakarta.tracing.SentryTracingFilter
3334
import io.sentry.spring.jakarta.tracing.SpringServletTransactionNameProvider
@@ -925,6 +926,14 @@ class SentryAutoConfigurationTest {
925926
}
926927
}
927928

929+
@Test
930+
fun `registers SpringProfilesEventProcessor on SentryOptions`() {
931+
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj")
932+
.run {
933+
assertThat(it.getBean(SentryOptions::class.java).eventProcessors).anyMatch { processor -> processor.javaClass == SpringProfilesEventProcessor::class.java }
934+
}
935+
}
936+
928937
@Configuration(proxyBeanMethods = false)
929938
open class CustomSchedulerFactoryBeanCustomizerConfiguration {
930939
class MyJobListener : JobListener {

sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
import io.sentry.spring.SentryUserFilter;
2323
import io.sentry.spring.SentryUserProvider;
2424
import io.sentry.spring.SentryWebConfiguration;
25+
import io.sentry.spring.SpringProfilesEventProcessor;
2526
import io.sentry.spring.SpringSecuritySentryUserProvider;
2627
import io.sentry.spring.boot.graphql.SentryGraphqlAutoConfiguration;
2728
import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration;
@@ -67,6 +68,7 @@
6768
import org.springframework.context.annotation.Import;
6869
import org.springframework.core.Ordered;
6970
import org.springframework.core.annotation.Order;
71+
import org.springframework.core.env.Environment;
7072
import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter;
7173
import org.springframework.scheduling.quartz.SchedulerFactoryBean;
7274
import org.springframework.security.core.context.SecurityContextHolder;
@@ -439,4 +441,14 @@ private static class SentryTracesSampleRateCondition {}
439441
@SuppressWarnings("UnusedNestedClass")
440442
private static class SentryTracesSamplerBeanCondition {}
441443
}
444+
445+
@Configuration(proxyBeanMethods = false)
446+
@Open
447+
static class SpringProfilesEventProcessorConfiguration {
448+
@Bean
449+
public @NotNull SpringProfilesEventProcessor springProfilesEventProcessor(
450+
final Environment environment) {
451+
return new SpringProfilesEventProcessor(environment);
452+
}
453+
}
442454
}

sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import io.sentry.spring.HttpServletRequestSentryUserProvider
2828
import io.sentry.spring.SentryExceptionResolver
2929
import io.sentry.spring.SentryUserFilter
3030
import io.sentry.spring.SentryUserProvider
31+
import io.sentry.spring.SpringProfilesEventProcessor
3132
import io.sentry.spring.SpringSecuritySentryUserProvider
3233
import io.sentry.spring.tracing.SentryTracingFilter
3334
import io.sentry.spring.tracing.SpringServletTransactionNameProvider
@@ -865,6 +866,14 @@ class SentryAutoConfigurationTest {
865866
}
866867
}
867868

869+
@Test
870+
fun `registers SpringProfilesEventProcessor on SentryOptions`() {
871+
contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj")
872+
.run {
873+
assertThat(it.getBean(SentryOptions::class.java).eventProcessors).anyMatch { processor -> processor.javaClass == SpringProfilesEventProcessor::class.java }
874+
}
875+
}
876+
868877
@Configuration(proxyBeanMethods = false)
869878
open class CustomSchedulerFactoryBeanCustomizerConfiguration {
870879
class MyJobListener : JobListener {

sentry-spring-jakarta/api/sentry-spring-jakarta.api

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ public class io/sentry/spring/jakarta/SentryWebConfiguration {
8686
public fun httpServletRequestSentryUserProvider (Lio/sentry/SentryOptions;)Lio/sentry/spring/jakarta/HttpServletRequestSentryUserProvider;
8787
}
8888

89+
public final class io/sentry/spring/jakarta/SpringProfilesEventProcessor : io/sentry/EventProcessor {
90+
public fun <init> (Lorg/springframework/core/env/Environment;)V
91+
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
92+
public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent;
93+
public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
94+
}
95+
8996
public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : io/sentry/spring/jakarta/SentryUserProvider {
9097
public fun <init> (Lio/sentry/SentryOptions;)V
9198
public fun provideUser ()Lio/sentry/protocol/User;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.sentry.spring.jakarta;
2+
3+
import io.sentry.EventProcessor;
4+
import io.sentry.Hint;
5+
import io.sentry.SentryBaseEvent;
6+
import io.sentry.SentryEvent;
7+
import io.sentry.SentryReplayEvent;
8+
import io.sentry.protocol.SentryTransaction;
9+
import io.sentry.protocol.Spring;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
12+
import org.springframework.core.env.Environment;
13+
14+
/**
15+
* Attaches the list of active Spring profiles (an empty list if only the default profile is active)
16+
* to the {@link io.sentry.TraceContext} associated with the event.
17+
*/
18+
public final class SpringProfilesEventProcessor implements EventProcessor {
19+
private final @NotNull Environment environment;
20+
21+
@Override
22+
public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) {
23+
processInternal(event);
24+
return event;
25+
}
26+
27+
@Override
28+
public @NotNull SentryTransaction process(
29+
final @NotNull SentryTransaction transaction, final @NotNull Hint hint) {
30+
processInternal(transaction);
31+
return transaction;
32+
}
33+
34+
@Override
35+
public @NotNull SentryReplayEvent process(
36+
final @NotNull SentryReplayEvent event, final @NotNull Hint hint) {
37+
processInternal(event);
38+
return event;
39+
}
40+
41+
private void processInternal(final @NotNull SentryBaseEvent event) {
42+
@Nullable String[] activeProfiles = environment.getActiveProfiles();
43+
@NotNull Spring springContext = new Spring();
44+
springContext.setActiveProfiles(activeProfiles);
45+
event.getContexts().setSpring(springContext);
46+
}
47+
48+
public SpringProfilesEventProcessor(final @NotNull Environment environment) {
49+
this.environment = environment;
50+
}
51+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.sentry.spring.jakarta
2+
3+
import io.sentry.ITransportFactory
4+
import io.sentry.Sentry
5+
import io.sentry.checkEvent
6+
import io.sentry.protocol.Spring
7+
import io.sentry.transport.ITransport
8+
import org.assertj.core.api.Assertions.assertThat
9+
import org.mockito.kotlin.any
10+
import org.mockito.kotlin.anyOrNull
11+
import org.mockito.kotlin.mock
12+
import org.mockito.kotlin.verify
13+
import org.mockito.kotlin.whenever
14+
import org.springframework.boot.test.context.runner.ApplicationContextRunner
15+
import org.springframework.context.annotation.Bean
16+
import org.springframework.context.annotation.Configuration
17+
import org.springframework.core.env.Environment
18+
import kotlin.test.Test
19+
20+
class SpringProfilesEventProcessorTest {
21+
22+
private val contextRunner = ApplicationContextRunner()
23+
.withUserConfiguration(AppConfiguration::class.java)
24+
.withUserConfiguration(SpringProfilesEventProcessorConfiguration::class.java)
25+
.withUserConfiguration(MockTransportConfiguration::class.java)
26+
27+
@Test
28+
fun `when default Spring profile is active, sets active_profiles in Spring context to empty list on sent event`() {
29+
contextRunner
30+
.run {
31+
Sentry.captureMessage("test")
32+
val transport = it.getBean(ITransport::class.java)
33+
verify(transport).send(
34+
checkEvent { event ->
35+
val expected = Spring()
36+
expected.activeProfiles = listOf<String>().toTypedArray()
37+
assertThat(event.contexts.spring).isEqualTo(expected)
38+
},
39+
anyOrNull()
40+
)
41+
}
42+
}
43+
44+
@Test
45+
fun `when non-default Spring profiles are active, sets active profiles in Spring context to list of profile names`() {
46+
contextRunner
47+
.withPropertyValues(
48+
"spring.profiles.active=test1,test2"
49+
)
50+
.run {
51+
Sentry.captureMessage("test")
52+
val transport = it.getBean(ITransport::class.java)
53+
verify(transport).send(
54+
checkEvent { event ->
55+
val expected = Spring()
56+
expected.activeProfiles = listOf("test1", "test2").toTypedArray()
57+
assertThat(event.contexts.spring).isEqualTo(expected)
58+
},
59+
anyOrNull()
60+
)
61+
}
62+
}
63+
64+
@EnableSentry(dsn = "http://key@localhost/proj")
65+
class AppConfiguration
66+
67+
@Configuration(proxyBeanMethods = false)
68+
open class SpringProfilesEventProcessorConfiguration {
69+
@Bean
70+
open fun springProfilesEventProcessor(environment: Environment): SpringProfilesEventProcessor {
71+
return SpringProfilesEventProcessor(environment)
72+
}
73+
}
74+
75+
@Configuration(proxyBeanMethods = false)
76+
open class MockTransportConfiguration {
77+
78+
private val transport = mock<ITransport>()
79+
80+
@Bean
81+
open fun mockTransportFactory(): ITransportFactory {
82+
val factory = mock<ITransportFactory>()
83+
whenever(factory.create(any(), any())).thenReturn(transport)
84+
return factory
85+
}
86+
87+
@Bean
88+
open fun sentryTransport() = transport
89+
}
90+
}

sentry-spring/api/sentry-spring.api

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,13 @@ public class io/sentry/spring/SentryWebConfiguration {
8686
public fun httpServletRequestSentryUserProvider (Lio/sentry/SentryOptions;)Lio/sentry/spring/HttpServletRequestSentryUserProvider;
8787
}
8888

89+
public final class io/sentry/spring/SpringProfilesEventProcessor : io/sentry/EventProcessor {
90+
public fun <init> (Lorg/springframework/core/env/Environment;)V
91+
public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
92+
public fun process (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent;
93+
public fun process (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
94+
}
95+
8996
public final class io/sentry/spring/SpringSecuritySentryUserProvider : io/sentry/spring/SentryUserProvider {
9097
public fun <init> (Lio/sentry/SentryOptions;)V
9198
public fun provideUser ()Lio/sentry/protocol/User;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package io.sentry.spring;
2+
3+
import io.sentry.EventProcessor;
4+
import io.sentry.Hint;
5+
import io.sentry.SentryBaseEvent;
6+
import io.sentry.SentryEvent;
7+
import io.sentry.SentryReplayEvent;
8+
import io.sentry.protocol.SentryTransaction;
9+
import io.sentry.protocol.Spring;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
12+
import org.springframework.core.env.Environment;
13+
14+
/**
15+
* Attaches the list of active Spring profiles (an empty list if only the default profile is active)
16+
* to the {@link io.sentry.TraceContext} associated with the event.
17+
*/
18+
public final class SpringProfilesEventProcessor implements EventProcessor {
19+
private final @NotNull Environment environment;
20+
21+
@Override
22+
public @NotNull SentryEvent process(final @NotNull SentryEvent event, final @NotNull Hint hint) {
23+
processInternal(event);
24+
return event;
25+
}
26+
27+
@Override
28+
public @NotNull SentryTransaction process(
29+
final @NotNull SentryTransaction transaction, final @NotNull Hint hint) {
30+
processInternal(transaction);
31+
return transaction;
32+
}
33+
34+
@Override
35+
public @NotNull SentryReplayEvent process(
36+
final @NotNull SentryReplayEvent event, final @NotNull Hint hint) {
37+
processInternal(event);
38+
return event;
39+
}
40+
41+
private void processInternal(final @NotNull SentryBaseEvent event) {
42+
@Nullable String[] activeProfiles = environment.getActiveProfiles();
43+
@NotNull Spring springContext = new Spring();
44+
springContext.setActiveProfiles(activeProfiles);
45+
event.getContexts().setSpring(springContext);
46+
}
47+
48+
public SpringProfilesEventProcessor(final @NotNull Environment environment) {
49+
this.environment = environment;
50+
}
51+
}

0 commit comments

Comments
 (0)