Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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/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 {
@NotNull
IScopesStorage create();
}
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();
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)
}
}