Skip to content

Commit ae6907d

Browse files
adinauerclaude
andauthored
feat(core): Add configurable IScopesStorageFactory to SentryOptions (#5199)
* feat(core): Add configurable IScopesStorageFactory to SentryOptions Allow users to provide a custom IScopesStorage factory via SentryOptions.setScopesStorageFactory(). When set, the custom factory takes precedence over the default auto-detection logic. Fixes #5193 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * changelog * ref(core): Have ScopesStorageFactory implement IScopesStorageFactory Add LoadClass and ILogger parameters to IScopesStorageFactory.create() so custom factories have access to class loading utilities. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Revert "ref(core): Have ScopesStorageFactory implement IScopesStorageFactory" This reverts commit a0d77eb. * feat(core): Pass SentryOptions to IScopesStorageFactory.create() SPI-discovered factory implementations are instantiated via ServiceLoader with no-arg constructors, so they need access to options like logger and DSN at creation time. Change the interface method signature to accept SentryOptions as a parameter. Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4c09f52 commit ae6907d

File tree

7 files changed

+114
-1
lines changed

7 files changed

+114
-1
lines changed

CHANGELOG.md

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

55
### Features
66

7+
- Add configurable `IScopesStorageFactory` to `SentryOptions` for providing a custom `IScopesStorage`, e.g. when the default `ThreadLocal`-backed storage is incompatible with non-pinning thread models ([#5199](https://github.com/getsentry/sentry-java/pull/5199))
78
- Android: Add `beforeErrorSampling` callback to Session Replay ([#5214](https://github.com/getsentry/sentry-java/pull/5214))
89
- Allows filtering which errors trigger replay capture before the `onErrorSampleRate` is checked
910
- Returning `false` skips replay capture entirely for that error; returning `true` proceeds with the normal sample rate check

sentry/api/sentry.api

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1060,6 +1060,10 @@ public abstract interface class io/sentry/IScopesStorage {
10601060
public abstract fun set (Lio/sentry/IScopes;)Lio/sentry/ISentryLifecycleToken;
10611061
}
10621062

1063+
public abstract interface class io/sentry/IScopesStorageFactory {
1064+
public abstract fun create (Lio/sentry/SentryOptions;)Lio/sentry/IScopesStorage;
1065+
}
1066+
10631067
public abstract interface class io/sentry/ISentryClient {
10641068
public abstract fun captureBatchedLogEvents (Lio/sentry/SentryLogEvents;)V
10651069
public abstract fun captureBatchedMetricsEvents (Lio/sentry/SentryMetricsEvents;)V
@@ -3631,6 +3635,7 @@ public class io/sentry/SentryOptions {
36313635
public fun getReplayController ()Lio/sentry/ReplayController;
36323636
public fun getSampleRate ()Ljava/lang/Double;
36333637
public fun getScopeObservers ()Ljava/util/List;
3638+
public fun getScopesStorageFactory ()Lio/sentry/IScopesStorageFactory;
36343639
public fun getSdkVersion ()Lio/sentry/protocol/SdkVersion;
36353640
public fun getSentryClientName ()Ljava/lang/String;
36363641
public fun getSerializer ()Lio/sentry/ISerializer;
@@ -3783,6 +3788,7 @@ public class io/sentry/SentryOptions {
37833788
public fun setRelease (Ljava/lang/String;)V
37843789
public fun setReplayController (Lio/sentry/ReplayController;)V
37853790
public fun setSampleRate (Ljava/lang/Double;)V
3791+
public fun setScopesStorageFactory (Lio/sentry/IScopesStorageFactory;)V
37863792
public fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V
37873793
public fun setSendClientReports (Z)V
37883794
public fun setSendDefaultPii (Z)V
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package io.sentry;
2+
3+
import org.jetbrains.annotations.ApiStatus;
4+
import org.jetbrains.annotations.NotNull;
5+
6+
/** Factory for creating custom {@link IScopesStorage} implementations. */
7+
@ApiStatus.Experimental
8+
public interface IScopesStorageFactory {
9+
@NotNull
10+
IScopesStorage create(@NotNull SentryOptions options);
11+
}

sentry/src/main/java/io/sentry/Sentry.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -439,7 +439,10 @@ private static void initFatalLogger(final @NotNull SentryOptions options) {
439439

440440
private static void initScopesStorage(SentryOptions options) {
441441
getScopesStorage().close();
442-
if (SentryOpenTelemetryMode.OFF == options.getOpenTelemetryMode()) {
442+
if (options.getScopesStorageFactory() != null) {
443+
scopesStorage = options.getScopesStorageFactory().create(options);
444+
scopesStorage.init();
445+
} else if (SentryOpenTelemetryMode.OFF == options.getOpenTelemetryMode()) {
443446
scopesStorage = new DefaultScopesStorage();
444447
} else {
445448
scopesStorage = ScopesStorageFactory.create(new LoadClass(), NoOpLogger.getInstance());

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ public class SentryOptions {
557557

558558
private @NotNull ISpanFactory spanFactory = NoOpSpanFactory.getInstance();
559559

560+
private @Nullable IScopesStorageFactory scopesStorageFactory;
561+
560562
/**
561563
* Profiling traces rate. 101 hz means 101 traces in 1 second. Defaults to 101 to avoid possible
562564
* lockstep sampling. More on
@@ -3557,6 +3559,27 @@ public void setSpanFactory(final @NotNull ISpanFactory spanFactory) {
35573559
this.spanFactory = spanFactory;
35583560
}
35593561

3562+
/**
3563+
* Returns the custom scopes storage factory, or null if auto-detection should be used.
3564+
*
3565+
* @return the custom scopes storage factory or null
3566+
*/
3567+
@ApiStatus.Experimental
3568+
public @Nullable IScopesStorageFactory getScopesStorageFactory() {
3569+
return scopesStorageFactory;
3570+
}
3571+
3572+
/**
3573+
* Sets a custom factory for creating {@link IScopesStorage} implementations. When set, this
3574+
* factory takes precedence over the default auto-detection logic.
3575+
*
3576+
* @param scopesStorageFactory the custom factory, or null to use auto-detection
3577+
*/
3578+
@ApiStatus.Experimental
3579+
public void setScopesStorageFactory(final @Nullable IScopesStorageFactory scopesStorageFactory) {
3580+
this.scopesStorageFactory = scopesStorageFactory;
3581+
}
3582+
35603583
@ApiStatus.Experimental
35613584
public @NotNull SentryOptions.Logs getLogs() {
35623585
return logs;

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -964,4 +964,27 @@ class SentryOptionsTest {
964964
options.logs.loggerBatchProcessorFactory = mock
965965
assertSame(mock, options.logs.loggerBatchProcessorFactory)
966966
}
967+
968+
@Test
969+
fun `scopesStorageFactory is null by default`() {
970+
val options = SentryOptions()
971+
assertNull(options.scopesStorageFactory)
972+
}
973+
974+
@Test
975+
fun `scopesStorageFactory can be set and retrieved`() {
976+
val options = SentryOptions()
977+
val factory = IScopesStorageFactory { _ -> DefaultScopesStorage() }
978+
options.scopesStorageFactory = factory
979+
assertSame(factory, options.scopesStorageFactory)
980+
}
981+
982+
@Test
983+
fun `scopesStorageFactory can be set to null`() {
984+
val options = SentryOptions()
985+
val factory = IScopesStorageFactory { _ -> DefaultScopesStorage() }
986+
options.scopesStorageFactory = factory
987+
options.scopesStorageFactory = null
988+
assertNull(options.scopesStorageFactory)
989+
}
967990
}

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

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1723,4 +1723,50 @@ class SentryTest {
17231723
javaClass.injectForField("name", "io.sentry.SentryTest\$CustomAndroidOptions")
17241724
}
17251725
}
1726+
1727+
@Test
1728+
fun `when scopesStorageFactory is set, it is used instead of default storage`() {
1729+
val customStorage = mock<IScopesStorage>()
1730+
whenever(customStorage.set(anyOrNull())).thenReturn(mock())
1731+
whenever(customStorage.get()).thenReturn(null)
1732+
1733+
initForTest {
1734+
it.dsn = dsn
1735+
it.scopesStorageFactory = IScopesStorageFactory { _ -> customStorage }
1736+
}
1737+
1738+
verify(customStorage).init()
1739+
verify(customStorage).set(any())
1740+
}
1741+
1742+
@Test
1743+
fun `when scopesStorageFactory is null, default auto-detection is used`() {
1744+
initForTest {
1745+
it.dsn = dsn
1746+
it.scopesStorageFactory = null
1747+
}
1748+
1749+
// Should work normally with DefaultScopesStorage
1750+
val scopes = Sentry.getCurrentScopes()
1751+
assertFalse(scopes.isNoOp)
1752+
}
1753+
1754+
@Test
1755+
fun `custom scopes storage from factory is functional`() {
1756+
val backingStorage = DefaultScopesStorage()
1757+
val factoryCalled = AtomicBoolean(false)
1758+
1759+
initForTest {
1760+
it.dsn = dsn
1761+
it.scopesStorageFactory = IScopesStorageFactory { _ ->
1762+
factoryCalled.set(true)
1763+
backingStorage
1764+
}
1765+
}
1766+
1767+
assertTrue(factoryCalled.get())
1768+
1769+
val scopes = Sentry.getCurrentScopes()
1770+
assertFalse(scopes.isNoOp)
1771+
}
17261772
}

0 commit comments

Comments
 (0)