From 3b2debeb3d476221f4e4e87df7641f02c74c2fee Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 11 Sep 2023 07:58:30 +0200 Subject: [PATCH 01/22] checkin --- .../core/SessionTrackingIntegrationTest.kt | 5 + sentry/api/sentry.api | 110 +++++++++ sentry/src/main/java/io/sentry/CheckIn.java | 225 ++++++++++++++++++ .../main/java/io/sentry/CheckInStatus.java | 27 +++ sentry/src/main/java/io/sentry/Hub.java | 28 +++ .../src/main/java/io/sentry/HubAdapter.java | 5 + sentry/src/main/java/io/sentry/IHub.java | 3 + .../main/java/io/sentry/ISentryClient.java | 3 + .../main/java/io/sentry/MonitorConfig.java | 155 ++++++++++++ .../main/java/io/sentry/MonitorSchedule.java | 144 +++++++++++ sentry/src/main/java/io/sentry/NoOpHub.java | 5 + .../main/java/io/sentry/NoOpSentryClient.java | 6 + sentry/src/main/java/io/sentry/Sentry.java | 4 + .../src/main/java/io/sentry/SentryClient.java | 50 ++++ .../java/io/sentry/SentryEnvelopeItem.java | 27 +++ .../main/java/io/sentry/SentryItemType.java | 1 + 16 files changed, 798 insertions(+) create mode 100644 sentry/src/main/java/io/sentry/CheckIn.java create mode 100644 sentry/src/main/java/io/sentry/CheckInStatus.java create mode 100644 sentry/src/main/java/io/sentry/MonitorConfig.java create mode 100644 sentry/src/main/java/io/sentry/MonitorSchedule.java diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index b9bd902b50..f0fb606964 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -6,6 +6,7 @@ import androidx.lifecycle.Lifecycle.Event.ON_STOP import androidx.lifecycle.LifecycleRegistry import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 +import io.sentry.CheckIn import io.sentry.Hint import io.sentry.ISentryClient import io.sentry.ProfilingTraceData @@ -162,5 +163,9 @@ class SessionTrackingIntegrationTest { ): SentryId { TODO("Not yet implemented") } + + override fun captureCheckIn(checkIn: CheckIn, scope: Scope?): SentryId { + TODO("Not yet implemented") + } } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 67cbd93d9a..b6dcecb511 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -155,6 +155,52 @@ public final class io/sentry/BuildConfig { public static final field VERSION_NAME Ljava/lang/String; } +public final class io/sentry/CheckIn : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Lio/sentry/protocol/SentryId;Ljava/lang/String;Ljava/lang/String;)V + public fun getCheckInId ()Lio/sentry/protocol/SentryId; + public fun getDuration ()Ljava/lang/Double; + public fun getEnvironment ()Ljava/lang/String; + public fun getMonitorConfig ()Lio/sentry/MonitorConfig; + public fun getMonitorSlug ()Ljava/lang/String; + public fun getRelease ()Ljava/lang/String; + public fun getStatus ()Ljava/lang/String; + public fun getUnknown ()Ljava/util/Map; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setDuration (Ljava/lang/Double;)V + public fun setEnvironment (Ljava/lang/String;)V + public fun setMonitorConfig (Lio/sentry/MonitorConfig;)V + public fun setMonitorSlug (Ljava/lang/String;)V + public fun setRelease (Ljava/lang/String;)V + public fun setStatus (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/CheckIn$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/CheckIn; + public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/CheckIn$JsonKeys { + public static final field CHECK_IN_ID Ljava/lang/String; + public static final field DURATION Ljava/lang/String; + public static final field ENVIRONMENT Ljava/lang/String; + public static final field MONITOR_CONFIG Ljava/lang/String; + public static final field MONITOR_SLUG Ljava/lang/String; + public static final field RELEASE Ljava/lang/String; + public static final field STATUS Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/CheckInStatus : java/lang/Enum, io/sentry/JsonSerializable { + public static final field ERROR Lio/sentry/CheckInStatus; + public static final field IN_PROGRESS Lio/sentry/CheckInStatus; + public static final field OK Lio/sentry/CheckInStatus; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public static fun valueOf (Ljava/lang/String;)Lio/sentry/CheckInStatus; + public static fun values ()[Lio/sentry/CheckInStatus; +} + public final class io/sentry/CpuCollectionData { public fun (JD)V public fun getCpuUsagePercentage ()D @@ -352,6 +398,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public fun (Lio/sentry/SentryOptions;)V public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V + public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; @@ -398,6 +445,7 @@ public final class io/sentry/Hub : io/sentry/IHub { public final class io/sentry/HubAdapter : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V + public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; @@ -462,6 +510,7 @@ public abstract interface class io/sentry/IHub { public fun addBreadcrumb (Ljava/lang/String;)V public fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V public abstract fun bindClient (Lio/sentry/ISentryClient;)V + public abstract fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;)Lio/sentry/protocol/SentryId; public abstract fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;)Lio/sentry/protocol/SentryId; @@ -561,6 +610,7 @@ public abstract interface class io/sentry/IScopeObserver { } public abstract interface class io/sentry/ISentryClient { + public abstract fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;)Lio/sentry/protocol/SentryId; public abstract fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;)Lio/sentry/protocol/SentryId; @@ -848,6 +898,61 @@ public final class io/sentry/MemoryCollectionData { public fun getUsedNativeMemory ()J } +public final class io/sentry/MonitorConfig : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Lio/sentry/MonitorSchedule;)V + public fun getCheckinMargin ()Ljava/lang/Long; + public fun getMaxRuntime ()Ljava/lang/Long; + public fun getSchedule ()Lio/sentry/MonitorSchedule; + public fun getTimezone ()Ljava/lang/String; + public fun getUnknown ()Ljava/util/Map; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setCheckinMargin (Ljava/lang/Long;)V + public fun setMaxRuntime (Ljava/lang/Long;)V + public fun setSchedule (Lio/sentry/MonitorSchedule;)V + public fun setTimezone (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V +} + +public final class io/sentry/MonitorConfig$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/MonitorConfig; + public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/MonitorConfig$JsonKeys { + public static final field CHECKIN_MARGIN Ljava/lang/String; + public static final field MAX_RUNTIME Ljava/lang/String; + public static final field SCHEDULE Ljava/lang/String; + public static final field TIMEZONE Ljava/lang/String; + public fun ()V +} + +public final class io/sentry/MonitorSchedule : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun getType ()Ljava/lang/String; + public fun getUnit ()Ljava/lang/String; + public fun getUnknown ()Ljava/util/Map; + public fun getValue ()Ljava/lang/String; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setType (Ljava/lang/String;)V + public fun setUnit (Ljava/lang/String;)V + public fun setUnknown (Ljava/util/Map;)V + public fun setValue (Ljava/lang/String;)V +} + +public final class io/sentry/MonitorSchedule$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/MonitorSchedule; + public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + +public final class io/sentry/MonitorSchedule$JsonKeys { + public static final field TYPE Ljava/lang/String; + public static final field UNIT Ljava/lang/String; + public static final field VALUE Ljava/lang/String; + public fun ()V +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; @@ -856,6 +961,7 @@ public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public final class io/sentry/NoOpHub : io/sentry/IHub { public fun addBreadcrumb (Lio/sentry/Breadcrumb;Lio/sentry/Hint;)V public fun bindClient (Lio/sentry/ISentryClient;)V + public fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; @@ -1308,6 +1414,7 @@ public final class io/sentry/Sentry { public static fun addBreadcrumb (Ljava/lang/String;)V public static fun addBreadcrumb (Ljava/lang/String;Ljava/lang/String;)V public static fun bindClient (Lio/sentry/ISentryClient;)V + public static fun captureCheckIn (Lio/sentry/CheckIn;)Lio/sentry/protocol/SentryId; public static fun captureEvent (Lio/sentry/SentryEvent;)Lio/sentry/protocol/SentryId; public static fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public static fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Hint;Lio/sentry/ScopeCallback;)Lio/sentry/protocol/SentryId; @@ -1454,6 +1561,7 @@ public final class io/sentry/SentryBaseEvent$Serializer { } public final class io/sentry/SentryClient : io/sentry/ISentryClient { + public fun captureCheckIn (Lio/sentry/CheckIn;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEnvelope (Lio/sentry/SentryEnvelope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureEvent (Lio/sentry/SentryEvent;Lio/sentry/Scope;Lio/sentry/Hint;)Lio/sentry/protocol/SentryId; public fun captureSession (Lio/sentry/Session;Lio/sentry/Hint;)V @@ -1528,6 +1636,7 @@ public final class io/sentry/SentryEnvelopeHeader$JsonKeys { public final class io/sentry/SentryEnvelopeItem { public static fun fromAttachment (Lio/sentry/ISerializer;Lio/sentry/ILogger;Lio/sentry/Attachment;J)Lio/sentry/SentryEnvelopeItem; + public static fun fromCheckIn (Lio/sentry/ISerializer;Lio/sentry/CheckIn;)Lio/sentry/SentryEnvelopeItem; public static fun fromClientReport (Lio/sentry/ISerializer;Lio/sentry/clientreport/ClientReport;)Lio/sentry/SentryEnvelopeItem; public static fun fromEvent (Lio/sentry/ISerializer;Lio/sentry/SentryBaseEvent;)Lio/sentry/SentryEnvelopeItem; public static fun fromProfilingTrace (Lio/sentry/ProfilingTraceData;JLio/sentry/ISerializer;)Lio/sentry/SentryEnvelopeItem; @@ -1646,6 +1755,7 @@ public final class io/sentry/SentryIntegrationPackageStorage { public final class io/sentry/SentryItemType : java/lang/Enum, io/sentry/JsonSerializable { public static final field Attachment Lio/sentry/SentryItemType; + public static final field CheckIn Lio/sentry/SentryItemType; public static final field ClientReport Lio/sentry/SentryItemType; public static final field Event Lio/sentry/SentryItemType; public static final field Profile Lio/sentry/SentryItemType; diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java new file mode 100644 index 0000000000..eef0d1b417 --- /dev/null +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -0,0 +1,225 @@ +package io.sentry; + +import io.sentry.protocol.SentryId; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** Adds additional information about what happened to an event. */ +public final class CheckIn implements JsonUnknown, JsonSerializable { + + private final @NotNull SentryId checkInId; + private @NotNull String monitorSlug; + private @NotNull String status; + private @Nullable Double duration; // in seconds + private @Nullable String release; + private @Nullable String environment; + // private @Nullable Map<...> contexts; + private @Nullable MonitorConfig monitorConfig; + + private @Nullable Map unknown; + + public CheckIn(SentryId checkInId, String monitorSlug, String status) { + this.checkInId = checkInId; + this.monitorSlug = monitorSlug; + this.status = status; + } + + // JsonKeys + + public static final class JsonKeys { + public static final String CHECK_IN_ID = "check_in_id"; + public static final String MONITOR_SLUG = "monitor_slug"; + public static final String STATUS = "status"; + public static final String DURATION = "duration"; + public static final String RELEASE = "release"; + public static final String ENVIRONMENT = "environment"; + // public static final String CONTEXTS = "contexts"; + public static final String MONITOR_CONFIG = "monitor_config"; + } + + public @NotNull SentryId getCheckInId() { + return checkInId; + } + + public @NotNull String getMonitorSlug() { + return monitorSlug; + } + + public void setMonitorSlug(@NotNull String monitorSlug) { + this.monitorSlug = monitorSlug; + } + + public @NotNull String getStatus() { + return status; + } + + public void setStatus(@NotNull String status) { + this.status = status; + } + + public @Nullable Double getDuration() { + return duration; + } + + public void setDuration(@Nullable Double duration) { + this.duration = duration; + } + + public @Nullable String getRelease() { + return release; + } + + public void setRelease(@Nullable String release) { + this.release = release; + } + + public @Nullable String getEnvironment() { + return environment; + } + + public void setEnvironment(@Nullable String environment) { + this.environment = environment; + } + + public @Nullable MonitorConfig getMonitorConfig() { + return monitorConfig; + } + + public void setMonitorConfig(@Nullable MonitorConfig monitorConfig) { + this.monitorConfig = monitorConfig; + } + // JsonUnknown + + @Override + public @Nullable Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + // JsonSerializable + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.CHECK_IN_ID); + checkInId.serialize(writer, logger); + writer.name(JsonKeys.MONITOR_SLUG).value(monitorSlug); + writer.name(JsonKeys.STATUS).value(status); + if (duration != null) { + writer.name(JsonKeys.DURATION).value(duration); + } + if (release != null) { + writer.name(JsonKeys.RELEASE).value(release); + } + if (environment != null) { + writer.name(JsonKeys.ENVIRONMENT).value(environment); + } + if (monitorConfig != null) { + writer.name(JsonKeys.MONITOR_CONFIG); + monitorConfig.serialize(writer, logger); + } + // if (contexts != null) { + // writer.name(JsonKeys.CONTEXTS).value(contexts); + // } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + // JsonDeserializer + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull CheckIn deserialize(@NotNull JsonObjectReader reader, @NotNull ILogger logger) + throws Exception { + SentryId sentryId = null; + MonitorConfig monitorConfig = null; + String monitorSlug = null; + String status = null; + Double duration = null; + String release = null; + String environment = null; + Map unknown = null; + + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.CHECK_IN_ID: + sentryId = new SentryId.Deserializer().deserialize(reader, logger); + break; + case JsonKeys.MONITOR_SLUG: + monitorSlug = reader.nextStringOrNull(); + break; + case JsonKeys.STATUS: + status = reader.nextStringOrNull(); + break; + case JsonKeys.DURATION: + duration = reader.nextDoubleOrNull(); + break; + case JsonKeys.RELEASE: + release = reader.nextStringOrNull(); + break; + case JsonKeys.ENVIRONMENT: + environment = reader.nextStringOrNull(); + break; + case JsonKeys.MONITOR_CONFIG: + monitorConfig = new MonitorConfig.Deserializer().deserialize(reader, logger); + break; + // case JsonKeys.CONTEXTS: + // contexts = reader.nextStringOrNull(); + // break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + reader.endObject(); + + if (sentryId == null) { + String message = "Missing required field \"" + JsonKeys.CHECK_IN_ID + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + if (monitorSlug == null) { + String message = "Missing required field \"" + JsonKeys.MONITOR_SLUG + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + if (status == null) { + String message = "Missing required field \"" + JsonKeys.STATUS + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + CheckIn checkIn = new CheckIn(sentryId, monitorSlug, status); + checkIn.setDuration(duration); + checkIn.setRelease(release); + checkIn.setEnvironment(environment); + checkIn.setMonitorConfig(monitorConfig); + checkIn.setUnknown(unknown); + return checkIn; + } + } +} diff --git a/sentry/src/main/java/io/sentry/CheckInStatus.java b/sentry/src/main/java/io/sentry/CheckInStatus.java new file mode 100644 index 0000000000..61ece39504 --- /dev/null +++ b/sentry/src/main/java/io/sentry/CheckInStatus.java @@ -0,0 +1,27 @@ +package io.sentry; + +import java.io.IOException; +import java.util.Locale; +import org.jetbrains.annotations.NotNull; + +/** Status of a CheckIn */ +public enum CheckInStatus implements JsonSerializable { + IN_PROGRESS, + OK, + ERROR; + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.value(name().toLowerCase(Locale.ROOT)); + } + + static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull CheckInStatus deserialize( + @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { + return CheckInStatus.valueOf(reader.nextString().toUpperCase(Locale.ROOT)); + } + } +} diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 3171c67a95..4a1e4c9017 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -862,4 +862,32 @@ private Scope buildLocalScope( return null; } + + @Override + public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { + return captureCheckInInternal(checkIn); + } + + private @NotNull SentryId captureCheckInInternal(final @NotNull CheckIn checkIn) { + SentryId sentryId = SentryId.EMPTY_ID; + if (!isEnabled()) { + options + .getLogger() + .log( + SentryLevel.WARNING, + "Instance is disabled and this 'captureCheckIn' call is a no-op."); + // } else if (message == null) { + // options.getLogger().log(SentryLevel.WARNING, "captureCheckIn called with null + // parameter."); + } else { + try { + StackItem item = stack.peek(); + sentryId = item.getClient().captureCheckIn(checkIn, item.getScope(), null); + } catch (Throwable e) { + options.getLogger().log(SentryLevel.ERROR, "Error while capturing check-in for slug", e); + } + } + this.lastEventId = sentryId; + return sentryId; + } } diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index e6ec220874..05f348cb15 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -251,4 +251,9 @@ public void reportFullyDisplayed() { public @Nullable BaggageHeader getBaggage() { return Sentry.getBaggage(); } + + @Override + public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { + return Sentry.captureCheckIn(checkIn); + } } diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 06f36f1a54..71bbb0f731 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -626,4 +626,7 @@ TransactionContext continueTrace( */ @Nullable BaggageHeader getBaggage(); + + @NotNull + SentryId captureCheckIn(final @NotNull CheckIn checkIn); } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 4789eb3dec..28ce8111d0 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -264,4 +264,7 @@ SentryId captureTransaction( default @NotNull SentryId captureTransaction(@NotNull SentryTransaction transaction) { return captureTransaction(transaction, null, null, null); } + + @NotNull + SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint); } diff --git a/sentry/src/main/java/io/sentry/MonitorConfig.java b/sentry/src/main/java/io/sentry/MonitorConfig.java new file mode 100644 index 0000000000..b6ad81ee8f --- /dev/null +++ b/sentry/src/main/java/io/sentry/MonitorConfig.java @@ -0,0 +1,155 @@ +package io.sentry; + +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class MonitorConfig implements JsonUnknown, JsonSerializable { + + private @NotNull MonitorSchedule schedule; + private @Nullable Long checkinMargin; + private @Nullable Long maxRuntime; + private @Nullable String timezone; + + private @Nullable Map unknown; + + public MonitorConfig(final @NotNull MonitorSchedule schedule) { + this.schedule = schedule; + } + + public @NotNull MonitorSchedule getSchedule() { + return schedule; + } + + public void setSchedule(@NotNull MonitorSchedule schedule) { + this.schedule = schedule; + } + + public @Nullable Long getCheckinMargin() { + return checkinMargin; + } + + public void setCheckinMargin(@Nullable Long checkinMargin) { + this.checkinMargin = checkinMargin; + } + + public @Nullable Long getMaxRuntime() { + return maxRuntime; + } + + public void setMaxRuntime(@Nullable Long maxRuntime) { + this.maxRuntime = maxRuntime; + } + + public @Nullable String getTimezone() { + return timezone; + } + + public void setTimezone(@Nullable String timezone) { + this.timezone = timezone; + } + + // JsonKeys + + public static final class JsonKeys { + public static final String SCHEDULE = "schedule"; + public static final String CHECKIN_MARGIN = "checkin_margin"; + public static final String MAX_RUNTIME = "max_runtime"; + public static final String TIMEZONE = "timezone"; + } + + // JsonUnknown + + @Override + public @Nullable Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + // JsonSerializable + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.SCHEDULE); + schedule.serialize(writer, logger); + if (checkinMargin != null) { + writer.name(JsonKeys.CHECKIN_MARGIN).value(checkinMargin); + } + if (maxRuntime != null) { + writer.name(JsonKeys.MAX_RUNTIME).value(maxRuntime); + } + if (timezone != null) { + writer.name(JsonKeys.TIMEZONE).value(timezone); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + // JsonDeserializer + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull MonitorConfig deserialize( + @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { + MonitorSchedule schedule = null; + Long checkinMargin = null; + Long maxRuntime = null; + String timezone = null; + Map unknown = null; + + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.SCHEDULE: + schedule = new MonitorSchedule.Deserializer().deserialize(reader, logger); + break; + case JsonKeys.CHECKIN_MARGIN: + checkinMargin = reader.nextLongOrNull(); + break; + case JsonKeys.MAX_RUNTIME: + maxRuntime = reader.nextLongOrNull(); + break; + case JsonKeys.TIMEZONE: + timezone = reader.nextStringOrNull(); + break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + reader.endObject(); + + if (schedule == null) { + String message = "Missing required field \"" + JsonKeys.SCHEDULE + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + MonitorConfig monitorConfig = new MonitorConfig(schedule); + monitorConfig.setCheckinMargin(checkinMargin); + monitorConfig.setMaxRuntime(maxRuntime); + monitorConfig.setTimezone(timezone); + monitorConfig.setUnknown(unknown); + return monitorConfig; + } + } +} diff --git a/sentry/src/main/java/io/sentry/MonitorSchedule.java b/sentry/src/main/java/io/sentry/MonitorSchedule.java new file mode 100644 index 0000000000..59ca0cf102 --- /dev/null +++ b/sentry/src/main/java/io/sentry/MonitorSchedule.java @@ -0,0 +1,144 @@ +package io.sentry; + +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class MonitorSchedule implements JsonUnknown, JsonSerializable { + + /** crontab | interval */ + private @NotNull String type; + + private @NotNull String value; + /** only required for type=interval */ + private @Nullable String unit; + + private @Nullable Map unknown; + + public MonitorSchedule(final @NotNull String type, final @NotNull String value) { + this.type = type; + this.value = value; + } + + public @NotNull String getType() { + return type; + } + + public void setType(final @NotNull String type) { + this.type = type; + } + + public @NotNull String getValue() { + return value; + } + + public void setValue(final @NotNull String value) { + this.value = value; + } + + public @Nullable String getUnit() { + return unit; + } + + public void setUnit(final @Nullable String unit) { + this.unit = unit; + } + + // JsonKeys + + public static final class JsonKeys { + public static final String TYPE = "type"; + public static final String VALUE = "value"; + public static final String UNIT = "unit"; + } + + // JsonUnknown + + @Override + public @Nullable Map getUnknown() { + return unknown; + } + + @Override + public void setUnknown(@Nullable Map unknown) { + this.unknown = unknown; + } + + // JsonSerializable + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + writer.name(JsonKeys.TYPE).value(type); + writer.name(JsonKeys.VALUE).value(value); + if (unit != null) { + writer.name(JsonKeys.UNIT).value(unit); + } + if (unknown != null) { + for (String key : unknown.keySet()) { + Object value = unknown.get(key); + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + // JsonDeserializer + + public static final class Deserializer implements JsonDeserializer { + @Override + public @NotNull MonitorSchedule deserialize( + @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { + String type = null; + String value = null; + String unit = null; + Map unknown = null; + + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case JsonKeys.TYPE: + type = reader.nextStringOrNull(); + break; + case JsonKeys.VALUE: + value = reader.nextStringOrNull(); + break; + case JsonKeys.UNIT: + unit = reader.nextStringOrNull(); + break; + default: + if (unknown == null) { + unknown = new HashMap<>(); + } + reader.nextUnknown(logger, unknown, nextName); + break; + } + } + reader.endObject(); + + if (type == null) { + String message = "Missing required field \"" + JsonKeys.TYPE + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + if (value == null) { + String message = "Missing required field \"" + JsonKeys.VALUE + "\""; + Exception exception = new IllegalStateException(message); + logger.log(SentryLevel.ERROR, message, exception); + throw exception; + } + + MonitorSchedule monitorSchedule = new MonitorSchedule(type, value); + monitorSchedule.setUnit(unit); + monitorSchedule.setUnknown(unknown); + return monitorSchedule; + } + } +} diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index aa5d846975..4d40efe976 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -207,4 +207,9 @@ public void reportFullyDisplayed() {} public @Nullable BaggageHeader getBaggage() { return null; } + + @Override + public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { + return SentryId.EMPTY_ID; + } } diff --git a/sentry/src/main/java/io/sentry/NoOpSentryClient.java b/sentry/src/main/java/io/sentry/NoOpSentryClient.java index 4afbdbb8c8..adeb63e356 100644 --- a/sentry/src/main/java/io/sentry/NoOpSentryClient.java +++ b/sentry/src/main/java/io/sentry/NoOpSentryClient.java @@ -52,4 +52,10 @@ public SentryId captureEnvelope(@NotNull SentryEnvelope envelope, @Nullable Hint @Nullable ProfilingTraceData profilingTraceData) { return SentryId.EMPTY_ID; } + + @Override + public @NotNull SentryId captureCheckIn( + @NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint) { + return SentryId.EMPTY_ID; + } } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index bdaa61490e..a42b557a54 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1009,4 +1009,8 @@ public interface OptionsConfiguration { public static @Nullable BaggageHeader getBaggage() { return getCurrentHub().getBaggage(); } + + public static @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { + return getCurrentHub().captureCheckIn(checkIn); + } } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index bd981842e3..4d6f6208dc 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -449,6 +449,20 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { return new SentryEnvelope(envelopeHeader, envelopeItems); } + private @NotNull SentryEnvelope buildEnvelope(final @NotNull CheckIn checkIn) { + final List envelopeItems = new ArrayList<>(); + + final SentryEnvelopeItem checkInItem = + SentryEnvelopeItem.fromCheckIn(options.getSerializer(), checkIn); + envelopeItems.add(checkInItem); + + // TODO do we need trace context? + final SentryEnvelopeHeader envelopeHeader = + new SentryEnvelopeHeader(checkIn.getCheckInId(), options.getSdkVersion()); + + return new SentryEnvelope(envelopeHeader, envelopeItems); + } + /** * Updates the session data based on the event, hint and scope data * @@ -642,6 +656,42 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return sentryId; } + @Override + public @NotNull SentryId captureCheckIn( + final @NotNull CheckIn checkIn, final @Nullable Scope scope, @Nullable Hint hint) { + if (hint == null) { + hint = new Hint(); + } + + // if (shouldApplyScopeData(transaction, hint)) { + // addScopeAttachmentsToHint(scope, hint); + // } + + options.getLogger().log(SentryLevel.DEBUG, "Capturing check-in: %s", checkIn.getCheckInId()); + + SentryId sentryId = SentryId.EMPTY_ID; + if (checkIn.getCheckInId() != null) { + sentryId = checkIn.getCheckInId(); + } + + try { + final SentryEnvelope envelope = buildEnvelope(checkIn); + + hint.clear(); + if (envelope != null) { + transport.send(envelope, hint); + } else { + sentryId = SentryId.EMPTY_ID; + } + } catch (IOException e) { + options.getLogger().log(SentryLevel.WARNING, e, "Capturing check-in %s failed.", sentryId); + // if there was an error capturing the event, we return an emptyId + sentryId = SentryId.EMPTY_ID; + } + + return sentryId; + } + private @Nullable List filterForTransaction(@Nullable List attachments) { if (attachments == null) { return null; diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index f095dea15b..8ae83a5f9b 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -168,6 +168,33 @@ public static SentryEnvelopeItem fromUserFeedback( return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); } + public static SentryEnvelopeItem fromCheckIn( + final @NotNull ISerializer serializer, final @NotNull CheckIn checkIn) { + Objects.requireNonNull(serializer, "ISerializer is required."); + Objects.requireNonNull(checkIn, "CheckIn is required."); + + final CachedItem cachedItem = + new CachedItem( + () -> { + try (final ByteArrayOutputStream stream = new ByteArrayOutputStream(); + final Writer writer = new BufferedWriter(new OutputStreamWriter(stream, UTF_8))) { + serializer.serialize(checkIn, writer); + return stream.toByteArray(); + } + }); + + SentryEnvelopeItemHeader itemHeader = + new SentryEnvelopeItemHeader( + SentryItemType.UserFeedback, + () -> cachedItem.getBytes().length, + "application/json", + null); + + // avoid method refs on Android due to some issues with older AGP setups + // noinspection Convert2MethodRef + return new SentryEnvelopeItem(itemHeader, () -> cachedItem.getBytes()); + } + public static SentryEnvelopeItem fromAttachment( final @NotNull ISerializer serializer, final @NotNull ILogger logger, diff --git a/sentry/src/main/java/io/sentry/SentryItemType.java b/sentry/src/main/java/io/sentry/SentryItemType.java index 69b94faa8e..c4535cb6a1 100644 --- a/sentry/src/main/java/io/sentry/SentryItemType.java +++ b/sentry/src/main/java/io/sentry/SentryItemType.java @@ -18,6 +18,7 @@ public enum SentryItemType implements JsonSerializable { ClientReport("client_report"), ReplayEvent("replay_event"), ReplayRecording("replay_recording"), + CheckIn("check_in"), Unknown("__unknown__"); // DataCategory.Unknown private final String itemType; From 1de0ba7c1f8615154d9fc6a175994371a18f7d0f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 12 Sep 2023 14:34:57 +0200 Subject: [PATCH 02/22] Add trace context, release and env; factory methods; fix serialization problems --- .../core/SessionTrackingIntegrationTest.kt | 2 +- sentry/api/sentry.api | 38 +++++++- sentry/src/main/java/io/sentry/CheckIn.java | 34 +++++-- .../main/java/io/sentry/CheckInStatus.java | 4 + .../main/java/io/sentry/MonitorContexts.java | 90 +++++++++++++++++++ .../main/java/io/sentry/MonitorSchedule.java | 36 +++++++- .../java/io/sentry/MonitorScheduleUnit.java | 34 +++++++ .../src/main/java/io/sentry/SentryClient.java | 47 +++++++++- .../java/io/sentry/SentryEnvelopeItem.java | 5 +- 9 files changed, 268 insertions(+), 22 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/MonitorContexts.java create mode 100644 sentry/src/main/java/io/sentry/MonitorScheduleUnit.java diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt index f0fb606964..73f445940e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SessionTrackingIntegrationTest.kt @@ -164,7 +164,7 @@ class SessionTrackingIntegrationTest { TODO("Not yet implemented") } - override fun captureCheckIn(checkIn: CheckIn, scope: Scope?): SentryId { + override fun captureCheckIn(checkIn: CheckIn, scope: Scope?, hint: Hint?): SentryId { TODO("Not yet implemented") } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index b6dcecb511..afa4871de4 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -157,7 +157,9 @@ public final class io/sentry/BuildConfig { public final class io/sentry/CheckIn : io/sentry/JsonSerializable, io/sentry/JsonUnknown { public fun (Lio/sentry/protocol/SentryId;Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Lio/sentry/CheckInStatus;)V public fun getCheckInId ()Lio/sentry/protocol/SentryId; + public fun getContexts ()Lio/sentry/MonitorContexts; public fun getDuration ()Ljava/lang/Double; public fun getEnvironment ()Ljava/lang/String; public fun getMonitorConfig ()Lio/sentry/MonitorConfig; @@ -171,6 +173,7 @@ public final class io/sentry/CheckIn : io/sentry/JsonSerializable, io/sentry/Jso public fun setMonitorConfig (Lio/sentry/MonitorConfig;)V public fun setMonitorSlug (Ljava/lang/String;)V public fun setRelease (Ljava/lang/String;)V + public fun setStatus (Lio/sentry/CheckInStatus;)V public fun setStatus (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V } @@ -183,6 +186,7 @@ public final class io/sentry/CheckIn$Deserializer : io/sentry/JsonDeserializer { public final class io/sentry/CheckIn$JsonKeys { public static final field CHECK_IN_ID Ljava/lang/String; + public static final field CONTEXTS Ljava/lang/String; public static final field DURATION Ljava/lang/String; public static final field ENVIRONMENT Ljava/lang/String; public static final field MONITOR_CONFIG Ljava/lang/String; @@ -196,6 +200,7 @@ public final class io/sentry/CheckInStatus : java/lang/Enum, io/sentry/JsonSeria public static final field ERROR Lio/sentry/CheckInStatus; public static final field IN_PROGRESS Lio/sentry/CheckInStatus; public static final field OK Lio/sentry/CheckInStatus; + public fun apiName ()Ljava/lang/String; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public static fun valueOf (Ljava/lang/String;)Lio/sentry/CheckInStatus; public static fun values ()[Lio/sentry/CheckInStatus; @@ -927,16 +932,34 @@ public final class io/sentry/MonitorConfig$JsonKeys { public fun ()V } +public final class io/sentry/MonitorContexts : java/util/concurrent/ConcurrentHashMap, io/sentry/JsonSerializable { + public fun ()V + public fun (Lio/sentry/MonitorContexts;)V + public fun getTrace ()Lio/sentry/SpanContext; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public fun setTrace (Lio/sentry/SpanContext;)V +} + +public final class io/sentry/MonitorContexts$Deserializer : io/sentry/JsonDeserializer { + public fun ()V + public fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Lio/sentry/MonitorContexts; + public synthetic fun deserialize (Lio/sentry/JsonObjectReader;Lio/sentry/ILogger;)Ljava/lang/Object; +} + public final class io/sentry/MonitorSchedule : io/sentry/JsonSerializable, io/sentry/JsonUnknown { - public fun (Ljava/lang/String;Ljava/lang/String;)V + public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V + public static fun crontab (Ljava/lang/String;)Lio/sentry/MonitorSchedule; public fun getType ()Ljava/lang/String; public fun getUnit ()Ljava/lang/String; public fun getUnknown ()Ljava/util/Map; public fun getValue ()Ljava/lang/String; + public static fun interval (Ljava/lang/Integer;Lio/sentry/MonitorScheduleUnit;)Lio/sentry/MonitorSchedule; public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public fun setType (Ljava/lang/String;)V + public fun setUnit (Lio/sentry/MonitorScheduleUnit;)V public fun setUnit (Ljava/lang/String;)V public fun setUnknown (Ljava/util/Map;)V + public fun setValue (Ljava/lang/Integer;)V public fun setValue (Ljava/lang/String;)V } @@ -953,6 +976,19 @@ public final class io/sentry/MonitorSchedule$JsonKeys { public fun ()V } +public final class io/sentry/MonitorScheduleUnit : java/lang/Enum, io/sentry/JsonSerializable { + public static final field DAY Lio/sentry/MonitorScheduleUnit; + public static final field HOUR Lio/sentry/MonitorScheduleUnit; + public static final field MINUTE Lio/sentry/MonitorScheduleUnit; + public static final field MONTH Lio/sentry/MonitorScheduleUnit; + public static final field WEEK Lio/sentry/MonitorScheduleUnit; + public static final field YEAR Lio/sentry/MonitorScheduleUnit; + public fun apiName ()Ljava/lang/String; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public static fun valueOf (Ljava/lang/String;)Lio/sentry/MonitorScheduleUnit; + public static fun values ()[Lio/sentry/MonitorScheduleUnit; +} + public final class io/sentry/NoOpEnvelopeReader : io/sentry/IEnvelopeReader { public static fun getInstance ()Lio/sentry/NoOpEnvelopeReader; public fun read (Ljava/io/InputStream;)Lio/sentry/SentryEnvelope; diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index eef0d1b417..f92b1ee56c 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -5,6 +5,7 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -17,11 +18,17 @@ public final class CheckIn implements JsonUnknown, JsonSerializable { private @Nullable Double duration; // in seconds private @Nullable String release; private @Nullable String environment; - // private @Nullable Map<...> contexts; + + private final @NotNull MonitorContexts contexts = new MonitorContexts(); private @Nullable MonitorConfig monitorConfig; private @Nullable Map unknown; + public CheckIn(String monitorSlug, CheckInStatus status) { + this(new SentryId(), monitorSlug, status.apiName()); + } + + @ApiStatus.Internal public CheckIn(SentryId checkInId, String monitorSlug, String status) { this.checkInId = checkInId; this.monitorSlug = monitorSlug; @@ -37,7 +44,7 @@ public static final class JsonKeys { public static final String DURATION = "duration"; public static final String RELEASE = "release"; public static final String ENVIRONMENT = "environment"; - // public static final String CONTEXTS = "contexts"; + public static final String CONTEXTS = "contexts"; public static final String MONITOR_CONFIG = "monitor_config"; } @@ -61,6 +68,10 @@ public void setStatus(@NotNull String status) { this.status = status; } + public void setStatus(@NotNull CheckInStatus status) { + this.status = status.apiName(); + } + public @Nullable Double getDuration() { return duration; } @@ -92,6 +103,10 @@ public void setEnvironment(@Nullable String environment) { public void setMonitorConfig(@Nullable MonitorConfig monitorConfig) { this.monitorConfig = monitorConfig; } + + public @NotNull MonitorContexts getContexts() { + return contexts; + } // JsonUnknown @Override @@ -127,9 +142,10 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.name(JsonKeys.MONITOR_CONFIG); monitorConfig.serialize(writer, logger); } - // if (contexts != null) { - // writer.name(JsonKeys.CONTEXTS).value(contexts); - // } + if (contexts != null) { + writer.name(JsonKeys.CONTEXTS); + contexts.serialize(writer, logger); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -152,6 +168,7 @@ public static final class Deserializer implements JsonDeserializer { Double duration = null; String release = null; String environment = null; + MonitorContexts contexts = null; Map unknown = null; reader.beginObject(); @@ -179,9 +196,9 @@ public static final class Deserializer implements JsonDeserializer { case JsonKeys.MONITOR_CONFIG: monitorConfig = new MonitorConfig.Deserializer().deserialize(reader, logger); break; - // case JsonKeys.CONTEXTS: - // contexts = reader.nextStringOrNull(); - // break; + case JsonKeys.CONTEXTS: + contexts = new MonitorContexts.Deserializer().deserialize(reader, logger); + break; default: if (unknown == null) { unknown = new HashMap<>(); @@ -218,6 +235,7 @@ public static final class Deserializer implements JsonDeserializer { checkIn.setRelease(release); checkIn.setEnvironment(environment); checkIn.setMonitorConfig(monitorConfig); + checkIn.getContexts().putAll(contexts); checkIn.setUnknown(unknown); return checkIn; } diff --git a/sentry/src/main/java/io/sentry/CheckInStatus.java b/sentry/src/main/java/io/sentry/CheckInStatus.java index 61ece39504..22c5228a6d 100644 --- a/sentry/src/main/java/io/sentry/CheckInStatus.java +++ b/sentry/src/main/java/io/sentry/CheckInStatus.java @@ -16,6 +16,10 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger writer.value(name().toLowerCase(Locale.ROOT)); } + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } + static final class Deserializer implements JsonDeserializer { @Override diff --git a/sentry/src/main/java/io/sentry/MonitorContexts.java b/sentry/src/main/java/io/sentry/MonitorContexts.java new file mode 100644 index 0000000000..461a4d549a --- /dev/null +++ b/sentry/src/main/java/io/sentry/MonitorContexts.java @@ -0,0 +1,90 @@ +package io.sentry; + +import io.sentry.util.Objects; +import io.sentry.vendor.gson.stream.JsonToken; +import java.io.IOException; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +public final class MonitorContexts extends ConcurrentHashMap + implements JsonSerializable { + private static final long serialVersionUID = 3987329379811822556L; + + public MonitorContexts() {} + + public MonitorContexts(final @NotNull MonitorContexts contexts) { + for (final Entry entry : contexts.entrySet()) { + if (entry != null) { + final Object value = entry.getValue(); + if (SpanContext.TYPE.equals(entry.getKey()) && value instanceof SpanContext) { + this.setTrace(new SpanContext((SpanContext) value)); + } else { + this.put(entry.getKey(), value); + } + } + } + } + + private @Nullable T toContextType(final @NotNull String key, final @NotNull Class clazz) { + final Object item = get(key); + return clazz.isInstance(item) ? clazz.cast(item) : null; + } + + public @Nullable SpanContext getTrace() { + return toContextType(SpanContext.TYPE, SpanContext.class); + } + + public void setTrace(final @Nullable SpanContext traceContext) { + Objects.requireNonNull(traceContext, "traceContext is required"); + this.put(SpanContext.TYPE, traceContext); + } + + // region json + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.beginObject(); + // Serialize in alphabetical order to keep determinism. + final List sortedKeys = Collections.list(keys()); + Collections.sort(sortedKeys); + for (final String key : sortedKeys) { + final Object value = get(key); + if (value != null) { + writer.name(key).value(logger, value); + } + } + writer.endObject(); + } + + public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull MonitorContexts deserialize( + final @NotNull JsonObjectReader reader, final @NotNull ILogger logger) throws Exception { + final MonitorContexts contexts = new MonitorContexts(); + reader.beginObject(); + while (reader.peek() == JsonToken.NAME) { + final String nextName = reader.nextName(); + switch (nextName) { + case SpanContext.TYPE: + contexts.setTrace(new SpanContext.Deserializer().deserialize(reader, logger)); + break; + default: + Object object = reader.nextObjectOrNull(); + if (object != null) { + contexts.put(nextName, object); + } + break; + } + } + reader.endObject(); + return contexts; + } + } + + // endregion +} diff --git a/sentry/src/main/java/io/sentry/MonitorSchedule.java b/sentry/src/main/java/io/sentry/MonitorSchedule.java index 59ca0cf102..97c4e510f5 100644 --- a/sentry/src/main/java/io/sentry/MonitorSchedule.java +++ b/sentry/src/main/java/io/sentry/MonitorSchedule.java @@ -4,11 +4,21 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; public final class MonitorSchedule implements JsonUnknown, JsonSerializable { + public static @NotNull MonitorSchedule crontab(final @NotNull String value) { + return new MonitorSchedule("crontab", value, null); + } + + public static @NotNull MonitorSchedule interval( + final @NotNull Integer value, final @NotNull MonitorScheduleUnit unit) { + return new MonitorSchedule("interval", value.toString(), unit.apiName()); + } + /** crontab | interval */ private @NotNull String type; @@ -18,9 +28,12 @@ public final class MonitorSchedule implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; - public MonitorSchedule(final @NotNull String type, final @NotNull String value) { + @ApiStatus.Internal + public MonitorSchedule( + final @NotNull String type, final @NotNull String value, final @Nullable String unit) { this.type = type; this.value = value; + this.unit = unit; } public @NotNull String getType() { @@ -39,6 +52,10 @@ public void setValue(final @NotNull String value) { this.value = value; } + public void setValue(final @NotNull Integer value) { + this.value = value.toString(); + } + public @Nullable String getUnit() { return unit; } @@ -47,6 +64,10 @@ public void setUnit(final @Nullable String unit) { this.unit = unit; } + public void setUnit(final @Nullable MonitorScheduleUnit unit) { + this.unit = unit == null ? null : unit.apiName(); + } + // JsonKeys public static final class JsonKeys { @@ -74,7 +95,15 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger throws IOException { writer.beginObject(); writer.name(JsonKeys.TYPE).value(type); - writer.name(JsonKeys.VALUE).value(value); + if ("interval".equalsIgnoreCase(type)) { + try { + writer.name(JsonKeys.VALUE).value(Integer.valueOf(value)); + } catch (Throwable t) { + // ignored + } + } else { + writer.name(JsonKeys.VALUE).value(value); + } if (unit != null) { writer.name(JsonKeys.UNIT).value(unit); } @@ -135,8 +164,7 @@ public static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull MonitorScheduleUnit deserialize( + @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { + return MonitorScheduleUnit.valueOf(reader.nextString().toUpperCase(Locale.ROOT)); + } + } +} diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index 4d6f6208dc..eb82ca1bd6 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -71,6 +71,20 @@ private boolean shouldApplyScopeData( } } + private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNull Hint hint) { + if (HintUtils.shouldApplyScopeData(hint)) { + return true; + } else { + options + .getLogger() + .log( + SentryLevel.DEBUG, + "Check-in was cached so not applying scope: %s", + event.getCheckInId()); + return false; + } + } + @Override public @NotNull SentryId captureEvent( @NotNull SentryEvent event, final @Nullable Scope scope, @Nullable Hint hint) { @@ -658,14 +672,22 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint @Override public @NotNull SentryId captureCheckIn( - final @NotNull CheckIn checkIn, final @Nullable Scope scope, @Nullable Hint hint) { + @NotNull CheckIn checkIn, final @Nullable Scope scope, @Nullable Hint hint) { if (hint == null) { hint = new Hint(); } - // if (shouldApplyScopeData(transaction, hint)) { - // addScopeAttachmentsToHint(scope, hint); - // } + if (checkIn.getEnvironment() == null) { + checkIn.setEnvironment(options.getEnvironment()); + } + + if (checkIn.getRelease() == null) { + checkIn.setRelease(options.getRelease()); + } + + if (shouldApplyScopeData(checkIn, hint)) { + checkIn = applyScope(checkIn, scope); + } options.getLogger().log(SentryLevel.DEBUG, "Capturing check-in: %s", checkIn.getCheckInId()); @@ -739,6 +761,23 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint return event; } + private @NotNull CheckIn applyScope(@NotNull CheckIn checkIn, final @Nullable Scope scope) { + if (scope != null) { + // Set trace data from active span to connect events with transactions + final ISpan span = scope.getSpan(); + if (checkIn.getContexts().getTrace() == null) { + if (span == null) { + checkIn + .getContexts() + .setTrace(TransactionContext.fromPropagationContext(scope.getPropagationContext())); + } else { + checkIn.getContexts().setTrace(span.getSpanContext()); + } + } + } + return checkIn; + } + private @NotNull T applyScope( final @NotNull T sentryBaseEvent, final @Nullable Scope scope) { if (scope != null) { diff --git a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java index 8ae83a5f9b..61a7819942 100644 --- a/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java +++ b/sentry/src/main/java/io/sentry/SentryEnvelopeItem.java @@ -185,10 +185,7 @@ public static SentryEnvelopeItem fromCheckIn( SentryEnvelopeItemHeader itemHeader = new SentryEnvelopeItemHeader( - SentryItemType.UserFeedback, - () -> cachedItem.getBytes().length, - "application/json", - null); + SentryItemType.CheckIn, () -> cachedItem.getBytes().length, "application/json", null); // avoid method refs on Android due to some issues with older AGP setups // noinspection Convert2MethodRef From 3dd4fceb489d9fa8cef13556fcaaf91e70b668f2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 Sep 2023 11:08:54 +0200 Subject: [PATCH 03/22] add tests --- sentry/api/sentry.api | 9 ++ .../main/java/io/sentry/MonitorSchedule.java | 9 +- .../java/io/sentry/MonitorScheduleType.java | 30 ++++ .../io/sentry/CheckInSerializationTest.kt | 145 ++++++++++++++++++ .../src/test/java/io/sentry/HubAdapterTest.kt | 6 + sentry/src/test/java/io/sentry/HubTest.kt | 38 +++++ .../test/java/io/sentry/SentryClientTest.kt | 53 +++++++ sentry/src/test/java/io/sentry/SentryTest.kt | 20 +++ .../test/resources/json/checkin_crontab.json | 29 ++++ .../test/resources/json/checkin_interval.json | 30 ++++ 10 files changed, 365 insertions(+), 4 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/MonitorScheduleType.java create mode 100644 sentry/src/test/java/io/sentry/CheckInSerializationTest.kt create mode 100644 sentry/src/test/resources/json/checkin_crontab.json create mode 100644 sentry/src/test/resources/json/checkin_interval.json diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index afa4871de4..fcc0e54612 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -976,6 +976,15 @@ public final class io/sentry/MonitorSchedule$JsonKeys { public fun ()V } +public final class io/sentry/MonitorScheduleType : java/lang/Enum, io/sentry/JsonSerializable { + public static final field CRONTAB Lio/sentry/MonitorScheduleType; + public static final field INTERVAL Lio/sentry/MonitorScheduleType; + public fun apiName ()Ljava/lang/String; + public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V + public static fun valueOf (Ljava/lang/String;)Lio/sentry/MonitorScheduleType; + public static fun values ()[Lio/sentry/MonitorScheduleType; +} + public final class io/sentry/MonitorScheduleUnit : java/lang/Enum, io/sentry/JsonSerializable { public static final field DAY Lio/sentry/MonitorScheduleUnit; public static final field HOUR Lio/sentry/MonitorScheduleUnit; diff --git a/sentry/src/main/java/io/sentry/MonitorSchedule.java b/sentry/src/main/java/io/sentry/MonitorSchedule.java index 97c4e510f5..904e84aeac 100644 --- a/sentry/src/main/java/io/sentry/MonitorSchedule.java +++ b/sentry/src/main/java/io/sentry/MonitorSchedule.java @@ -11,12 +11,13 @@ public final class MonitorSchedule implements JsonUnknown, JsonSerializable { public static @NotNull MonitorSchedule crontab(final @NotNull String value) { - return new MonitorSchedule("crontab", value, null); + return new MonitorSchedule(MonitorScheduleType.CRONTAB.apiName(), value, null); } public static @NotNull MonitorSchedule interval( final @NotNull Integer value, final @NotNull MonitorScheduleUnit unit) { - return new MonitorSchedule("interval", value.toString(), unit.apiName()); + return new MonitorSchedule( + MonitorScheduleType.INTERVAL.apiName(), value.toString(), unit.apiName()); } /** crontab | interval */ @@ -95,11 +96,11 @@ public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger throws IOException { writer.beginObject(); writer.name(JsonKeys.TYPE).value(type); - if ("interval".equalsIgnoreCase(type)) { + if (MonitorScheduleType.INTERVAL.apiName().equalsIgnoreCase(type)) { try { writer.name(JsonKeys.VALUE).value(Integer.valueOf(value)); } catch (Throwable t) { - // ignored + logger.log(SentryLevel.ERROR, "Unable to serialize monitor schedule value: %s", value); } } else { writer.name(JsonKeys.VALUE).value(value); diff --git a/sentry/src/main/java/io/sentry/MonitorScheduleType.java b/sentry/src/main/java/io/sentry/MonitorScheduleType.java new file mode 100644 index 0000000000..bd9abebfc7 --- /dev/null +++ b/sentry/src/main/java/io/sentry/MonitorScheduleType.java @@ -0,0 +1,30 @@ +package io.sentry; + +import java.io.IOException; +import java.util.Locale; +import org.jetbrains.annotations.NotNull; + +/** Type of a monitor schedule */ +public enum MonitorScheduleType implements JsonSerializable { + CRONTAB, + INTERVAL; + + @Override + public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) + throws IOException { + writer.value(name().toLowerCase(Locale.ROOT)); + } + + public @NotNull String apiName() { + return name().toLowerCase(Locale.ROOT); + } + + static final class Deserializer implements JsonDeserializer { + + @Override + public @NotNull MonitorScheduleType deserialize( + @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { + return MonitorScheduleType.valueOf(reader.nextString().toUpperCase(Locale.ROOT)); + } + } +} diff --git a/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt new file mode 100644 index 0000000000..d7a3860fe6 --- /dev/null +++ b/sentry/src/test/java/io/sentry/CheckInSerializationTest.kt @@ -0,0 +1,145 @@ +package io.sentry + +import io.sentry.protocol.SentryId +import io.sentry.protocol.SerializationUtils +import org.junit.Test +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import java.io.StringReader +import java.io.StringWriter +import java.time.ZoneId +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue +import kotlin.test.fail + +class CheckInSerializationTest { + + private class Fixture { + val logger = mock() + + fun getSut(type: MonitorScheduleType): CheckIn { + return CheckIn("some_slug", CheckInStatus.ERROR).apply { + contexts.trace = TransactionContext.fromPropagationContext( + PropagationContext().also { + it.traceId = SentryId("f382e3180c714217a81371f8c644aefe") + it.spanId = SpanId("85694b9f567145a6") + } + ) + duration = 12.3 + environment = "env" + release = "1.0.1" + val monitorConfigTmp = + if (MonitorScheduleType.CRONTAB.equals(type)) { + MonitorConfig(MonitorSchedule.crontab("0 * * * *")) + } else { + MonitorConfig(MonitorSchedule.interval(42, MonitorScheduleUnit.MINUTE)) + } + monitorConfig = monitorConfigTmp.apply { + checkinMargin = 8L + maxRuntime = 9L + timezone = ZoneId.of("Europe/Vienna").id + } + } + } + } + private val fixture = Fixture() + + @Test + fun serializeInterval() { + val checkIn = fixture.getSut(MonitorScheduleType.INTERVAL) + val actual = serialize(checkIn) + val expected = SerializationUtils.sanitizedFile("json/checkin_interval.json") + .replace("6d55218195564d6d88cf3883b84666f1", checkIn.checkInId.toString()) + + assertEquals(expected, actual) + } + + @Test + fun serializeCrontab() { + val checkIn = fixture.getSut(MonitorScheduleType.CRONTAB) + val actual = serialize(checkIn) + val expected = SerializationUtils.sanitizedFile("json/checkin_crontab.json") + .replace("6d55218195564d6d88cf3883b84666f1", checkIn.checkInId.toString()) + .replace("0_*_*_*_*", "0 * * * *") + + assertEquals(expected, actual) + } + + @Test + fun deserializeCrontab() { + val checkIn = fixture.getSut(MonitorScheduleType.CRONTAB) + val jsonCheckIn = SerializationUtils.sanitizedFile("json/checkin_crontab.json") + .replace("0_*_*_*_*", "0 * * * *") + val reader = JsonObjectReader(StringReader(jsonCheckIn)) + val actual = CheckIn.Deserializer().deserialize(reader, fixture.logger) + assertNotNull(actual) + assertEquals("6d55218195564d6d88cf3883b84666f1", actual.checkInId.toString()) + assertEquals(checkIn.status, actual.status) + assertTrue((checkIn.duration!! - actual.duration!!) < 0.01) + assertEquals(checkIn.release, actual.release) + assertEquals(checkIn.environment, actual.environment) + val actualContext = actual.contexts + assertEquals(checkIn.contexts.trace!!.traceId, actualContext.trace!!.traceId) + val actualConfig = actual.monitorConfig!! + val actualSchedule = actualConfig.schedule!! + val expectedConfig = checkIn.monitorConfig!! + val expectedSchedule = expectedConfig.schedule!! + assertEquals(expectedConfig.maxRuntime, actualConfig.maxRuntime) + assertEquals(expectedConfig.checkinMargin, actualConfig.checkinMargin) + assertEquals(expectedConfig.timezone, actualConfig.timezone) + assertEquals(expectedSchedule.type, actualSchedule.type) + assertEquals(expectedSchedule.value, actualSchedule.value) + assertEquals(expectedSchedule.unit, actualSchedule.unit) + } + + @Test + fun deserializeInterval() { + val checkIn = fixture.getSut(MonitorScheduleType.INTERVAL) + val jsonCheckIn = SerializationUtils.sanitizedFile("json/checkin_interval.json") + val reader = JsonObjectReader(StringReader(jsonCheckIn)) + val actual = CheckIn.Deserializer().deserialize(reader, fixture.logger) + assertNotNull(actual) + assertEquals("6d55218195564d6d88cf3883b84666f1", actual.checkInId.toString()) + assertEquals(checkIn.status, actual.status) + assertTrue((checkIn.duration!! - actual.duration!!) < 0.01) + assertEquals(checkIn.release, actual.release) + assertEquals(checkIn.environment, actual.environment) + val actualContext = actual.contexts + assertEquals(checkIn.contexts.trace!!.traceId, actualContext.trace!!.traceId) + val actualConfig = actual.monitorConfig!! + val actualSchedule = actualConfig.schedule!! + val expectedConfig = checkIn.monitorConfig!! + val expectedSchedule = expectedConfig.schedule!! + assertEquals(expectedConfig.maxRuntime, actualConfig.maxRuntime) + assertEquals(expectedConfig.checkinMargin, actualConfig.checkinMargin) + assertEquals(expectedConfig.timezone, actualConfig.timezone) + assertEquals(expectedSchedule.type, actualSchedule.type) + assertEquals(expectedSchedule.value, actualSchedule.value) + assertEquals(expectedSchedule.unit, actualSchedule.unit) + } + + @Test + fun `deserializing checkin with missing required fields`() { + val jsonCheckInWithoutId = "{\"status\":\"error\",\"monitor_slug\":\"some_slug\"}" + val reader = JsonObjectReader(StringReader(jsonCheckInWithoutId)) + + try { + CheckIn.Deserializer().deserialize(reader, fixture.logger) + fail() + } catch (exception: Exception) { + verify(fixture.logger).log(eq(SentryLevel.ERROR), any(), any()) + } + } + + // Helper + + private fun serialize(jsonSerializable: JsonSerializable): String { + val wrt = StringWriter() + val jsonWrt = JsonObjectWriter(wrt, 100) + jsonSerializable.serialize(jsonWrt, fixture.logger) + return wrt.toString() + } +} diff --git a/sentry/src/test/java/io/sentry/HubAdapterTest.kt b/sentry/src/test/java/io/sentry/HubAdapterTest.kt index 6fec20ca7d..aa302efb34 100644 --- a/sentry/src/test/java/io/sentry/HubAdapterTest.kt +++ b/sentry/src/test/java/io/sentry/HubAdapterTest.kt @@ -73,6 +73,12 @@ class HubAdapterTest { verify(hub).captureUserFeedback(eq(userFeedback)) } + @Test fun `captureCheckIn calls Hub`() { + val checkIn = mock() + HubAdapter.getInstance().captureCheckIn(checkIn) + verify(hub).captureCheckIn(eq(checkIn)) + } + @Test fun `startSession calls Hub`() { HubAdapter.getInstance().startSession() verify(hub).startSession() diff --git a/sentry/src/test/java/io/sentry/HubTest.kt b/sentry/src/test/java/io/sentry/HubTest.kt index 96a44462d4..2a7991f120 100644 --- a/sentry/src/test/java/io/sentry/HubTest.kt +++ b/sentry/src/test/java/io/sentry/HubTest.kt @@ -726,6 +726,44 @@ class HubTest { } } + //region captureCheckIn tests + + @Test + fun `when captureCheckIn is called it is forwarded to the client`() { + val (sut, mockClient) = getEnabledHub() + sut.captureCheckIn(checkIn) + + verify(mockClient).captureCheckIn( + check { + assertEquals(checkIn.checkInId, it.checkInId) + assertEquals(checkIn.monitorSlug, it.monitorSlug) + assertEquals(checkIn.status, it.status) + }, + any(), + anyOrNull() + ) + } + + @Test + fun `when captureCheckIn is called on disabled client, do nothing`() { + val (sut, mockClient) = getEnabledHub() + sut.close() + + sut.captureCheckIn(checkIn) + verify(mockClient, never()).captureCheckIn(any(), any(), anyOrNull()) + } + + @Test + fun `when captureCheckIn is called and client throws, don't crash`() { + val (sut, mockClient) = getEnabledHub() + + whenever(mockClient.captureCheckIn(any(), any(), anyOrNull())).doThrow(IllegalArgumentException("")) + + sut.captureCheckIn(checkIn) + } + + private val checkIn: CheckIn = CheckIn("some_slug", CheckInStatus.OK) + //endregion //region close tests diff --git a/sentry/src/test/java/io/sentry/SentryClientTest.kt b/sentry/src/test/java/io/sentry/SentryClientTest.kt index 41d3947a5e..6f259743c1 100644 --- a/sentry/src/test/java/io/sentry/SentryClientTest.kt +++ b/sentry/src/test/java/io/sentry/SentryClientTest.kt @@ -535,6 +535,57 @@ class SentryClientTest { ) } + @Test + fun `when captureCheckIn, envelope is sent`() { + val sut = fixture.getSut() + + 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() + ) + } + + private fun assertEnvelopeItemDataForCheckIn(item: SentryEnvelopeItem) { + val stream = ByteArrayOutputStream() + val writer = stream.bufferedWriter(Charset.forName("UTF-8")) + fixture.sentryOptions.serializer.serialize(checkIn, writer) + val expectedData = stream.toByteArray() + assertTrue(Arrays.equals(expectedData, item.data)) + } + + @Test + fun `when captureCheckIn and connection throws, log exception`() { + val sut = fixture.getSut() + + val exception = IOException("No connection") + whenever(fixture.transport.send(any(), any())).thenThrow(exception) + + val logger = mock() + fixture.sentryOptions.setLogger(logger) + + sut.captureCheckIn(checkIn, null, null) + + verify(logger) + .log( + SentryLevel.WARNING, + exception, + "Capturing check-in %s failed.", + checkIn.checkInId + ) + } + @Test fun `when hint is Cached, scope is not applied`() { val sut = fixture.getSut() @@ -2477,6 +2528,8 @@ class SentryClientTest { return userFeedback } + private val checkIn = CheckIn("some_slug", CheckInStatus.OK) + internal class CustomTransportGate : ITransportGate { override fun isConnected(): Boolean = false } diff --git a/sentry/src/test/java/io/sentry/SentryTest.kt b/sentry/src/test/java/io/sentry/SentryTest.kt index eabb26e69d..254c3a7b2d 100644 --- a/sentry/src/test/java/io/sentry/SentryTest.kt +++ b/sentry/src/test/java/io/sentry/SentryTest.kt @@ -16,6 +16,7 @@ import org.junit.Assert.assertThrows import org.junit.Rule import org.junit.rules.TemporaryFolder import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argThat import org.mockito.kotlin.eq import org.mockito.kotlin.mock @@ -767,6 +768,25 @@ class SentryTest { assertFalse(previousSessionFile.exists()) } + @Test + fun `captureCheckIn gets forwarded to client`() { + Sentry.init { it.dsn = dsn } + + val client = mock() + Sentry.getCurrentHub().bindClient(client) + + val checkIn = CheckIn("some_slug", CheckInStatus.OK) + Sentry.captureCheckIn(checkIn) + + verify(client).captureCheckIn( + argThat { + checkInId == checkIn.checkInId + }, + anyOrNull(), + anyOrNull() + ) + } + private class InMemoryOptionsObserver : IOptionsObserver { var release: String? = null private set diff --git a/sentry/src/test/resources/json/checkin_crontab.json b/sentry/src/test/resources/json/checkin_crontab.json new file mode 100644 index 0000000000..ee3bdf2ca9 --- /dev/null +++ b/sentry/src/test/resources/json/checkin_crontab.json @@ -0,0 +1,29 @@ +{ + "check_in_id": "6d55218195564d6d88cf3883b84666f1", + "monitor_slug": "some_slug", + "status": "error", + "duration": 12.3, + "release": "1.0.1", + "environment": "env", + "monitor_config": + { + "schedule": + { + "type": "crontab", + "value": "0_*_*_*_*" + }, + "checkin_margin": 8, + "max_runtime": 9, + "timezone": "Europe/Vienna" + }, + "contexts": + { + "trace": + { + "trace_id": "f382e3180c714217a81371f8c644aefe", + "span_id": "85694b9f567145a6", + "op": "default", + "origin": "manual" + } + } +} diff --git a/sentry/src/test/resources/json/checkin_interval.json b/sentry/src/test/resources/json/checkin_interval.json new file mode 100644 index 0000000000..b404d610c6 --- /dev/null +++ b/sentry/src/test/resources/json/checkin_interval.json @@ -0,0 +1,30 @@ +{ + "check_in_id": "6d55218195564d6d88cf3883b84666f1", + "monitor_slug": "some_slug", + "status": "error", + "duration": 12.3, + "release": "1.0.1", + "environment": "env", + "monitor_config": + { + "schedule": + { + "type": "interval", + "value": 42, + "unit": "minute" + }, + "checkin_margin": 8, + "max_runtime": 9, + "timezone": "Europe/Vienna" + }, + "contexts": + { + "trace": + { + "trace_id": "f382e3180c714217a81371f8c644aefe", + "span_id": "85694b9f567145a6", + "op": "default", + "origin": "manual" + } + } +} From 41cd16de74ed2c05073b46ac0a18e267a4e0d11e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 Sep 2023 12:30:27 +0200 Subject: [PATCH 04/22] remove serial version uid --- sentry/src/main/java/io/sentry/MonitorContexts.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/MonitorContexts.java b/sentry/src/main/java/io/sentry/MonitorContexts.java index 461a4d549a..8843dcaa09 100644 --- a/sentry/src/main/java/io/sentry/MonitorContexts.java +++ b/sentry/src/main/java/io/sentry/MonitorContexts.java @@ -11,7 +11,6 @@ public final class MonitorContexts extends ConcurrentHashMap implements JsonSerializable { - private static final long serialVersionUID = 3987329379811822556L; public MonitorContexts() {} From 083e02140d6cb70474676c9a61212c49e4f94e6d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 13 Sep 2023 13:31:22 +0200 Subject: [PATCH 05/22] add back serial version uid --- sentry/src/main/java/io/sentry/MonitorContexts.java | 1 + 1 file changed, 1 insertion(+) diff --git a/sentry/src/main/java/io/sentry/MonitorContexts.java b/sentry/src/main/java/io/sentry/MonitorContexts.java index 8843dcaa09..461a4d549a 100644 --- a/sentry/src/main/java/io/sentry/MonitorContexts.java +++ b/sentry/src/main/java/io/sentry/MonitorContexts.java @@ -11,6 +11,7 @@ public final class MonitorContexts extends ConcurrentHashMap implements JsonSerializable { + private static final long serialVersionUID = 3987329379811822556L; public MonitorContexts() {} From 4828fa32ae0f399e4cb095ffce1068d2c3b72f43 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 15 Sep 2023 12:42:22 +0200 Subject: [PATCH 06/22] quartz support for spring jakarta --- buildSrc/src/main/java/Config.kt | 1 + .../build.gradle.kts | 1 + .../boot/jakarta/SentryDemoApplication.java | 70 ++++++ .../spring/boot/jakarta/quartz/SampleJob.java | 19 ++ .../src/main/resources/application.properties | 2 + .../src/main/resources/quartz.properties | 1 + sentry-spring-boot-jakarta/build.gradle.kts | 1 + .../boot/jakarta/SentryAutoConfiguration.java | 9 + .../api/sentry-spring-jakarta.api | 20 ++ sentry-spring-jakarta/build.gradle.kts | 1 + .../jakarta/checkin/SentryJobListener.java | 199 ++++++++++++++++++ .../checkin/SentryQuartzConfiguration.java | 41 ++++ .../SentrySchedulerFactoryBeanCustomizer.java | 11 + sentry/api/sentry.api | 1 + sentry/src/main/java/io/sentry/CheckIn.java | 14 +- 15 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java create mode 100644 sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/quartz.properties create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryJobListener.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index eda3873d81..ae63fd87cb 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -85,6 +85,7 @@ object Config { val springBoot3Starter = "org.springframework.boot:spring-boot-starter:$springBoot3Version" val springBoot3StarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBoot3Version" + val springBoot3StarterQuartz = "org.springframework.boot:spring-boot-starter-quartz:$springBoot3Version" val springBoot3StarterTest = "org.springframework.boot:spring-boot-starter-test:$springBoot3Version" val springBoot3StarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBoot3Version" val springBoot3StarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBoot3Version" 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 c326142f55..dbcec367d8 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -22,6 +22,7 @@ dependencies { implementation(Config.Libs.springBoot3StarterWeb) implementation(Config.Libs.springBoot3StarterWebsocket) implementation(Config.Libs.springBoot3StarterGraphql) + implementation(Config.Libs.springBoot3StarterQuartz) implementation(Config.Libs.springBoot3StarterWebflux) implementation(Config.Libs.springBoot3StarterAop) implementation(Config.Libs.aspectj) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index 6d91b889a6..e5bf56ad7a 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -1,10 +1,14 @@ package io.sentry.samples.spring.boot.jakarta; +import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; +import org.quartz.JobDetail; 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.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -24,4 +28,70 @@ RestTemplate restTemplate(RestTemplateBuilder builder) { WebClient webClient(WebClient.Builder builder) { return builder.build(); } + + // @Bean + // public JobDetail jobDetail() { + // return JobBuilder.newJob().ofType(SampleJob.class) + // .storeDurably() + // .withIdentity("Qrtz_Job_Detail") + // .withDescription("Invoke Sample Job service...") + // .build(); + // } + @Bean + public JobDetailFactoryBean jobDetail() { + JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); + jobDetailFactory.setName("hello there 123"); + jobDetailFactory.setJobClass(SampleJob.class); + jobDetailFactory.setDescription("Invoke Sample Job service..."); + jobDetailFactory.setDurability(true); + return jobDetailFactory; + } + + // @Bean + // public SimpleTriggerFactoryBean trigger(JobDetail job) { + // SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + // trigger.setJobDetail(job); + // trigger.setRepeatInterval(2 * 60 * 1000); + // trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + // return trigger; + // } + + @Bean + public CronTriggerFactoryBean trigger(JobDetail job) { + CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setCronExpression("0 /5 * ? * *"); + return trigger; + } + + // @Bean + // public Scheduler scheduler(Trigger trigger, JobDetail job, SchedulerFactoryBean factory) + // throws SchedulerException { + // Scheduler scheduler = factory.getScheduler(); + // scheduler.scheduleJob(job, trigger); + // scheduler.start(); + // return scheduler; + // } + + // @Bean + // public SchedulerFactoryBean scheduler(Trigger trigger, JobDetail job, DataSource + // quartzDataSource) { + // SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); + // schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties")); + //// schedulerFactory.setGlobalJobListeners(new SentryJobListener()); + //// schedulerFactory.setGlobalTriggerListeners(new SentryTriggerListener()); + //// try { + //// ListenerManager listenerManager = schedulerFactory.getScheduler().getListenerManager(); + //// listenerManager.addJobListener(new SentryJobListener()); + //// listenerManager.addTriggerListener(new SentryTriggerListener()); + //// } catch (SchedulerException e) { + //// throw new RuntimeException(e); + //// } + // + //// schedulerFactory.setJobFactory(springBeanJobFactory()); + // schedulerFactory.setJobDetails(job); + // schedulerFactory.setTriggers(trigger); + // schedulerFactory.setDataSource(quartzDataSource); + // return schedulerFactory; + // } } diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/quartz/SampleJob.java new file mode 100644 index 0000000000..d0f0973c86 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/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-jakarta/src/main/resources/application.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/application.properties index 8151eacc6c..9813adbb65 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 @@ -20,3 +20,5 @@ 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-jakarta/src/main/resources/quartz.properties b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/quartz.properties new file mode 100644 index 0000000000..6e302ce765 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/resources/quartz.properties @@ -0,0 +1 @@ +org.quartz.jobStore.class=org.quartz.simpl.RAMJobStore diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 0b2292d06e..5d5cb9c9fc 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -36,6 +36,7 @@ dependencies { compileOnly(Config.Libs.springBoot3StarterAop) compileOnly(Config.Libs.springBoot3StarterSecurity) compileOnly(Config.Libs.springBoot3StarterGraphql) + compileOnly(Config.Libs.springBoot3StarterQuartz) compileOnly(Config.Libs.reactorCore) compileOnly(Config.Libs.contextPropagation) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore) diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 3ca17f75b4..673d07c8d0 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -21,6 +21,7 @@ import io.sentry.spring.jakarta.SentryUserProvider; import io.sentry.spring.jakarta.SentryWebConfiguration; import io.sentry.spring.jakarta.SpringSecuritySentryUserProvider; +import io.sentry.spring.jakarta.checkin.SentryQuartzConfiguration; import io.sentry.spring.jakarta.graphql.SentryGraphqlConfiguration; import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration; import io.sentry.spring.jakarta.tracing.SentrySpanPointcutConfiguration; @@ -35,6 +36,7 @@ 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; @@ -56,6 +58,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; +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; @@ -167,6 +170,12 @@ static class OpenTelemetryLinkErrorEventProcessorConfiguration { }) static class GraphqlConfiguration {} + @Configuration(proxyBeanMethods = false) + @Import(SentryQuartzConfiguration.class) + @Open + @ConditionalOnClass({QuartzScheduler.class, SchedulerFactoryBean.class}) + static class QuartzConfiguration {} + /** Registers beans specific to Spring MVC. */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 8f35b6075f..4e7af69b89 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -88,6 +88,26 @@ public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : i public fun provideUser ()Lio/sentry/protocol/User; } +public final class io/sentry/spring/jakarta/checkin/SentryJobListener : org/quartz/JobListener { + public static final field SENTRY_CHECK_IN_ID_KEY Ljava/lang/String; + public static final field SENTRY_CHECK_IN_SLUG_KEY Ljava/lang/String; + public fun ()V + public fun getName ()Ljava/lang/String; + public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V + public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V + public fun jobWasExecuted (Lorg/quartz/JobExecutionContext;Lorg/quartz/JobExecutionException;)V +} + +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 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; diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index 6f16d29a08..df4bf72ae6 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { compileOnly(Config.Libs.springAop) compileOnly(Config.Libs.springSecurityWeb) compileOnly(Config.Libs.springBoot3StarterGraphql) + compileOnly(Config.Libs.springBoot3StarterQuartz) compileOnly(Config.Libs.aspectj) compileOnly(Config.Libs.servletApiJakarta) compileOnly(Config.Libs.slf4jApi) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryJobListener.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryJobListener.java new file mode 100644 index 0000000000..850962c8c1 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryJobListener.java @@ -0,0 +1,199 @@ +package io.sentry.spring.jakarta.checkin; + +import io.sentry.CheckIn; +import io.sentry.CheckInStatus; +import io.sentry.MonitorConfig; +import io.sentry.MonitorSchedule; +import io.sentry.MonitorScheduleUnit; +import io.sentry.Sentry; +import io.sentry.SentryLevel; +import io.sentry.protocol.SentryId; +import java.util.List; +import java.util.TimeZone; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import org.quartz.CalendarIntervalTrigger; +import org.quartz.CronTrigger; +import org.quartz.DateBuilder; +import org.quartz.Job; +import org.quartz.JobDetail; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.quartz.JobKey; +import org.quartz.JobListener; +import org.quartz.SimpleTrigger; +import org.quartz.Trigger; + +public final class SentryJobListener implements JobListener { + + public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id"; + public static final String SENTRY_CHECK_IN_SLUG_KEY = "sentry-checkin-slug"; + + @Override + public String getName() { + return "sentry-job-listener"; + } + + @Override + public void jobToBeExecuted(JobExecutionContext context) { + try { + final @NotNull String slug = getSlug(context.getJobDetail()); + final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS); + + final @Nullable MonitorConfig monitorConfig = extractMonitorConfig(context); + if (monitorConfig != null) { + checkIn.setMonitorConfig(monitorConfig); + } + + final @NotNull SentryId checkInId = Sentry.captureCheckIn(checkIn); + context.put(SENTRY_CHECK_IN_ID_KEY, checkInId); + context.put(SENTRY_CHECK_IN_SLUG_KEY, slug); + } catch (Throwable t) { + Sentry.getCurrentHub() + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Unable to capture check-in in jobToBeExecuted.", t); + } + } + + private @NotNull String getSlug(final @Nullable JobDetail jobDetail) { + if (jobDetail == null) { + return "fallback"; + } + final @NotNull StringBuilder slugBuilder = new StringBuilder(); + + final @Nullable JobKey key = jobDetail.getKey(); + if (key != null) { + slugBuilder.append(key.getName()); + } + + slugBuilder.append("__"); + + final @Nullable Class jobClass = jobDetail.getJobClass(); + if (jobClass != null) { + slugBuilder.append(jobClass.getCanonicalName()); + } + + return slugBuilder.toString(); + } + + private @Nullable MonitorConfig extractMonitorConfig(final @NotNull JobExecutionContext context) { + @Nullable MonitorSchedule schedule = null; + @Nullable String cronExpression = null; + @Nullable TimeZone timeZone = TimeZone.getDefault(); + @Nullable Integer repeatInterval = null; + @Nullable MonitorScheduleUnit timeUnit = null; + + try { + List triggersOfJob = + context.getScheduler().getTriggersOfJob(context.getTrigger().getJobKey()); + for (Trigger trigger : triggersOfJob) { + if (trigger instanceof CronTrigger) { + final CronTrigger cronTrigger = (CronTrigger) trigger; + cronExpression = cronTrigger.getCronExpression(); + timeZone = cronTrigger.getTimeZone(); + } else if (trigger instanceof SimpleTrigger) { + final SimpleTrigger simpleTrigger = (SimpleTrigger) trigger; + long tmpRepeatInterval = simpleTrigger.getRepeatInterval(); + repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval)); + timeUnit = MonitorScheduleUnit.MINUTE; + } else if (trigger instanceof CalendarIntervalTrigger) { + final CalendarIntervalTrigger calendarIntervalTrigger = (CalendarIntervalTrigger) trigger; + DateBuilder.IntervalUnit repeatIntervalUnit = + calendarIntervalTrigger.getRepeatIntervalUnit(); + int tmpRepeatInterval = calendarIntervalTrigger.getRepeatInterval(); + if (DateBuilder.IntervalUnit.SECOND.equals(repeatIntervalUnit)) { + repeatInterval = secondsToMinutes(Double.valueOf(tmpRepeatInterval)); + timeUnit = MonitorScheduleUnit.MINUTE; + } else if (DateBuilder.IntervalUnit.MILLISECOND.equals(repeatIntervalUnit)) { + repeatInterval = millisToMinutes(Double.valueOf(tmpRepeatInterval)); + timeUnit = MonitorScheduleUnit.MINUTE; + } else { + repeatInterval = tmpRepeatInterval; + timeUnit = convertUnit(repeatIntervalUnit); + } + } + } + } catch (Throwable t) { + Sentry.getCurrentHub() + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Unable to extract monitor config for check-in.", t); + } + if (cronExpression != null) { + schedule = MonitorSchedule.crontab(cronExpression); + } else if (repeatInterval != null && timeUnit != null) { + schedule = MonitorSchedule.interval(repeatInterval.intValue(), timeUnit); + } + + if (schedule != null) { + final @Nullable MonitorConfig monitorConfig = new MonitorConfig(schedule); + if (timeZone != null) { + monitorConfig.setTimezone(timeZone.getID()); + } + return monitorConfig; + } else { + return null; + } + } + + private @Nullable Integer millisToMinutes(final @NotNull Double milis) { + return Double.valueOf((milis / 1000.0) / 60.0).intValue(); + } + + private @Nullable Integer secondsToMinutes(final @NotNull Double seconds) { + return Double.valueOf(seconds / 60.0).intValue(); + } + + private @Nullable MonitorScheduleUnit convertUnit( + final @Nullable DateBuilder.IntervalUnit intervalUnit) { + if (intervalUnit == null) { + return null; + } + + if (DateBuilder.IntervalUnit.MINUTE.equals(intervalUnit)) { + return MonitorScheduleUnit.MINUTE; + } else if (DateBuilder.IntervalUnit.HOUR.equals(intervalUnit)) { + return MonitorScheduleUnit.HOUR; + } else if (DateBuilder.IntervalUnit.DAY.equals(intervalUnit)) { + return MonitorScheduleUnit.DAY; + } else if (DateBuilder.IntervalUnit.WEEK.equals(intervalUnit)) { + return MonitorScheduleUnit.WEEK; + } else if (DateBuilder.IntervalUnit.MONTH.equals(intervalUnit)) { + return MonitorScheduleUnit.MONTH; + } else if (DateBuilder.IntervalUnit.YEAR.equals(intervalUnit)) { + return MonitorScheduleUnit.YEAR; + } + + return null; + } + + @Override + public void jobExecutionVetoed(JobExecutionContext context) { + // do nothing + } + + @Override + public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { + try { + final @Nullable Object checkInIdObjectFromContext = context.get(SENTRY_CHECK_IN_ID_KEY); + final @Nullable Object slugObjectFromContext = context.get(SENTRY_CHECK_IN_SLUG_KEY); + final @NotNull SentryId checkInId = + checkInIdObjectFromContext == null + ? new SentryId() + : (SentryId) checkInIdObjectFromContext; + final @Nullable String slug = + slugObjectFromContext == null ? null : (String) slugObjectFromContext; + if (slug != null) { + final boolean isFailed = jobException != null; + final @NotNull CheckInStatus status = isFailed ? CheckInStatus.ERROR : CheckInStatus.OK; + Sentry.captureCheckIn(new CheckIn(checkInId, slug, status)); + } + } catch (Throwable t) { + Sentry.getCurrentHub() + .getOptions() + .getLogger() + .log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", t); + } + } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java new file mode 100644 index 0000000000..81cedf296f --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java @@ -0,0 +1,41 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryQuartzConfiguration { + + @Bean + public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() { + return new SentrySchedulerFactoryBeanCustomizer(); + } + + // @Bean + // public TaskSchedulerCustomizer taskSchedulerCustomizer() { + // return new SentryTaskSchedulerCustomizer(); + // } + + // public final class SentryTaskSchedulerCustomizer implements TaskSchedulerCustomizer { + // @Override + // public void customize(ThreadPoolTaskScheduler taskScheduler) { + // System.out.println("customize taskScheduler"); + //// taskScheduler.getScheduledExecutor(). + // } + // } + + // @Autowired + // private SchedulerFactoryBean schedulerFactoryBean; + + // @PostConstruct + // public void addListeners() throws SchedulerException { + // ListenerManager listenerManager = schedulerFactoryBean.getScheduler() + // .getListenerManager(); + // + // listenerManager.addTriggerListener(new SentryTriggerListener()); + // listenerManager.addJobListener(new SentryJobListener()); + // } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java new file mode 100644 index 0000000000..68373b3142 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -0,0 +1,11 @@ +package io.sentry.spring.jakarta.checkin; + +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +public final class SentrySchedulerFactoryBeanCustomizer implements SchedulerFactoryBeanCustomizer { + @Override + public void customize(SchedulerFactoryBean schedulerFactoryBean) { + schedulerFactoryBean.setGlobalJobListeners(new SentryJobListener()); + } +} diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4104693f1a..0323bb62eb 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -156,6 +156,7 @@ public final class io/sentry/BuildConfig { } public final class io/sentry/CheckIn : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Lio/sentry/protocol/SentryId;Ljava/lang/String;Lio/sentry/CheckInStatus;)V public fun (Lio/sentry/protocol/SentryId;Ljava/lang/String;Ljava/lang/String;)V public fun (Ljava/lang/String;Lio/sentry/CheckInStatus;)V public fun getCheckInId ()Lio/sentry/protocol/SentryId; diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index f92b1ee56c..2a7ef4ba26 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -24,12 +24,22 @@ public final class CheckIn implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; - public CheckIn(String monitorSlug, CheckInStatus status) { + public CheckIn(final @NotNull String monitorSlug, final @NotNull CheckInStatus status) { this(new SentryId(), monitorSlug, status.apiName()); } + public CheckIn( + final @NotNull SentryId id, + final @NotNull String monitorSlug, + final @NotNull CheckInStatus status) { + this(id, monitorSlug, status.apiName()); + } + @ApiStatus.Internal - public CheckIn(SentryId checkInId, String monitorSlug, String status) { + public CheckIn( + final @NotNull SentryId checkInId, + final @NotNull String monitorSlug, + final @NotNull String status) { this.checkInId = checkInId; this.monitorSlug = monitorSlug; this.status = status; From ef54e768ecd1defb84f0218b4bcbc571d1c7d0b0 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 15 Sep 2023 12:55:42 +0200 Subject: [PATCH 07/22] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a5c523ed..0563bd87e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - 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)) ### Dependencies From cc23d3c2328b9fc529569ad87e4e3d193579ac15 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 15 Sep 2023 15:02:46 +0200 Subject: [PATCH 08/22] code review changes; add sample code to spring scheduled job --- sentry/api/sentry.api | 10 +++--- sentry/src/main/java/io/sentry/CheckIn.java | 14 +++++++-- .../main/java/io/sentry/CheckInStatus.java | 18 +---------- sentry/src/main/java/io/sentry/Hub.java | 7 ----- .../java/io/sentry/MonitorScheduleType.java | 18 +---------- .../java/io/sentry/MonitorScheduleUnit.java | 20 ++---------- .../src/main/java/io/sentry/SentryClient.java | 31 +++++++++++-------- 7 files changed, 38 insertions(+), 80 deletions(-) diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 4104693f1a..8f44d415e1 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -156,6 +156,7 @@ public final class io/sentry/BuildConfig { } public final class io/sentry/CheckIn : io/sentry/JsonSerializable, io/sentry/JsonUnknown { + public fun (Lio/sentry/protocol/SentryId;Ljava/lang/String;Lio/sentry/CheckInStatus;)V public fun (Lio/sentry/protocol/SentryId;Ljava/lang/String;Ljava/lang/String;)V public fun (Ljava/lang/String;Lio/sentry/CheckInStatus;)V public fun getCheckInId ()Lio/sentry/protocol/SentryId; @@ -196,12 +197,11 @@ public final class io/sentry/CheckIn$JsonKeys { public fun ()V } -public final class io/sentry/CheckInStatus : java/lang/Enum, io/sentry/JsonSerializable { +public final class io/sentry/CheckInStatus : java/lang/Enum { public static final field ERROR Lio/sentry/CheckInStatus; public static final field IN_PROGRESS Lio/sentry/CheckInStatus; public static final field OK Lio/sentry/CheckInStatus; public fun apiName ()Ljava/lang/String; - public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public static fun valueOf (Ljava/lang/String;)Lio/sentry/CheckInStatus; public static fun values ()[Lio/sentry/CheckInStatus; } @@ -978,16 +978,15 @@ public final class io/sentry/MonitorSchedule$JsonKeys { public fun ()V } -public final class io/sentry/MonitorScheduleType : java/lang/Enum, io/sentry/JsonSerializable { +public final class io/sentry/MonitorScheduleType : java/lang/Enum { public static final field CRONTAB Lio/sentry/MonitorScheduleType; public static final field INTERVAL Lio/sentry/MonitorScheduleType; public fun apiName ()Ljava/lang/String; - public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public static fun valueOf (Ljava/lang/String;)Lio/sentry/MonitorScheduleType; public static fun values ()[Lio/sentry/MonitorScheduleType; } -public final class io/sentry/MonitorScheduleUnit : java/lang/Enum, io/sentry/JsonSerializable { +public final class io/sentry/MonitorScheduleUnit : java/lang/Enum { public static final field DAY Lio/sentry/MonitorScheduleUnit; public static final field HOUR Lio/sentry/MonitorScheduleUnit; public static final field MINUTE Lio/sentry/MonitorScheduleUnit; @@ -995,7 +994,6 @@ public final class io/sentry/MonitorScheduleUnit : java/lang/Enum, io/sentry/Jso public static final field WEEK Lio/sentry/MonitorScheduleUnit; public static final field YEAR Lio/sentry/MonitorScheduleUnit; public fun apiName ()Ljava/lang/String; - public fun serialize (Lio/sentry/ObjectWriter;Lio/sentry/ILogger;)V public static fun valueOf (Ljava/lang/String;)Lio/sentry/MonitorScheduleUnit; public static fun values ()[Lio/sentry/MonitorScheduleUnit; } diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index f92b1ee56c..7e6822971f 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -24,12 +24,22 @@ public final class CheckIn implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; - public CheckIn(String monitorSlug, CheckInStatus status) { + public CheckIn(final @NotNull String monitorSlug, final @NotNull CheckInStatus status) { this(new SentryId(), monitorSlug, status.apiName()); } + public CheckIn( + final @NotNull SentryId checkInId, + final @NotNull String monitorSlug, + final @NotNull CheckInStatus status) { + this(checkInId, monitorSlug, status.apiName()); + } + @ApiStatus.Internal - public CheckIn(SentryId checkInId, String monitorSlug, String status) { + public CheckIn( + final @NotNull SentryId checkInId, + final @NotNull String monitorSlug, + final @NotNull String status) { this.checkInId = checkInId; this.monitorSlug = monitorSlug; this.status = status; diff --git a/sentry/src/main/java/io/sentry/CheckInStatus.java b/sentry/src/main/java/io/sentry/CheckInStatus.java index 22c5228a6d..42ed7bac54 100644 --- a/sentry/src/main/java/io/sentry/CheckInStatus.java +++ b/sentry/src/main/java/io/sentry/CheckInStatus.java @@ -1,31 +1,15 @@ package io.sentry; -import java.io.IOException; import java.util.Locale; import org.jetbrains.annotations.NotNull; /** Status of a CheckIn */ -public enum CheckInStatus implements JsonSerializable { +public enum CheckInStatus { IN_PROGRESS, OK, ERROR; - @Override - public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) - throws IOException { - writer.value(name().toLowerCase(Locale.ROOT)); - } - public @NotNull String apiName() { return name().toLowerCase(Locale.ROOT); } - - static final class Deserializer implements JsonDeserializer { - - @Override - public @NotNull CheckInStatus deserialize( - @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { - return CheckInStatus.valueOf(reader.nextString().toUpperCase(Locale.ROOT)); - } - } } diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 4a1e4c9017..5810390069 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -865,10 +865,6 @@ private Scope buildLocalScope( @Override public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { - return captureCheckInInternal(checkIn); - } - - private @NotNull SentryId captureCheckInInternal(final @NotNull CheckIn checkIn) { SentryId sentryId = SentryId.EMPTY_ID; if (!isEnabled()) { options @@ -876,9 +872,6 @@ private Scope buildLocalScope( .log( SentryLevel.WARNING, "Instance is disabled and this 'captureCheckIn' call is a no-op."); - // } else if (message == null) { - // options.getLogger().log(SentryLevel.WARNING, "captureCheckIn called with null - // parameter."); } else { try { StackItem item = stack.peek(); diff --git a/sentry/src/main/java/io/sentry/MonitorScheduleType.java b/sentry/src/main/java/io/sentry/MonitorScheduleType.java index bd9abebfc7..ac168e3845 100644 --- a/sentry/src/main/java/io/sentry/MonitorScheduleType.java +++ b/sentry/src/main/java/io/sentry/MonitorScheduleType.java @@ -1,30 +1,14 @@ package io.sentry; -import java.io.IOException; import java.util.Locale; import org.jetbrains.annotations.NotNull; /** Type of a monitor schedule */ -public enum MonitorScheduleType implements JsonSerializable { +public enum MonitorScheduleType { CRONTAB, INTERVAL; - @Override - public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) - throws IOException { - writer.value(name().toLowerCase(Locale.ROOT)); - } - public @NotNull String apiName() { return name().toLowerCase(Locale.ROOT); } - - static final class Deserializer implements JsonDeserializer { - - @Override - public @NotNull MonitorScheduleType deserialize( - @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { - return MonitorScheduleType.valueOf(reader.nextString().toUpperCase(Locale.ROOT)); - } - } } diff --git a/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java b/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java index c80b0af867..adfb0771fe 100644 --- a/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java +++ b/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java @@ -1,11 +1,10 @@ package io.sentry; -import java.io.IOException; import java.util.Locale; import org.jetbrains.annotations.NotNull; -/** Status of a CheckIn */ -public enum MonitorScheduleUnit implements JsonSerializable { +/** Time unit of a monitor schedule. */ +public enum MonitorScheduleUnit { MINUTE, HOUR, DAY, @@ -13,22 +12,7 @@ public enum MonitorScheduleUnit implements JsonSerializable { MONTH, YEAR; - @Override - public void serialize(final @NotNull ObjectWriter writer, final @NotNull ILogger logger) - throws IOException { - writer.value(name().toLowerCase(Locale.ROOT)); - } - public @NotNull String apiName() { return name().toLowerCase(Locale.ROOT); } - - static final class Deserializer implements JsonDeserializer { - - @Override - public @NotNull MonitorScheduleUnit deserialize( - @NotNull JsonObjectReader reader, @NotNull ILogger logger) throws Exception { - return MonitorScheduleUnit.valueOf(reader.nextString().toUpperCase(Locale.ROOT)); - } - } } diff --git a/sentry/src/main/java/io/sentry/SentryClient.java b/sentry/src/main/java/io/sentry/SentryClient.java index eb82ca1bd6..8640a3985c 100644 --- a/sentry/src/main/java/io/sentry/SentryClient.java +++ b/sentry/src/main/java/io/sentry/SentryClient.java @@ -463,16 +463,16 @@ public void captureUserFeedback(final @NotNull UserFeedback userFeedback) { return new SentryEnvelope(envelopeHeader, envelopeItems); } - private @NotNull SentryEnvelope buildEnvelope(final @NotNull CheckIn checkIn) { + private @NotNull SentryEnvelope buildEnvelope( + final @NotNull CheckIn checkIn, final @Nullable TraceContext traceContext) { final List envelopeItems = new ArrayList<>(); final SentryEnvelopeItem checkInItem = SentryEnvelopeItem.fromCheckIn(options.getSerializer(), checkIn); envelopeItems.add(checkInItem); - // TODO do we need trace context? final SentryEnvelopeHeader envelopeHeader = - new SentryEnvelopeHeader(checkIn.getCheckInId(), options.getSdkVersion()); + new SentryEnvelopeHeader(checkIn.getCheckInId(), options.getSdkVersion(), traceContext); return new SentryEnvelope(envelopeHeader, envelopeItems); } @@ -691,20 +691,25 @@ public void captureSession(final @NotNull Session session, final @Nullable Hint options.getLogger().log(SentryLevel.DEBUG, "Capturing check-in: %s", checkIn.getCheckInId()); - SentryId sentryId = SentryId.EMPTY_ID; - if (checkIn.getCheckInId() != null) { - sentryId = checkIn.getCheckInId(); - } + SentryId sentryId = checkIn.getCheckInId(); try { - final SentryEnvelope envelope = buildEnvelope(checkIn); + @Nullable TraceContext traceContext = null; + if (scope != null) { + final @Nullable ITransaction transaction = scope.getTransaction(); + if (transaction != null) { + traceContext = transaction.traceContext(); + } else { + final @NotNull PropagationContext propagationContext = + TracingUtils.maybeUpdateBaggage(scope, options); + traceContext = propagationContext.traceContext(); + } + } + + final SentryEnvelope envelope = buildEnvelope(checkIn, traceContext); hint.clear(); - if (envelope != null) { - transport.send(envelope, hint); - } else { - sentryId = SentryId.EMPTY_ID; - } + transport.send(envelope, hint); } catch (IOException e) { options.getLogger().log(SentryLevel.WARNING, e, "Capturing check-in %s failed.", sentryId); // if there was an error capturing the event, we return an emptyId From 44a3c756021914c2f90cd34d17927ac4d6096567 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Fri, 15 Sep 2023 15:46:23 +0200 Subject: [PATCH 09/22] add checkin example to scheduled job --- .../spring/boot/jakarta/CustomJob.java | 27 ++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java index a9be71fb3e..4bc9c817e2 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java @@ -1,6 +1,12 @@ package io.sentry.samples.spring.boot.jakarta; +import io.sentry.CheckIn; +import io.sentry.CheckInStatus; +import io.sentry.DateUtils; +import io.sentry.Sentry; +import io.sentry.protocol.SentryId; import io.sentry.spring.jakarta.tracing.SentryTransaction; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -16,9 +22,24 @@ public class CustomJob { private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class); - @Scheduled(fixedRate = 3 * 1000L) + @Scheduled(fixedRate = 3 * 60 * 1000L) void execute() throws InterruptedException { - LOGGER.info("Executing scheduled job"); - Thread.sleep(2000L); + final @NotNull SentryId checkInId = + Sentry.captureCheckIn(new CheckIn("my_monitor_slug", CheckInStatus.IN_PROGRESS)); + final long startTime = System.currentTimeMillis(); + boolean didError = false; + try { + LOGGER.info("Executing scheduled job"); + Thread.sleep(2000L); + Sentry.captureCheckIn(new CheckIn(checkInId, "my_monitor_slug", CheckInStatus.OK)); + } catch (Throwable t) { + didError = true; + throw t; + } finally { + final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; + CheckIn checkIn = new CheckIn(checkInId, "my_monitor_slug", status); + checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); + Sentry.captureCheckIn(checkIn); + } } } From 0c29f126f883e2698aa0b77d7de6e1db5a6ad76f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 18 Sep 2023 15:24:31 +0200 Subject: [PATCH 10/22] Move quartz job listener into a separate module --- .craft.yml | 1 + .github/ISSUE_TEMPLATE/bug_report_java.yml | 1 + README.md | 1 + buildSrc/src/main/java/Config.kt | 3 + sentry-quartz/api/sentry-quartz.api | 15 ++++ sentry-quartz/build.gradle.kts | 83 +++++++++++++++++++ .../io/sentry/quartz}/SentryJobListener.java | 10 ++- sentry-spring-boot-jakarta/build.gradle.kts | 1 + .../boot/jakarta/SentryAutoConfiguration.java | 7 +- .../api/sentry-spring-jakarta.api | 10 --- sentry-spring-jakarta/build.gradle.kts | 1 + .../checkin/SentryQuartzConfiguration.java | 25 ------ .../SentrySchedulerFactoryBeanCustomizer.java | 1 + settings.gradle.kts | 1 + 14 files changed, 123 insertions(+), 37 deletions(-) create mode 100644 sentry-quartz/api/sentry-quartz.api create mode 100644 sentry-quartz/build.gradle.kts rename {sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin => sentry-quartz/src/main/java/io/sentry/quartz}/SentryJobListener.java (95%) diff --git a/.craft.yml b/.craft.yml index 57e92b7995..8caca1d0b9 100644 --- a/.craft.yml +++ b/.craft.yml @@ -48,6 +48,7 @@ targets: maven:io.sentry:sentry-apollo: maven:io.sentry:sentry-jdbc: maven:io.sentry:sentry-graphql: +# maven:io.sentry:sentry-quartz: maven:io.sentry:sentry-android-navigation: maven:io.sentry:sentry-compose: maven:io.sentry:sentry-compose-android: diff --git a/.github/ISSUE_TEMPLATE/bug_report_java.yml b/.github/ISSUE_TEMPLATE/bug_report_java.yml index c71b3fb494..429fbdee73 100644 --- a/.github/ISSUE_TEMPLATE/bug_report_java.yml +++ b/.github/ISSUE_TEMPLATE/bug_report_java.yml @@ -27,6 +27,7 @@ body: - sentry-logback - sentry-log4j2 - sentry-graphql + - sentry-quartz - sentry-openfeign - sentry-apache-http-client-5 - other diff --git a/README.md b/README.md index d10da65505..afc81b60bd 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Sentry SDK for Java and Android | sentry-log4j2 | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-log4j2) | | sentry-bom | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-bom) | | sentry-graphql | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-graphql) | +| sentry-quartz | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-quartz) | | sentry-openfeign | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-openfeign) | | sentry-opentelemetry-agent | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agent) | | sentry-opentelemetry-agentcustomization | [![Maven Central](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization/badge.svg)](https://maven-badges.herokuapp.com/maven-central/io.sentry/sentry-opentelemetry-agentcustomization) | diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index ae63fd87cb..9837bfd49c 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -129,6 +129,8 @@ object Config { val graphQlJava = "com.graphql-java:graphql-java:17.3" + val quartz = "org.quartz-scheduler:quartz:2.3.0" + val kotlinReflect = "org.jetbrains.kotlin:kotlin-reflect" val kotlinStdLib = "org.jetbrains.kotlin:kotlin-stdlib" @@ -228,6 +230,7 @@ object Config { val SENTRY_APOLLO3_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo3" val SENTRY_APOLLO_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.apollo" val SENTRY_GRAPHQL_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.graphql" + val SENTRY_QUARTZ_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.quartz" val SENTRY_JDBC_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.jdbc" val SENTRY_SERVLET_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet" val SENTRY_SERVLET_JAKARTA_SDK_NAME = "$SENTRY_JAVA_SDK_NAME.servlet.jakarta" diff --git a/sentry-quartz/api/sentry-quartz.api b/sentry-quartz/api/sentry-quartz.api new file mode 100644 index 0000000000..34a7129584 --- /dev/null +++ b/sentry-quartz/api/sentry-quartz.api @@ -0,0 +1,15 @@ +public final class io/sentry/quartz/BuildConfig { + public static final field SENTRY_QUARTZ_SDK_NAME Ljava/lang/String; + public static final field VERSION_NAME Ljava/lang/String; +} + +public final class io/sentry/quartz/SentryJobListener : org/quartz/JobListener { + public static final field SENTRY_CHECK_IN_ID_KEY Ljava/lang/String; + public static final field SENTRY_CHECK_IN_SLUG_KEY Ljava/lang/String; + public fun ()V + public fun getName ()Ljava/lang/String; + public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V + public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V + public fun jobWasExecuted (Lorg/quartz/JobExecutionContext;Lorg/quartz/JobExecutionException;)V +} + diff --git a/sentry-quartz/build.gradle.kts b/sentry-quartz/build.gradle.kts new file mode 100644 index 0000000000..8731f6a40b --- /dev/null +++ b/sentry-quartz/build.gradle.kts @@ -0,0 +1,83 @@ +import net.ltgt.gradle.errorprone.errorprone +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + `java-library` + kotlin("jvm") + jacoco + id(Config.QualityPlugins.errorProne) + id(Config.QualityPlugins.gradleVersions) + id(Config.BuildPlugins.buildConfig) version Config.BuildPlugins.buildConfigVersion +} + +configure { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType().configureEach { + kotlinOptions.jvmTarget = JavaVersion.VERSION_1_8.toString() + kotlinOptions.languageVersion = Config.kotlinCompatibleLanguageVersion +} + +dependencies { + api(projects.sentry) + compileOnly(Config.Libs.quartz) + + compileOnly(Config.CompileOnly.nopen) + errorprone(Config.CompileOnly.nopenChecker) + errorprone(Config.CompileOnly.errorprone) + errorprone(Config.CompileOnly.errorProneNullAway) + compileOnly(Config.CompileOnly.jetbrainsAnnotations) + + // tests + testImplementation(projects.sentry) + testImplementation(projects.sentryTestSupport) + testImplementation(kotlin(Config.kotlinStdLib)) + testImplementation(Config.TestLibs.kotlinTestJunit) + testImplementation(Config.TestLibs.mockitoKotlin) + testImplementation(Config.TestLibs.mockitoInline) +} + +configure { + test { + java.srcDir("src/test/java") + } +} + +jacoco { + toolVersion = Config.QualityPlugins.Jacoco.version +} + +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") + } +} + +buildConfig { + useJavaOutput() + packageName("io.sentry.quartz") + buildConfigField("String", "SENTRY_QUARTZ_SDK_NAME", "\"${Config.Sentry.SENTRY_QUARTZ_SDK_NAME}\"") + buildConfigField("String", "VERSION_NAME", "\"${project.version}\"") +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java similarity index 95% rename from sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryJobListener.java rename to sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index 850962c8c1..b10e94fc6b 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -1,11 +1,13 @@ -package io.sentry.spring.jakarta.checkin; +package io.sentry.quartz; +import io.sentry.BuildConfig; import io.sentry.CheckIn; import io.sentry.CheckInStatus; import io.sentry.MonitorConfig; import io.sentry.MonitorSchedule; import io.sentry.MonitorScheduleUnit; import io.sentry.Sentry; +import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; import java.util.List; @@ -29,6 +31,12 @@ public final class SentryJobListener implements JobListener { public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id"; public static final String SENTRY_CHECK_IN_SLUG_KEY = "sentry-checkin-slug"; + public SentryJobListener() { + SentryIntegrationPackageStorage.getInstance().addIntegration("Quartz"); + SentryIntegrationPackageStorage.getInstance() + .addPackage("maven:io.sentry:sentry-quartz", BuildConfig.VERSION_NAME); + } + @Override public String getName() { return "sentry-job-listener"; diff --git a/sentry-spring-boot-jakarta/build.gradle.kts b/sentry-spring-boot-jakarta/build.gradle.kts index 5d5cb9c9fc..e216a11452 100644 --- a/sentry-spring-boot-jakarta/build.gradle.kts +++ b/sentry-spring-boot-jakarta/build.gradle.kts @@ -30,6 +30,7 @@ dependencies { compileOnly(Config.Libs.springBoot3Starter) compileOnly(platform(SpringBootPlugin.BOM_COORDINATES)) compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryQuartz) compileOnly(Config.Libs.springWeb) compileOnly(Config.Libs.springWebflux) compileOnly(Config.Libs.servletApiJakarta) diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 673d07c8d0..d0623a346d 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -13,6 +13,7 @@ import io.sentry.graphql.SentryGraphqlExceptionHandler; import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor; import io.sentry.protocol.SdkVersion; +import io.sentry.quartz.SentryJobListener; import io.sentry.spring.jakarta.ContextTagsEventProcessor; import io.sentry.spring.jakarta.SentryExceptionResolver; import io.sentry.spring.jakarta.SentryRequestResolver; @@ -173,7 +174,11 @@ static class GraphqlConfiguration {} @Configuration(proxyBeanMethods = false) @Import(SentryQuartzConfiguration.class) @Open - @ConditionalOnClass({QuartzScheduler.class, SchedulerFactoryBean.class}) + @ConditionalOnClass({ + SentryJobListener.class, + QuartzScheduler.class, + SchedulerFactoryBean.class + }) static class QuartzConfiguration {} /** Registers beans specific to Spring MVC. */ diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 4e7af69b89..0a229caaf3 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -88,16 +88,6 @@ public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : i public fun provideUser ()Lio/sentry/protocol/User; } -public final class io/sentry/spring/jakarta/checkin/SentryJobListener : org/quartz/JobListener { - public static final field SENTRY_CHECK_IN_ID_KEY Ljava/lang/String; - public static final field SENTRY_CHECK_IN_SLUG_KEY Ljava/lang/String; - public fun ()V - public fun getName ()Ljava/lang/String; - public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V - public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V - public fun jobWasExecuted (Lorg/quartz/JobExecutionContext;Lorg/quartz/JobExecutionException;)V -} - public class io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration { public fun ()V public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; diff --git a/sentry-spring-jakarta/build.gradle.kts b/sentry-spring-jakarta/build.gradle.kts index df4bf72ae6..be3c00583e 100644 --- a/sentry-spring-jakarta/build.gradle.kts +++ b/sentry-spring-jakarta/build.gradle.kts @@ -44,6 +44,7 @@ dependencies { errorprone(Config.CompileOnly.errorProneNullAway) compileOnly(Config.CompileOnly.jetbrainsAnnotations) compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryQuartz) // tests testImplementation(projects.sentryTestSupport) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java index 81cedf296f..8819e246e5 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java @@ -13,29 +13,4 @@ public class SentryQuartzConfiguration { public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() { return new SentrySchedulerFactoryBeanCustomizer(); } - - // @Bean - // public TaskSchedulerCustomizer taskSchedulerCustomizer() { - // return new SentryTaskSchedulerCustomizer(); - // } - - // public final class SentryTaskSchedulerCustomizer implements TaskSchedulerCustomizer { - // @Override - // public void customize(ThreadPoolTaskScheduler taskScheduler) { - // System.out.println("customize taskScheduler"); - //// taskScheduler.getScheduledExecutor(). - // } - // } - - // @Autowired - // private SchedulerFactoryBean schedulerFactoryBean; - - // @PostConstruct - // public void addListeners() throws SchedulerException { - // ListenerManager listenerManager = schedulerFactoryBean.getScheduler() - // .getListenerManager(); - // - // listenerManager.addTriggerListener(new SentryTriggerListener()); - // listenerManager.addJobListener(new SentryJobListener()); - // } } diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java index 68373b3142..70dafd24e2 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -1,5 +1,6 @@ package io.sentry.spring.jakarta.checkin; +import io.sentry.quartz.SentryJobListener; import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; import org.springframework.scheduling.quartz.SchedulerFactoryBean; diff --git a/settings.gradle.kts b/settings.gradle.kts index 45b562da80..f87b9e0126 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -45,6 +45,7 @@ include( "sentry-opentelemetry:sentry-opentelemetry-core", "sentry-opentelemetry:sentry-opentelemetry-agentcustomization", "sentry-opentelemetry:sentry-opentelemetry-agent", + "sentry-quartz", "sentry-samples:sentry-samples-android", "sentry-samples:sentry-samples-console", "sentry-samples:sentry-samples-jul", From 24215e5064ed5d19bf821a7880c3fde5dd87130d Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 19 Sep 2023 09:07:07 +0200 Subject: [PATCH 11/22] add quartz auto config to spring boot 2 --- buildSrc/src/main/java/Config.kt | 1 + .../spring/boot/jakarta/CustomJob.java | 1 - .../boot/jakarta/SentryDemoApplication.java | 60 ++++--------------- .../build.gradle.kts | 2 + .../spring/boot/SentryDemoApplication.java | 32 ++++++++++ .../samples/spring/boot/quartz/SampleJob.java | 19 ++++++ sentry-spring-boot/build.gradle.kts | 2 + .../spring/boot/SentryAutoConfiguration.java | 14 +++++ sentry-spring/api/sentry-spring.api | 10 ++++ sentry-spring/build.gradle.kts | 2 + .../checkin/SentryQuartzConfiguration.java | 16 +++++ .../SentrySchedulerFactoryBeanCustomizer.java | 12 ++++ 12 files changed, 121 insertions(+), 50 deletions(-) create mode 100644 sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java diff --git a/buildSrc/src/main/java/Config.kt b/buildSrc/src/main/java/Config.kt index 9837bfd49c..af4d49a3bd 100644 --- a/buildSrc/src/main/java/Config.kt +++ b/buildSrc/src/main/java/Config.kt @@ -75,6 +75,7 @@ object Config { val springBootStarter = "org.springframework.boot:spring-boot-starter:$springBootVersion" val springBootStarterGraphql = "org.springframework.boot:spring-boot-starter-graphql:$springBootVersion" + val springBootStarterQuartz = "org.springframework.boot:spring-boot-starter-quartz:$springBootVersion" val springBootStarterTest = "org.springframework.boot:spring-boot-starter-test:$springBootVersion" val springBootStarterWeb = "org.springframework.boot:spring-boot-starter-web:$springBootVersion" val springBootStarterWebsocket = "org.springframework.boot:spring-boot-starter-websocket:$springBootVersion" diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java index 4bc9c817e2..994203b414 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java @@ -31,7 +31,6 @@ void execute() throws InterruptedException { try { LOGGER.info("Executing scheduled job"); Thread.sleep(2000L); - Sentry.captureCheckIn(new CheckIn(checkInId, "my_monitor_slug", CheckInStatus.OK)); } catch (Throwable t) { didError = true; throw t; diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java index e5bf56ad7a..d9e4a7cdb5 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/SentryDemoApplication.java @@ -2,13 +2,14 @@ import io.sentry.samples.spring.boot.jakarta.quartz.SampleJob; 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.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -29,14 +30,6 @@ WebClient webClient(WebClient.Builder builder) { return builder.build(); } - // @Bean - // public JobDetail jobDetail() { - // return JobBuilder.newJob().ofType(SampleJob.class) - // .storeDurably() - // .withIdentity("Qrtz_Job_Detail") - // .withDescription("Invoke Sample Job service...") - // .build(); - // } @Bean public JobDetailFactoryBean jobDetail() { JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); @@ -47,51 +40,20 @@ public JobDetailFactoryBean jobDetail() { return jobDetailFactory; } - // @Bean - // public SimpleTriggerFactoryBean trigger(JobDetail job) { - // SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); - // trigger.setJobDetail(job); - // trigger.setRepeatInterval(2 * 60 * 1000); - // trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); - // return trigger; - // } - @Bean - public CronTriggerFactoryBean trigger(JobDetail job) { - CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + public SimpleTriggerFactoryBean trigger(JobDetail job) { + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); trigger.setJobDetail(job); - trigger.setCronExpression("0 /5 * ? * *"); + trigger.setRepeatInterval(2 * 60 * 1000); + trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); return trigger; } // @Bean - // public Scheduler scheduler(Trigger trigger, JobDetail job, SchedulerFactoryBean factory) - // throws SchedulerException { - // Scheduler scheduler = factory.getScheduler(); - // scheduler.scheduleJob(job, trigger); - // scheduler.start(); - // return scheduler; - // } - - // @Bean - // public SchedulerFactoryBean scheduler(Trigger trigger, JobDetail job, DataSource - // quartzDataSource) { - // SchedulerFactoryBean schedulerFactory = new SchedulerFactoryBean(); - // schedulerFactory.setConfigLocation(new ClassPathResource("quartz.properties")); - //// schedulerFactory.setGlobalJobListeners(new SentryJobListener()); - //// schedulerFactory.setGlobalTriggerListeners(new SentryTriggerListener()); - //// try { - //// ListenerManager listenerManager = schedulerFactory.getScheduler().getListenerManager(); - //// listenerManager.addJobListener(new SentryJobListener()); - //// listenerManager.addTriggerListener(new SentryTriggerListener()); - //// } catch (SchedulerException e) { - //// throw new RuntimeException(e); - //// } - // - //// schedulerFactory.setJobFactory(springBeanJobFactory()); - // schedulerFactory.setJobDetails(job); - // schedulerFactory.setTriggers(trigger); - // schedulerFactory.setDataSource(quartzDataSource); - // return schedulerFactory; + // public CronTriggerFactoryBean trigger(JobDetail job) { + // CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + // trigger.setJobDetail(job); + // trigger.setCronExpression("0 /5 * ? * *"); + // return trigger; // } } diff --git a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts index e5506d4229..e43de9d2b0 100644 --- a/sentry-samples/sentry-samples-spring-boot/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot/build.gradle.kts @@ -23,6 +23,7 @@ dependencies { implementation(Config.Libs.springBootStarterWebsocket) implementation(Config.Libs.springBootStarterWebflux) implementation(Config.Libs.springBootStarterGraphql) + implementation(Config.Libs.springBootStarterQuartz) implementation(Config.Libs.springBootStarterAop) implementation(Config.Libs.aspectj) implementation(Config.Libs.springBootStarter) @@ -32,6 +33,7 @@ dependencies { implementation(projects.sentrySpringBootStarter) implementation(projects.sentryLogback) implementation(projects.sentryGraphql) + implementation(projects.sentryQuartz) // database query tracing implementation(projects.sentryJdbc) diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java index 18afe2838b..ff02e3d01c 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/SentryDemoApplication.java @@ -1,10 +1,15 @@ package io.sentry.samples.spring.boot; +import io.sentry.samples.spring.boot.quartz.SampleJob; +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.JobDetailFactoryBean; +import org.springframework.scheduling.quartz.SimpleTriggerFactoryBean; import org.springframework.web.client.RestTemplate; import org.springframework.web.reactive.function.client.WebClient; @@ -24,4 +29,31 @@ RestTemplate restTemplate(RestTemplateBuilder builder) { WebClient webClient(WebClient.Builder builder) { return builder.build(); } + + @Bean + public JobDetailFactoryBean jobDetail() { + JobDetailFactoryBean jobDetailFactory = new JobDetailFactoryBean(); + jobDetailFactory.setName("hello_spring_boot_2"); + jobDetailFactory.setJobClass(SampleJob.class); + jobDetailFactory.setDescription("Invoke Sample Job service..."); + jobDetailFactory.setDurability(true); + return jobDetailFactory; + } + + // @Bean + // public CronTriggerFactoryBean trigger(JobDetail job) { + // CronTriggerFactoryBean trigger = new CronTriggerFactoryBean(); + // trigger.setJobDetail(job); + // trigger.setCronExpression("0 * * ? * *"); + // return trigger; + // } + + @Bean + public SimpleTriggerFactoryBean trigger(JobDetail job) { + SimpleTriggerFactoryBean trigger = new SimpleTriggerFactoryBean(); + trigger.setJobDetail(job); + trigger.setRepeatInterval(2 * 60 * 1000); + trigger.setRepeatCount(SimpleTrigger.REPEAT_INDEFINITELY); + return trigger; + } } diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java new file mode 100644 index 0000000000..d238e02681 --- /dev/null +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/quartz/SampleJob.java @@ -0,0 +1,19 @@ +package io.sentry.samples.spring.boot.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-spring-boot/build.gradle.kts b/sentry-spring-boot/build.gradle.kts index 3d144730a5..20b1d4a942 100644 --- a/sentry-spring-boot/build.gradle.kts +++ b/sentry-spring-boot/build.gradle.kts @@ -35,9 +35,11 @@ dependencies { compileOnly(Config.Libs.springBootStarterAop) compileOnly(Config.Libs.springBootStarterSecurity) compileOnly(Config.Libs.springBootStarterGraphql) + compileOnly(Config.Libs.springBootStarterQuartz) compileOnly(Config.Libs.reactorCore) compileOnly(projects.sentryOpentelemetry.sentryOpentelemetryCore) compileOnly(projects.sentryGraphql) + compileOnly(projects.sentryQuartz) annotationProcessor(platform(SpringBootPlugin.BOM_COORDINATES)) annotationProcessor(Config.AnnotationProcessors.springBootAutoConfigure) diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 0b51b22d60..aa01874b18 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -13,6 +13,7 @@ import io.sentry.graphql.SentryGraphqlExceptionHandler; import io.sentry.opentelemetry.OpenTelemetryLinkErrorEventProcessor; import io.sentry.protocol.SdkVersion; +import io.sentry.quartz.SentryJobListener; import io.sentry.spring.ContextTagsEventProcessor; import io.sentry.spring.SentryExceptionResolver; import io.sentry.spring.SentryRequestResolver; @@ -21,6 +22,7 @@ import io.sentry.spring.SentryUserProvider; import io.sentry.spring.SentryWebConfiguration; import io.sentry.spring.SpringSecuritySentryUserProvider; +import io.sentry.spring.checkin.SentryQuartzConfiguration; import io.sentry.spring.graphql.SentryGraphqlConfiguration; import io.sentry.spring.tracing.SentryAdviceConfiguration; import io.sentry.spring.tracing.SentrySpanPointcutConfiguration; @@ -35,6 +37,7 @@ import javax.servlet.http.HttpServletRequest; 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; @@ -56,6 +59,7 @@ import org.springframework.core.Ordered; import org.springframework.core.annotation.Order; import org.springframework.graphql.execution.DataFetcherExceptionResolverAdapter; +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; @@ -167,6 +171,16 @@ static class OpenTelemetryLinkErrorEventProcessorConfiguration { }) static class GraphqlConfiguration {} + @Configuration(proxyBeanMethods = false) + @Import(SentryQuartzConfiguration.class) + @Open + @ConditionalOnClass({ + SentryJobListener.class, + QuartzScheduler.class, + SchedulerFactoryBean.class + }) + static class QuartzConfiguration {} + /** Registers beans specific to Spring MVC. */ @Configuration(proxyBeanMethods = false) @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 7efeb59f56..05a59c477f 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -88,6 +88,16 @@ public final class io/sentry/spring/SpringSecuritySentryUserProvider : io/sentry public fun provideUser ()Lio/sentry/protocol/User; } +public class io/sentry/spring/checkin/SentryQuartzConfiguration { + public fun ()V + public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; +} + +public final class io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer { + public fun ()V + public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V +} + public final class io/sentry/spring/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; diff --git a/sentry-spring/build.gradle.kts b/sentry-spring/build.gradle.kts index 75a33d0767..ac444c25ca 100644 --- a/sentry-spring/build.gradle.kts +++ b/sentry-spring/build.gradle.kts @@ -36,6 +36,8 @@ dependencies { compileOnly(Config.Libs.springWebflux) compileOnly(Config.Libs.springBootStarterGraphql) compileOnly(projects.sentryGraphql) + compileOnly(Config.Libs.springBootStarterQuartz) + compileOnly(projects.sentryQuartz) compileOnly(Config.CompileOnly.nopen) errorprone(Config.CompileOnly.nopenChecker) diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java new file mode 100644 index 0000000000..842011cb1f --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java @@ -0,0 +1,16 @@ +package io.sentry.spring.checkin; + +import com.jakewharton.nopen.annotation.Open; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +public class SentryQuartzConfiguration { + + @Bean + public SchedulerFactoryBeanCustomizer schedulerFactoryBeanCustomizer() { + return new SentrySchedulerFactoryBeanCustomizer(); + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java new file mode 100644 index 0000000000..f266d478f1 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -0,0 +1,12 @@ +package io.sentry.spring.checkin; + +import io.sentry.quartz.SentryJobListener; +import org.springframework.boot.autoconfigure.quartz.SchedulerFactoryBeanCustomizer; +import org.springframework.scheduling.quartz.SchedulerFactoryBean; + +public final class SentrySchedulerFactoryBeanCustomizer implements SchedulerFactoryBeanCustomizer { + @Override + public void customize(SchedulerFactoryBean schedulerFactoryBean) { + schedulerFactoryBean.setGlobalJobListeners(new SentryJobListener()); + } +} From a709a4097e5dac2188a156629f6c8c3bf9f71b93 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Tue, 19 Sep 2023 09:39:06 +0200 Subject: [PATCH 12/22] fix comment --- sentry/src/main/java/io/sentry/CheckIn.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index 2a7ef4ba26..4a72d8b5d0 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -9,7 +9,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -/** Adds additional information about what happened to an event. */ +/** A check-in for a monitor (CRON). */ public final class CheckIn implements JsonUnknown, JsonSerializable { private final @NotNull SentryId checkInId; From 372a2f0085816f513c71bcd8b35fd11719b8b320 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 20 Sep 2023 11:41:48 +0200 Subject: [PATCH 13/22] Add enableAutomaticCheckIns and ignoreCheckIns options --- sentry-quartz/api/sentry-quartz.api | 1 + .../io/sentry/quartz/SentryJobListener.java | 21 ++++++++ .../src/main/resources/application.properties | 1 + sentry-spring-boot-jakarta/build.gradle.kts | 2 + .../boot/jakarta/SentryAutoConfiguration.java | 1 + .../jakarta/SentryAutoConfigurationTest.kt | 45 +++++++++++++++- sentry-spring-boot/build.gradle.kts | 2 + .../spring/boot/SentryAutoConfiguration.java | 1 + .../boot/SentryAutoConfigurationTest.kt | 45 +++++++++++++++- sentry/api/sentry.api | 13 +++++ .../main/java/io/sentry/ExternalOptions.java | 24 +++++++++ .../src/main/java/io/sentry/SentryClient.java | 15 ++++++ .../main/java/io/sentry/SentryOptions.java | 51 +++++++++++++++++++ .../java/io/sentry/util/CheckInUtils.java | 34 +++++++++++++ .../java/io/sentry/ExternalOptionsTest.kt | 14 +++++ .../test/java/io/sentry/SentryClientTest.kt | 38 ++++++++++++++ .../java/io/sentry/util/CheckInUtilsTest.kt | 38 ++++++++++++++ 17 files changed, 344 insertions(+), 2 deletions(-) create mode 100644 sentry/src/main/java/io/sentry/util/CheckInUtils.java create mode 100644 sentry/src/test/java/io/sentry/util/CheckInUtilsTest.kt diff --git a/sentry-quartz/api/sentry-quartz.api b/sentry-quartz/api/sentry-quartz.api index 34a7129584..0c11a26b53 100644 --- a/sentry-quartz/api/sentry-quartz.api +++ b/sentry-quartz/api/sentry-quartz.api @@ -7,6 +7,7 @@ public final class io/sentry/quartz/SentryJobListener : org/quartz/JobListener { public static final field SENTRY_CHECK_IN_ID_KEY Ljava/lang/String; public static final field SENTRY_CHECK_IN_SLUG_KEY Ljava/lang/String; public fun ()V + public fun (Lio/sentry/IHub;)V public fun getName ()Ljava/lang/String; public fun jobExecutionVetoed (Lorg/quartz/JobExecutionContext;)V public fun jobToBeExecuted (Lorg/quartz/JobExecutionContext;)V diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index b10e94fc6b..493d667319 100644 --- a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -3,6 +3,8 @@ import io.sentry.BuildConfig; import io.sentry.CheckIn; import io.sentry.CheckInStatus; +import io.sentry.HubAdapter; +import io.sentry.IHub; import io.sentry.MonitorConfig; import io.sentry.MonitorSchedule; import io.sentry.MonitorScheduleUnit; @@ -10,6 +12,7 @@ import io.sentry.SentryIntegrationPackageStorage; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; +import io.sentry.util.Objects; import java.util.List; import java.util.TimeZone; import org.jetbrains.annotations.NotNull; @@ -31,7 +34,14 @@ public final class SentryJobListener implements JobListener { public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id"; public static final String SENTRY_CHECK_IN_SLUG_KEY = "sentry-checkin-slug"; + private final @NotNull IHub hub; + public SentryJobListener() { + this(HubAdapter.getInstance()); + } + + public SentryJobListener(final @NotNull IHub hub) { + this.hub = Objects.requireNonNull(hub, "hub is required"); SentryIntegrationPackageStorage.getInstance().addIntegration("Quartz"); SentryIntegrationPackageStorage.getInstance() .addPackage("maven:io.sentry:sentry-quartz", BuildConfig.VERSION_NAME); @@ -45,6 +55,9 @@ public String getName() { @Override public void jobToBeExecuted(JobExecutionContext context) { try { + if (isDisabled()) { + return; + } final @NotNull String slug = getSlug(context.getJobDetail()); final @NotNull CheckIn checkIn = new CheckIn(slug, CheckInStatus.IN_PROGRESS); @@ -184,6 +197,10 @@ public void jobExecutionVetoed(JobExecutionContext context) { @Override public void jobWasExecuted(JobExecutionContext context, JobExecutionException jobException) { try { + if (isDisabled()) { + return; + } + final @Nullable Object checkInIdObjectFromContext = context.get(SENTRY_CHECK_IN_ID_KEY); final @Nullable Object slugObjectFromContext = context.get(SENTRY_CHECK_IN_SLUG_KEY); final @NotNull SentryId checkInId = @@ -204,4 +221,8 @@ public void jobWasExecuted(JobExecutionContext context, JobExecutionException jo .log(SentryLevel.ERROR, "Unable to capture check-in in jobWasExecuted.", t); } } + + private boolean isDisabled() { + return !hub.getOptions().isEnableAutomaticCheckIns(); + } } 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..cfd7cf9a5d 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,7 @@ sentry.logging.minimum-breadcrumb-level=debug # Performance configuration sentry.traces-sample-rate=1.0 sentry.enable-tracing=true +#sentry.enable-automatic-checkins=true 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/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 4a489adb04..4f5e3bc5b2 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -174,6 +174,7 @@ static class GraphqlConfiguration {} @Configuration(proxyBeanMethods = false) @Import(SentryQuartzConfiguration.class) @Open + @ConditionalOnProperty(name = "sentry.enable-automatic-checkins") @ConditionalOnClass({ SentryJobListener.class, QuartzScheduler.class, 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..580ed08e4a 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,41 @@ 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) + } + } + @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/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index aa01874b18..8b57a757d0 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -174,6 +174,7 @@ static class GraphqlConfiguration {} @Configuration(proxyBeanMethods = false) @Import(SentryQuartzConfiguration.class) @Open + @ConditionalOnProperty(name = "sentry.enable-automatic-checkins") @ConditionalOnClass({ SentryJobListener.class, QuartzScheduler.class, 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..7a04439d2b 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,41 @@ 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) + } + } + @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..7cbadd8a26 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()); + setTracePropagationTargets(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/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")) + } +} From 66cb75455b3f170c93acb25f38dd8b7749265e6f Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 20 Sep 2023 11:57:34 +0200 Subject: [PATCH 14/22] move __ slug logic --- .../src/main/java/io/sentry/quartz/SentryJobListener.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index b10e94fc6b..68d5afceaa 100644 --- a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -73,10 +73,9 @@ public void jobToBeExecuted(JobExecutionContext context) { final @Nullable JobKey key = jobDetail.getKey(); if (key != null) { slugBuilder.append(key.getName()); + slugBuilder.append("__"); } - slugBuilder.append("__"); - final @Nullable Class jobClass = jobDetail.getJobClass(); if (jobClass != null) { slugBuilder.append(jobClass.getCanonicalName()); From f4fb1dd55bce7f175cc5ef461649188cc4915bd2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 20 Sep 2023 13:20:30 +0200 Subject: [PATCH 15/22] fix sample by adding quartz lib --- .../sentry-samples-spring-boot-jakarta/build.gradle.kts | 1 + 1 file changed, 1 insertion(+) 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 dbcec367d8..68a9937be5 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/build.gradle.kts @@ -33,6 +33,7 @@ dependencies { implementation(projects.sentrySpringBootStarterJakarta) implementation(projects.sentryLogback) implementation(projects.sentryGraphql) + implementation(projects.sentryQuartz) // database query tracing implementation(projects.sentryJdbc) From 86e5dede072c91559b0aeb8af95674482ff87c7e Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 20 Sep 2023 13:30:57 +0200 Subject: [PATCH 16/22] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29490479ab..7621b8edb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - 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)) +- Automatic CRON checkins for Quartz ([#2940](https://github.com/getsentry/sentry-java/pull/2940)) ### Fixes From 409827e5169e5a8aed18d3a47c21c934cc77c39b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 20 Sep 2023 13:51:14 +0200 Subject: [PATCH 17/22] mark crons features experimental --- .../src/main/java/io/sentry/quartz/SentryJobListener.java | 2 ++ .../spring/jakarta/checkin/SentryQuartzConfiguration.java | 2 ++ .../checkin/SentrySchedulerFactoryBeanCustomizer.java | 2 ++ .../sentry/spring/checkin/SentryQuartzConfiguration.java | 2 ++ .../checkin/SentrySchedulerFactoryBeanCustomizer.java | 2 ++ sentry/src/main/java/io/sentry/CheckIn.java | 1 + sentry/src/main/java/io/sentry/CheckInStatus.java | 2 ++ sentry/src/main/java/io/sentry/ExternalOptions.java | 5 +++++ sentry/src/main/java/io/sentry/Hub.java | 1 + sentry/src/main/java/io/sentry/HubAdapter.java | 1 + sentry/src/main/java/io/sentry/IHub.java | 1 + sentry/src/main/java/io/sentry/ISentryClient.java | 1 + sentry/src/main/java/io/sentry/MonitorConfig.java | 2 ++ sentry/src/main/java/io/sentry/MonitorContexts.java | 2 ++ sentry/src/main/java/io/sentry/MonitorSchedule.java | 1 + sentry/src/main/java/io/sentry/MonitorScheduleType.java | 2 ++ sentry/src/main/java/io/sentry/MonitorScheduleUnit.java | 2 ++ sentry/src/main/java/io/sentry/NoOpHub.java | 2 ++ sentry/src/main/java/io/sentry/Sentry.java | 1 + sentry/src/main/java/io/sentry/SentryOptions.java | 8 ++++++-- 20 files changed, 40 insertions(+), 2 deletions(-) diff --git a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java index 0e437f286c..83ca663a2f 100644 --- a/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java +++ b/sentry-quartz/src/main/java/io/sentry/quartz/SentryJobListener.java @@ -15,6 +15,7 @@ import io.sentry.util.Objects; import java.util.List; import java.util.TimeZone; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.quartz.CalendarIntervalTrigger; @@ -29,6 +30,7 @@ import org.quartz.SimpleTrigger; import org.quartz.Trigger; +@ApiStatus.Experimental public final class SentryJobListener implements JobListener { public static final String SENTRY_CHECK_IN_ID_KEY = "sentry-checkin-id"; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java index 8819e246e5..eb396c9692 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryQuartzConfiguration.java @@ -1,12 +1,14 @@ 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; @Configuration(proxyBeanMethods = false) @Open +@ApiStatus.Experimental public class SentryQuartzConfiguration { @Bean diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java index 70dafd24e2..e37f5ac467 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -1,9 +1,11 @@ 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) { diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java index 842011cb1f..640507dd7c 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryQuartzConfiguration.java @@ -1,12 +1,14 @@ package io.sentry.spring.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; @Configuration(proxyBeanMethods = false) @Open +@ApiStatus.Experimental public class SentryQuartzConfiguration { @Bean diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java index f266d478f1..d0f68c6712 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentrySchedulerFactoryBeanCustomizer.java @@ -1,9 +1,11 @@ package io.sentry.spring.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) { diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index 4a72d8b5d0..e8ad8d57c6 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -9,6 +9,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental /** A check-in for a monitor (CRON). */ public final class CheckIn implements JsonUnknown, JsonSerializable { diff --git a/sentry/src/main/java/io/sentry/CheckInStatus.java b/sentry/src/main/java/io/sentry/CheckInStatus.java index 42ed7bac54..0f0b47b4a4 100644 --- a/sentry/src/main/java/io/sentry/CheckInStatus.java +++ b/sentry/src/main/java/io/sentry/CheckInStatus.java @@ -1,9 +1,11 @@ package io.sentry; import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Status of a CheckIn */ +@ApiStatus.Experimental public enum CheckInStatus { IN_PROGRESS, OK, diff --git a/sentry/src/main/java/io/sentry/ExternalOptions.java b/sentry/src/main/java/io/sentry/ExternalOptions.java index e139cd2cc4..604a001da9 100644 --- a/sentry/src/main/java/io/sentry/ExternalOptions.java +++ b/sentry/src/main/java/io/sentry/ExternalOptions.java @@ -8,6 +8,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CopyOnWriteArraySet; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -392,18 +393,22 @@ public void setSendModules(final @Nullable Boolean sendModules) { this.sendModules = sendModules; } + @ApiStatus.Experimental public @Nullable Boolean isEnableAutomaticCheckIns() { return enableAutomaticCheckIns; } + @ApiStatus.Experimental public void setEnableAutomaticCheckIns(final @Nullable Boolean enableAutomaticCheckIns) { this.enableAutomaticCheckIns = enableAutomaticCheckIns; } + @ApiStatus.Experimental public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { this.ignoredCheckIns = ignoredCheckIns; } + @ApiStatus.Experimental public @Nullable List getIgnoredCheckIns() { return ignoredCheckIns; } diff --git a/sentry/src/main/java/io/sentry/Hub.java b/sentry/src/main/java/io/sentry/Hub.java index 5810390069..0ce59cc05c 100644 --- a/sentry/src/main/java/io/sentry/Hub.java +++ b/sentry/src/main/java/io/sentry/Hub.java @@ -864,6 +864,7 @@ private Scope buildLocalScope( } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { SentryId sentryId = SentryId.EMPTY_ID; if (!isEnabled()) { diff --git a/sentry/src/main/java/io/sentry/HubAdapter.java b/sentry/src/main/java/io/sentry/HubAdapter.java index 05f348cb15..88445b5232 100644 --- a/sentry/src/main/java/io/sentry/HubAdapter.java +++ b/sentry/src/main/java/io/sentry/HubAdapter.java @@ -253,6 +253,7 @@ public void reportFullyDisplayed() { } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return Sentry.captureCheckIn(checkIn); } diff --git a/sentry/src/main/java/io/sentry/IHub.java b/sentry/src/main/java/io/sentry/IHub.java index 71bbb0f731..8ff69727b0 100644 --- a/sentry/src/main/java/io/sentry/IHub.java +++ b/sentry/src/main/java/io/sentry/IHub.java @@ -627,6 +627,7 @@ TransactionContext continueTrace( @Nullable BaggageHeader getBaggage(); + @ApiStatus.Experimental @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn); } diff --git a/sentry/src/main/java/io/sentry/ISentryClient.java b/sentry/src/main/java/io/sentry/ISentryClient.java index 28ce8111d0..6eb8d7d1d0 100644 --- a/sentry/src/main/java/io/sentry/ISentryClient.java +++ b/sentry/src/main/java/io/sentry/ISentryClient.java @@ -266,5 +266,6 @@ SentryId captureTransaction( } @NotNull + @ApiStatus.Experimental SentryId captureCheckIn(@NotNull CheckIn checkIn, @Nullable Scope scope, @Nullable Hint hint); } diff --git a/sentry/src/main/java/io/sentry/MonitorConfig.java b/sentry/src/main/java/io/sentry/MonitorConfig.java index b6ad81ee8f..c25884c2bd 100644 --- a/sentry/src/main/java/io/sentry/MonitorConfig.java +++ b/sentry/src/main/java/io/sentry/MonitorConfig.java @@ -4,9 +4,11 @@ import java.io.IOException; import java.util.HashMap; import java.util.Map; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental public final class MonitorConfig implements JsonUnknown, JsonSerializable { private @NotNull MonitorSchedule schedule; diff --git a/sentry/src/main/java/io/sentry/MonitorContexts.java b/sentry/src/main/java/io/sentry/MonitorContexts.java index 461a4d549a..3a15aa4113 100644 --- a/sentry/src/main/java/io/sentry/MonitorContexts.java +++ b/sentry/src/main/java/io/sentry/MonitorContexts.java @@ -6,9 +6,11 @@ import java.util.Collections; import java.util.List; import java.util.concurrent.ConcurrentHashMap; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental public final class MonitorContexts extends ConcurrentHashMap implements JsonSerializable { private static final long serialVersionUID = 3987329379811822556L; diff --git a/sentry/src/main/java/io/sentry/MonitorSchedule.java b/sentry/src/main/java/io/sentry/MonitorSchedule.java index 904e84aeac..23740d45ba 100644 --- a/sentry/src/main/java/io/sentry/MonitorSchedule.java +++ b/sentry/src/main/java/io/sentry/MonitorSchedule.java @@ -8,6 +8,7 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +@ApiStatus.Experimental public final class MonitorSchedule implements JsonUnknown, JsonSerializable { public static @NotNull MonitorSchedule crontab(final @NotNull String value) { diff --git a/sentry/src/main/java/io/sentry/MonitorScheduleType.java b/sentry/src/main/java/io/sentry/MonitorScheduleType.java index ac168e3845..a1ec20c97e 100644 --- a/sentry/src/main/java/io/sentry/MonitorScheduleType.java +++ b/sentry/src/main/java/io/sentry/MonitorScheduleType.java @@ -1,9 +1,11 @@ package io.sentry; import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Type of a monitor schedule */ +@ApiStatus.Experimental public enum MonitorScheduleType { CRONTAB, INTERVAL; diff --git a/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java b/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java index adfb0771fe..994d9c4be1 100644 --- a/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java +++ b/sentry/src/main/java/io/sentry/MonitorScheduleUnit.java @@ -1,9 +1,11 @@ package io.sentry; import java.util.Locale; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; /** Time unit of a monitor schedule. */ +@ApiStatus.Experimental public enum MonitorScheduleUnit { MINUTE, HOUR, diff --git a/sentry/src/main/java/io/sentry/NoOpHub.java b/sentry/src/main/java/io/sentry/NoOpHub.java index 4d40efe976..ec6b45e389 100644 --- a/sentry/src/main/java/io/sentry/NoOpHub.java +++ b/sentry/src/main/java/io/sentry/NoOpHub.java @@ -4,6 +4,7 @@ import io.sentry.protocol.SentryTransaction; import io.sentry.protocol.User; import java.util.List; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -209,6 +210,7 @@ public void reportFullyDisplayed() {} } @Override + @ApiStatus.Experimental public @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return SentryId.EMPTY_ID; } diff --git a/sentry/src/main/java/io/sentry/Sentry.java b/sentry/src/main/java/io/sentry/Sentry.java index 822822340f..71b588d55b 100644 --- a/sentry/src/main/java/io/sentry/Sentry.java +++ b/sentry/src/main/java/io/sentry/Sentry.java @@ -1012,6 +1012,7 @@ public interface OptionsConfiguration { return getCurrentHub().getBaggage(); } + @ApiStatus.Experimental public static @NotNull SentryId captureCheckIn(final @NotNull CheckIn checkIn) { return getCurrentHub().captureCheckIn(checkIn); } diff --git a/sentry/src/main/java/io/sentry/SentryOptions.java b/sentry/src/main/java/io/sentry/SentryOptions.java index 7cbadd8a26..69fa7bed54 100644 --- a/sentry/src/main/java/io/sentry/SentryOptions.java +++ b/sentry/src/main/java/io/sentry/SentryOptions.java @@ -444,10 +444,10 @@ public class SentryOptions { private boolean sendModules = true; /** Whether to automatically send check-ins for monitors (CRONS). */ - private boolean enableAutomaticCheckIns = false; + @ApiStatus.Experimental private boolean enableAutomaticCheckIns = false; /** Contains a list of monitor slugs for which check-ins should not be sent. */ - private @Nullable List ignoredCheckIns = null; + @ApiStatus.Experimental private @Nullable List ignoredCheckIns = null; /** * Adds an event processor @@ -2152,6 +2152,7 @@ public boolean isSendModules() { * * @return true if check-ins should be sent automatically. */ + @ApiStatus.Experimental public boolean isEnableAutomaticCheckIns() { return enableAutomaticCheckIns; } @@ -2179,10 +2180,12 @@ public void setSendModules(boolean sendModules) { * * @param enableAutomaticCheckIns true if check-ins should be sent automatically. */ + @ApiStatus.Experimental public void setEnableAutomaticCheckIns(boolean enableAutomaticCheckIns) { this.enableAutomaticCheckIns = enableAutomaticCheckIns; } + @ApiStatus.Experimental public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { if (ignoredCheckIns == null) { this.ignoredCheckIns = null; @@ -2198,6 +2201,7 @@ public void setIgnoredCheckIns(final @Nullable List ignoredCheckIns) { } } + @ApiStatus.Experimental public @Nullable List getIgnoredCheckIns() { return ignoredCheckIns; } From 49adb9ec04ff78c001f0724235c84cab640f809b Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 21 Sep 2023 09:30:05 +0200 Subject: [PATCH 18/22] SentryCheckIn annotation and advice config for jakarta --- .../spring/boot/jakarta/CustomJob.java | 26 +----- .../boot/jakarta/SentryAutoConfiguration.java | 18 +++++ .../api/sentry-spring-jakarta.api | 22 ++++++ .../spring/jakarta/checkin/SentryCheckIn.java | 41 ++++++++++ .../jakarta/checkin/SentryCheckInAdvice.java | 79 +++++++++++++++++++ .../SentryCheckInAdviceConfiguration.java | 31 ++++++++ .../SentryCheckInPointcutConfiguration.java | 29 +++++++ sentry/src/main/java/io/sentry/CheckIn.java | 8 +- 8 files changed, 228 insertions(+), 26 deletions(-) create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java create mode 100644 sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java diff --git a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java index 994203b414..cac83e6d79 100644 --- a/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot-jakarta/src/main/java/io/sentry/samples/spring/boot/jakarta/CustomJob.java @@ -1,12 +1,7 @@ package io.sentry.samples.spring.boot.jakarta; -import io.sentry.CheckIn; -import io.sentry.CheckInStatus; -import io.sentry.DateUtils; -import io.sentry.Sentry; -import io.sentry.protocol.SentryId; +import io.sentry.spring.jakarta.checkin.SentryCheckIn; import io.sentry.spring.jakarta.tracing.SentryTransaction; -import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -22,23 +17,10 @@ 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 { - final @NotNull SentryId checkInId = - Sentry.captureCheckIn(new CheckIn("my_monitor_slug", CheckInStatus.IN_PROGRESS)); - final long startTime = System.currentTimeMillis(); - boolean didError = false; - try { - LOGGER.info("Executing scheduled job"); - Thread.sleep(2000L); - } catch (Throwable t) { - didError = true; - throw t; - } finally { - final @NotNull CheckInStatus status = didError ? CheckInStatus.ERROR : CheckInStatus.OK; - CheckIn checkIn = new CheckIn(checkInId, "my_monitor_slug", status); - checkIn.setDuration(DateUtils.millisToSeconds(System.currentTimeMillis() - startTime)); - Sentry.captureCheckIn(checkIn); - } + LOGGER.info("Executing scheduled job"); + Thread.sleep(2000L); } } diff --git a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java index 4f5e3bc5b2..c38c384631 100644 --- a/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java +++ b/sentry-spring-boot-jakarta/src/main/java/io/sentry/spring/boot/jakarta/SentryAutoConfiguration.java @@ -22,6 +22,8 @@ import io.sentry.spring.jakarta.SentryUserProvider; import io.sentry.spring.jakarta.SentryWebConfiguration; 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.graphql.SentryGraphqlConfiguration; import io.sentry.spring.jakarta.tracing.SentryAdviceConfiguration; @@ -182,6 +184,22 @@ static class GraphqlConfiguration {} }) 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) diff --git a/sentry-spring-jakarta/api/sentry-spring-jakarta.api b/sentry-spring-jakarta/api/sentry-spring-jakarta.api index 0a229caaf3..d30cda5c60 100644 --- a/sentry-spring-jakarta/api/sentry-spring-jakarta.api +++ b/sentry-spring-jakarta/api/sentry-spring-jakarta.api @@ -88,6 +88,28 @@ public final class io/sentry/spring/jakarta/SpringSecuritySentryUserProvider : i 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 { + public fun (Lio/sentry/IHub;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; +} + +public class io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration { + public fun ()V + public fun sentryCheckInAdvice (Lio/sentry/IHub;)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; diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java new file mode 100644 index 0000000000..e823f2ef3b --- /dev/null +++ b/sentry-spring-jakarta/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, ElementType.TYPE}) +@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-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java new file mode 100644 index 0000000000..f6313bcadc --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java @@ -0,0 +1,79 @@ +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.IHub; +import io.sentry.SentryLevel; +import io.sentry.protocol.SentryId; +import io.sentry.spring.jakarta.tracing.SentryTransaction; +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.ObjectUtils; + +/** + * 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 +@ApiStatus.Experimental +@Open +public class SentryCheckInAdvice implements MethodInterceptor { + private final @NotNull IHub hub; + + public SentryCheckInAdvice(final @NotNull IHub hub) { + this.hub = Objects.requireNonNull(hub, "hub is required"); + } + + @Override + public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { + final Method mostSpecificMethod = + AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()); + + @Nullable + SentryCheckIn checkInAnnotation = + AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCheckIn.class); + if (checkInAnnotation == null) { + return invocation.proceed(); + } + + final boolean isHeartbeatOnly = checkInAnnotation.heartbeat(); + final @Nullable String monitorSlug = checkInAnnotation.value(); + + if (ObjectUtils.isEmpty(monitorSlug)) { + hub.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(); + } + + @Nullable SentryId checkInId = null; + final long startTime = System.currentTimeMillis(); + boolean didError = false; + + try { + if (!isHeartbeatOnly) { + checkInId = hub.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)); + hub.captureCheckIn(checkIn); + } + } +} diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java new file mode 100644 index 0000000000..505c08f77a --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdviceConfiguration.java @@ -0,0 +1,31 @@ +package io.sentry.spring.jakarta.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IHub; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInAdviceConfiguration { + + @Bean + public @NotNull Advice sentryCheckInAdvice(final @NotNull IHub hub) { + return new SentryCheckInAdvice(hub); + } + + @Bean + 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-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java new file mode 100644 index 0000000000..8371d2e684 --- /dev/null +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInPointcutConfiguration.java @@ -0,0 +1,29 @@ +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** AOP pointcut configuration for {@link SentryCheckIn}. */ +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInPointcutConfiguration { + + /** + * Pointcut around which check-ins are created. + * + * @return pointcut used by {@link SentryCheckInAdvice}. + */ + @Bean + public @NotNull Pointcut sentryCheckInPointcut() { + return new ComposablePointcut(new AnnotationClassFilter(SentryCheckIn.class, true)) + .union(new AnnotationMatchingPointcut(null, SentryCheckIn.class)); + } +} diff --git a/sentry/src/main/java/io/sentry/CheckIn.java b/sentry/src/main/java/io/sentry/CheckIn.java index e8ad8d57c6..4c83771324 100644 --- a/sentry/src/main/java/io/sentry/CheckIn.java +++ b/sentry/src/main/java/io/sentry/CheckIn.java @@ -26,11 +26,11 @@ public final class CheckIn implements JsonUnknown, JsonSerializable { private @Nullable Map unknown; public CheckIn(final @NotNull String monitorSlug, final @NotNull CheckInStatus status) { - this(new SentryId(), monitorSlug, status.apiName()); + this(null, monitorSlug, status.apiName()); } public CheckIn( - final @NotNull SentryId id, + final @Nullable SentryId id, final @NotNull String monitorSlug, final @NotNull CheckInStatus status) { this(id, monitorSlug, status.apiName()); @@ -38,10 +38,10 @@ public CheckIn( @ApiStatus.Internal public CheckIn( - final @NotNull SentryId checkInId, + final @Nullable SentryId checkInId, final @NotNull String monitorSlug, final @NotNull String status) { - this.checkInId = checkInId; + this.checkInId = checkInId == null ? new SentryId() : checkInId; this.monitorSlug = monitorSlug; this.status = status; } From e522b75457bf61a81db770ddff2d8758b2c32b09 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Thu, 21 Sep 2023 11:40:24 +0200 Subject: [PATCH 19/22] SentryCheckIn annotation and advice config for non jakarta --- .../sentry/samples/spring/boot/CustomJob.java | 4 +- .../spring/boot/SentryAutoConfiguration.java | 18 +++++ .../jakarta/checkin/SentryCheckInAdvice.java | 5 +- sentry-spring/api/sentry-spring.api | 22 ++++++ .../sentry/spring/checkin/SentryCheckIn.java | 41 ++++++++++ .../spring/checkin/SentryCheckInAdvice.java | 78 +++++++++++++++++++ .../SentryCheckInAdviceConfiguration.java | 31 ++++++++ .../SentryCheckInPointcutConfiguration.java | 29 +++++++ 8 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java create mode 100644 sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java diff --git a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java index cdebaeac1b..67a726ed42 100644 --- a/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java +++ b/sentry-samples/sentry-samples-spring-boot/src/main/java/io/sentry/samples/spring/boot/CustomJob.java @@ -1,5 +1,6 @@ package io.sentry.samples.spring.boot; +import io.sentry.spring.checkin.SentryCheckIn; import io.sentry.spring.tracing.SentryTransaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -16,7 +17,8 @@ public class CustomJob { private static final Logger LOGGER = LoggerFactory.getLogger(CustomJob.class); - @Scheduled(fixedRate = 3 * 1000L) + @Scheduled(fixedRate = 3 * 60 * 1000L) + @SentryCheckIn("monitor_slug_2") void execute() throws InterruptedException { LOGGER.info("Executing scheduled job"); Thread.sleep(2000L); diff --git a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java index 8b57a757d0..e5148c14c2 100644 --- a/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java +++ b/sentry-spring-boot/src/main/java/io/sentry/spring/boot/SentryAutoConfiguration.java @@ -22,6 +22,8 @@ import io.sentry.spring.SentryUserProvider; import io.sentry.spring.SentryWebConfiguration; import io.sentry.spring.SpringSecuritySentryUserProvider; +import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration; +import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration; import io.sentry.spring.checkin.SentryQuartzConfiguration; import io.sentry.spring.graphql.SentryGraphqlConfiguration; import io.sentry.spring.tracing.SentryAdviceConfiguration; @@ -182,6 +184,22 @@ static class GraphqlConfiguration {} }) 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) diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java index f6313bcadc..c7805bf607 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckInAdvice.java @@ -7,7 +7,6 @@ import io.sentry.IHub; import io.sentry.SentryLevel; import io.sentry.protocol.SentryId; -import io.sentry.spring.jakarta.tracing.SentryTransaction; import io.sentry.util.Objects; import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInterceptor; @@ -20,8 +19,8 @@ import org.springframework.util.ObjectUtils; /** - * Reports execution of every bean method annotated with {@link SentryTransaction} or a execution of - * a bean method within a class annotated with {@link SentryTransaction}. + * Reports execution of every bean method annotated with {@link SentryCheckIn} as a monitor + * check-in. */ @ApiStatus.Internal @ApiStatus.Experimental diff --git a/sentry-spring/api/sentry-spring.api b/sentry-spring/api/sentry-spring.api index 05a59c477f..ac4111e208 100644 --- a/sentry-spring/api/sentry-spring.api +++ b/sentry-spring/api/sentry-spring.api @@ -88,6 +88,28 @@ public final class io/sentry/spring/SpringSecuritySentryUserProvider : io/sentry public fun provideUser ()Lio/sentry/protocol/User; } +public abstract interface annotation class io/sentry/spring/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/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor { + public fun (Lio/sentry/IHub;)V + public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object; +} + +public class io/sentry/spring/checkin/SentryCheckInAdviceConfiguration { + public fun ()V + public fun sentryCheckInAdvice (Lio/sentry/IHub;)Lorg/aopalliance/aop/Advice; + public fun sentryCheckInAdvisor (Lorg/springframework/aop/Pointcut;Lorg/aopalliance/aop/Advice;)Lorg/springframework/aop/Advisor; +} + +public class io/sentry/spring/checkin/SentryCheckInPointcutConfiguration { + public fun ()V + public fun sentryCheckInPointcut ()Lorg/springframework/aop/Pointcut; +} + public class io/sentry/spring/checkin/SentryQuartzConfiguration { public fun ()V public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/autoconfigure/quartz/SchedulerFactoryBeanCustomizer; diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java new file mode 100644 index 0000000000..056a0cbcaf --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java @@ -0,0 +1,41 @@ +package io.sentry.spring.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, ElementType.TYPE}) +@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/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java new file mode 100644 index 0000000000..1f66ebae28 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdvice.java @@ -0,0 +1,78 @@ +package io.sentry.spring.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.CheckIn; +import io.sentry.CheckInStatus; +import io.sentry.DateUtils; +import io.sentry.IHub; +import io.sentry.SentryLevel; +import io.sentry.protocol.SentryId; +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.ObjectUtils; + +/** + * 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 { + private final @NotNull IHub hub; + + public SentryCheckInAdvice(final @NotNull IHub hub) { + this.hub = Objects.requireNonNull(hub, "hub is required"); + } + + @Override + public Object invoke(final @NotNull MethodInvocation invocation) throws Throwable { + final Method mostSpecificMethod = + AopUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()); + + @Nullable + SentryCheckIn checkInAnnotation = + AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCheckIn.class); + if (checkInAnnotation == null) { + return invocation.proceed(); + } + + final boolean isHeartbeatOnly = checkInAnnotation.heartbeat(); + final @Nullable String monitorSlug = checkInAnnotation.value(); + + if (ObjectUtils.isEmpty(monitorSlug)) { + hub.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(); + } + + @Nullable SentryId checkInId = null; + final long startTime = System.currentTimeMillis(); + boolean didError = false; + + try { + if (!isHeartbeatOnly) { + checkInId = hub.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)); + hub.captureCheckIn(checkIn); + } + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java new file mode 100644 index 0000000000..ec6433cee6 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInAdviceConfiguration.java @@ -0,0 +1,31 @@ +package io.sentry.spring.checkin; + +import com.jakewharton.nopen.annotation.Open; +import io.sentry.IHub; +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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInAdviceConfiguration { + + @Bean + public @NotNull Advice sentryCheckInAdvice(final @NotNull IHub hub) { + return new SentryCheckInAdvice(hub); + } + + @Bean + 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/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java new file mode 100644 index 0000000000..4d18ec4867 --- /dev/null +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckInPointcutConfiguration.java @@ -0,0 +1,29 @@ +package io.sentry.spring.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.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** AOP pointcut configuration for {@link SentryCheckIn}. */ +@Configuration(proxyBeanMethods = false) +@Open +@ApiStatus.Experimental +public class SentryCheckInPointcutConfiguration { + + /** + * Pointcut around which check-ins are created. + * + * @return pointcut used by {@link SentryCheckInAdvice}. + */ + @Bean + public @NotNull Pointcut sentryCheckInPointcut() { + return new ComposablePointcut(new AnnotationClassFilter(SentryCheckIn.class, true)) + .union(new AnnotationMatchingPointcut(null, SentryCheckIn.class)); + } +} From b5e164637682c4b228229cd5b252b50e651b20c2 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Mon, 25 Sep 2023 15:24:06 +0200 Subject: [PATCH 20/22] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 08c8000f8c..667662b829 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - Add API for sending checkins (CRONS) manually ([#2935](https://github.com/getsentry/sentry-java/pull/2935)) - 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)) +- `@SentryCheckIn` annotation and advice config for Spring ([#2946](https://github.com/getsentry/sentry-java/pull/2946)) ### Fixes From e37edb48f12b66e2e79318f5ea09aa16cbacc394 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 27 Sep 2023 07:13:03 +0200 Subject: [PATCH 21/22] code review changes --- .../spring/jakarta/checkin/SentryCheckIn.java | 2 +- .../spring/jakarta/SentryCheckInAdviceTest.kt | 173 ++++++++++++++++++ .../sentry/spring/checkin/SentryCheckIn.java | 2 +- .../sentry/spring/SentryCheckInAdviceTest.kt | 173 ++++++++++++++++++ 4 files changed, 348 insertions(+), 2 deletions(-) create mode 100644 sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt create mode 100644 sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt diff --git a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java index e823f2ef3b..a2f53da955 100644 --- a/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java +++ b/sentry-spring-jakarta/src/main/java/io/sentry/spring/jakarta/checkin/SentryCheckIn.java @@ -9,7 +9,7 @@ /** Sends a {@link io.sentry.CheckIn} for the annotated method. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD}) @ApiStatus.Experimental public @interface SentryCheckIn { diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt new file mode 100644 index 0000000000..b44555ba2f --- /dev/null +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt @@ -0,0 +1,173 @@ +package io.sentry.spring.jakarta + +import io.sentry.CheckIn +import io.sentry.CheckInStatus +import io.sentry.IHub +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 org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +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 +import kotlin.RuntimeException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentryCheckInAdviceTest.Config::class) +class SentryCheckInAdviceTest { + + @Autowired + lateinit var sampleService: SampleService + + @Autowired + lateinit var sampleServiceNoSlug: SampleServiceNoSlug + + @Autowired + lateinit var sampleServiceHeartbeat: SampleServiceHeartbeat + + @Autowired + lateinit var hub: IHub + + @BeforeTest + fun setup() { + whenever(hub.options).thenReturn(SentryOptions()) + } + + @Test + fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.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) + } + + @Test + fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.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) + } + + @Test + fun `when class 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(hub.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) + } + + @Test + fun `when class 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(hub.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) + } + + @Test + fun `when class is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceNoSlug.hello() + assertEquals(1, result) + assertEquals(0, checkInCaptor.allValues.size) + } + + @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 hub() = mock() + } + + 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") + } + } +} diff --git a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java index 056a0cbcaf..4662bc531c 100644 --- a/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java +++ b/sentry-spring/src/main/java/io/sentry/spring/checkin/SentryCheckIn.java @@ -9,7 +9,7 @@ /** Sends a {@link io.sentry.CheckIn} for the annotated method. */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.METHOD, ElementType.TYPE}) +@Target({ElementType.METHOD}) @ApiStatus.Experimental public @interface SentryCheckIn { diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt new file mode 100644 index 0000000000..ed5d9f63e5 --- /dev/null +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt @@ -0,0 +1,173 @@ +package io.sentry.spring + +import io.sentry.CheckIn +import io.sentry.CheckInStatus +import io.sentry.IHub +import io.sentry.SentryOptions +import io.sentry.protocol.SentryId +import io.sentry.spring.checkin.SentryCheckIn +import io.sentry.spring.checkin.SentryCheckInAdviceConfiguration +import io.sentry.spring.checkin.SentryCheckInPointcutConfiguration +import org.junit.jupiter.api.assertThrows +import org.junit.runner.RunWith +import org.mockito.kotlin.argumentCaptor +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 +import kotlin.RuntimeException +import kotlin.test.BeforeTest +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(SpringRunner::class) +@SpringJUnitConfig(SentryCheckInAdviceTest.Config::class) +class SentryCheckInAdviceTest { + + @Autowired + lateinit var sampleService: SampleService + + @Autowired + lateinit var sampleServiceNoSlug: SampleServiceNoSlug + + @Autowired + lateinit var sampleServiceHeartbeat: SampleServiceHeartbeat + + @Autowired + lateinit var hub: IHub + + @BeforeTest + fun setup() { + whenever(hub.options).thenReturn(SentryOptions()) + } + + @Test + fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.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) + } + + @Test + fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins error`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.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) + } + + @Test + fun `when class 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(hub.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) + } + + @Test + fun `when class 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(hub.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) + } + + @Test + fun `when class is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { + val checkInId = SentryId() + val checkInCaptor = argumentCaptor() + whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) + val result = sampleServiceNoSlug.hello() + assertEquals(1, result) + assertEquals(0, checkInCaptor.allValues.size) + } + + @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 hub() = mock() + } + + 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") + } + } +} From 81799663f9c78bc5c0c174d5edf059f8fdb3de70 Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 27 Sep 2023 08:54:41 +0200 Subject: [PATCH 22/22] Apply suggestions from code review Co-authored-by: Lukas Bloder --- .../sentry/spring/jakarta/SentryCheckInAdviceTest.kt | 10 +++++----- .../kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt | 10 +++++----- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt index b44555ba2f..06f1da81a1 100644 --- a/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt +++ b/sentry-spring-jakarta/src/test/kotlin/io/sentry/spring/jakarta/SentryCheckInAdviceTest.kt @@ -48,7 +48,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins`() { + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins`() { val checkInId = SentryId() val checkInCaptor = argumentCaptor() whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -66,7 +66,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins error`() { + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins error`() { val checkInId = SentryId() val checkInCaptor = argumentCaptor() whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -85,7 +85,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end`() { + 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(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -100,7 +100,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end with error`() { + 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(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -116,7 +116,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { + fun `when method is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { val checkInId = SentryId() val checkInCaptor = argumentCaptor() whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) diff --git a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt index ed5d9f63e5..d71ed0d61b 100644 --- a/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt +++ b/sentry-spring/src/test/kotlin/io/sentry/spring/SentryCheckInAdviceTest.kt @@ -48,7 +48,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins`() { + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins`() { val checkInId = SentryId() val checkInCaptor = argumentCaptor() whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -66,7 +66,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn, every method call creates two check-ins error`() { + fun `when method is annotated with @SentryCheckIn, every method call creates two check-ins error`() { val checkInId = SentryId() val checkInCaptor = argumentCaptor() whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -85,7 +85,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end`() { + 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(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -100,7 +100,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn and heartbeat only, every method call creates only one check-in at the end with error`() { + 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(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId) @@ -116,7 +116,7 @@ class SentryCheckInAdviceTest { } @Test - fun `when class is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { + fun `when method is annotated with @SentryCheckIn but slug is missing, does not create check-in`() { val checkInId = SentryId() val checkInCaptor = argumentCaptor() whenever(hub.captureCheckIn(checkInCaptor.capture())).thenReturn(checkInId)