From 1863883c3124c6c361cee99c542a7be4a744c88a Mon Sep 17 00:00:00 2001 From: Alexander Dinauer Date: Wed, 28 Oct 2026 13:44:22 +0100 Subject: [PATCH] feat(core): Add event processor fork semantics Allow stateful event processors to create independent instances when scopes are cloned while preserving existing sharing behavior by default. Reuse the original processor on recoverable extension failures so scope cloning remains host-safe. Refs GH-6365 Co-Authored-By: Claude --- sentry/api/sentry.api | 2 + .../main/java/io/sentry/EventProcessor.java | 13 +++ sentry/src/main/java/io/sentry/Scope.java | 5 +- .../EventProcessorAndOrder.java | 30 ++++- .../java/io/sentry/CombinedScopeViewTest.kt | 32 +++++ sentry/src/test/java/io/sentry/ScopeTest.kt | 109 ++++++++++++++++++ 6 files changed, 186 insertions(+), 5 deletions(-) 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 } + } + } }