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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## Unreleased

### Features

- 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))

## 8.35.0

### 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 @@ -1060,6 +1060,10 @@ public abstract interface class io/sentry/IScopesStorage {
public abstract fun set (Lio/sentry/IScopes;)Lio/sentry/ISentryLifecycleToken;
}

public abstract interface class io/sentry/IScopesStorageFactory {
public abstract fun create (Lio/sentry/SentryOptions;)Lio/sentry/IScopesStorage;
}

public abstract interface class io/sentry/ISentryClient {
public abstract fun captureBatchedLogEvents (Lio/sentry/SentryLogEvents;)V
public abstract fun captureBatchedMetricsEvents (Lio/sentry/SentryMetricsEvents;)V
Expand Down Expand Up @@ -3629,6 +3633,7 @@ public class io/sentry/SentryOptions {
public fun getReplayController ()Lio/sentry/ReplayController;
public fun getSampleRate ()Ljava/lang/Double;
public fun getScopeObservers ()Ljava/util/List;
public fun getScopesStorageFactory ()Lio/sentry/IScopesStorageFactory;
public fun getSdkVersion ()Lio/sentry/protocol/SdkVersion;
public fun getSentryClientName ()Ljava/lang/String;
public fun getSerializer ()Lio/sentry/ISerializer;
Expand Down Expand Up @@ -3781,6 +3786,7 @@ public class io/sentry/SentryOptions {
public fun setRelease (Ljava/lang/String;)V
public fun setReplayController (Lio/sentry/ReplayController;)V
public fun setSampleRate (Ljava/lang/Double;)V
public fun setScopesStorageFactory (Lio/sentry/IScopesStorageFactory;)V
public fun setSdkVersion (Lio/sentry/protocol/SdkVersion;)V
public fun setSendClientReports (Z)V
public fun setSendDefaultPii (Z)V
Expand Down
11 changes: 11 additions & 0 deletions sentry/src/main/java/io/sentry/IScopesStorageFactory.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package io.sentry;

import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;

/** Factory for creating custom {@link IScopesStorage} implementations. */
@ApiStatus.Experimental
public interface IScopesStorageFactory {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Opted to keep this separate from ScopesStorageFactory since passing LoadClass and ILogger doesn't seem necessary.

@NotNull
IScopesStorage create(@NotNull SentryOptions options);
}
5 changes: 4 additions & 1 deletion sentry/src/main/java/io/sentry/Sentry.java
Original file line number Diff line number Diff line change
Expand Up @@ -439,7 +439,10 @@ private static void initFatalLogger(final @NotNull SentryOptions options) {

private static void initScopesStorage(SentryOptions options) {
getScopesStorage().close();
if (SentryOpenTelemetryMode.OFF == options.getOpenTelemetryMode()) {
if (options.getScopesStorageFactory() != null) {
scopesStorage = options.getScopesStorageFactory().create(options);
scopesStorage.init();
} else if (SentryOpenTelemetryMode.OFF == options.getOpenTelemetryMode()) {
scopesStorage = new DefaultScopesStorage();
} else {
scopesStorage = ScopesStorageFactory.create(new LoadClass(), NoOpLogger.getInstance());
Expand Down
23 changes: 23 additions & 0 deletions sentry/src/main/java/io/sentry/SentryOptions.java
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,8 @@ public class SentryOptions {

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

private @Nullable IScopesStorageFactory scopesStorageFactory;

/**
* Profiling traces rate. 101 hz means 101 traces in 1 second. Defaults to 101 to avoid possible
* lockstep sampling. More on
Expand Down Expand Up @@ -3557,6 +3559,27 @@ public void setSpanFactory(final @NotNull ISpanFactory spanFactory) {
this.spanFactory = spanFactory;
}

/**
* Returns the custom scopes storage factory, or null if auto-detection should be used.
*
* @return the custom scopes storage factory or null
*/
@ApiStatus.Experimental
public @Nullable IScopesStorageFactory getScopesStorageFactory() {
return scopesStorageFactory;
}

/**
* Sets a custom factory for creating {@link IScopesStorage} implementations. When set, this
* factory takes precedence over the default auto-detection logic.
*
* @param scopesStorageFactory the custom factory, or null to use auto-detection
*/
@ApiStatus.Experimental
public void setScopesStorageFactory(final @Nullable IScopesStorageFactory scopesStorageFactory) {
this.scopesStorageFactory = scopesStorageFactory;
}

@ApiStatus.Experimental
public @NotNull SentryOptions.Logs getLogs() {
return logs;
Expand Down
23 changes: 23 additions & 0 deletions sentry/src/test/java/io/sentry/SentryOptionsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -964,4 +964,27 @@ class SentryOptionsTest {
options.logs.loggerBatchProcessorFactory = mock
assertSame(mock, options.logs.loggerBatchProcessorFactory)
}

@Test
fun `scopesStorageFactory is null by default`() {
val options = SentryOptions()
assertNull(options.scopesStorageFactory)
}

@Test
fun `scopesStorageFactory can be set and retrieved`() {
val options = SentryOptions()
val factory = IScopesStorageFactory { _ -> DefaultScopesStorage() }
options.scopesStorageFactory = factory
assertSame(factory, options.scopesStorageFactory)
}

@Test
fun `scopesStorageFactory can be set to null`() {
val options = SentryOptions()
val factory = IScopesStorageFactory { _ -> DefaultScopesStorage() }
options.scopesStorageFactory = factory
options.scopesStorageFactory = null
assertNull(options.scopesStorageFactory)
}
}
46 changes: 46 additions & 0 deletions sentry/src/test/java/io/sentry/SentryTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -1723,4 +1723,50 @@ class SentryTest {
javaClass.injectForField("name", "io.sentry.SentryTest\$CustomAndroidOptions")
}
}

@Test
fun `when scopesStorageFactory is set, it is used instead of default storage`() {
val customStorage = mock<IScopesStorage>()
whenever(customStorage.set(anyOrNull())).thenReturn(mock())
whenever(customStorage.get()).thenReturn(null)

initForTest {
it.dsn = dsn
it.scopesStorageFactory = IScopesStorageFactory { _ -> customStorage }
}

verify(customStorage).init()
verify(customStorage).set(any())
}

@Test
fun `when scopesStorageFactory is null, default auto-detection is used`() {
initForTest {
it.dsn = dsn
it.scopesStorageFactory = null
}

// Should work normally with DefaultScopesStorage
val scopes = Sentry.getCurrentScopes()
assertFalse(scopes.isNoOp)
}

@Test
fun `custom scopes storage from factory is functional`() {
val backingStorage = DefaultScopesStorage()
val factoryCalled = AtomicBoolean(false)

initForTest {
it.dsn = dsn
it.scopesStorageFactory = IScopesStorageFactory { _ ->
factoryCalled.set(true)
backingStorage
}
}

assertTrue(factoryCalled.get())

val scopes = Sentry.getCurrentScopes()
assertFalse(scopes.isNoOp)
}
}