Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

- Android 15: Add support for 16KB page sizes ([#3620](https://github.com/getsentry/sentry-java/pull/3620))
- See https://developer.android.com/guide/practices/page-sizes for more details
- Session Replay: Add `beforeSendReplay` callback ([#3855](https://github.com/getsentry/sentry-java/pull/3855))

### Fixes

Expand Down
6 changes: 6 additions & 0 deletions sentry/api/sentry.api
Original file line number Diff line number Diff line change
Expand Up @@ -2372,6 +2372,7 @@ public class io/sentry/SentryOptions {
public fun getBeforeEmitMetricCallback ()Lio/sentry/SentryOptions$BeforeEmitMetricCallback;
public fun getBeforeEnvelopeCallback ()Lio/sentry/SentryOptions$BeforeEnvelopeCallback;
public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback;
public fun getBeforeSendReplay ()Lio/sentry/SentryOptions$BeforeSendReplayCallback;
public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback;
public fun getBundleIds ()Ljava/util/Set;
public fun getCacheDirPath ()Ljava/lang/String;
Expand Down Expand Up @@ -2487,6 +2488,7 @@ public class io/sentry/SentryOptions {
public fun setBeforeEmitMetricCallback (Lio/sentry/SentryOptions$BeforeEmitMetricCallback;)V
public fun setBeforeEnvelopeCallback (Lio/sentry/SentryOptions$BeforeEnvelopeCallback;)V
public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V
public fun setBeforeSendReplay (Lio/sentry/SentryOptions$BeforeSendReplayCallback;)V
public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V
public fun setCacheDirPath (Ljava/lang/String;)V
public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V
Expand Down Expand Up @@ -2593,6 +2595,10 @@ public abstract interface class io/sentry/SentryOptions$BeforeSendCallback {
public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
}

public abstract interface class io/sentry/SentryOptions$BeforeSendReplayCallback {
public abstract fun execute (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent;
}

public abstract interface class io/sentry/SentryOptions$BeforeSendTransactionCallback {
public abstract fun execute (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
}
Expand Down
33 changes: 32 additions & 1 deletion sentry/src/main/java/io/sentry/SentryClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -285,8 +285,18 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin

event = processReplayEvent(event, hint, options.getEventProcessors());

if (event != null) {
event = executeBeforeSendReplay(event, hint);

if (event == null) {
options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by beforeSendReplay");
options
.getClientReportRecorder()
.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Replay);
}
}

if (event == null) {
options.getLogger().log(SentryLevel.DEBUG, "Replay was dropped by Event processors.");
return SentryId.EMPTY_ID;
}

Expand Down Expand Up @@ -1126,6 +1136,27 @@ private void sortBreadcrumbsByDate(
return transaction;
}

private @Nullable SentryReplayEvent executeBeforeSendReplay(
@NotNull SentryReplayEvent event, final @NotNull Hint hint) {
final SentryOptions.BeforeSendReplayCallback beforeSendReplay = options.getBeforeSendReplay();
if (beforeSendReplay != null) {
try {
event = beforeSendReplay.execute(event, hint);
} catch (Throwable e) {
options
.getLogger()
.log(
SentryLevel.ERROR,
"The BeforeSendReplay callback threw an exception. It will be added as breadcrumb and continue.",
e);

// drop event in case of an error in beforeSend due to PII concerns
event = null;
}
}
return event;
}

@Override
public void close() {
close(false);
Expand Down
41 changes: 41 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,12 @@ public class SentryOptions {
*/
private @Nullable BeforeSendTransactionCallback beforeSendTransaction;

/**
* This function is called with an SDK specific replay object and can return a modified replay
* object or nothing to skip reporting the replay
*/
private @Nullable BeforeSendReplayCallback beforeSendReplay;

/**
* This function is called with an SDK specific breadcrumb object before the breadcrumb is added
* to the scope. When nothing is returned from the function, the breadcrumb is dropped
Expand Down Expand Up @@ -761,6 +767,24 @@ public void setBeforeSendTransaction(
this.beforeSendTransaction = beforeSendTransaction;
}

/**
* Returns the BeforeSendReplay callback
*
* @return the beforeSend callback or null if not set
*/
public @Nullable BeforeSendReplayCallback getBeforeSendReplay() {
return beforeSendReplay;
}

/**
* Sets the beforeSendReplay callback
*
* @param beforeSendReplay the beforeSend callback
*/
public void setBeforeSendReplay(@Nullable BeforeSendReplayCallback beforeSendReplay) {
this.beforeSendReplay = beforeSendReplay;
}

/**
* Returns the beforeBreadcrumb callback
*
Expand Down Expand Up @@ -2493,6 +2517,23 @@ public interface BeforeSendTransactionCallback {
SentryTransaction execute(@NotNull SentryTransaction transaction, @NotNull Hint hint);
}

/** The BeforeSendReplay callback */
public interface BeforeSendReplayCallback {

/**
* Mutate or drop a replay event before being sent. Note that there might be many replay events
* for a single replay (i.e. segments), you can check {@link SentryReplayEvent#getReplayId()} to
* identify that the segments belong to the same replay.
*
* @param event the event
* @param hint the hint, contains {@link ReplayRecording}, can be accessed via {@link
* Hint#getReplayRecording()}
* @return the original event or the mutated event or null if event was dropped
*/
@Nullable
SentryReplayEvent execute(@NotNull SentryReplayEvent event, @NotNull Hint hint);
}

/** The BeforeBreadcrumb callback */
public interface BeforeBreadcrumbCallback {

Expand Down
81 changes: 81 additions & 0 deletions sentry/src/test/java/io/sentry/SentryClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2800,6 +2800,68 @@ class SentryClientTest {
assertFalse(called)
}

@Test
fun `when beforeSendReplay is set, callback is invoked`() {
var invoked = false
fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> invoked = true; replay }

fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())

assertTrue(invoked)
}

@Test
fun `when beforeSendReplay returns null, event is dropped`() {
fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> null }

fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())

verify(fixture.transport, never()).send(any(), anyOrNull())

assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)
)
)
}

@Test
fun `when beforeSendReplay returns new instance, new instance is sent`() {
val expected = SentryReplayEvent().apply { tags = mapOf("test" to "test") }
fixture.sentryOptions.setBeforeSendReplay { _, _ -> expected }

fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())

verify(fixture.transport).send(
check {
val replay = getReplayFromData(it.items.first().data)
assertEquals("test", replay!!.tags!!["test"])
},
anyOrNull()
)
verifyNoMoreInteractions(fixture.transport)
}

@Test
fun `when beforeSendReplay throws an exception, replay is dropped`() {
val exception = Exception("test")

exception.stackTrace.toString()
fixture.sentryOptions.setBeforeSendReplay { _, _ -> throw exception }

val id = fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())

assertEquals(SentryId.EMPTY_ID, id)

assertClientReport(
fixture.sentryOptions.clientReportRecorder,
listOf(
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)
)
)
}

private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): IScope {
val scope = createScope(fixture.sentryOptions)
scope.startSession()
Expand Down Expand Up @@ -2977,6 +3039,25 @@ class SentryClientTest {
)!!
}

private fun getReplayFromData(data: ByteArray): SentryReplayEvent? {
val unpacker = MessagePack.newDefaultUnpacker(data)
val mapSize = unpacker.unpackMapHeader()
for (i in 0 until mapSize) {
val key = unpacker.unpackString()
when (key) {
SentryItemType.ReplayEvent.itemType -> {
val replayEventLength = unpacker.unpackBinaryHeader()
val replayEventBytes = unpacker.readPayload(replayEventLength)
return fixture.sentryOptions.serializer.deserialize(
InputStreamReader(replayEventBytes.inputStream()),
SentryReplayEvent::class.java
)!!
}
}
}
return null
}

private fun verifyAttachmentsInEnvelope(eventId: SentryId?) {
verify(fixture.transport).send(
check { actual ->
Expand Down
Loading