diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fb41cc5c7..08c8000f8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,8 @@ - Add `sendModules` option for disable sending modules ([#2926](https://github.com/getsentry/sentry-java/pull/2926)) - Send `db.system` and `db.name` in span data for androidx.sqlite spans ([#2928](https://github.com/getsentry/sentry-java/pull/2928)) - Add API for sending checkins (CRONS) manually ([#2935](https://github.com/getsentry/sentry-java/pull/2935)) -- Support check-ins for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) +- Support check-ins (CRONS) for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) +- Add option for ignoring certain monitor slugs ([#2943](https://github.com/getsentry/sentry-java/pull/2943)) ### Fixes diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index 6a386aca64..ba90224f37 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties @@ -10,6 +10,8 @@ sentry.logging.minimum-breadcrumb-level=debug # Performance configuration sentry.traces-sample-rate=1.0 sentry.enable-tracing=true +sentry.enable-automatic-checkins=true +sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true in-app-includes="io.sentry.samples" diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties index 8151eacc6c..ef8e5484b9 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties +++ b/sentry-samples/sentry-samples-spring-boot/src/main/resources/application.properties @@ -10,6 +10,8 @@ sentry.logging.minimum-breadcrumb-level=debug # Performance configuration sentry.traces-sample-rate=1.0 sentry.enable-tracing=true +sentry.enable-automatic-checkins=true +sentry.ignored-checkins=ignored_monitor_slug_1,ignored_monitor_slug_2 sentry.debug=true in-app-includes="io.sentry.samples" diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index e216a11452..473a8b5de3 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -54,6 +54,7 @@ dependencies { // tests testImplementation(projects.sentryLogback) + testImplementation(projects.sentryQuartz) testImplementation(projects.sentryApacheHttpClient5) testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) @@ -69,6 +70,7 @@ dependencies { testImplementation(Config.Libs.springBoot3StarterWebflux) testImplementation(Config.Libs.springBoot3StarterSecurity) testImplementation(Config.Libs.springBoot3StarterAop) + testImplementation(Config.Libs.springBoot3StarterQuartz) testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryCore) testImplementation(Config.Libs.contextPropagation) } diff --git a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt index 1c4af4f91d..015b22cfac 100644 --- a/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot-jakarta/src/test/kotlin/io/sentry/spring/boot/jakarta/SentryAutoConfigurationTest.kt @@ -18,6 +18,7 @@ import io.sentry.checkEvent import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor 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 @@ -36,9 +37,11 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +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.SchedulerFactoryBeanCustomizer import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration import org.springframework.boot.context.annotation.UserConfigurations import org.springframework.boot.info.GitProperties @@ -51,6 +54,7 @@ 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.RestTemplate import org.springframework.web.reactive.function.client.WebClient @@ -157,7 +161,9 @@ class SentryAutoConfigurationTest { "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.send-modules=false", + "sentry.enable-automatic-checkins=true", + "sentry.ignored-checkins=slug1,slugB" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -188,6 +194,8 @@ class SentryAutoConfigurationTest { assertThat(options.tracePropagationTargets).containsOnly("localhost", "^(http|https)://api\\..*\$") assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) + assertThat(options.isEnableAutomaticCheckIns).isEqualTo(true) + assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") } } @@ -724,6 +732,57 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when auto checkins is enabled, 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 `when auto checkins is enabled, 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 `when auto checkins is enabled, 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 `when auto checkins is enabled, 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 `when auto checkins is disabled, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=false") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins option is skipped, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + @Configuration(proxyBeanMethods = false) open class CustomOptionsConfigurationConfiguration { diff --git a/sentry-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 20b1d4a942..548b02face 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -53,6 +53,7 @@ dependencies { // tests testImplementation(projects.sentryLogback) + testImplementation(projects.sentryQuartz) testImplementation(projects.sentryApacheHttpClient5) testImplementation(projects.sentryTestSupport) testImplementation(kotlin(Config.kotlinStdLib)) @@ -66,6 +67,7 @@ dependencies { testImplementation(Config.Libs.springBootStarterWebflux) testImplementation(Config.Libs.springBootStarterSecurity) testImplementation(Config.Libs.springBootStarterAop) + testImplementation(Config.Libs.springBootStarterQuartz) testImplementation(projects.sentryOpentelemetry.sentryOpentelemetryCore) } diff --git a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt index b6c6ec380e..0336b2f6d2 100644 --- a/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt +++ b/sentry-spring-boot/src/test/kotlin/io/sentry/spring/boot/SentryAutoConfigurationTest.kt @@ -18,6 +18,7 @@ import io.sentry.checkEvent import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor import io.sentry.protocol.SentryTransaction import io.sentry.protocol.User +import io.sentry.quartz.SentryJobListener import io.sentry.spring.ContextTagsEventProcessor import io.sentry.spring.HttpServletRequestSentryUserProvider import io.sentry.spring.SentryExceptionResolver @@ -35,9 +36,11 @@ import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.mock import org.mockito.kotlin.verify import org.mockito.kotlin.whenever +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.SchedulerFactoryBeanCustomizer import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration import org.springframework.boot.context.annotation.UserConfigurations import org.springframework.boot.info.GitProperties @@ -50,6 +53,7 @@ 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.RestTemplate import org.springframework.web.reactive.function.client.WebClient @@ -157,7 +161,9 @@ class SentryAutoConfigurationTest { "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.send-modules=false", + "sentry.enable-automatic-checkins=true", + "sentry.ignored-checkins=slug1,slugB" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -188,6 +194,8 @@ class SentryAutoConfigurationTest { assertThat(options.tracePropagationTargets).containsOnly("localhost", "^(http|https)://api\\..*\$") assertThat(options.isEnabled).isEqualTo(false) assertThat(options.isSendModules).isEqualTo(false) + assertThat(options.isEnableAutomaticCheckIns).isEqualTo(true) + assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") } } @@ -724,6 +732,57 @@ class SentryAutoConfigurationTest { } } + @Test + fun `when auto checkins is enabled, 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 `when auto checkins is enabled, 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 `when auto checkins is enabled, 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 `when auto checkins is enabled, 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 `when auto checkins is disabled, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj", "sentry.enable-automatic-checkins=false") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + + @Test + fun `when auto checkins option is skipped, does not create quartz config`() { + contextRunner.withPropertyValues("sentry.dsn=http://key@localhost/proj") + .run { + assertThat(it).doesNotHaveBean(SchedulerFactoryBeanCustomizer::class.java) + } + } + @Configuration(proxyBeanMethods = false) open class CustomOptionsConfigurationConfiguration { diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8f44d415e1..40efd78815 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -320,6 +320,7 @@ public final class io/sentry/ExternalOptions { public fun getEnableUncaughtExceptionHandler ()Ljava/lang/Boolean; public fun getEnvironment ()Ljava/lang/String; public fun getIdleTimeout ()Ljava/lang/Long; + public fun getIgnoredCheckIns ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; @@ -335,12 +336,14 @@ public final class io/sentry/ExternalOptions { public fun getTracePropagationTargets ()Ljava/util/List; public fun getTracesSampleRate ()Ljava/lang/Double; public fun getTracingOrigins ()Ljava/util/List; + public fun isEnableAutomaticCheckIns ()Ljava/lang/Boolean; public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; public fun setDebug (Ljava/lang/Boolean;)V public fun setDist (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V + public fun setEnableAutomaticCheckIns (Ljava/lang/Boolean;)V public fun setEnableDeduplication (Ljava/lang/Boolean;)V public fun setEnablePrettySerializationOutput (Ljava/lang/Boolean;)V public fun setEnableTracing (Ljava/lang/Boolean;)V @@ -348,6 +351,7 @@ public final class io/sentry/ExternalOptions { public fun setEnabled (Ljava/lang/Boolean;)V public fun setEnvironment (Ljava/lang/String;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setMaxRequestBodySize (Lio/sentry/SentryOptions$RequestSize;)V public fun setPrintUncaughtStackTrace (Ljava/lang/Boolean;)V public fun setProfilesSampleRate (Ljava/lang/Double;)V @@ -1929,6 +1933,7 @@ public class io/sentry/SentryOptions { public fun getGestureTargetLocators ()Ljava/util/List; public fun getHostnameVerifier ()Ljavax/net/ssl/HostnameVerifier; public fun getIdleTimeout ()Ljava/lang/Long; + public fun getIgnoredCheckIns ()Ljava/util/List; public fun getIgnoredExceptionsForType ()Ljava/util/Set; public fun getInAppExcludes ()Ljava/util/List; public fun getInAppIncludes ()Ljava/util/List; @@ -1979,6 +1984,7 @@ public class io/sentry/SentryOptions { public fun isAttachThreads ()Z public fun isDebug ()Z public fun isEnableAutoSessionTracking ()Z + public fun isEnableAutomaticCheckIns ()Z public fun isEnableDeduplication ()Z public fun isEnableExternalConfiguration ()Z public fun isEnableNdk ()Z @@ -2015,6 +2021,7 @@ public class io/sentry/SentryOptions { public fun setDistinctId (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V public fun setEnableAutoSessionTracking (Z)V + public fun setEnableAutomaticCheckIns (Z)V public fun setEnableDeduplication (Z)V public fun setEnableExternalConfiguration (Z)V public fun setEnableNdk (Z)V @@ -2035,6 +2042,7 @@ public class io/sentry/SentryOptions { public fun setGestureTargetLocators (Ljava/util/List;)V public fun setHostnameVerifier (Ljavax/net/ssl/HostnameVerifier;)V public fun setIdleTimeout (Ljava/lang/Long;)V + public fun setIgnoredCheckIns (Ljava/util/List;)V public fun setInstrumenter (Lio/sentry/Instrumenter;)V public fun setLogger (Lio/sentry/ILogger;)V public fun setMainThreadChecker (Lio/sentry/util/thread/IMainThreadChecker;)V @@ -4310,6 +4318,11 @@ public abstract class io/sentry/transport/TransportResult { public static fun success ()Lio/sentry/transport/TransportResult; } +public final class io/sentry/util/CheckInUtils { + public fun ()V + public static fun isIgnored (Ljava/util/List;Ljava/lang/String;)Z +} + public final class io/sentry/util/ClassLoaderUtils { public fun ()V public static fun classLoaderOrDefault (Ljava/lang/ClassLoader;)Ljava/lang/ClassLoader; diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index 0f2e241f62..e139cd2cc4 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -45,6 +45,9 @@ public final class ExternalOptions { private @Nullable Boolean enabled; private @Nullable Boolean enablePrettySerializationOutput; + private @Nullable Boolean enableAutomaticCheckIns; + private @Nullable List ignoredCheckIns; + private @Nullable Boolean sendModules; @SuppressWarnings("unchecked") @@ -126,6 +129,11 @@ public final class ExternalOptions { options.setSendModules(propertiesProvider.getBooleanProperty("send-modules")); + options.setEnableAutomaticCheckIns( + propertiesProvider.getBooleanProperty("enable-automatic-checkins")); + + options.setIgnoredCheckIns(propertiesProvider.getList("ignored-checkins")); + for (final String ignoredExceptionType : propertiesProvider.getList("ignored-exceptions-for-type")) { try { @@ -383,4 +391,20 @@ public void setEnablePrettySerializationOutput( public void setSendModules(final @Nullable Boolean sendModules) { this.sendModules = sendModules; } + + public @Nullable Boolean isEnableAutomaticCheckIns() { + return enableAutomaticCheckIns; + } + + public void setEnableAutomaticCheckIns(final @Nullable Boolean enableAutomaticCheckIns) { + this.enableAutomaticCheckIns = enableAutomaticCheckIns; + } + + public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { + this.ignoredCheckIns = ignoredCheckIns; + } + + public @Nullable List getIgnoredCheckIns() { + return ignoredCheckIns; + } } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 8640a3985c..19b0bb6627 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -9,6 +9,7 @@ import io.sentry.protocol.SentryId; import io.sentry.protocol.SentryTransaction; import io.sentry.transport.ITransport; +import io.sentry.util.CheckInUtils; import io.sentry.util.HintUtils; import io.sentry.util.Objects; import io.sentry.util.TracingUtils; @@ -689,6 +690,20 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint checkIn = applyScope(checkIn, scope); } + if (CheckInUtils.isIgnored(options.getIgnoredCheckIns(), checkIn.getMonitorSlug())) { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Check-in was dropped as slug %s is ignored", + checkIn.getMonitorSlug()); + // TODO in a follow up PR with DataCategory.Monitor + // options + // .getClientReportRecorder() + // .recordLostEvent(DiscardReason.EVENT_PROCESSOR, DataCategory.Error); + return SentryId.EMPTY_ID; + } + options.getLogger().log(SentryLevel.DEBUG, "Capturing check-in: %s", checkIn.getCheckInId()); SentryId sentryId = checkIn.getCheckInId(); diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 4305c79e1e..7265b2feda 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -443,6 +443,12 @@ public class SentryOptions { /** Whether to send modules containing information about versions. */ private boolean sendModules = true; + /** Whether to automatically send check-ins for monitors (CRONS). */ + private boolean enableAutomaticCheckIns = false; + + /** Contains a list of monitor slugs for which check-ins should not be sent. */ + private @Nullable List ignoredCheckIns = null; + /** * Adds an event processor * @@ -2141,6 +2147,15 @@ public boolean isSendModules() { return sendModules; } + /** + * Whether to send check-ins for monitors (CRONS) automatically. + * + * @return true if check-ins should be sent automatically. + */ + public boolean isEnableAutomaticCheckIns() { + return enableAutomaticCheckIns; + } + /** * Whether to format serialized data, e.g. events logged to console in debug mode * @@ -2159,6 +2174,34 @@ public void setSendModules(boolean sendModules) { this.sendModules = sendModules; } + /** + * Whether to send check-ins for monitors (CRONS) automatically. + * + * @param enableAutomaticCheckIns true if check-ins should be sent automatically. + */ + public void setEnableAutomaticCheckIns(boolean enableAutomaticCheckIns) { + this.enableAutomaticCheckIns = enableAutomaticCheckIns; + } + + public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { + if (ignoredCheckIns == null) { + this.ignoredCheckIns = null; + } else { + @NotNull final List filteredIgnoredCheckIns = new ArrayList<>(); + for (String slug : ignoredCheckIns) { + if (!slug.isEmpty()) { + filteredIgnoredCheckIns.add(slug); + } + } + + this.ignoredCheckIns = filteredIgnoredCheckIns; + } + } + + public @Nullable List getIgnoredCheckIns() { + return ignoredCheckIns; + } + /** Returns the current {@link SentryDateProvider} that is used to retrieve the current date. */ @ApiStatus.Internal public @NotNull SentryDateProvider getDateProvider() { @@ -2407,6 +2450,14 @@ public void merge(final @NotNull ExternalOptions options) { if (options.isSendModules() != null) { setSendModules(options.isSendModules()); } + + if (options.isEnableAutomaticCheckIns() != null) { + setEnableAutomaticCheckIns(options.isEnableAutomaticCheckIns()); + } + if (options.getIgnoredCheckIns() != null) { + final List ignoredCheckIns = new ArrayList<>(options.getIgnoredCheckIns()); + setIgnoredCheckIns(ignoredCheckIns); + } } private @NotNull SdkVersion createSdkVersion() { diff --git a/sentry/src/main/java/io/sentry/util/CheckInUtils.java b/sentry/src/main/java/io/sentry/util/CheckInUtils.java new file mode 100644 index 0000000000..41ba19499e --- /dev/null +++ b/sentry/src/main/java/io/sentry/util/CheckInUtils.java @@ -0,0 +1,34 @@ +package io.sentry.util; + +import java.util.List; +import org.jetbrains.annotations.ApiStatus; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Checks if a check-in for a monitor (CRON) has been ignored. */ +@ApiStatus.Internal +public final class CheckInUtils { + + public static boolean isIgnored( + final @Nullable List ignoredSlugs, final @NotNull String slug) { + if (ignoredSlugs == null || ignoredSlugs.isEmpty()) { + return false; + } + + for (final String ignoredSlug : ignoredSlugs) { + if (ignoredSlug.equalsIgnoreCase(slug)) { + return true; + } + + try { + if (slug.matches(ignoredSlug)) { + return true; + } + } catch (Throwable t) { + // ignore invalid regex + } + } + + return false; + } +} diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 9d57b694c3..fc95bed9e7 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -261,6 +261,20 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with enableAutomaticCheckIns set to true`() { + withPropertiesFile("enable-automatic-checkins=true") { options -> + assertTrue(options.isEnableAutomaticCheckIns == true) + } + } + + @Test + fun `creates options with ignoredCheckIns`() { + withPropertiesFile("ignored-checkins=slugA,slug2") { options -> + assertTrue(options.ignoredCheckIns!!.containsAll(listOf("slugA", "slug2"))) + } + } + private fun withPropertiesFile(textLines: List = emptyList(), logger: ILogger = mock(), fn: (ExternalOptions) -> Unit) { // create a sentry.properties file in temporary folder val temporaryFolder = TemporaryFolder() diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 6f259743c1..ccc749b76a 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -557,6 +557,44 @@ class SentryClientTest { ) } + @Test + fun `when captureCheckIn, envelope is sent if ignored slug does not match`() { + val sut = fixture.getSut { options -> + options.ignoredCheckIns = listOf("non_matching_slug") + } + + sut.captureCheckIn(checkIn, null, null) + + verify(fixture.transport).send( + check { actual -> + assertEquals(checkIn.checkInId, actual.header.eventId) + assertEquals(fixture.sentryOptions.sdkVersion, actual.header.sdkVersion) + + assertEquals(1, actual.items.count()) + val item = actual.items.first() + assertEquals(SentryItemType.CheckIn, item.header.type) + assertEquals("application/json", item.header.contentType) + + assertEnvelopeItemDataForCheckIn(item) + }, + any() + ) + } + + @Test + fun `when captureCheckIn, envelope is not sent if slug is ignored`() { + val sut = fixture.getSut { options -> + options.ignoredCheckIns = listOf("some_slug") + } + + sut.captureCheckIn(checkIn, null, null) + + verify(fixture.transport, never()).send( + any(), + any() + ) + } + private fun assertEnvelopeItemDataForCheckIn(item: SentryEnvelopeItem) { val stream = ByteArrayOutputStream() val writer = stream.bufferedWriter(Charset.forName("UTF-8")) diff --git a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index 32278d531d..2b13764916 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -371,6 +371,9 @@ class SentryOptionsTest { externalOptions.isEnabled = false externalOptions.isEnablePrettySerializationOutput = false externalOptions.isSendModules = false + externalOptions.isEnableAutomaticCheckIns = true + externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") + val options = SentryOptions() options.merge(externalOptions) @@ -398,6 +401,8 @@ class SentryOptionsTest { assertFalse(options.isEnabled) assertFalse(options.isEnablePrettySerializationOutput) assertFalse(options.isSendModules) + assertTrue(options.isEnableAutomaticCheckIns) + assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) } @Test diff --git a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt new file mode 100644 index 0000000000..2475504fe3 --- /dev/null +++ b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt @@ -0,0 +1,38 @@ +package io.sentry.util + +import kotlin.test.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class CheckInUtilsTest { + + @Test + fun `ignores exact match`() { + assertTrue(CheckInUtils.isIgnored(listOf("slugA"), "slugA")) + } + + @Test + fun `ignores regex match`() { + assertTrue(CheckInUtils.isIgnored(listOf("slug-.*"), "slug-A")) + } + + @Test + fun `does not ignore if ignored list is null`() { + assertFalse(CheckInUtils.isIgnored(null, "slugA")) + } + + @Test + fun `does not ignore if ignored list is empty`() { + assertFalse(CheckInUtils.isIgnored(emptyList(), "slugA")) + } + + @Test + fun `does not ignore if slug is not in ignored list`() { + assertFalse(CheckInUtils.isIgnored(listOf("slugB"), "slugA")) + } + + @Test + fun `does not ignore if slug is does not match ignored list`() { + assertFalse(CheckInUtils.isIgnored(listOf("slug-.*"), "slugA")) + } +}