From 3572c9fa1836c49bccbbe6fd736bd4ef5e367480 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 12 Jun 2025 16:47:13 +0200 Subject: [PATCH 01/11] SentryLogcatAdapter now forwards output to Sentry Logs, if enabled --- .../android/core/SentryLogcatAdapter.java | 35 ++++ .../android/core/SentryLogcatAdapterTest.kt | 192 +++++++++++------- 2 files changed, 148 insertions(+), 79 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java index a942d51878c..24d3831ed7f 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java @@ -2,8 +2,12 @@ import android.util.Log; import io.sentry.Breadcrumb; +import io.sentry.ScopesAdapter; import io.sentry.Sentry; import io.sentry.SentryLevel; +import io.sentry.SentryLogLevel; +import java.io.PrintWriter; +import java.io.StringWriter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -44,73 +48,104 @@ private static void addAsBreadcrumb( Sentry.addBreadcrumb(breadcrumb); } + private static void addAsLog( + @NotNull final SentryLogLevel level, + @Nullable final String msg, + @Nullable final Throwable tr) { + final @NotNull ScopesAdapter scopes = ScopesAdapter.getInstance(); + if (tr == null) { + scopes.logger().log(level, msg); + } else { + StringWriter sw = new StringWriter(256); + PrintWriter pw = new PrintWriter(sw, false); + tr.printStackTrace(pw); + pw.flush(); + scopes.logger().log(level, msg != null ? (msg + "\n" + sw.toString()) : sw.toString()); + pw.close(); + } + } + public static int v(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg); + addAsLog(SentryLogLevel.TRACE, msg, null); return Log.v(tag, msg); } public static int v(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg, tr); + addAsLog(SentryLogLevel.TRACE, msg, tr); return Log.v(tag, msg, tr); } public static int d(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg); + addAsLog(SentryLogLevel.DEBUG, msg, null); return Log.d(tag, msg); } public static int d(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.DEBUG, msg, tr); + addAsLog(SentryLogLevel.DEBUG, msg, tr); return Log.d(tag, msg, tr); } public static int i(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.INFO, msg); + addAsLog(SentryLogLevel.INFO, msg, null); return Log.i(tag, msg); } public static int i(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.INFO, msg, tr); + addAsLog(SentryLogLevel.INFO, msg, tr); return Log.i(tag, msg, tr); } public static int w(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.WARNING, msg); + addAsLog(SentryLogLevel.WARN, msg, null); return Log.w(tag, msg); } public static int w(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.WARNING, msg, tr); + addAsLog(SentryLogLevel.WARN, msg, tr); return Log.w(tag, msg, tr); } public static int w(@Nullable String tag, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.WARNING, tr); + addAsLog(SentryLogLevel.WARN, null, tr); return Log.w(tag, tr); } public static int e(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg); + addAsLog(SentryLogLevel.ERROR, msg, null); return Log.e(tag, msg); } public static int e(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg, tr); + addAsLog(SentryLogLevel.ERROR, msg, tr); return Log.e(tag, msg, tr); } public static int wtf(@Nullable String tag, @Nullable String msg) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg); + addAsLog(SentryLogLevel.FATAL, msg, null); return Log.wtf(tag, msg); } public static int wtf(@Nullable String tag, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.ERROR, tr); + addAsLog(SentryLogLevel.FATAL, null, tr); return Log.wtf(tag, tr); } public static int wtf(@Nullable String tag, @Nullable String msg, @Nullable Throwable tr) { addAsBreadcrumb(tag, SentryLevel.ERROR, msg, tr); + addAsLog(SentryLogLevel.FATAL, msg, tr); return Log.wtf(tag, msg, tr); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index 72e3d68dc09..7b9ed6ea146 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -5,22 +5,26 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import io.sentry.Breadcrumb import io.sentry.Sentry import io.sentry.SentryLevel +import io.sentry.SentryLogEvent +import io.sentry.SentryLogLevel import io.sentry.SentryOptions import io.sentry.android.core.performance.AppStartMetrics import org.junit.runner.RunWith -import java.lang.RuntimeException +import kotlin.test.AfterTest import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class SentryLogcatAdapterTest { - private val breadcrumbs = mutableListOf() private val tag = "my-tag" private val commonMsg = "SentryLogcatAdapter" private val throwable = RuntimeException("Test Exception") class Fixture { + val breadcrumbs = mutableListOf() + val logs = mutableListOf() fun initSut( options: Sentry.OptionsConfiguration? = null @@ -29,9 +33,17 @@ class SentryLogcatAdapterTest { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") } val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metadata) - when { - options != null -> initForTest(mockContext, options) - else -> initForTest(mockContext) + initForTest(mockContext) { + it.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> + breadcrumbs.add(breadcrumb) + breadcrumb + } + it.logs.isEnabled = true + it.logs.beforeSend = SentryOptions.Logs.BeforeSendLogCallback { logEvent -> + logs.add(logEvent) + logEvent + } + options?.configure(it) } } } @@ -43,144 +55,166 @@ class SentryLogcatAdapterTest { Sentry.close() AppStartMetrics.getInstance().clear() ContextUtils.resetInstance() - breadcrumbs.clear() - - fixture.initSut { - it.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> - breadcrumbs.add(breadcrumb) - breadcrumb - } - } + fixture.breadcrumbs.clear() + fixture.logs.clear() + } - SentryLogcatAdapter.v(tag, "$commonMsg verbose") - SentryLogcatAdapter.i(tag, "$commonMsg info") - SentryLogcatAdapter.d(tag, "$commonMsg debug") - SentryLogcatAdapter.w(tag, "$commonMsg warning") - SentryLogcatAdapter.e(tag, "$commonMsg error") - SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") + @AfterTest + fun `clean up`() { + AppStartMetrics.getInstance().clear() + ContextUtils.resetInstance() + Sentry.close() } @Test fun `verbose log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.DEBUG && it.message?.contains("verbose") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("verbose") == true) + fixture.initSut() + SentryLogcatAdapter.v(tag, "$commonMsg verbose") + fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg verbose", SentryLogLevel.TRACE) } @Test fun `info log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.INFO && it.message?.contains("info") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("info") == true) + fixture.initSut() + SentryLogcatAdapter.i(tag, "$commonMsg info") + fixture.breadcrumbs.first().assert(tag, "$commonMsg info", SentryLevel.INFO) + fixture.logs.first().assert("$commonMsg info", SentryLogLevel.INFO) } @Test fun `debug log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.DEBUG && it.message?.contains("debug") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("debug") == true) + fixture.initSut() + SentryLogcatAdapter.d(tag, "$commonMsg debug") + fixture.breadcrumbs.first().assert(tag, "$commonMsg debug", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg debug", SentryLogLevel.DEBUG) } @Test fun `warning log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.WARNING && it.message?.contains("warning") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("warning") == true) + fixture.initSut() + SentryLogcatAdapter.w(tag, "$commonMsg warning") + fixture.breadcrumbs.first().assert(tag, "$commonMsg warning", SentryLevel.WARNING) + fixture.logs.first().assert("$commonMsg warning", SentryLogLevel.WARN) } @Test fun `error log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.ERROR && it.message?.contains("error") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("error") == true) + fixture.initSut() + SentryLogcatAdapter.e(tag, "$commonMsg error") + fixture.breadcrumbs.first().assert(tag, "$commonMsg error", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg error", SentryLogLevel.ERROR) } @Test fun `wtf log message has expected content`() { - val breadcrumb = breadcrumbs.find { it.level == SentryLevel.ERROR && it.message?.contains("wtf") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.data?.get("tag")) - assert(breadcrumb?.message?.contains("wtf") == true) + fixture.initSut() + SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") + fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg wtf", SentryLogLevel.FATAL) } @Test fun `e log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.e(tag, "$commonMsg error exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.ERROR, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg error exception", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg error exception\n${throwable.stackTraceToString()}", SentryLogLevel.ERROR) } @Test fun `v log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.v(tag, "$commonMsg verbose exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.DEBUG, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose exception", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg verbose exception\n${throwable.stackTraceToString()}", SentryLogLevel.TRACE) } @Test fun `i log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.i(tag, "$commonMsg info exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.INFO, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg info exception", SentryLevel.INFO) + fixture.logs.first().assert("$commonMsg info exception\n${throwable.stackTraceToString()}", SentryLogLevel.INFO) } @Test fun `d log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.d(tag, "$commonMsg debug exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.DEBUG, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg debug exception", SentryLevel.DEBUG) + fixture.logs.first().assert("$commonMsg debug exception\n${throwable.stackTraceToString()}", SentryLogLevel.DEBUG) } @Test fun `w log throwable has expected content`() { + fixture.initSut() SentryLogcatAdapter.w(tag, "$commonMsg warning exception", throwable) - - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.WARNING, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + fixture.breadcrumbs.first().assert(tag, "$commonMsg warning exception", SentryLevel.WARNING) + fixture.logs.first().assert("$commonMsg warning exception\n${throwable.stackTraceToString()}", SentryLogLevel.WARN) } @Test fun `wtf log throwable has expected content`() { + fixture.initSut() + SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) + fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf exception", SentryLevel.ERROR) + fixture.logs.first().assert("$commonMsg wtf exception\n${throwable.stackTraceToString()}", SentryLogLevel.FATAL) + } + + @Test + fun `do not send logs if logs is disabled`() { + fixture.initSut { it.logs.isEnabled = false } + + SentryLogcatAdapter.v(tag, "$commonMsg verbose") + SentryLogcatAdapter.i(tag, "$commonMsg info") + SentryLogcatAdapter.d(tag, "$commonMsg debug") + SentryLogcatAdapter.w(tag, "$commonMsg warning") + SentryLogcatAdapter.e(tag, "$commonMsg error") + SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") + SentryLogcatAdapter.e(tag, "$commonMsg error exception", throwable) + SentryLogcatAdapter.v(tag, "$commonMsg verbose exception", throwable) + SentryLogcatAdapter.i(tag, "$commonMsg info exception", throwable) + SentryLogcatAdapter.d(tag, "$commonMsg debug exception", throwable) + SentryLogcatAdapter.w(tag, "$commonMsg warning exception", throwable) SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) - val breadcrumb = breadcrumbs.find { it.message?.contains("exception") ?: false } - assertEquals("Logcat", breadcrumb?.category) - assertEquals(tag, breadcrumb?.getData("tag")) - assertEquals(SentryLevel.ERROR, breadcrumb?.level) - assertEquals(throwable.message, breadcrumb?.getData("throwable")) + assertTrue(fixture.logs.isEmpty()) } @Test fun `logs add correct number of breadcrumb`() { + fixture.initSut() + SentryLogcatAdapter.v(tag, commonMsg) + SentryLogcatAdapter.d(tag, commonMsg) + SentryLogcatAdapter.i(tag, commonMsg) + SentryLogcatAdapter.w(tag, commonMsg) + SentryLogcatAdapter.e(tag, commonMsg) + SentryLogcatAdapter.wtf(tag, commonMsg) assertEquals( 6, - breadcrumbs.filter { + fixture.breadcrumbs.filter { it.message?.contains("SentryLogcatAdapter") ?: false }.size ) } + + private fun Breadcrumb.assert( + expectedTag: String, + expectedMessage: String, + expectedLevel: SentryLevel + ) { + assertEquals(expectedMessage, message) + assertEquals(expectedTag, data["tag"]) + assertEquals(expectedLevel, level) + assertEquals("Logcat", category) + } + + private fun SentryLogEvent.assert( + expectedMessage: String, + expectedLevel: SentryLogLevel + ) { + assertEquals(expectedMessage, body) + assertEquals(expectedLevel, level) + } } From ead3f38c938e706ffbabe1031974ec7a8c80b9bc Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 12 Jun 2025 16:51:35 +0200 Subject: [PATCH 02/11] updated changelog --- CHANGELOG.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a0774abddf7..b5900f815bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## Unreleased + +### Features + +- Send logcat through Sentry Logs ([#4487](https://github.com/getsentry/sentry-java/pull/4487)) + - Enable the Logs feature in your `SentryOptions` or with the `io.sentry.logs.enabled` manifest option and the SDK will automatically send logcat logs to Sentry, if the Sentry Android Gradle plugin is applied. + ## 8.13.3 ### Fixes From c8e6a6ebb5ad1889479bf125f6351681a7037fa3 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Thu, 12 Jun 2025 19:09:13 +0200 Subject: [PATCH 03/11] fix tests --- .../io/sentry/android/core/InternalSentrySdkTest.kt | 3 +++ .../io/sentry/android/core/SentryLogcatAdapterTest.kt | 11 ++--------- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt index 3174bd2824e..7acccda2dda 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/InternalSentrySdkTest.kt @@ -241,6 +241,9 @@ class InternalSentrySdkTest { Sentry.configureScope { scope -> assertEquals(3, scope.breadcrumbs.size) } + + // Ensure we don't interfere with other tests + Sentry.configureScope(ScopeType.GLOBAL) { scope -> scope.clear() } } @Test diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index 7b9ed6ea146..ef85e0afe68 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -50,20 +50,13 @@ class SentryLogcatAdapterTest { private val fixture = Fixture() - @BeforeTest - fun `set up`() { - Sentry.close() - AppStartMetrics.getInstance().clear() - ContextUtils.resetInstance() - fixture.breadcrumbs.clear() - fixture.logs.clear() - } - @AfterTest fun `clean up`() { AppStartMetrics.getInstance().clear() ContextUtils.resetInstance() Sentry.close() + fixture.breadcrumbs.clear() + fixture.logs.clear() } @Test From eacca576dc27c84c415a8a82fc69fdb3c51087e0 Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Thu, 12 Jun 2025 17:10:50 +0000 Subject: [PATCH 04/11] Format code --- .../test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index ef85e0afe68..1c35b022faa 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -11,7 +11,6 @@ import io.sentry.SentryOptions import io.sentry.android.core.performance.AppStartMetrics import org.junit.runner.RunWith import kotlin.test.AfterTest -import kotlin.test.BeforeTest import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue From e566455f8ffdadcec75fe53c50c3f79a9763b208 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Wed, 18 Jun 2025 16:59:30 +0200 Subject: [PATCH 05/11] add log enabled check in logcat adapter --- .../main/java/io/sentry/android/core/SentryLogcatAdapter.java | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java index 24d3831ed7f..73057194678 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java @@ -53,6 +53,10 @@ private static void addAsLog( @Nullable final String msg, @Nullable final Throwable tr) { final @NotNull ScopesAdapter scopes = ScopesAdapter.getInstance(); + // Check if logs are enabled before doing expensive operations + if (!scopes.getOptions().getLogs().isEnabled()) { + return; + } if (tr == null) { scopes.logger().log(level, msg); } else { From 77c5c538e95cc25788215e7d175b9706e224c0c7 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 20 Jun 2025 11:52:46 +0200 Subject: [PATCH 06/11] updated changelog with doc reference --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 784dd3cfe71..cd64b3151a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Send logcat through Sentry Logs ([#4487](https://github.com/getsentry/sentry-java/pull/4487)) - Enable the Logs feature in your `SentryOptions` or with the `io.sentry.logs.enabled` manifest option and the SDK will automatically send logcat logs to Sentry, if the Sentry Android Gradle plugin is applied. + - To set the logcat level check the [Logcat integration documentation](https://docs.sentry.io/platforms/android/integrations/logcat/#configure). - No longer send out empty log envelopes ([#4497](https://github.com/getsentry/sentry-java/pull/4497)) ### Dependencies From a42eb855652a3baf35fa68c3361dbeefa5402d6e Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 30 Jun 2025 18:40:44 +0200 Subject: [PATCH 07/11] merged main --- .../android/core/SentryLogcatAdapterTest.kt | 187 ++++++++++-------- 1 file changed, 100 insertions(+), 87 deletions(-) diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index a7d0b7010a7..b5749c61447 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -18,144 +18,162 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class SentryLogcatAdapterTest { - private val tag = "my-tag" - private val commonMsg = "SentryLogcatAdapter" - private val throwable = RuntimeException("Test Exception") + private val tag = "my-tag" + private val commonMsg = "SentryLogcatAdapter" + private val throwable = RuntimeException("Test Exception") class Fixture { - val breadcrumbs = mutableListOf() - val logs = mutableListOf() + val breadcrumbs = mutableListOf() + val logs = mutableListOf() - fun initSut(options: Sentry.OptionsConfiguration? = null) { + fun initSut(options: Sentry.OptionsConfiguration? = null) { val metadata = Bundle().apply { putString(ManifestMetadataReader.DSN, "https://key@sentry.io/123") } val mockContext = ContextUtilsTestHelper.mockMetaData(metaData = metadata) - initForTest(mockContext) { - it.beforeBreadcrumb = SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> - breadcrumbs.add(breadcrumb) - breadcrumb - } - it.logs.isEnabled = true - it.logs.beforeSend = SentryOptions.Logs.BeforeSendLogCallback { logEvent -> - logs.add(logEvent) - logEvent - } - options?.configure(it) + initForTest(mockContext) { + it.beforeBreadcrumb = + SentryOptions.BeforeBreadcrumbCallback { breadcrumb, _ -> + breadcrumbs.add(breadcrumb) + breadcrumb } + it.logs.isEnabled = true + it.logs.beforeSend = + SentryOptions.Logs.BeforeSendLogCallback { logEvent -> + logs.add(logEvent) + logEvent + } + options?.configure(it) } } } private val fixture = Fixture() - @AfterTest - fun `clean up`() { - AppStartMetrics.getInstance().clear() - ContextUtils.resetInstance() - Sentry.close() - fixture.breadcrumbs.clear() - fixture.logs.clear() - } - + @AfterTest + fun `clean up`() { + AppStartMetrics.getInstance().clear() + ContextUtils.resetInstance() + Sentry.close() + fixture.breadcrumbs.clear() + fixture.logs.clear() + } -@Test -fun `verbose log message has expected content`() { + @Test + fun `verbose log message has expected content`() { fixture.initSut() SentryLogcatAdapter.v(tag, "$commonMsg verbose") fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose", SentryLevel.DEBUG) fixture.logs.first().assert("$commonMsg verbose", SentryLogLevel.TRACE) -} + } -@Test -fun `info log message has expected content`() { + @Test + fun `info log message has expected content`() { fixture.initSut() SentryLogcatAdapter.i(tag, "$commonMsg info") fixture.breadcrumbs.first().assert(tag, "$commonMsg info", SentryLevel.INFO) fixture.logs.first().assert("$commonMsg info", SentryLogLevel.INFO) -} + } -@Test -fun `debug log message has expected content`() { + @Test + fun `debug log message has expected content`() { fixture.initSut() SentryLogcatAdapter.d(tag, "$commonMsg debug") fixture.breadcrumbs.first().assert(tag, "$commonMsg debug", SentryLevel.DEBUG) fixture.logs.first().assert("$commonMsg debug", SentryLogLevel.DEBUG) -} + } -@Test -fun `warning log message has expected content`() { + @Test + fun `warning log message has expected content`() { fixture.initSut() SentryLogcatAdapter.w(tag, "$commonMsg warning") fixture.breadcrumbs.first().assert(tag, "$commonMsg warning", SentryLevel.WARNING) fixture.logs.first().assert("$commonMsg warning", SentryLogLevel.WARN) -} + } -@Test -fun `error log message has expected content`() { + @Test + fun `error log message has expected content`() { fixture.initSut() SentryLogcatAdapter.e(tag, "$commonMsg error") fixture.breadcrumbs.first().assert(tag, "$commonMsg error", SentryLevel.ERROR) fixture.logs.first().assert("$commonMsg error", SentryLogLevel.ERROR) -} + } -@Test -fun `wtf log message has expected content`() { + @Test + fun `wtf log message has expected content`() { fixture.initSut() SentryLogcatAdapter.wtf(tag, "$commonMsg wtf") fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf", SentryLevel.ERROR) fixture.logs.first().assert("$commonMsg wtf", SentryLogLevel.FATAL) -} + } -@Test -fun `e log throwable has expected content`() { + @Test + fun `e log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.e(tag, "$commonMsg error exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg error exception", SentryLevel.ERROR) - fixture.logs.first().assert("$commonMsg error exception\n${throwable.stackTraceToString()}", SentryLogLevel.ERROR) -} + fixture.logs + .first() + .assert("$commonMsg error exception\n${throwable.stackTraceToString()}", SentryLogLevel.ERROR) + } -@Test -fun `v log throwable has expected content`() { + @Test + fun `v log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.v(tag, "$commonMsg verbose exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose exception", SentryLevel.DEBUG) - fixture.logs.first().assert("$commonMsg verbose exception\n${throwable.stackTraceToString()}", SentryLogLevel.TRACE) -} + fixture.logs + .first() + .assert( + "$commonMsg verbose exception\n${throwable.stackTraceToString()}", + SentryLogLevel.TRACE, + ) + } -@Test -fun `i log throwable has expected content`() { + @Test + fun `i log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.i(tag, "$commonMsg info exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg info exception", SentryLevel.INFO) - fixture.logs.first().assert("$commonMsg info exception\n${throwable.stackTraceToString()}", SentryLogLevel.INFO) -} + fixture.logs + .first() + .assert("$commonMsg info exception\n${throwable.stackTraceToString()}", SentryLogLevel.INFO) + } -@Test -fun `d log throwable has expected content`() { + @Test + fun `d log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.d(tag, "$commonMsg debug exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg debug exception", SentryLevel.DEBUG) - fixture.logs.first().assert("$commonMsg debug exception\n${throwable.stackTraceToString()}", SentryLogLevel.DEBUG) -} + fixture.logs + .first() + .assert("$commonMsg debug exception\n${throwable.stackTraceToString()}", SentryLogLevel.DEBUG) + } -@Test -fun `w log throwable has expected content`() { + @Test + fun `w log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.w(tag, "$commonMsg warning exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg warning exception", SentryLevel.WARNING) - fixture.logs.first().assert("$commonMsg warning exception\n${throwable.stackTraceToString()}", SentryLogLevel.WARN) -} + fixture.logs + .first() + .assert( + "$commonMsg warning exception\n${throwable.stackTraceToString()}", + SentryLogLevel.WARN, + ) + } -@Test -fun `wtf log throwable has expected content`() { + @Test + fun `wtf log throwable has expected content`() { fixture.initSut() SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf exception", SentryLevel.ERROR) - fixture.logs.first().assert("$commonMsg wtf exception\n${throwable.stackTraceToString()}", SentryLogLevel.FATAL) -} + fixture.logs + .first() + .assert("$commonMsg wtf exception\n${throwable.stackTraceToString()}", SentryLogLevel.FATAL) + } -@Test -fun `do not send logs if logs is disabled`() { + @Test + fun `do not send logs if logs is disabled`() { fixture.initSut { it.logs.isEnabled = false } SentryLogcatAdapter.v(tag, "$commonMsg verbose") @@ -172,10 +190,10 @@ fun `do not send logs if logs is disabled`() { SentryLogcatAdapter.wtf(tag, "$commonMsg wtf exception", throwable) assertTrue(fixture.logs.isEmpty()) -} + } -@Test -fun `logs add correct number of breadcrumb`() { + @Test + fun `logs add correct number of breadcrumb`() { fixture.initSut() SentryLogcatAdapter.v(tag, commonMsg) SentryLogcatAdapter.d(tag, commonMsg) @@ -184,29 +202,24 @@ fun `logs add correct number of breadcrumb`() { SentryLogcatAdapter.e(tag, commonMsg) SentryLogcatAdapter.wtf(tag, commonMsg) assertEquals( - 6, - fixture.breadcrumbs.filter { - it.message?.contains("SentryLogcatAdapter") ?: false - }.size + 6, + fixture.breadcrumbs.filter { it.message?.contains("SentryLogcatAdapter") ?: false }.size, ) -} + } -private fun Breadcrumb.assert( + private fun Breadcrumb.assert( expectedTag: String, expectedMessage: String, - expectedLevel: SentryLevel -) { + expectedLevel: SentryLevel, + ) { assertEquals(expectedMessage, message) assertEquals(expectedTag, data["tag"]) assertEquals(expectedLevel, level) assertEquals("Logcat", category) -} + } -private fun SentryLogEvent.assert( - expectedMessage: String, - expectedLevel: SentryLogLevel -) { + private fun SentryLogEvent.assert(expectedMessage: String, expectedLevel: SentryLogLevel) { assertEquals(expectedMessage, body) assertEquals(expectedLevel, level) -} + } } From 67541defd59a37cb5aeadf960f2769eb47b896e4 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 4 Jul 2025 18:11:17 +0200 Subject: [PATCH 08/11] exceptions log message instead of stacktraces --- .../io/sentry/android/core/SentryLogcatAdapter.java | 10 +++------- .../sentry/android/core/SentryLogcatAdapterTest.kt | 12 ++++++------ 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java index 73057194678..a56df7b4831 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java @@ -57,15 +57,11 @@ private static void addAsLog( if (!scopes.getOptions().getLogs().isEnabled()) { return; } - if (tr == null) { + final @Nullable String trMessage = tr != null ? tr.getMessage() : null; + if (tr == null || trMessage == null) { scopes.logger().log(level, msg); } else { - StringWriter sw = new StringWriter(256); - PrintWriter pw = new PrintWriter(sw, false); - tr.printStackTrace(pw); - pw.flush(); - scopes.logger().log(level, msg != null ? (msg + "\n" + sw.toString()) : sw.toString()); - pw.close(); + scopes.logger().log(level, msg != null ? (msg + "\n" + trMessage) : trMessage); } } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index b5749c61447..f2e618b803e 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -113,7 +113,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg error exception", SentryLevel.ERROR) fixture.logs .first() - .assert("$commonMsg error exception\n${throwable.stackTraceToString()}", SentryLogLevel.ERROR) + .assert("$commonMsg error exception\n${throwable.message}", SentryLogLevel.ERROR) } @Test @@ -124,7 +124,7 @@ class SentryLogcatAdapterTest { fixture.logs .first() .assert( - "$commonMsg verbose exception\n${throwable.stackTraceToString()}", + "$commonMsg verbose exception\n${throwable.message}", SentryLogLevel.TRACE, ) } @@ -136,7 +136,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg info exception", SentryLevel.INFO) fixture.logs .first() - .assert("$commonMsg info exception\n${throwable.stackTraceToString()}", SentryLogLevel.INFO) + .assert("$commonMsg info exception\n${throwable.message}", SentryLogLevel.INFO) } @Test @@ -146,7 +146,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg debug exception", SentryLevel.DEBUG) fixture.logs .first() - .assert("$commonMsg debug exception\n${throwable.stackTraceToString()}", SentryLogLevel.DEBUG) + .assert("$commonMsg debug exception\n${throwable.message}", SentryLogLevel.DEBUG) } @Test @@ -157,7 +157,7 @@ class SentryLogcatAdapterTest { fixture.logs .first() .assert( - "$commonMsg warning exception\n${throwable.stackTraceToString()}", + "$commonMsg warning exception\n${throwable.message}", SentryLogLevel.WARN, ) } @@ -169,7 +169,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg wtf exception", SentryLevel.ERROR) fixture.logs .first() - .assert("$commonMsg wtf exception\n${throwable.stackTraceToString()}", SentryLogLevel.FATAL) + .assert("$commonMsg wtf exception\n${throwable.message}", SentryLogLevel.FATAL) } @Test From c44c57e434d4c058b6024ba91085748e2c47d51f Mon Sep 17 00:00:00 2001 From: Sentry Github Bot Date: Fri, 4 Jul 2025 16:12:50 +0000 Subject: [PATCH 09/11] Format code --- .../io/sentry/android/core/SentryLogcatAdapter.java | 2 -- .../io/sentry/android/core/SentryLogcatAdapterTest.kt | 10 ++-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java index a56df7b4831..ebf99e2b970 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java @@ -6,8 +6,6 @@ import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.SentryLogLevel; -import java.io.PrintWriter; -import java.io.StringWriter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index f2e618b803e..1e58bbf6e7a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -123,10 +123,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose exception", SentryLevel.DEBUG) fixture.logs .first() - .assert( - "$commonMsg verbose exception\n${throwable.message}", - SentryLogLevel.TRACE, - ) + .assert("$commonMsg verbose exception\n${throwable.message}", SentryLogLevel.TRACE) } @Test @@ -156,10 +153,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg warning exception", SentryLevel.WARNING) fixture.logs .first() - .assert( - "$commonMsg warning exception\n${throwable.message}", - SentryLogLevel.WARN, - ) + .assert("$commonMsg warning exception\n${throwable.message}", SentryLogLevel.WARN) } @Test From 40b4ce19e732ea20a9a149b62ae5188300f1a528 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Fri, 4 Jul 2025 18:14:50 +0200 Subject: [PATCH 10/11] exceptions log message instead of stacktraces --- .../io/sentry/android/core/SentryLogcatAdapter.java | 2 -- .../io/sentry/android/core/SentryLogcatAdapterTest.kt | 10 ++-------- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java index a56df7b4831..ebf99e2b970 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/SentryLogcatAdapter.java @@ -6,8 +6,6 @@ import io.sentry.Sentry; import io.sentry.SentryLevel; import io.sentry.SentryLogLevel; -import java.io.PrintWriter; -import java.io.StringWriter; import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt index f2e618b803e..1e58bbf6e7a 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/SentryLogcatAdapterTest.kt @@ -123,10 +123,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg verbose exception", SentryLevel.DEBUG) fixture.logs .first() - .assert( - "$commonMsg verbose exception\n${throwable.message}", - SentryLogLevel.TRACE, - ) + .assert("$commonMsg verbose exception\n${throwable.message}", SentryLogLevel.TRACE) } @Test @@ -156,10 +153,7 @@ class SentryLogcatAdapterTest { fixture.breadcrumbs.first().assert(tag, "$commonMsg warning exception", SentryLevel.WARNING) fixture.logs .first() - .assert( - "$commonMsg warning exception\n${throwable.message}", - SentryLogLevel.WARN, - ) + .assert("$commonMsg warning exception\n${throwable.message}", SentryLogLevel.WARN) } @Test From 4a90aba315110c054518ead1ad6934154b3762d7 Mon Sep 17 00:00:00 2001 From: stefanosiano Date: Mon, 7 Jul 2025 11:12:05 +0200 Subject: [PATCH 11/11] merged main --- CHANGELOG.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 741c93e60e6..baf158193ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,12 +1,17 @@ # Changelog -## 8.16.1-alpha.2 +## Unreleased ### Fixes - Send logcat through Sentry Logs ([#4487](https://github.com/getsentry/sentry-java/pull/4487)) - - Enable the Logs feature in your `SentryOptions` or with the `io.sentry.logs.enabled` manifest option and the SDK will automatically send logcat logs to Sentry, if the Sentry Android Gradle plugin is applied. - - To set the logcat level check the [Logcat integration documentation](https://docs.sentry.io/platforms/android/integrations/logcat/#configure). + - Enable the Logs feature in your `SentryOptions` or with the `io.sentry.logs.enabled` manifest option and the SDK will automatically send logcat logs to Sentry, if the Sentry Android Gradle plugin is applied. + - To set the logcat level check the [Logcat integration documentation](https://docs.sentry.io/platforms/android/integrations/logcat/#configure). + +## 8.16.1-alpha.2 + +### Fixes + - Optimize scope when maxBreadcrumb is 0 ([#4504](https://github.com/getsentry/sentry-java/pull/4504)) - Fix javadoc on TransportResult ([#4528](https://github.com/getsentry/sentry-java/pull/4528)) - Session Replay: Fix `IllegalArgumentException` when `Bitmap` is initialized with non-positive values ([#4536](https://github.com/getsentry/sentry-java/pull/4536))