Skip to content
Merged
Show file tree
Hide file tree
Changes from 13 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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
### Fixes

- `SentryOptions.setTracePropagationTargets` is no longer marked internal ([#4170](https://github.com/getsentry/sentry-java/pull/4170))
- Reduce excessive CPU usage when serializing breadcrumbs to disk for ANRs ([#4181](https://github.com/getsentry/sentry-java/pull/4181))

### Behavioural Changes

Expand Down
1 change: 1 addition & 0 deletions buildSrc/src/main/java/Config.kt
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,7 @@ object Config {
val msgpack = "org.msgpack:msgpack-core:0.9.8"
val leakCanaryInstrumentation = "com.squareup.leakcanary:leakcanary-android-instrumentation:2.14"
val composeUiTestJunit4 = "androidx.compose.ui:ui-test-junit4:1.6.8"
val okio = "com.squareup.okio:okio:1.13.0"
}

object QualityPlugins {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,11 @@ static void initializeIntegrationsAndProcessors(
new AndroidConnectionStatusProvider(context, options.getLogger(), buildInfoProvider));
}

if (options.getCacheDirPath() != null) {
options.addScopeObserver(new PersistingScopeObserver(options));
options.addOptionsObserver(new PersistingOptionsObserver(options));
}

options.addEventProcessor(new DeduplicateMultithreadedEventProcessor(options));
options.addEventProcessor(
new DefaultAndroidEventProcessor(context, buildInfoProvider, options));
Expand Down Expand Up @@ -225,13 +230,6 @@ static void initializeIntegrationsAndProcessors(
}
}
options.setTransactionPerformanceCollector(new DefaultTransactionPerformanceCollector(options));

if (options.getCacheDirPath() != null) {
if (options.isEnableScopePersistence()) {
options.addScopeObserver(new PersistingScopeObserver(options));
}
options.addOptionsObserver(new PersistingOptionsObserver(options));
}
}

static void installDefaultIntegrations(
Expand Down Expand Up @@ -277,6 +275,8 @@ static void installDefaultIntegrations(
// AppLifecycleIntegration has to be installed before AnrIntegration, because AnrIntegration
// relies on AppState set by it
options.addIntegration(new AppLifecycleIntegration());
// AnrIntegration must be installed before ReplayIntegration, as ReplayIntegration relies on
// it to set the replayId in case of an ANR
options.addIntegration(AnrIntegrationFactory.create(context, buildInfoProvider));

// registerActivityLifecycleCallbacks is only available if Context is an AppContext
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import io.sentry.SentryEvent;
import io.sentry.SentryExceptionFactory;
import io.sentry.SentryLevel;
import io.sentry.SentryOptions;
import io.sentry.SentryStackTraceFactory;
import io.sentry.SpanContext;
import io.sentry.android.core.internal.util.CpuInfoUtils;
Expand Down Expand Up @@ -83,13 +84,16 @@ public final class AnrV2EventProcessor implements BackfillingEventProcessor {

private final @NotNull SentryExceptionFactory sentryExceptionFactory;

private final @Nullable PersistingScopeObserver persistingScopeObserver;

public AnrV2EventProcessor(
final @NotNull Context context,
final @NotNull SentryAndroidOptions options,
final @NotNull BuildInfoProvider buildInfoProvider) {
this.context = ContextUtils.getApplicationContext(context);
this.options = options;
this.buildInfoProvider = buildInfoProvider;
this.persistingScopeObserver = options.findPersistingScopeObserver();

final SentryStackTraceFactory sentryStackTraceFactory =
new SentryStackTraceFactory(this.options);
Expand Down Expand Up @@ -188,8 +192,7 @@ private boolean sampleReplay(final @NotNull SentryEvent event) {
}

private void setReplayId(final @NotNull SentryEvent event) {
@Nullable
String persistedReplayId = PersistingScopeObserver.read(options, REPLAY_FILENAME, String.class);
@Nullable String persistedReplayId = readFromDisk(options, REPLAY_FILENAME, String.class);
final @NotNull File replayFolder =
new File(options.getCacheDirPath(), "replay_" + persistedReplayId);
if (!replayFolder.exists()) {
Expand Down Expand Up @@ -224,8 +227,7 @@ private void setReplayId(final @NotNull SentryEvent event) {
}

private void setTrace(final @NotNull SentryEvent event) {
final SpanContext spanContext =
PersistingScopeObserver.read(options, TRACE_FILENAME, SpanContext.class);
final SpanContext spanContext = readFromDisk(options, TRACE_FILENAME, SpanContext.class);
if (event.getContexts().getTrace() == null) {
if (spanContext != null
&& spanContext.getSpanId() != null
Expand All @@ -236,8 +238,7 @@ private void setTrace(final @NotNull SentryEvent event) {
}

private void setLevel(final @NotNull SentryEvent event) {
final SentryLevel level =
PersistingScopeObserver.read(options, LEVEL_FILENAME, SentryLevel.class);
final SentryLevel level = readFromDisk(options, LEVEL_FILENAME, SentryLevel.class);
if (event.getLevel() == null) {
event.setLevel(level);
}
Expand All @@ -246,7 +247,7 @@ private void setLevel(final @NotNull SentryEvent event) {
@SuppressWarnings("unchecked")
private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Object hint) {
final List<String> fingerprint =
(List<String>) PersistingScopeObserver.read(options, FINGERPRINT_FILENAME, List.class);
(List<String>) readFromDisk(options, FINGERPRINT_FILENAME, List.class);
if (event.getFingerprints() == null) {
event.setFingerprints(fingerprint);
}
Expand All @@ -262,16 +263,14 @@ private void setFingerprints(final @NotNull SentryEvent event, final @NotNull Ob
}

private void setTransaction(final @NotNull SentryEvent event) {
final String transaction =
PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String.class);
final String transaction = readFromDisk(options, TRANSACTION_FILENAME, String.class);
if (event.getTransaction() == null) {
event.setTransaction(transaction);
}
}

private void setContexts(final @NotNull SentryBaseEvent event) {
final Contexts persistedContexts =
PersistingScopeObserver.read(options, CONTEXTS_FILENAME, Contexts.class);
final Contexts persistedContexts = readFromDisk(options, CONTEXTS_FILENAME, Contexts.class);
if (persistedContexts == null) {
return;
}
Expand All @@ -291,7 +290,7 @@ private void setContexts(final @NotNull SentryBaseEvent event) {
@SuppressWarnings("unchecked")
private void setExtras(final @NotNull SentryBaseEvent event) {
final Map<String, Object> extras =
(Map<String, Object>) PersistingScopeObserver.read(options, EXTRAS_FILENAME, Map.class);
(Map<String, Object>) readFromDisk(options, EXTRAS_FILENAME, Map.class);
if (extras == null) {
return;
}
Expand All @@ -309,14 +308,12 @@ private void setExtras(final @NotNull SentryBaseEvent event) {
@SuppressWarnings("unchecked")
private void setBreadcrumbs(final @NotNull SentryBaseEvent event) {
final List<Breadcrumb> breadcrumbs =
(List<Breadcrumb>)
PersistingScopeObserver.read(
options, BREADCRUMBS_FILENAME, List.class, new Breadcrumb.Deserializer());
(List<Breadcrumb>) readFromDisk(options, BREADCRUMBS_FILENAME, List.class);
if (breadcrumbs == null) {
return;
}
if (event.getBreadcrumbs() == null) {
event.setBreadcrumbs(new ArrayList<>(breadcrumbs));
event.setBreadcrumbs(breadcrumbs);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Well spotted, nice!

} else {
event.getBreadcrumbs().addAll(breadcrumbs);
}
Expand All @@ -326,7 +323,7 @@ private void setBreadcrumbs(final @NotNull SentryBaseEvent event) {
private void setScopeTags(final @NotNull SentryBaseEvent event) {
final Map<String, String> tags =
(Map<String, String>)
PersistingScopeObserver.read(options, PersistingScopeObserver.TAGS_FILENAME, Map.class);
readFromDisk(options, PersistingScopeObserver.TAGS_FILENAME, Map.class);
if (tags == null) {
return;
}
Expand All @@ -343,19 +340,29 @@ private void setScopeTags(final @NotNull SentryBaseEvent event) {

private void setUser(final @NotNull SentryBaseEvent event) {
if (event.getUser() == null) {
final User user = PersistingScopeObserver.read(options, USER_FILENAME, User.class);
final User user = readFromDisk(options, USER_FILENAME, User.class);
event.setUser(user);
}
}

private void setRequest(final @NotNull SentryBaseEvent event) {
if (event.getRequest() == null) {
final Request request =
PersistingScopeObserver.read(options, REQUEST_FILENAME, Request.class);
final Request request = readFromDisk(options, REQUEST_FILENAME, Request.class);
event.setRequest(request);
}
}

private <T> @Nullable T readFromDisk(
final @NotNull SentryOptions options,
final @NotNull String fileName,
final @NotNull Class<T> clazz) {
if (persistingScopeObserver == null) {
return null;
}

return persistingScopeObserver.read(options, fileName, clazz);
}

// endregion

// region options persisted values
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import io.sentry.android.timber.SentryTimberIntegration
import io.sentry.cache.PersistingOptionsObserver
import io.sentry.cache.PersistingScopeObserver
import io.sentry.compose.gestures.ComposeGestureTargetLocator
import io.sentry.test.ImmediateExecutorService
import org.junit.runner.RunWith
import org.mockito.kotlin.any
import org.mockito.kotlin.eq
Expand Down Expand Up @@ -55,6 +56,7 @@ class AndroidOptionsInitializerTest {
configureContext: Context.() -> Unit = {},
assets: AssetManager? = null
) {
sentryOptions.executorService = ImmediateExecutorService()
mockContext = if (metadata != null) {
ContextUtilsTestHelper.mockMetaData(
mockContext = ContextUtilsTestHelper.createMockContext(hasAppContext),
Expand Down Expand Up @@ -724,9 +726,10 @@ class AndroidOptionsInitializerTest {
}

@Test
fun `PersistingScopeObserver is not set to options, if scope persistence is disabled`() {
fun `PersistingScopeObserver is no-op, if scope persistence is disabled`() {
fixture.initSut(configureOptions = { isEnableScopePersistence = false })

assertTrue { fixture.sentryOptions.scopeObservers.none { it is PersistingScopeObserver } }
fixture.sentryOptions.findPersistingScopeObserver()?.setTags(mapOf("key" to "value"))
assertFalse(File(AndroidOptionsInitializer.getCacheDir(fixture.context), PersistingScopeObserver.SCOPE_CACHE).exists())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import io.sentry.cache.PersistingScopeObserver.TAGS_FILENAME
import io.sentry.cache.PersistingScopeObserver.TRACE_FILENAME
import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME
import io.sentry.cache.PersistingScopeObserver.USER_FILENAME
import io.sentry.cache.tape.QueueFile
import io.sentry.hints.AbnormalExit
import io.sentry.hints.Backfillable
import io.sentry.protocol.Browser
Expand All @@ -61,6 +62,7 @@ import org.robolectric.annotation.Config
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowActivityManager
import org.robolectric.shadows.ShadowBuild
import java.io.ByteArrayOutputStream
import java.io.File
import kotlin.test.BeforeTest
import kotlin.test.Test
Expand Down Expand Up @@ -98,6 +100,7 @@ class AnrV2EventProcessorTest {
options.cacheDirPath = dir.newFolder().absolutePath
options.environment = "release"
options.isSendDefaultPii = isSendDefaultPii
options.addScopeObserver(PersistingScopeObserver(options))

whenever(buildInfo.sdkInfoVersion).thenReturn(currentSdk)
whenever(buildInfo.isEmulator).thenReturn(true)
Expand Down Expand Up @@ -147,7 +150,16 @@ class AnrV2EventProcessorTest {
fun <T : Any> persistScope(filename: String, entity: T) {
val dir = File(options.cacheDirPath, SCOPE_CACHE).also { it.mkdirs() }
val file = File(dir, filename)
options.serializer.serialize(entity, file.writer())
if (filename == BREADCRUMBS_FILENAME) {
val queueFile = QueueFile.Builder(file).build()
(entity as List<Breadcrumb>).forEach { crumb ->
val baos = ByteArrayOutputStream()
options.serializer.serialize(crumb, baos.writer())
queueFile.add(baos.toByteArray())
}
} else {
options.serializer.serialize(entity, file.writer())
}
}

fun <T : Any> persistOptions(filename: String, entity: T) {
Expand Down Expand Up @@ -621,7 +633,7 @@ class AnrV2EventProcessorTest {
val processed = processor.process(SentryEvent(), hint)!!

assertEquals(replayId1.toString(), processed.contexts[Contexts.REPLAY_ID].toString())
assertEquals(replayId1.toString(), PersistingScopeObserver.read(fixture.options, REPLAY_FILENAME, String::class.java))
assertEquals(replayId1.toString(), fixture.options.findPersistingScopeObserver()?.read(fixture.options, REPLAY_FILENAME, String::class.java))
}

private fun processEvent(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import android.os.SystemClock
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import io.sentry.Breadcrumb
import io.sentry.DateUtils
import io.sentry.Hint
import io.sentry.ILogger
import io.sentry.ISentryClient
Expand All @@ -37,10 +38,13 @@ import io.sentry.cache.PersistingOptionsObserver
import io.sentry.cache.PersistingOptionsObserver.ENVIRONMENT_FILENAME
import io.sentry.cache.PersistingOptionsObserver.OPTIONS_CACHE
import io.sentry.cache.PersistingOptionsObserver.RELEASE_FILENAME
import io.sentry.cache.PersistingScopeObserver
import io.sentry.cache.PersistingScopeObserver.BREADCRUMBS_FILENAME
import io.sentry.cache.PersistingScopeObserver.REPLAY_FILENAME
import io.sentry.cache.PersistingScopeObserver.SCOPE_CACHE
import io.sentry.cache.PersistingScopeObserver.TRANSACTION_FILENAME
import io.sentry.cache.tape.QueueFile
import io.sentry.protocol.Contexts
import io.sentry.protocol.SentryId
import io.sentry.test.applyTestOptions
import io.sentry.test.initForTest
import io.sentry.transport.NoOpEnvelopeCache
Expand All @@ -64,6 +68,7 @@ import org.robolectric.annotation.Config
import org.robolectric.shadow.api.Shadow
import org.robolectric.shadows.ShadowActivityManager
import org.robolectric.shadows.ShadowActivityManager.ApplicationExitInfoBuilder
import java.io.ByteArrayOutputStream
import java.io.File
import java.nio.file.Files
import java.util.concurrent.TimeUnit
Expand Down Expand Up @@ -417,27 +422,31 @@ class SentryAndroidTest {
assertEquals("Debug!", event.breadcrumbs!![0].message)
assertEquals("staging", event.environment)
assertEquals("io.sentry.sample@2.0.0", event.release)
assertEquals("afcb46b1140ade5187c4bbb5daa804df", event.contexts[Contexts.REPLAY_ID])
asserted.set(true)
null
}

// have to do it after the cacheDir is set to options, because it adds a dsn hash after
prefillOptionsCache(it.cacheDirPath!!)
prefillScopeCache(it.cacheDirPath!!)
prefillScopeCache(it, it.cacheDirPath!!)

it.release = "io.sentry.sample@1.1.0+220"
it.environment = "debug"
// this is necessary to delay the AnrV2Integration processing to execute the configure
// scope block below (otherwise it won't be possible as scopes is no-op before .init)
it.executorService.submit {
Sentry.configureScope { scope ->
// make sure the scope values changed to test that we're still using previously
// persisted values for the old ANR events
assertEquals("TestActivity", scope.transactionName)
}
}
options = it
}
options.executorService.submit {
// verify we reset the persisted scope values after the init bg tasks have run to ensure
// clean state for a new process.
assertEquals(
emptyList<Breadcrumb>(),
options.findPersistingScopeObserver()?.read(options, BREADCRUMBS_FILENAME, List::class.java)
)
assertEquals(
SentryId.EMPTY_ID.toString(),
options.findPersistingScopeObserver()?.read(options, REPLAY_FILENAME, String::class.java)
)
}
Sentry.configureScope {
it.setTransaction("TestActivity")
it.addBreadcrumb(Breadcrumb.error("Error!"))
Expand All @@ -451,7 +460,7 @@ class SentryAndroidTest {
// assert that persisted values have changed
assertEquals(
"TestActivity",
PersistingScopeObserver.read(options, TRANSACTION_FILENAME, String::class.java)
options.findPersistingScopeObserver()?.read(options, TRANSACTION_FILENAME, String::class.java)
)
assertEquals(
"io.sentry.sample@1.1.0+220",
Expand Down Expand Up @@ -532,19 +541,22 @@ class SentryAndroidTest {
assertTrue(optionsRef.eventProcessors.any { it is AnrV2EventProcessor })
}

private fun prefillScopeCache(cacheDir: String) {
private fun prefillScopeCache(options: SentryOptions, cacheDir: String) {
val scopeDir = File(cacheDir, SCOPE_CACHE).also { it.mkdirs() }
File(scopeDir, BREADCRUMBS_FILENAME).writeText(
"""
[{
"timestamp": "2009-11-16T01:08:47.000Z",
"message": "Debug!",
"type": "debug",
"level": "debug"
}]
""".trimIndent()
val queueFile = QueueFile.Builder(File(scopeDir, BREADCRUMBS_FILENAME)).build()
val baos = ByteArrayOutputStream()
options.serializer.serialize(
Breadcrumb(DateUtils.getDateTime("2009-11-16T01:08:47.000Z")).apply {
message = "Debug!"
type = "debug"
level = DEBUG
},
baos.writer()
)
queueFile.add(baos.toByteArray())
File(scopeDir, TRANSACTION_FILENAME).writeText("\"MainActivity\"")
File(scopeDir, REPLAY_FILENAME).writeText("\"afcb46b1140ade5187c4bbb5daa804df\"")
File(options.getCacheDirPath(), "replay_afcb46b1140ade5187c4bbb5daa804df").mkdirs()
}

private fun prefillOptionsCache(cacheDir: String) {
Expand Down
Loading