Skip to content

Commit 1ffacfc

Browse files
authored
Merge branch 'main' into feat/sync-file-attachment
2 parents 273a9ea + 55aaf9b commit 1ffacfc

File tree

6 files changed

+181
-2
lines changed

6 files changed

+181
-2
lines changed

.github/workflows/integration-tests-benchmarks.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ jobs:
106106
run: ./gradlew :sentry-android-integration-tests:test-app-sentry:assembleRelease
107107

108108
- name: Collect app metrics
109-
uses: getsentry/action-app-sdk-overhead-metrics@v1
109+
uses: getsentry/action-app-sdk-overhead-metrics@ecce2e2718b6d97ad62220fca05627900b061ed5
110110
with:
111111
config: sentry-android-integration-tests/metrics-test.yml
112112
sauce-user: ${{ secrets.SAUCE_USERNAME }}

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,20 @@
55
### Features
66

77
- Android: Attachments on the scope will now be synced to native ([#5211](https://github.com/getsentry/sentry-java/pull/5211))
8+
- Android: Add `beforeErrorSampling` callback to Session Replay ([#5214](https://github.com/getsentry/sentry-java/pull/5214))
9+
- Allows filtering which errors trigger replay capture before the `onErrorSampleRate` is checked
10+
- Returning `false` skips replay capture entirely for that error; returning `true` proceeds with the normal sample rate check
11+
- Example usage:
12+
```java
13+
SentryAndroid.init(context) { options ->
14+
options.sessionReplay.beforeErrorSampling =
15+
SentryReplayOptions.BeforeErrorSamplingCallback { event, hint ->
16+
// Skip replay for handled exceptions
17+
val hasUnhandled = event.exceptions?.any { it.mechanism?.isHandled == false } == true
18+
hasUnhandled
19+
}
20+
}
21+
```
822

923
## 8.36.0
1024

sentry/api/sentry.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4002,6 +4002,7 @@ public final class io/sentry/SentryReplayOptions : io/sentry/SentryMaskingOption
40024002
public fun <init> (ZLio/sentry/protocol/SdkVersion;)V
40034003
public fun addMaskViewClass (Ljava/lang/String;)V
40044004
public fun addUnmaskViewClass (Ljava/lang/String;)V
4005+
public fun getBeforeErrorSampling ()Lio/sentry/SentryReplayOptions$BeforeErrorSamplingCallback;
40054006
public fun getErrorReplayDuration ()J
40064007
public fun getFrameRate ()I
40074008
public fun getNetworkDetailAllowUrls ()Ljava/util/List;
@@ -4021,6 +4022,7 @@ public final class io/sentry/SentryReplayOptions : io/sentry/SentryMaskingOption
40214022
public fun isSessionReplayEnabled ()Z
40224023
public fun isSessionReplayForErrorsEnabled ()Z
40234024
public fun isTrackConfiguration ()Z
4025+
public fun setBeforeErrorSampling (Lio/sentry/SentryReplayOptions$BeforeErrorSamplingCallback;)V
40244026
public fun setDebug (Z)V
40254027
public fun setMaskAllImages (Z)V
40264028
public fun setMaskAllText (Z)V
@@ -4038,6 +4040,10 @@ public final class io/sentry/SentryReplayOptions : io/sentry/SentryMaskingOption
40384040
public fun trackCustomMasking ()V
40394041
}
40404042

4043+
public abstract interface class io/sentry/SentryReplayOptions$BeforeErrorSamplingCallback {
4044+
public abstract fun execute (Lio/sentry/SentryEvent;Lio/sentry/Hint;)Z
4045+
}
4046+
40414047
public final class io/sentry/SentryReplayOptions$SentryReplayQuality : java/lang/Enum {
40424048
public static final field HIGH Lio/sentry/SentryReplayOptions$SentryReplayQuality;
40434049
public static final field LOW Lio/sentry/SentryReplayOptions$SentryReplayQuality;

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

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -231,7 +231,25 @@ private boolean shouldApplyScopeData(final @NotNull CheckIn event, final @NotNul
231231
// an event from the past. If it's cached, but with ApplyScopeData, it comes from the outbox
232232
// folder and we still want to capture replay (e.g. a native captureException error)
233233
if (event != null && !isBackfillable && !isCached && (event.isErrored() || event.isCrashed())) {
234-
options.getReplayController().captureReplay(event.isCrashed());
234+
boolean shouldCaptureReplay = true;
235+
final SentryReplayOptions.BeforeErrorSamplingCallback beforeErrorSampling =
236+
options.getSessionReplay().getBeforeErrorSampling();
237+
if (beforeErrorSampling != null) {
238+
try {
239+
shouldCaptureReplay = beforeErrorSampling.execute(event, hint);
240+
} catch (Throwable e) {
241+
options
242+
.getLogger()
243+
.log(
244+
SentryLevel.ERROR,
245+
"The beforeErrorSampling callback threw an exception. Proceeding with replay capture.",
246+
e);
247+
shouldCaptureReplay = true;
248+
}
249+
}
250+
if (shouldCaptureReplay) {
251+
options.getReplayController().captureReplay(event.isCrashed());
252+
}
235253
}
236254

237255
try {

sentry/src/main/java/io/sentry/SentryReplayOptions.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,25 @@
1717

1818
public final class SentryReplayOptions extends SentryMaskingOptions {
1919

20+
/**
21+
* Callback that is called before the error sample rate is checked for session replay. If the
22+
* callback returns {@code false}, the replay will not be captured for this error event, and the
23+
* {@code onErrorSampleRate} will not be checked. If the callback returns {@code true}, the {@code
24+
* onErrorSampleRate} will be checked as usual. This allows developers to filter which errors
25+
* trigger replay capture.
26+
*/
27+
public interface BeforeErrorSamplingCallback {
28+
/**
29+
* Determines whether replay capture should proceed for the given error event.
30+
*
31+
* @param event the error event that triggered the replay capture
32+
* @param hint the hint associated with the event
33+
* @return {@code true} if the error sample rate should be checked, {@code false} to skip replay
34+
* capture entirely
35+
*/
36+
boolean execute(@NotNull SentryEvent event, @NotNull Hint hint);
37+
}
38+
2039
private static final String CUSTOM_MASKING_INTEGRATION_NAME = "ReplayCustomMasking";
2140
private volatile boolean customMaskingTracked = false;
2241

@@ -172,6 +191,12 @@ public enum SentryReplayQuality {
172191
*/
173192
private @NotNull List<String> networkResponseHeaders = DEFAULT_HEADERS;
174193

194+
/**
195+
* A callback that is called before the error sample rate is checked for session replay. Can be
196+
* used to filter which errors trigger replay capture.
197+
*/
198+
private @Nullable BeforeErrorSamplingCallback beforeErrorSampling;
199+
175200
public SentryReplayOptions(final boolean empty, final @Nullable SdkVersion sdkVersion) {
176201
if (!empty) {
177202
// Add default mask classes directly without setting usingCustomMasking flag
@@ -469,4 +494,26 @@ public void setNetworkResponseHeaders(final @NotNull List<String> networkRespons
469494
merged.addAll(additionalHeaders);
470495
return Collections.unmodifiableList(new ArrayList<>(merged));
471496
}
497+
498+
/**
499+
* Gets the callback that is called before the error sample rate is checked for session replay.
500+
*
501+
* @return the callback, or {@code null} if not set
502+
*/
503+
public @Nullable BeforeErrorSamplingCallback getBeforeErrorSampling() {
504+
return beforeErrorSampling;
505+
}
506+
507+
/**
508+
* Sets the callback that is called before the error sample rate is checked for session replay.
509+
* Returning {@code false} from the callback will skip replay capture for the error event entirely
510+
* (the {@code onErrorSampleRate} will not be checked). Returning {@code true} will proceed with
511+
* the normal error sample rate check.
512+
*
513+
* @param beforeErrorSampling the callback, or {@code null} to disable filtering
514+
*/
515+
public void setBeforeErrorSampling(
516+
final @Nullable BeforeErrorSamplingCallback beforeErrorSampling) {
517+
this.beforeErrorSampling = beforeErrorSampling;
518+
}
472519
}

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

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3195,6 +3195,100 @@ class SentryClientTest {
31953195
assertFalse(called)
31963196
}
31973197

3198+
@Test
3199+
fun `beforeErrorSampling returning false skips captureReplay`() {
3200+
var called = false
3201+
fixture.sentryOptions.setReplayController(
3202+
object : ReplayController by NoOpReplayController.getInstance() {
3203+
override fun captureReplay(isTerminating: Boolean?) {
3204+
called = true
3205+
}
3206+
}
3207+
)
3208+
fixture.sentryOptions.sessionReplay.beforeErrorSampling =
3209+
SentryReplayOptions.BeforeErrorSamplingCallback { _, _ -> false }
3210+
val sut = fixture.getSut()
3211+
3212+
sut.captureEvent(SentryEvent().apply { exceptions = listOf(SentryException()) })
3213+
assertFalse(called)
3214+
}
3215+
3216+
@Test
3217+
fun `beforeErrorSampling returning true proceeds with captureReplay`() {
3218+
var called = false
3219+
fixture.sentryOptions.setReplayController(
3220+
object : ReplayController by NoOpReplayController.getInstance() {
3221+
override fun captureReplay(isTerminating: Boolean?) {
3222+
called = true
3223+
}
3224+
}
3225+
)
3226+
fixture.sentryOptions.sessionReplay.beforeErrorSampling =
3227+
SentryReplayOptions.BeforeErrorSamplingCallback { _, _ -> true }
3228+
val sut = fixture.getSut()
3229+
3230+
sut.captureEvent(SentryEvent().apply { exceptions = listOf(SentryException()) })
3231+
assertTrue(called)
3232+
}
3233+
3234+
@Test
3235+
fun `beforeErrorSampling not set proceeds with captureReplay`() {
3236+
var called = false
3237+
fixture.sentryOptions.setReplayController(
3238+
object : ReplayController by NoOpReplayController.getInstance() {
3239+
override fun captureReplay(isTerminating: Boolean?) {
3240+
called = true
3241+
}
3242+
}
3243+
)
3244+
val sut = fixture.getSut()
3245+
3246+
sut.captureEvent(SentryEvent().apply { exceptions = listOf(SentryException()) })
3247+
assertTrue(called)
3248+
}
3249+
3250+
@Test
3251+
fun `beforeErrorSampling throwing exception proceeds with captureReplay`() {
3252+
var called = false
3253+
fixture.sentryOptions.setReplayController(
3254+
object : ReplayController by NoOpReplayController.getInstance() {
3255+
override fun captureReplay(isTerminating: Boolean?) {
3256+
called = true
3257+
}
3258+
}
3259+
)
3260+
fixture.sentryOptions.sessionReplay.beforeErrorSampling =
3261+
SentryReplayOptions.BeforeErrorSamplingCallback { _, _ -> throw RuntimeException("test") }
3262+
val sut = fixture.getSut()
3263+
3264+
sut.captureEvent(SentryEvent().apply { exceptions = listOf(SentryException()) })
3265+
assertTrue(called)
3266+
}
3267+
3268+
@Test
3269+
fun `beforeErrorSampling receives correct event and hint`() {
3270+
var receivedEvent: SentryEvent? = null
3271+
var receivedHint: Hint? = null
3272+
fixture.sentryOptions.setReplayController(
3273+
object : ReplayController by NoOpReplayController.getInstance() {
3274+
override fun captureReplay(isTerminating: Boolean?) {}
3275+
}
3276+
)
3277+
fixture.sentryOptions.sessionReplay.beforeErrorSampling =
3278+
SentryReplayOptions.BeforeErrorSamplingCallback { event, hint ->
3279+
receivedEvent = event
3280+
receivedHint = hint
3281+
true
3282+
}
3283+
val sut = fixture.getSut()
3284+
3285+
val event = SentryEvent().apply { exceptions = listOf(SentryException()) }
3286+
val hint = Hint()
3287+
sut.captureEvent(event, hint)
3288+
assertSame(event, receivedEvent)
3289+
assertSame(hint, receivedHint)
3290+
}
3291+
31983292
@Test
31993293
fun `captures replay for cached events with apply scope`() {
32003294
var called = false

0 commit comments

Comments
 (0)