From 0104d6765e94f0280f2950b070825ff7db89dbda Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Mon, 3 Aug 2026 11:32:00 +0200 Subject: [PATCH] fix(android): Reduce SQLite cursor SDK crash false positives Replace Kotlin interface delegation in SentryCrossProcessCursor with Android's CursorWrapper for ordinary Cursor methods. This keeps lazy-query span instrumentation on getCount, onMove, and fillWindow, but avoids generating Sentry-owned methods such as getString for pass-through cursor calls. When app database code throws from those ordinary cursor methods, the stack should now point at Android's cursor wrapper and the underlying SQLite failure instead of making SDK crash detection treat the event as caused by Sentry's SQLite integration. Co-Authored-By: OpenCode --- .../android/sqlite/SentryCrossProcessCursor.kt | 10 ++++++---- .../android/sqlite/SentryCrossProcessCursorTest.kt | 12 +++++++----- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentryCrossProcessCursor.kt b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentryCrossProcessCursor.kt index f5f8424aca3..01da8c476db 100644 --- a/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentryCrossProcessCursor.kt +++ b/sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SentryCrossProcessCursor.kt @@ -2,20 +2,20 @@ package io.sentry.android.sqlite import android.database.CrossProcessCursor import android.database.CursorWindow +import android.database.CursorWrapper /* * SQLiteCursor executes the query lazily, when one of getCount() and onMove() is called. * Also, by docs, fillWindow() can be used to fill the cursor with data. * So we wrap these methods to create a span. - * SQLiteCursor is never used directly in the code, but only the Cursor interface. - * This means we can use CrossProcessCursor - that extends Cursor - as wrapper, since - * CrossProcessCursor is an interface and we can use Kotlin delegation. + * Ordinary Cursor methods are delegated through CursorWrapper to avoid adding Sentry frames to + * app database exceptions that the wrapper did not instrument. */ internal class SentryCrossProcessCursor( private val delegate: CrossProcessCursor, private val spans: OpenHelperSpans, private val sql: String, -) : CrossProcessCursor by delegate { +) : CursorWrapper(delegate), CrossProcessCursor { // We have to start the span only the first time, regardless of how many times its methods get // called. private var isSpanStarted = false @@ -36,6 +36,8 @@ internal class SentryCrossProcessCursor( return spans.performSql(sql) { delegate.onMove(oldPosition, newPosition) } } + override fun getWindow(): CursorWindow? = delegate.window + override fun fillWindow(position: Int, window: CursorWindow?) { if (isSpanStarted) { return delegate.fillWindow(position, window) diff --git a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt index 27eff29c9f3..ba77b2398c7 100644 --- a/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt +++ b/sentry-android-sqlite/src/test/java/io/sentry/android/sqlite/SentryCrossProcessCursorTest.kt @@ -1,6 +1,7 @@ package io.sentry.android.sqlite import android.database.CrossProcessCursor +import android.database.CursorWrapper import io.sentry.IScopes import io.sentry.ISpan import io.sentry.SentryOptions @@ -52,13 +53,14 @@ class SentryCrossProcessCursorTest { cursor.fillWindow(0, mock()) verify(fixture.mockCursor).fillWindow(eq(0), any()) + } - // Let's verify other methods are delegated, even if not explicitly - cursor.close() - verify(fixture.mockCursor).close() + @Test + fun `ordinary cursor methods are delegated by Android CursorWrapper`() { + val getStringMethod = + SentryCrossProcessCursor::class.java.getMethod("getString", Int::class.javaPrimitiveType!!) - cursor.getString(1) - verify(fixture.mockCursor).getString(eq(1)) + assertEquals(CursorWrapper::class.java, getStringMethod.declaringClass) } @Test