Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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: 2 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -5457,6 +5458,7 @@ public final class io/sentry/internal/eventprocessor/EventProcessorAndOrder : ja
public fun <init> (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;
}
Expand Down
13 changes: 13 additions & 0 deletions sentry/src/main/java/io/sentry/EventProcessor.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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.
*
Expand Down
5 changes: 4 additions & 1 deletion sentry/src/main/java/io/sentry/Scope.java
Original file line number Diff line number Diff line change
Expand Up @@ -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]);

Expand Down
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -11,12 +14,31 @@ public final class EventProcessorAndOrder implements Comparable<EventProcessorAn

public EventProcessorAndOrder(
final @NotNull EventProcessor eventProcessor, final @Nullable Long order) {
this(eventProcessor, order != null ? order : System.nanoTime());
}

private EventProcessorAndOrder(final @NotNull EventProcessor eventProcessor, final long order) {
this.eventProcessor = eventProcessor;
if (order == null) {
this.order = System.nanoTime();
} else {
this.order = order;
this.order = order;
}

public @NotNull EventProcessorAndOrder fork(final @NotNull ILogger logger) {
EventProcessor forkedProcessor = eventProcessor;
try {
final @Nullable EventProcessor candidate = eventProcessor.fork();
if (candidate != null) {
forkedProcessor = candidate;
} else {
logger.log(
SentryLevel.ERROR,
"EventProcessor.fork() returned null; reusing the original processor.");
}
} catch (Throwable t) {
ExceptionUtils.rethrowIfFatal(t);
logger.log(
SentryLevel.ERROR, "EventProcessor.fork() failed; reusing the original processor.", t);
}
return new EventProcessorAndOrder(forkedProcessor, order);
}

public @NotNull EventProcessor getEventProcessor() {
Expand Down
32 changes: 32 additions & 0 deletions sentry/src/test/java/io/sentry/CombinedScopeViewTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,26 @@ class CombinedScopeViewTest {
assertEquals(fourth, eventProcessors[3])
}

@Test
fun `clone forks processors on current and isolation scopes`() {
val combined = fixture.getSut()
val globalProcessor =
ForkingTestEventProcessor(0).also { fixture.globalScope.addEventProcessor(it) }
val isolationProcessor =
ForkingTestEventProcessor(1).also { fixture.isolationScope.addEventProcessor(it) }
val currentProcessor = ForkingTestEventProcessor(2).also { fixture.scope.addEventProcessor(it) }

val clone = combined.clone()
val clonedProcessors = clone.eventProcessors

assertEquals(0, globalProcessor.forkInvocations)
assertEquals(1, isolationProcessor.forkInvocations)
assertEquals(1, currentProcessor.forkInvocations)
assertSame(globalProcessor, clonedProcessors[0])
assertSame(isolationProcessor.forkedProcessor, clonedProcessors[1])
assertSame(currentProcessor.forkedProcessor, clonedProcessors[2])
}

@Test
fun `adds event processor to default scope`() {
val combined = fixture.getSut()
Expand Down Expand Up @@ -1401,4 +1421,16 @@ class CombinedScopeViewTest {
class TestEventProcessor(val orderNumber: Long?) : EventProcessor {
override fun getOrder() = orderNumber
}

private class ForkingTestEventProcessor(private val order: Long) : EventProcessor {
var forkInvocations = 0
var forkedProcessor: EventProcessor? = null

override fun getOrder(): Long = order

override fun fork(): EventProcessor {
forkInvocations++
return ForkingTestEventProcessor(order).also { forkedProcessor = it }
}
}
}
109 changes: 109 additions & 0 deletions sentry/src/test/java/io/sentry/ScopeTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import io.sentry.test.callMethod
import java.util.concurrent.CopyOnWriteArrayList
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith
import kotlin.test.assertFalse
import kotlin.test.assertNotNull
import kotlin.test.assertNotSame
Expand Down Expand Up @@ -477,6 +478,102 @@ class ScopeTest {
assertEquals(processor, scope.eventProcessors.first())
}

@Test
fun `event processor fork defaults to the same instance`() {
val processor = CustomEventProcessor()

assertSame(processor, processor.fork())
}

@Test
fun `copying scope forks each event processor once and preserves order`() {
val scope = Scope(SentryOptions())
val first = ForkingEventProcessor(null)
val second = ForkingEventProcessor(10)
scope.addEventProcessor(first)
scope.addEventProcessor(second)
val originalProcessors = scope.eventProcessorsWithOrder.toList()

val clone = scope.clone()
val clonedProcessors = clone.eventProcessorsWithOrder

assertEquals(1, first.forkInvocations)
assertEquals(1, second.forkInvocations)
assertEquals(2, clonedProcessors.size)
assertSame(first.forkedProcessor, clonedProcessors[0].eventProcessor)
assertSame(second.forkedProcessor, clonedProcessors[1].eventProcessor)
assertEquals(originalProcessors[0].order, clonedProcessors[0].order)
assertEquals(originalProcessors[1].order, clonedProcessors[1].order)
}

@Test
fun `copying scope reuses processor when fork returns null`() {
val logger = mock<ILogger>()
val options =
SentryOptions().apply {
isDebug = true
setLogger(logger)
}
val processor = mock<EventProcessor>()
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<ILogger>()
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<OutOfMemoryError> { scope.clone() }

assertSame(error, thrown)
}

@Test
fun `Scope starts a new session with release, env and user`() {
val options = SentryOptions().apply { distinctId = "123" }
Expand Down Expand Up @@ -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 }
}
}
}
Loading