Skip to content

Commit 0438c6f

Browse files
authored
[SR] Add beforeSendReplay callback (#3855)
* Do not capture screenshots in session mode when rate limit is active * Changelog * Add beforeSendReplay callback * Changelog * Remove excessive log * Update SessionCaptureStrategyTest.kt
1 parent adbc51d commit 0438c6f

File tree

5 files changed

+161
-1
lines changed

5 files changed

+161
-1
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

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

1011
### Fixes
1112

sentry/api/sentry.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2372,6 +2372,7 @@ public class io/sentry/SentryOptions {
23722372
public fun getBeforeEmitMetricCallback ()Lio/sentry/SentryOptions$BeforeEmitMetricCallback;
23732373
public fun getBeforeEnvelopeCallback ()Lio/sentry/SentryOptions$BeforeEnvelopeCallback;
23742374
public fun getBeforeSend ()Lio/sentry/SentryOptions$BeforeSendCallback;
2375+
public fun getBeforeSendReplay ()Lio/sentry/SentryOptions$BeforeSendReplayCallback;
23752376
public fun getBeforeSendTransaction ()Lio/sentry/SentryOptions$BeforeSendTransactionCallback;
23762377
public fun getBundleIds ()Ljava/util/Set;
23772378
public fun getCacheDirPath ()Ljava/lang/String;
@@ -2487,6 +2488,7 @@ public class io/sentry/SentryOptions {
24872488
public fun setBeforeEmitMetricCallback (Lio/sentry/SentryOptions$BeforeEmitMetricCallback;)V
24882489
public fun setBeforeEnvelopeCallback (Lio/sentry/SentryOptions$BeforeEnvelopeCallback;)V
24892490
public fun setBeforeSend (Lio/sentry/SentryOptions$BeforeSendCallback;)V
2491+
public fun setBeforeSendReplay (Lio/sentry/SentryOptions$BeforeSendReplayCallback;)V
24902492
public fun setBeforeSendTransaction (Lio/sentry/SentryOptions$BeforeSendTransactionCallback;)V
24912493
public fun setCacheDirPath (Ljava/lang/String;)V
24922494
public fun setConnectionStatusProvider (Lio/sentry/IConnectionStatusProvider;)V
@@ -2593,6 +2595,10 @@ public abstract interface class io/sentry/SentryOptions$BeforeSendCallback {
25932595
public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Lio/sentry/SentryEvent;
25942596
}
25952597

2598+
public abstract interface class io/sentry/SentryOptions$BeforeSendReplayCallback {
2599+
public abstract fun execute (Lio/sentry/SentryReplayEvent;Lio/sentry/Hint;)Lio/sentry/SentryReplayEvent;
2600+
}
2601+
25962602
public abstract interface class io/sentry/SentryOptions$BeforeSendTransactionCallback {
25972603
public abstract fun execute (Lio/sentry/protocol/SentryTransaction;Lio/sentry/Hint;)Lio/sentry/protocol/SentryTransaction;
25982604
}

sentry/src/main/java/io/sentry/SentryClient.java

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,18 @@ private void finalizeTransaction(final @NotNull IScope scope, final @NotNull Hin
285285

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

288+
if (event != null) {
289+
event = executeBeforeSendReplay(event, hint);
290+
291+
if (event == null) {
292+
options.getLogger().log(SentryLevel.DEBUG, "Event was dropped by beforeSendReplay");
293+
options
294+
.getClientReportRecorder()
295+
.recordLostEvent(DiscardReason.BEFORE_SEND, DataCategory.Replay);
296+
}
297+
}
298+
288299
if (event == null) {
289-
options.getLogger().log(SentryLevel.DEBUG, "Replay was dropped by Event processors.");
290300
return SentryId.EMPTY_ID;
291301
}
292302

@@ -1126,6 +1136,27 @@ private void sortBreadcrumbsByDate(
11261136
return transaction;
11271137
}
11281138

1139+
private @Nullable SentryReplayEvent executeBeforeSendReplay(
1140+
@NotNull SentryReplayEvent event, final @NotNull Hint hint) {
1141+
final SentryOptions.BeforeSendReplayCallback beforeSendReplay = options.getBeforeSendReplay();
1142+
if (beforeSendReplay != null) {
1143+
try {
1144+
event = beforeSendReplay.execute(event, hint);
1145+
} catch (Throwable e) {
1146+
options
1147+
.getLogger()
1148+
.log(
1149+
SentryLevel.ERROR,
1150+
"The BeforeSendReplay callback threw an exception. It will be added as breadcrumb and continue.",
1151+
e);
1152+
1153+
// drop event in case of an error in beforeSend due to PII concerns
1154+
event = null;
1155+
}
1156+
}
1157+
return event;
1158+
}
1159+
11291160
@Override
11301161
public void close() {
11311162
close(false);

sentry/src/main/java/io/sentry/SentryOptions.java

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,12 @@ public class SentryOptions {
150150
*/
151151
private @Nullable BeforeSendTransactionCallback beforeSendTransaction;
152152

153+
/**
154+
* This function is called with an SDK specific replay object and can return a modified replay
155+
* object or nothing to skip reporting the replay
156+
*/
157+
private @Nullable BeforeSendReplayCallback beforeSendReplay;
158+
153159
/**
154160
* This function is called with an SDK specific breadcrumb object before the breadcrumb is added
155161
* to the scope. When nothing is returned from the function, the breadcrumb is dropped
@@ -761,6 +767,24 @@ public void setBeforeSendTransaction(
761767
this.beforeSendTransaction = beforeSendTransaction;
762768
}
763769

770+
/**
771+
* Returns the BeforeSendReplay callback
772+
*
773+
* @return the beforeSend callback or null if not set
774+
*/
775+
public @Nullable BeforeSendReplayCallback getBeforeSendReplay() {
776+
return beforeSendReplay;
777+
}
778+
779+
/**
780+
* Sets the beforeSendReplay callback
781+
*
782+
* @param beforeSendReplay the beforeSend callback
783+
*/
784+
public void setBeforeSendReplay(@Nullable BeforeSendReplayCallback beforeSendReplay) {
785+
this.beforeSendReplay = beforeSendReplay;
786+
}
787+
764788
/**
765789
* Returns the beforeBreadcrumb callback
766790
*
@@ -2493,6 +2517,23 @@ public interface BeforeSendTransactionCallback {
24932517
SentryTransaction execute(@NotNull SentryTransaction transaction, @NotNull Hint hint);
24942518
}
24952519

2520+
/** The BeforeSendReplay callback */
2521+
public interface BeforeSendReplayCallback {
2522+
2523+
/**
2524+
* Mutate or drop a replay event before being sent. Note that there might be many replay events
2525+
* for a single replay (i.e. segments), you can check {@link SentryReplayEvent#getReplayId()} to
2526+
* identify that the segments belong to the same replay.
2527+
*
2528+
* @param event the event
2529+
* @param hint the hint, contains {@link ReplayRecording}, can be accessed via {@link
2530+
* Hint#getReplayRecording()}
2531+
* @return the original event or the mutated event or null if event was dropped
2532+
*/
2533+
@Nullable
2534+
SentryReplayEvent execute(@NotNull SentryReplayEvent event, @NotNull Hint hint);
2535+
}
2536+
24962537
/** The BeforeBreadcrumb callback */
24972538
public interface BeforeBreadcrumbCallback {
24982539

sentry/src/test/java/io/sentry/SentryClientTest.kt

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2800,6 +2800,68 @@ class SentryClientTest {
28002800
assertFalse(called)
28012801
}
28022802

2803+
@Test
2804+
fun `when beforeSendReplay is set, callback is invoked`() {
2805+
var invoked = false
2806+
fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> invoked = true; replay }
2807+
2808+
fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())
2809+
2810+
assertTrue(invoked)
2811+
}
2812+
2813+
@Test
2814+
fun `when beforeSendReplay returns null, event is dropped`() {
2815+
fixture.sentryOptions.setBeforeSendReplay { replay: SentryReplayEvent, _: Hint -> null }
2816+
2817+
fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())
2818+
2819+
verify(fixture.transport, never()).send(any(), anyOrNull())
2820+
2821+
assertClientReport(
2822+
fixture.sentryOptions.clientReportRecorder,
2823+
listOf(
2824+
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)
2825+
)
2826+
)
2827+
}
2828+
2829+
@Test
2830+
fun `when beforeSendReplay returns new instance, new instance is sent`() {
2831+
val expected = SentryReplayEvent().apply { tags = mapOf("test" to "test") }
2832+
fixture.sentryOptions.setBeforeSendReplay { _, _ -> expected }
2833+
2834+
fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())
2835+
2836+
verify(fixture.transport).send(
2837+
check {
2838+
val replay = getReplayFromData(it.items.first().data)
2839+
assertEquals("test", replay!!.tags!!["test"])
2840+
},
2841+
anyOrNull()
2842+
)
2843+
verifyNoMoreInteractions(fixture.transport)
2844+
}
2845+
2846+
@Test
2847+
fun `when beforeSendReplay throws an exception, replay is dropped`() {
2848+
val exception = Exception("test")
2849+
2850+
exception.stackTrace.toString()
2851+
fixture.sentryOptions.setBeforeSendReplay { _, _ -> throw exception }
2852+
2853+
val id = fixture.getSut().captureReplayEvent(SentryReplayEvent(), Scope(fixture.sentryOptions), Hint())
2854+
2855+
assertEquals(SentryId.EMPTY_ID, id)
2856+
2857+
assertClientReport(
2858+
fixture.sentryOptions.clientReportRecorder,
2859+
listOf(
2860+
DiscardedEvent(DiscardReason.BEFORE_SEND.reason, DataCategory.Replay.category, 1)
2861+
)
2862+
)
2863+
}
2864+
28032865
private fun givenScopeWithStartedSession(errored: Boolean = false, crashed: Boolean = false): IScope {
28042866
val scope = createScope(fixture.sentryOptions)
28052867
scope.startSession()
@@ -2977,6 +3039,25 @@ class SentryClientTest {
29773039
)!!
29783040
}
29793041

3042+
private fun getReplayFromData(data: ByteArray): SentryReplayEvent? {
3043+
val unpacker = MessagePack.newDefaultUnpacker(data)
3044+
val mapSize = unpacker.unpackMapHeader()
3045+
for (i in 0 until mapSize) {
3046+
val key = unpacker.unpackString()
3047+
when (key) {
3048+
SentryItemType.ReplayEvent.itemType -> {
3049+
val replayEventLength = unpacker.unpackBinaryHeader()
3050+
val replayEventBytes = unpacker.readPayload(replayEventLength)
3051+
return fixture.sentryOptions.serializer.deserialize(
3052+
InputStreamReader(replayEventBytes.inputStream()),
3053+
SentryReplayEvent::class.java
3054+
)!!
3055+
}
3056+
}
3057+
}
3058+
return null
3059+
}
3060+
29803061
private fun verifyAttachmentsInEnvelope(eventId: SentryId?) {
29813062
verify(fixture.transport).send(
29823063
check { actual ->

0 commit comments

Comments
 (0)