diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 2890298efb5..ae96a1abe45 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -532,6 +532,7 @@ public final class io/sentry/EnvelopeSender : io/sentry/IEnvelopeSender { } public abstract interface class io/sentry/EventProcessor { + public fun fork ()Lio/sentry/EventProcessor; public fun getOrder ()Ljava/lang/Long; public fun process (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent; public fun process (Lio/sentry/SentryLogEvent;)Lio/sentry/SentryLogEvent; @@ -5457,6 +5458,7 @@ public final class io/sentry/internal/eventprocessor/EventProcessorAndOrder : ja public fun (Lio/sentry/EventProcessor;Ljava/lang/Long;)V public fun compareTo (Lio/sentry/internal/eventprocessor/EventProcessorAndOrder;)I public synthetic fun compareTo (Ljava/lang/Object;)I + public fun fork (Lio/sentry/ILogger;)Lio/sentry/internal/eventprocessor/EventProcessorAndOrder; public fun getEventProcessor ()Lio/sentry/EventProcessor; public fun getOrder ()Ljava/lang/Long; } diff --git a/sentry/src/main/java/io/sentry/EventProcessor.java b/sentry/src/main/java/io/sentry/EventProcessor.java index 928fd022095..6c11b97c976 100644 --- a/sentry/src/main/java/io/sentry/EventProcessor.java +++ b/sentry/src/main/java/io/sentry/EventProcessor.java @@ -67,6 +67,19 @@ default SentryMetricsEvent process(@NotNull SentryMetricsEvent event, @NotNull H return event; } + /** + * Creates the processor instance to use when the SDK clones a scope. + * + *

The default implementation returns this processor so stateless processors remain shared. + * Stateful processors may return a new instance that preserves their configuration while + * resetting scope-local state. + * + * @return the processor to attach to the cloned scope + */ + default @NotNull EventProcessor fork() { + return this; + } + /** * Controls when this EventProcessor is invoked. * diff --git a/sentry/src/main/java/io/sentry/Scope.java b/sentry/src/main/java/io/sentry/Scope.java index f4ba9dbbb9c..abad6f90816 100644 --- a/sentry/src/main/java/io/sentry/Scope.java +++ b/sentry/src/main/java/io/sentry/Scope.java @@ -148,7 +148,10 @@ private Scope(final @NotNull Scope scope) { this.request = requestRef != null ? new Request(requestRef) : null; this.fingerprint = new ArrayList<>(scope.fingerprint); - this.eventProcessors = new CopyOnWriteArrayList<>(scope.eventProcessors); + this.eventProcessors = new CopyOnWriteArrayList<>(); + for (final EventProcessorAndOrder processor : scope.eventProcessors) { + this.eventProcessors.add(processor.fork(options.getLogger())); + } final Breadcrumb[] breadcrumbsRef = scope.breadcrumbs.toArray(new Breadcrumb[0]); diff --git a/sentry/src/main/java/io/sentry/internal/eventprocessor/EventProcessorAndOrder.java b/sentry/src/main/java/io/sentry/internal/eventprocessor/EventProcessorAndOrder.java index 38ff7802f58..87d15b0d388 100644 --- a/sentry/src/main/java/io/sentry/internal/eventprocessor/EventProcessorAndOrder.java +++ b/sentry/src/main/java/io/sentry/internal/eventprocessor/EventProcessorAndOrder.java @@ -1,6 +1,9 @@ package io.sentry.internal.eventprocessor; import io.sentry.EventProcessor; +import io.sentry.ILogger; +import io.sentry.SentryLevel; +import io.sentry.util.ExceptionUtils; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -11,12 +14,31 @@ public final class EventProcessorAndOrder implements Comparable() + val options = + SentryOptions().apply { + isDebug = true + setLogger(logger) + } + val processor = mock() + val scope = Scope(options) + scope.addEventProcessor(processor) + val originalOrder = scope.eventProcessorsWithOrder.single().order + + val clone = scope.clone() + + assertSame(processor, clone.eventProcessors.single()) + assertEquals(originalOrder, clone.eventProcessorsWithOrder.single().order) + verify(logger) + .log( + eq(SentryLevel.ERROR), + eq("EventProcessor.fork() returned null; reusing the original processor."), + ) + } + + @Test + fun `copying scope reuses processor when fork fails`() { + val logger = mock() + val options = + SentryOptions().apply { + isDebug = true + setLogger(logger) + } + val exception = IllegalStateException("failed") + val processor = + object : EventProcessor { + override fun fork(): EventProcessor = throw exception + } + val scope = Scope(options) + scope.addEventProcessor(processor) + val originalOrder = scope.eventProcessorsWithOrder.single().order + + val clone = scope.clone() + + assertSame(processor, clone.eventProcessors.single()) + assertEquals(originalOrder, clone.eventProcessorsWithOrder.single().order) + verify(logger) + .log( + eq(SentryLevel.ERROR), + eq("EventProcessor.fork() failed; reusing the original processor."), + eq(exception), + ) + } + + @Test + fun `copying scope rethrows fatal processor fork failures`() { + val error = OutOfMemoryError("fatal") + val scope = Scope(SentryOptions()) + scope.addEventProcessor( + object : EventProcessor { + override fun fork(): EventProcessor = throw error + } + ) + + val thrown = assertFailsWith { scope.clone() } + + assertSame(error, thrown) + } + @Test fun `Scope starts a new session with release, env and user`() { val options = SentryOptions().apply { distinctId = "123" } @@ -1399,4 +1496,16 @@ class ScopeTest { object : EventProcessor { override fun process(event: SentryEvent, hint: Hint): SentryEvent? = event } + + private class ForkingEventProcessor(private val order: Long?) : EventProcessor { + var forkInvocations = 0 + var forkedProcessor: EventProcessor? = null + + override fun getOrder(): Long? = order + + override fun fork(): EventProcessor { + forkInvocations++ + return ForkingEventProcessor(order).also { forkedProcessor = it } + } + } }