Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
ea62aa1
ref(core): Add MonitorConfigUtils to convert schedules to monitor con…
wedamija Oct 2, 2026
28761ae
fix(core): Only send monitor configs Sentry reads the same way
wedamija Oct 2, 2026
bd47026
ref(core): Add MonitorConfigUtils.fromSpringScheduled
wedamija Oct 2, 2026
fca4216
fix(core): Return no config for stepped single value ranges
wedamija Oct 2, 2026
780c605
fix(core): Read Spring crons per parser and skip fixed delays
wedamija Oct 2, 2026
5d745c7
feat(spring): Send monitor config from @Scheduled with @SentryCheckIn
wedamija Oct 5, 2026
bcd5682
feat(spring): Send @Scheduled monitor config by default
wedamija Oct 5, 2026
6f5ad4e
ref(spring): Shorten upsertMonitorConfig Javadoc
wedamija Oct 5, 2026
d758789
fix(core): Keep Sunday-only day of week ranges
wedamija Oct 6, 2026
d903a79
fix(spring): Only catch runtime exceptions when deriving monitor config
wedamija Oct 6, 2026
5db58a4
ref(spring): Shorten upsertMonitorConfig Javadoc
wedamija Oct 15, 2026
1ca0891
feat(spring-jakarta): Monitor all @Scheduled methods with sentry.moni…
wedamija Oct 17, 2026
358f88a
changelog
wedamija Oct 17, 2026
e20963e
feat(spring): Monitor all @Scheduled methods in Spring Boot 2
wedamija Oct 17, 2026
7b150ae
docs(changelog): Link Spring Boot 2 PR
wedamija Oct 17, 2026
fcd00c7
feat(spring): Monitor all @Scheduled methods in Spring Boot 4
wedamija Oct 17, 2026
b90f364
docs(changelog): Link Spring Boot 4 PR
wedamija Oct 17, 2026
552de8f
fix(spring-jakarta): Skip reactive @Scheduled methods when monitoring…
wedamija Oct 20, 2026
270d4bf
Merge branch 'danf/spring-monitor-scheduled-tasks' into danf/spring-m…
wedamija Oct 20, 2026
6261137
fix(spring): Skip reactive @Scheduled methods when monitoring all tasks
wedamija Oct 20, 2026
e922106
Merge branch 'danf/spring-monitor-scheduled-tasks-boot2' into danf/sp…
wedamija Oct 20, 2026
aa3995d
fix(spring-7): Skip reactive @Scheduled methods when monitoring all t…
wedamija Oct 20, 2026
26054cd
fix(spring): Skip jobs with unsupported schedules when monitoring all…
wedamija Oct 29, 2026
38bce82
Merge remote-tracking branch 'origin/danf/spring-monitor-scheduled-ta…
wedamija Oct 29, 2026
7baac07
fix(spring): Skip jobs with unsupported schedules when monitoring all…
wedamija Oct 29, 2026
647f38c
Merge branch 'danf-skip-tmp-6276' into danf-skip-tmp-6277
wedamija Oct 29, 2026
99505ac
fix(spring): Skip jobs with unsupported schedules when monitoring all…
wedamija Oct 29, 2026
408c89a
Merge remote-tracking branch 'origin/main' into danf/monitor-config-u…
wedamija Oct 29, 2026
4c08ff0
Merge branch 'danf/monitor-config-utils' into danf/spring-checkin-sch…
wedamija Oct 29, 2026
b63d62e
Merge branch 'danf/spring-checkin-scheduled-config' into danf/spring-…
wedamija Oct 29, 2026
40a2789
Merge branch 'danf/spring-monitor-scheduled-tasks' into danf/spring-m…
wedamija Oct 29, 2026
d242a14
Merge branch 'danf/spring-monitor-scheduled-tasks-boot2' into danf/sp…
wedamija Oct 29, 2026
1e7aa24
ref(spring): Use pattern-matching instanceof for the @Scheduled time …
wedamija Oct 29, 2026
e22b15e
Merge branch 'danf/spring-checkin-scheduled-config' into danf/spring-…
wedamija Oct 29, 2026
bf12297
Merge branch 'danf/spring-monitor-scheduled-tasks' into danf/spring-m…
wedamija Oct 29, 2026
dc71f65
Merge branch 'danf/spring-monitor-scheduled-tasks-boot2' into danf/sp…
wedamija Oct 29, 2026
5b38d5e
fix(spring): Satisfy NullAway in the Spring 7 scheduled check-in advice
wedamija Oct 29, 2026
0b9b852
fix(core): Use a 2 minute check-in margin for crons with fixed seconds
wedamija Nov 5, 2026
ae5ae63
Merge branch 'danf/monitor-config-utils' into danf/spring-checkin-sch…
wedamija Nov 5, 2026
129cb73
fix(spring): Default upsertMonitorConfig to false and read @Scheduled…
wedamija Nov 5, 2026
7683e57
Merge branch 'danf/spring-checkin-scheduled-config' into danf/spring-…
wedamija Nov 5, 2026
a496af8
fix(spring): Find @Scheduled tasks to monitor the way Spring does
wedamija Nov 5, 2026
e67963b
Merge branch 'danf/spring-monitor-scheduled-tasks' into danf/spring-m…
wedamija Nov 5, 2026
2f0e285
fix(spring): Find @Scheduled tasks to monitor the way Spring does
wedamija Nov 5, 2026
b6d56b2
Merge branch 'danf/spring-monitor-scheduled-tasks-boot2' into danf/sp…
wedamija Nov 5, 2026
f734070
fix(spring): Find @Scheduled tasks to monitor the way Spring does
wedamija Nov 5, 2026
2789b4c
Merge remote-tracking branch 'origin/main' into danf/spring-monitor-s…
wedamija Nov 5, 2026
d9ba607
feat(spring): Warn when two @Scheduled methods share a monitor slug
wedamija Nov 5, 2026
0b6e26c
Merge branch 'danf/spring-monitor-scheduled-tasks' into danf/spring-m…
wedamija Nov 5, 2026
f96738e
feat(spring): Warn when two @Scheduled methods share a monitor slug
wedamija Nov 5, 2026
4ea5c65
Merge branch 'danf/spring-monitor-scheduled-tasks-boot2' into danf/sp…
wedamija Nov 5, 2026
cc9e28a
feat(spring): Warn when two @Scheduled methods share a monitor slug
wedamija Nov 5, 2026
09e7160
ref(spring): Reuse the target class for the slug collision check
wedamija Nov 5, 2026
70c3f22
Merge branch 'danf/spring-monitor-scheduled-tasks' into danf/spring-m…
wedamija Nov 5, 2026
233f5b0
ref(spring): Reuse the target class for the slug collision check
wedamija Nov 5, 2026
480ea17
Merge branch 'danf/spring-monitor-scheduled-tasks-boot2' into danf/sp…
wedamija Nov 5, 2026
2a61e06
fix(spring): Skip private and static @Scheduled methods when monitori…
wedamija Nov 5, 2026
cd0a3ae
Merge branch 'danf/spring-monitor-scheduled-tasks' into danf/spring-m…
wedamija Nov 5, 2026
8ae1938
fix(spring): Skip private and static @Scheduled methods when monitori…
wedamija Nov 5, 2026
386f0d4
Merge branch 'danf/spring-monitor-scheduled-tasks-boot2' into danf/sp…
wedamija Nov 5, 2026
4484aa3
fix(spring): Skip private and static @Scheduled methods when monitori…
wedamija Nov 5, 2026
910cfbf
Merge commit 'refs/s2/main' into danf/spring-monitor-scheduled-tasks-…
wedamija Nov 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

### Features

- Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. Sentry creates each monitor from the method's `@Scheduled` schedule. Exclude methods with `sentry.exclude-scheduled-tasks` ([#6275](https://github.com/getsentry/sentry-java/pull/6275), [#6276](https://github.com/getsentry/sentry-java/pull/6276))
- Monitor every Spring `@Scheduled` method with Sentry Crons by setting `sentry.monitor-scheduled-tasks=true`, without adding `@SentryCheckIn` to each method. Sentry creates each monitor from the method's `@Scheduled` schedule. Exclude methods with `sentry.exclude-scheduled-tasks` ([#6275](https://github.com/getsentry/sentry-java/pull/6275), [#6276](https://github.com/getsentry/sentry-java/pull/6276), [#6277](https://github.com/getsentry/sentry-java/pull/6277))
- Quartz `SentryJobListener` can send a monitor config derived from the trigger with check-ins, so Sentry creates or updates the monitor from code. Set the `sentry-upsert-monitor-config` job or trigger data key to `"true"` to send the schedule ([#6216](https://github.com/getsentry/sentry-java/pull/6216))
- Send a monitor config derived from Spring `@Scheduled` with `@SentryCheckIn` check-ins, so Sentry creates or updates the monitor from code. Set `@SentryCheckIn(upsertMonitorConfig = true)` to send the schedule ([#6215](https://github.com/getsentry/sentry-java/pull/6215))

Expand Down
6 changes: 6 additions & 0 deletions sentry-spring-7/api/sentry-spring-7.api
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ public abstract interface annotation class io/sentry/spring7/checkin/SentryCheck
public class io/sentry/spring7/checkin/SentryCheckInAdvice : org/aopalliance/intercept/MethodInterceptor, org/springframework/context/EmbeddedValueResolverAware {
public fun <init> ()V
public fun <init> (Lio/sentry/IScopes;)V
public fun <init> (Lio/sentry/IScopes;Z)V
public fun invoke (Lorg/aopalliance/intercept/MethodInvocation;)Ljava/lang/Object;
public fun setEmbeddedValueResolver (Lorg/springframework/util/StringValueResolver;)V
}
Expand All @@ -168,6 +169,11 @@ public class io/sentry/spring7/checkin/SentryQuartzConfiguration {
public fun schedulerFactoryBeanCustomizer ()Lorg/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer;
}

public final class io/sentry/spring7/checkin/SentryScheduledCheckInPointcut : org/springframework/aop/support/StaticMethodMatcherPointcut {
public fun <init> (Ljava/util/List;)V
public fun matches (Ljava/lang/reflect/Method;Ljava/lang/Class;)Z
}

public final class io/sentry/spring7/checkin/SentrySchedulerFactoryBeanCustomizer : org/springframework/boot/quartz/autoconfigure/SchedulerFactoryBeanCustomizer {
public fun <init> ()V
public fun customize (Lorg/springframework/scheduling/quartz/SchedulerFactoryBean;)V
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@
import io.sentry.util.Objects;
import io.sentry.util.TracingUtils;
import java.lang.reflect.Method;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.format.DateTimeParseException;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.jetbrains.annotations.ApiStatus;
Expand Down Expand Up @@ -49,19 +53,38 @@ public class SentryCheckInAdvice implements MethodInterceptor, EmbeddedValueReso
"org.springframework.scheduling.support.CronExpression",
SentryCheckInAdvice.class.getClassLoader());

private static final @NotNull Pattern WORD_BOUNDARY =
Pattern.compile("(?<=[a-z0-9])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])");
private static final int MAX_SLUG_LENGTH = 50;

private final @NotNull IScopes scopes;

private final boolean monitorScheduledTasks;

private @Nullable StringValueResolver resolver;

private final @NotNull Map<Method, CachedMonitorConfig> monitorConfigs =
new ConcurrentHashMap<>();

private final @NotNull Map<String, String> derivedSlugOwners = new ConcurrentHashMap<>();

private final @NotNull Set<String> collidingSlugs = ConcurrentHashMap.newKeySet();

public SentryCheckInAdvice() {
this(ScopesAdapter.getInstance());
}

public SentryCheckInAdvice(final @NotNull IScopes scopes) {
this(scopes, false);
}

/**
* @param monitorScheduledTasks whether to also monitor {@code @Scheduled} methods without {@link
* SentryCheckIn}, using a slug derived from the class and method name
*/
public SentryCheckInAdvice(final @NotNull IScopes scopes, final boolean monitorScheduledTasks) {
this.scopes = Objects.requireNonNull(scopes, "scopes are required");
this.monitorScheduledTasks = monitorScheduledTasks;
}

@Override
Expand All @@ -75,6 +98,25 @@ public SentryCheckInAdvice(final @NotNull IScopes scopes) {
SentryCheckIn checkInAnnotation =
AnnotationUtils.findAnnotation(mostSpecificMethod, SentryCheckIn.class);
if (checkInAnnotation == null) {
if (monitorScheduledTasks && isScheduled(mostSpecificMethod)) {
final @NotNull Class<?> targetClass =
target != null ? target.getClass() : mostSpecificMethod.getDeclaringClass();
final @NotNull String taskName = scheduledTaskName(targetClass, mostSpecificMethod);
final @Nullable MonitorConfig monitorConfig = monitorConfig(mostSpecificMethod);
if (monitorConfig == null) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.DEBUG,
"Not monitoring %s because Sentry Crons can't express its @Scheduled schedule.",
taskName);
return invocation.proceed();
}
final @NotNull String monitorSlug = scheduledTaskSlug(taskName);
warnOnSlugCollision(monitorSlug, targetClass, mostSpecificMethod);
return captureCheckIns(invocation, monitorSlug, false, monitorConfig);
}
return invocation.proceed();
}

Expand Down Expand Up @@ -114,7 +156,15 @@ public SentryCheckInAdvice(final @NotNull IScopes scopes) {
!isHeartbeatOnly && checkInAnnotation.upsertMonitorConfig()
? monitorConfig(mostSpecificMethod)
: null;
return captureCheckIns(invocation, monitorSlug, isHeartbeatOnly, monitorConfig);
}

private @Nullable Object captureCheckIns(
final @NotNull MethodInvocation invocation,
final @NotNull String monitorSlug,
final boolean isHeartbeatOnly,
final @Nullable MonitorConfig monitorConfig)
throws Throwable {
try (final @NotNull ISentryLifecycleToken ignored =
scopes.forkedScopes("SentryCheckInAdvice").makeCurrent()) {
TracingUtils.startNewTrace(scopes);
Expand Down Expand Up @@ -143,6 +193,58 @@ public SentryCheckInAdvice(final @NotNull IScopes scopes) {
}
}

private void warnOnSlugCollision(
final @NotNull String monitorSlug,
final @NotNull Class<?> targetClass,
final @NotNull Method method) {
final @NotNull String identity =
ClassUtils.getUserClass(targetClass).getName() + "." + method.getName();
final @Nullable String owner = derivedSlugOwners.putIfAbsent(monitorSlug, identity);
if (owner != null && !owner.equals(identity) && collidingSlugs.add(monitorSlug)) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.WARNING,
"Monitor slug \"%s\" is used by both %s and %s, so their check-ins go to one monitor. Add @SentryCheckIn(\"your-slug\") to one of them.",
monitorSlug,
owner,
identity);
}
}

static boolean isScheduled(final @NotNull Method method) {
return !AnnotatedElementUtils.getMergedRepeatableAnnotations(
method, Scheduled.class, Schedules.class)
.isEmpty();
}

/** {@code ClassName.methodName}, with nested classes as {@code Outer.Inner}. */
static @NotNull String scheduledTaskName(
final @NotNull Class<?> targetClass, final @NotNull Method method) {
return ClassUtils.getShortName(ClassUtils.getUserClass(targetClass)) + "." + method.getName();
Comment thread
wedamija marked this conversation as resolved.
Resolved
}

static @NotNull String scheduledTaskSlug(final @NotNull String taskName) {
final @NotNull String slug =
WORD_BOUNDARY
.matcher(taskName)
.replaceAll("-")
.toLowerCase(Locale.ROOT)
.replaceAll("[^a-z0-9]+", "-")
.replaceAll("^-+|-+$", "");
if (slug.length() <= MAX_SLUG_LENGTH) {
return slug;
}
// keep truncated slugs unique
final @NotNull CRC32 crc = new CRC32();
crc.update(slug.getBytes(StandardCharsets.UTF_8));
final @NotNull String hash = String.format(Locale.ROOT, "%08x", crc.getValue());
return slug.substring(0, MAX_SLUG_LENGTH - hash.length() - 1).replaceAll("-+$", "")
+ "-"
+ hash;
}

private @Nullable MonitorConfig monitorConfig(final @NotNull Method method) {
return monitorConfigs.computeIfAbsent(
method, key -> new CachedMonitorConfig(monitorConfigFromScheduled(key)))
Expand Down Expand Up @@ -177,10 +279,7 @@ public SentryCheckInAdvice(final @NotNull IScopes scopes) {
scopes
.getOptions()
.getLogger()
.log(
SentryLevel.WARNING,
"Could not derive a monitor config from @Scheduled for method annotated with @SentryCheckIn.",
e);
.log(SentryLevel.WARNING, "Could not derive a monitor config from @Scheduled.", e);
return null;
}
}
Expand Down Expand Up @@ -211,7 +310,7 @@ public SentryCheckInAdvice(final @NotNull IScopes scopes) {
.getLogger()
.log(
SentryLevel.DEBUG,
"Not sending a monitor config for @SentryCheckIn because the @Scheduled period '%s' could not be parsed.",
"Not sending a monitor config because the @Scheduled period '%s' could not be parsed.",
trimmed);
}
return millis;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package io.sentry.spring7.checkin;

import io.sentry.FilterString;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.List;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.springframework.aop.support.AopUtils;
import org.springframework.aop.support.StaticMethodMatcherPointcut;
import org.springframework.core.ReactiveAdapterRegistry;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.util.ClassUtils;

/**
* Matches {@code @Scheduled} methods without {@link SentryCheckIn}, except excluded ones. Excludes
* match the task name ({@code ClassName.methodName}) exactly or as a regex.
*/
@ApiStatus.Internal
public final class SentryScheduledCheckInPointcut extends StaticMethodMatcherPointcut {
private final @NotNull List<FilterString> excludedScheduledTasks = new ArrayList<>();

public SentryScheduledCheckInPointcut(final @Nullable List<String> excludedScheduledTasks) {
if (excludedScheduledTasks != null) {
for (final @NotNull String task : excludedScheduledTasks) {
if (!task.trim().isEmpty()) {
this.excludedScheduledTasks.add(new FilterString(task.trim()));
}
}
}
}

@Override
public boolean matches(final @NotNull Method method, final @NotNull Class<?> targetClass) {
final @NotNull Class<?> userClass = ClassUtils.getUserClass(targetClass);
// CGLIB can't intercept these, and Spring calls private and static methods on the target
final int modifiers = method.getModifiers();
if (Modifier.isFinal(userClass.getModifiers())
|| Modifier.isFinal(modifiers)
|| Modifier.isPrivate(modifiers)
|| Modifier.isStatic(modifiers)) {
return false;
}
final @NotNull Method specificMethod = AopUtils.getMostSpecificMethod(method, userClass);
if (!SentryCheckInAdvice.isScheduled(specificMethod)
|| AnnotationUtils.findAnnotation(specificMethod, SentryCheckIn.class) != null) {
return false;
}
// Spring calls reactive and suspend methods once and subscribes to the result on each run
if (specificMethod.getParameterCount() > 0
|| ReactiveAdapterRegistry.getSharedInstance().getAdapter(specificMethod.getReturnType())
!= null) {
return false;
}
final @NotNull String taskName =
SentryCheckInAdvice.scheduledTaskName(userClass, specificMethod);
for (final @NotNull FilterString excluded : excludedScheduledTasks) {
if (excluded.getFilterString().equals(taskName) || excluded.matches(taskName)) {
return false;
}
}
return true;
}
}
Loading
Loading