diff --git a/CHANGELOG.md b/CHANGELOG.md index df1223a460..d9e3490feb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Add support for measurements at span level ([#3219](https://github.com/getsentry/sentry-java/pull/3219)) - Add `enableScopePersistence` option to disable `PersistingScopeObserver` used for ANR reporting which may increase performance overhead. Defaults to `true` ([#3218](https://github.com/getsentry/sentry-java/pull/3218)) - When disabled, the SDK will not enrich ANRv2 events with scope data (e.g. breadcrumbs, user, tags, etc.) +- Configurable defaults for Cron - MonitorConfig ([#3195](https://github.com/getsentry/sentry-java/pull/3195)) - We now display a warning on startup if an incompatible version of Spring Boot is detected ([#3233](https://github.com/getsentry/sentry-java/pull/3233)) - This should help notice a mismatching Sentry dependency, especially when upgrading a Spring Boot application 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 d469dbc281..f6b39127fc 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 @@ -168,7 +168,12 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", - "sentry.enable-backpressure-handling=true" + "sentry.enable-backpressure-handling=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" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -201,6 +206,12 @@ class SentryAutoConfigurationTest { assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") assertThat(options.isEnableBackpressureHandling).isEqualTo(true) + 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) } } 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 6502a57ad1..c7fd177ec0 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 @@ -168,7 +168,12 @@ class SentryAutoConfigurationTest { "sentry.enabled=false", "sentry.send-modules=false", "sentry.ignored-checkins=slug1,slugB", - "sentry.enable-backpressure-handling=true" + "sentry.enable-backpressure-handling=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" ).run { val options = it.getBean(SentryProperties::class.java) assertThat(options.readTimeoutMillis).isEqualTo(10) @@ -201,6 +206,12 @@ class SentryAutoConfigurationTest { assertThat(options.isSendModules).isEqualTo(false) assertThat(options.ignoredCheckIns).containsOnly("slug1", "slugB") assertThat(options.isEnableBackpressureHandling).isEqualTo(true) + 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) } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 1a6056fcd0..87cfc49646 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -315,6 +315,7 @@ public final class io/sentry/ExternalOptions { public static fun from (Lio/sentry/config/PropertiesProvider;Lio/sentry/ILogger;)Lio/sentry/ExternalOptions; public fun getBundleIds ()Ljava/util/Set; public fun getContextTags ()Ljava/util/List; + public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDebug ()Ljava/lang/Boolean; public fun getDist ()Ljava/lang/String; public fun getDsn ()Ljava/lang/String; @@ -343,6 +344,7 @@ public final class io/sentry/ExternalOptions { public fun isEnablePrettySerializationOutput ()Ljava/lang/Boolean; public fun isEnabled ()Ljava/lang/Boolean; public fun isSendModules ()Ljava/lang/Boolean; + public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDebug (Ljava/lang/Boolean;)V public fun setDist (Ljava/lang/String;)V public fun setDsn (Ljava/lang/String;)V @@ -2204,6 +2206,7 @@ public class io/sentry/SentryOptions { public fun getConnectionStatusProvider ()Lio/sentry/IConnectionStatusProvider; public fun getConnectionTimeoutMillis ()I public fun getContextTags ()Ljava/util/List; + public fun getCron ()Lio/sentry/SentryOptions$Cron; public fun getDateProvider ()Lio/sentry/SentryDateProvider; public fun getDebugMetaLoader ()Lio/sentry/internal/debugmeta/IDebugMetaLoader; public fun getDiagnosticLevel ()Lio/sentry/SentryLevel; @@ -2309,6 +2312,7 @@ public class io/sentry/SentryOptions { public fun setCacheDirPath (Ljava/lang/String;)V public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V public fun setConnectionTimeoutMillis (I)V + public fun setCron (Lio/sentry/SentryOptions$Cron;)V public fun setDateProvider (Lio/sentry/SentryDateProvider;)V public fun setDebug (Z)V public fun setDebugMetaLoader (Lio/sentry/internal/debugmeta/IDebugMetaLoader;)V @@ -2405,6 +2409,20 @@ public abstract interface class io/sentry/SentryOptions$BeforeSendTransactionCal public abstract fun execute (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction; } +public final class io/sentry/SentryOptions$Cron { + public fun ()V + public fun getDefaultCheckinMargin ()Ljava/lang/Long; + public fun getDefaultFailureIssueThreshold ()Ljava/lang/Long; + public fun getDefaultMaxRuntime ()Ljava/lang/Long; + public fun getDefaultRecoveryThreshold ()Ljava/lang/Long; + public fun getDefaultTimezone ()Ljava/lang/String; + public fun setDefaultCheckinMargin (Ljava/lang/Long;)V + public fun setDefaultFailureIssueThreshold (Ljava/lang/Long;)V + public fun setDefaultMaxRuntime (Ljava/lang/Long;)V + public fun setDefaultRecoveryThreshold (Ljava/lang/Long;)V + public fun setDefaultTimezone (Ljava/lang/String;)V +} + public abstract interface class io/sentry/SentryOptions$ProfilesSamplerCallback { public abstract fun sample (Lio/sentry/SamplingContext;)Ljava/lang/Double; } diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index a34f24df85..99eaacdd24 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -51,6 +51,8 @@ public final class ExternalOptions { private @Nullable Boolean sendModules; private @Nullable Boolean enableBackpressureHandling; + private @Nullable SentryOptions.Cron cron; + @SuppressWarnings("unchecked") public static @NotNull ExternalOptions from( final @NotNull PropertiesProvider propertiesProvider, final @NotNull ILogger logger) { @@ -156,6 +158,32 @@ public final class ExternalOptions { ignoredExceptionType); } } + + final Long cronDefaultCheckinMargin = + propertiesProvider.getLongProperty("cron.default-checkin-margin"); + final Long cronDefaultMaxRuntime = + propertiesProvider.getLongProperty("cron.default-max-runtime"); + final String cronDefaultTimezone = propertiesProvider.getProperty("cron.default-timezone"); + final Long cronDefaultFailureIssueThreshold = + propertiesProvider.getLongProperty("cron.default-failure-issue-threshold"); + final Long cronDefaultRecoveryThreshold = + propertiesProvider.getLongProperty("cron.default-recovery-threshold"); + + if (cronDefaultCheckinMargin != null + || cronDefaultMaxRuntime != null + || cronDefaultTimezone != null + || cronDefaultFailureIssueThreshold != null + || cronDefaultRecoveryThreshold != null) { + SentryOptions.Cron cron = new SentryOptions.Cron(); + cron.setDefaultCheckinMargin(cronDefaultCheckinMargin); + cron.setDefaultMaxRuntime(cronDefaultMaxRuntime); + cron.setDefaultTimezone(cronDefaultTimezone); + cron.setDefaultFailureIssueThreshold(cronDefaultFailureIssueThreshold); + cron.setDefaultRecoveryThreshold(cronDefaultRecoveryThreshold); + + options.setCron(cron); + } + return options; } @@ -412,4 +440,14 @@ public void setEnableBackpressureHandling(final @Nullable Boolean enableBackpres public @Nullable Boolean isEnableBackpressureHandling() { return enableBackpressureHandling; } + + @ApiStatus.Experimental + public @Nullable SentryOptions.Cron getCron() { + return cron; + } + + @ApiStatus.Experimental + public void setCron(final @Nullable SentryOptions.Cron cron) { + this.cron = cron; + } } diff --git a/sentry/src/main/java/io/sentry/MonitorConfig.java b/sentry/src/main/java/io/sentry/MonitorConfig.java index faa391fcf9..d954a50466 100644 --- a/sentry/src/main/java/io/sentry/MonitorConfig.java +++ b/sentry/src/main/java/io/sentry/MonitorConfig.java @@ -21,6 +21,14 @@ public final class MonitorConfig implements JsonUnknown, JsonSerializable { public MonitorConfig(final @NotNull MonitorSchedule schedule) { this.schedule = schedule; + final SentryOptions.Cron defaultCron = HubAdapter.getInstance().getOptions().getCron(); + if (defaultCron != null) { + this.checkinMargin = defaultCron.getDefaultCheckinMargin(); + this.maxRuntime = defaultCron.getDefaultMaxRuntime(); + this.timezone = defaultCron.getDefaultTimezone(); + this.failureIssueThreshold = defaultCron.getDefaultFailureIssueThreshold(); + this.recoveryThreshold = defaultCron.getDefaultRecoveryThreshold(); + } } public @NotNull MonitorSchedule getSchedule() { diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 83f4dfc7e1..3afe5622a1 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -471,6 +471,8 @@ public class SentryOptions { */ private int profilingTracesHz = 101; + @ApiStatus.Experimental private @Nullable Cron cron = null; + /** * Adds an event processor * @@ -2336,6 +2338,16 @@ public void setEnableMetrics(boolean enableMetrics) { this.enableMetrics = enableMetrics; } + @ApiStatus.Experimental + public @Nullable Cron getCron() { + return cron; + } + + @ApiStatus.Experimental + public void setCron(@Nullable Cron cron) { + this.cron = cron; + } + /** The BeforeSend callback */ public interface BeforeSendCallback { @@ -2569,6 +2581,29 @@ public void merge(final @NotNull ExternalOptions options) { if (options.isEnableBackpressureHandling() != null) { setEnableBackpressureHandling(options.isEnableBackpressureHandling()); } + + if (options.getCron() != null) { + if (getCron() == null) { + setCron(options.getCron()); + } else { + if (options.getCron().getDefaultCheckinMargin() != null) { + getCron().setDefaultCheckinMargin(options.getCron().getDefaultCheckinMargin()); + } + if (options.getCron().getDefaultMaxRuntime() != null) { + getCron().setDefaultMaxRuntime(options.getCron().getDefaultMaxRuntime()); + } + if (options.getCron().getDefaultTimezone() != null) { + getCron().setDefaultTimezone(options.getCron().getDefaultTimezone()); + } + if (options.getCron().getDefaultFailureIssueThreshold() != null) { + getCron() + .setDefaultFailureIssueThreshold(options.getCron().getDefaultFailureIssueThreshold()); + } + if (options.getCron().getDefaultRecoveryThreshold() != null) { + getCron().setDefaultRecoveryThreshold(options.getCron().getDefaultRecoveryThreshold()); + } + } + } } private @NotNull SdkVersion createSdkVersion() { @@ -2643,6 +2678,54 @@ public void setPass(final @Nullable String pass) { } } + public static final class Cron { + private @Nullable Long defaultCheckinMargin; + private @Nullable Long defaultMaxRuntime; + private @Nullable String defaultTimezone; + private @Nullable Long defaultFailureIssueThreshold; + private @Nullable Long defaultRecoveryThreshold; + + public @Nullable Long getDefaultCheckinMargin() { + return defaultCheckinMargin; + } + + public void setDefaultCheckinMargin(@Nullable Long defaultCheckinMargin) { + this.defaultCheckinMargin = defaultCheckinMargin; + } + + public @Nullable Long getDefaultMaxRuntime() { + return defaultMaxRuntime; + } + + public void setDefaultMaxRuntime(@Nullable Long defaultMaxRuntime) { + this.defaultMaxRuntime = defaultMaxRuntime; + } + + public @Nullable String getDefaultTimezone() { + return defaultTimezone; + } + + public void setDefaultTimezone(@Nullable String defaultTimezone) { + this.defaultTimezone = defaultTimezone; + } + + public @Nullable Long getDefaultFailureIssueThreshold() { + return defaultFailureIssueThreshold; + } + + public void setDefaultFailureIssueThreshold(@Nullable Long defaultFailureIssueThreshold) { + this.defaultFailureIssueThreshold = defaultFailureIssueThreshold; + } + + public @Nullable Long getDefaultRecoveryThreshold() { + return defaultRecoveryThreshold; + } + + public void setDefaultRecoveryThreshold(@Nullable Long defaultRecoveryThreshold) { + this.defaultRecoveryThreshold = defaultRecoveryThreshold; + } + } + public enum RequestSize { NONE, SMALL, diff --git a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt index 7abc5de474..b9bb296b89 100644 --- a/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/ExternalOptionsTest.kt @@ -275,6 +275,17 @@ class ExternalOptionsTest { } } + @Test + fun `creates options with cron defaults`() { + withPropertiesFile(listOf("cron.default-checkin-margin=1", "cron.default-max-runtime=30", "cron.default-timezone=America/New_York", "cron.default-failure-issue-threshold=40", "cron.default-recovery-threshold=50")) { options -> + assertEquals(1L, options.cron?.defaultCheckinMargin) + assertEquals(30L, options.cron?.defaultMaxRuntime) + assertEquals("America/New_York", options.cron?.defaultTimezone) + assertEquals(40L, options.cron?.defaultFailureIssueThreshold) + assertEquals(50L, options.cron?.defaultRecoveryThreshold) + } + } + 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/SentryOptionsTest.kt b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt index b446813421..b276daef66 100644 --- a/sentry/src/test/java/io/sentry/SentryOptionsTest.kt +++ b/sentry/src/test/java/io/sentry/SentryOptionsTest.kt @@ -370,6 +370,13 @@ class SentryOptionsTest { externalOptions.isSendModules = false externalOptions.ignoredCheckIns = listOf("slug1", "slug-B") externalOptions.isEnableBackpressureHandling = true + externalOptions.cron = SentryOptions.Cron().apply { + defaultCheckinMargin = 10L + defaultMaxRuntime = 30L + defaultTimezone = "America/New_York" + defaultFailureIssueThreshold = 40L + defaultRecoveryThreshold = 50L + } val options = SentryOptions() @@ -400,6 +407,12 @@ class SentryOptionsTest { assertFalse(options.isSendModules) assertEquals(listOf("slug1", "slug-B"), options.ignoredCheckIns) assertTrue(options.isEnableBackpressureHandling) + assertNotNull(options.cron) + assertEquals(10L, options.cron?.defaultCheckinMargin) + assertEquals(30L, options.cron?.defaultMaxRuntime) + assertEquals(40L, options.cron?.defaultFailureIssueThreshold) + assertEquals(50L, options.cron?.defaultRecoveryThreshold) + assertEquals("America/New_York", options.cron?.defaultTimezone) } @Test @@ -608,4 +621,60 @@ class SentryOptionsTest { fun `when options are initialized, enableScopePersistence is set to true by default`() { assertEquals(true, SentryOptions().isEnableScopePersistence) } + + @Test + fun `existing cron defaults are not overridden if not present in external options`() { + val options = SentryOptions().apply { + cron = SentryOptions.Cron().apply { + defaultCheckinMargin = 1 + defaultMaxRuntime = 2 + defaultTimezone = "America/New_York" + defaultFailureIssueThreshold = 3 + defaultRecoveryThreshold = 4 + } + } + + val externalOptions = ExternalOptions().apply { + cron = SentryOptions.Cron() + } + + options.merge(externalOptions) + + assertEquals(1, options.cron?.defaultCheckinMargin) + assertEquals(2, options.cron?.defaultMaxRuntime) + assertEquals("America/New_York", options.cron?.defaultTimezone) + assertEquals(3, options.cron?.defaultFailureIssueThreshold) + assertEquals(4, options.cron?.defaultRecoveryThreshold) + } + + @Test + fun `all cron properties set in external options override values set in sentry options`() { + val options = SentryOptions().apply { + cron = SentryOptions.Cron().apply { + defaultCheckinMargin = 1 + defaultMaxRuntime = 2 + defaultTimezone = "America/New_York" + defaultFailureIssueThreshold = 3 + defaultRecoveryThreshold = 4 + } + } + + val externalOptions = ExternalOptions().apply { + cron = SentryOptions.Cron().apply { + defaultCheckinMargin = 10 + defaultMaxRuntime = 20 + defaultTimezone = "Europe/Vienna" + defaultFailureIssueThreshold = 30 + defaultRecoveryThreshold = 40 + } + } + + options.merge(externalOptions) + + assertEquals(10, options.cron?.defaultCheckinMargin) + assertEquals(20, options.cron?.defaultMaxRuntime) + assertEquals("Europe/Vienna", options.cron?.defaultTimezone) + assertEquals(30, options.cron?.defaultFailureIssueThreshold) + assertEquals(40, options.cron?.defaultRecoveryThreshold) + } } diff --git a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt index 10cb0f134b..a285cd5832 100644 --- a/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt +++ b/sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt @@ -6,11 +6,13 @@ import io.sentry.MonitorConfig import io.sentry.MonitorSchedule import io.sentry.MonitorScheduleUnit import io.sentry.Sentry +import io.sentry.SentryOptions import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.check import org.mockito.kotlin.inOrder import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import java.lang.AssertionError import java.lang.RuntimeException import kotlin.test.Test @@ -56,6 +58,7 @@ class CheckInUtilsTest { Mockito.mockStatic(Sentry::class.java).use { sentry -> val hub = mock() sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) + whenever(hub.options).thenReturn(SentryOptions()) val returnValue = CheckInUtils.withCheckIn("monitor-1") { return@withCheckIn "test1" } @@ -121,6 +124,7 @@ class CheckInUtilsTest { Mockito.mockStatic(Sentry::class.java).use { sentry -> val hub = mock() sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) + whenever(hub.options).thenReturn(SentryOptions()) val monitorConfig = MonitorConfig(MonitorSchedule.interval(7, MonitorScheduleUnit.DAY)) val returnValue = CheckInUtils.withCheckIn("monitor-1", monitorConfig) { "test1" @@ -153,6 +157,7 @@ class CheckInUtilsTest { Mockito.mockStatic(Sentry::class.java).use { sentry -> val hub = mock() sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) + whenever(hub.options).thenReturn(SentryOptions()) val monitorConfig = MonitorConfig(MonitorSchedule.interval(7, MonitorScheduleUnit.DAY)).apply { failureIssueThreshold = 10 recoveryThreshold = 20 @@ -182,4 +187,58 @@ class CheckInUtilsTest { } } } + + @Test + fun `sets defaults for MonitorConfig from SentryOptions`() { + Mockito.mockStatic(Sentry::class.java).use { sentry -> + val hub = mock() + sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) + whenever(hub.options).thenReturn( + SentryOptions().apply { + cron = SentryOptions.Cron().apply { + defaultCheckinMargin = 20 + defaultMaxRuntime = 30 + defaultTimezone = "America/New_York" + defaultFailureIssueThreshold = 40 + defaultRecoveryThreshold = 50 + } + } + ) + + val monitorConfig = MonitorConfig(MonitorSchedule.interval(7, MonitorScheduleUnit.DAY)) + + assertEquals(20, monitorConfig.checkinMargin) + assertEquals(30, monitorConfig.maxRuntime) + assertEquals("America/New_York", monitorConfig.timezone) + assertEquals(40, monitorConfig.failureIssueThreshold) + assertEquals(50, monitorConfig.recoveryThreshold) + } + } + + @Test + fun `defaults for MonitorConfig from SentryOptions can be overridden`() { + Mockito.mockStatic(Sentry::class.java).use { sentry -> + val hub = mock() + sentry.`when` { Sentry.getCurrentHub() }.thenReturn(hub) + whenever(hub.options).thenReturn( + SentryOptions().apply { + cron = SentryOptions.Cron().apply { + defaultCheckinMargin = 20 + defaultMaxRuntime = 50 + defaultTimezone = "America/New_York" + } + } + ) + + val monitorConfig = MonitorConfig(MonitorSchedule.interval(7, MonitorScheduleUnit.DAY)).apply { + checkinMargin = 10 + maxRuntime = 30 + timezone = "America/Los_Angeles" + } + + assertEquals(10, monitorConfig.checkinMargin) + assertEquals(30, monitorConfig.maxRuntime) + assertEquals("America/Los_Angeles", monitorConfig.timezone) + } + } }