Skip to content

feat(android-sqlite): Add SentrySQLiteDriver (JAVA-275)#5466

Open
0xadam-brown wants to merge 6 commits into
mainfrom
feat/support-sqlite-driver
Open

feat(android-sqlite): Add SentrySQLiteDriver (JAVA-275)#5466
0xadam-brown wants to merge 6 commits into
mainfrom
feat/support-sqlite-driver

Conversation

@0xadam-brown
Copy link
Copy Markdown
Member

@0xadam-brown 0xadam-brown commented May 22, 2026

📜 Description

Introduces SentrySQLiteDriver for wrapping and instrumenting androidx.sqlite.SQLiteDriver. Like our existing SentrySupportSQLiteOpenHelper, the new wrapper produces a span per executed SQL statement.

Example use:

  Room.databaseBuilder(context, AppDatabase::class.java, "myapp.db")
      .setDriver(SentrySQLiteDriver.create(AndroidSQLiteDriver()))
      .build()

💡 Motivation and Context

Room 2.7 introduced the SQLiteDriver API as a replacement for SupportSQLiteOpenHelper; Room 3.0+ makes use of SQLiteDriver mandatory.

A key motivation behind Google's introduction of SQLiteDriver was Kotlin Multiplatform compatibility. This PR makes SentrySQLiteDriver available on Android only, but the wrapper has been packaged so that we can lift it into our KMP module in the future without having to break clients.

Addresses JAVA-275.

⚠️ Callouts

[1] Unlike SentrySupportSQLiteOpenHelper, the SentrySQLiteDriver is not automatically wrapped via the sentry-android-gradle-plugin. (We can add byte code support later if we want – or do it now if there's a strong interest.)

[2] API is not marked as @Experimental. (Super small surface area: a create(SQLiteDriver) constructor; future additions are non-breaking; sufficient alignment with SentrySupportSQLiteOpenHelper data model. That said, chime in if you think we should add it.)

[3] Behavior differences vs SentrySupportSQLiteHelper

Click to expand

Behavior differences

Aspect SentrySupportSQLiteOpenHelper (old) SentrySQLiteDriver (new) Evaluation
Span duration Whole performSql call (incl. app time during cursor materialization) Accumulated step() db time only – app time between steps excluded New is more accurate. Span duration is now limited to SQLite work, not the read loop plus arbitrary app-side row processing, I/O, or GC pauses between rows. Cleaner p95s for db performance dashboards; teams using span duration as a proxy for end-to-end read time will see it shrink.
Cursor iteration Only first getCount/onMove/fillWindow timed; later rows untimed Every step() contributes to cumulative span time New is more complete. A 10k-row read now reflects the full cumulative db cost rather than just the first window fill. The old path silently under-reported large reads.
Span wall-clock anchor Captured before the operation runs Captured at first step() of the cycle Visually, new span sits slightly later in trace New anchor tracks when SQLite actually started working; old anchor included setup/dispatch overhead in the span timeline.
db.name derivation Reads open helper databaseName (for Room, the builder name, e.g., "tracks" from databaseBuilder(ctx, MyDb, "tracks")). Reads File(fileName).name — the on-disk filename of the path Room passes to driver.open() (e.g., "tracks.db"). The same db can show up under two different db.name values during migration. Both paths report data correctly, but will attribute it to different sources.
Multi-statement scripts execSQL("CREATE TABLE …; INSERT …; INSERT …;") produces one span whose description is the full script. The Driver API compiles one statement per prepare(...), so multi-statement scripts must be split by Room (or the caller) into separate prepare/step cycles → one span per statement. New is more accurate but more verbose. Migration scripts and seed scripts that previously appeared as one bundled span will now appear as N smaller spans, each with its own timing and description. Useful for finding the slow statement; expect span counts to rise for these code paths.

[4] Risk of duplicate spans with Room's bridge adapter

SentrySQLiteDriver protects internally against duplicate span creation should a developer try to double-wrap it or any of its components. Room and SQLDelight ensure that use of the driver and SentrySupportSQLiteOpenHelper are mutually exclusive at the API level in virtually all instances, save for one:

Room 2.7+ (but not Room 3.0+) exposes a bridge adapter (SupportSQLiteDriver) that lets users delegate to a SQLiteDriver from an existing SupportSQLiteOpenHelper. Double-wrapping both components is possible if folks aren't mindful.

I'll be updating the Sentry Docs to warn users against wrapping both adapter components. Another option is to log an error, as mentioned here.

Updates to Sentry Docs: Click to expand

Avoiding duplicate spans with Room 2.7+

AndroidX ships an adapter class, SupportSQLiteDriver, that lets developers bridge an existing SupportSQLiteOpenHelper to a SQLiteDriver that Room 2.7+ accepts. Do not wrap both the open helper and the driver. (Remember that the Sentry Android Gradle Plugin will wrap the open helper for you at the byte code level if enabled.) If you double-wrap, you'll produce duplicate spans for every SQL statement:

// AVOID — this configuration produces duplicate spans for every SQL statement.

// Step 1: Wrap the open helper manually or via the Sentry Android Gradle Plugin.
val sentryWrappedHelper: SupportSQLiteOpenHelper =
    SentrySupportSQLiteOpenHelper.create(
        FrameworkSQLiteOpenHelperFactory().create(configuration)
    )

// Step 2: Pass the wrapped helper to the Room 2.7+ adapter.
val driver: SQLiteDriver = SupportSQLiteDriver(sentryWrappedHelper)

// Step 3: Also (wrongly!) wrap the driver. All spans will now be duplicated.
val sentryWrappedDriver: SQLiteDriver = SentrySQLiteDriver.create(driver)

Room.databaseBuilder(context, MyDb::class.java, "mydb")
    .setDriver(sentryWrappedDriver)
    .build()

💚 How did you test it?

Unit tests cover:

  • SentrySQLiteDriver delegation and wrapping
  • SentrySQLiteConnection delegation and wrapping
  • SentrySQLiteStatement span creation, cancellation, and success/error tagging
  • SQLiteSpanRecorder span lifecycle (start/finish/cancel)

I also dog-fooded SentrySQLiteDriver on my own example app via Maven local artifact + I verified spans are displayed in Sentry UI.

⚠️ Possibilities I did without:

  1. Testing against a real Room db in this PR (I did so manually in my example app, but this PR doesn't include a test that wires up Room)
  2. Instrumentation tests
  3. Add a sample to sentry-samples-android (not really a test, but it would exercise the code paths in an actual Android environment).

The legacy open helper didn't have the above, so I followed suit. Let me know if you think any is worth it and I'll be happy to address (eg, real Room db test, which would require a test dependency on androidx.sqlite:sqlite-bundled but wouldn't require Robolectric).

📝 Checklist

  • I added GH Issue ID & Linear ID
  • I added tests to verify the changes.
  • No new PII added or SDK only sends newly added PII if sendDefaultPII is enabled.
  • I updated the docs if needed.
  • I updated the wizard if needed.
  • Review from the native team if needed.
  • No breaking change or entry added to the changelog.
  • No breaking change for hybrid SDKs or communicated to hybrid SDKs.

🔮 Next steps

  1. Update the Room & SQLite Sentry docs.
  2. If there's sufficient demand, we could support auto-wrapping SQLiteDriver via the sentry-android-gradle-plugin at some point (not currently on the roadmap).

@linear-code
Copy link
Copy Markdown

linear-code Bot commented May 22, 2026

JAVA-275

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

Messages
📖 Do not forget to update Sentry-docs with your feature once the pull request gets approved.

Generated by 🚫 dangerJS against f2207a5

@sentry
Copy link
Copy Markdown

sentry Bot commented May 22, 2026

📲 Install Builds

Android

🔗 App Name App ID Version Configuration
SDK Size io.sentry.tests.size 8.43.1 (1) release

⚙️ sentry-android Build Distribution Settings

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 22, 2026

Performance metrics 🚀

  Plain With Sentry Diff
Startup time 310.02 ms 361.44 ms 51.42 ms
Size 0 B 0 B 0 B

Baseline results on branch: main

Startup times

Revision Plain With Sentry Diff
62b579c 349.26 ms 426.26 ms 77.00 ms
d501a7e 348.06 ms 431.42 ms 83.36 ms
cf708bd 434.73 ms 502.96 ms 68.22 ms
2195398 351.77 ms 433.22 ms 81.45 ms
cf708bd 408.35 ms 458.98 ms 50.63 ms
e2dce0b 308.96 ms 360.10 ms 51.14 ms
5dee26b 336.02 ms 402.62 ms 66.60 ms
4c04bb8 333.16 ms 408.16 ms 75.00 ms
a1eadfa 345.67 ms 411.26 ms 65.59 ms
5b1a06b 352.27 ms 413.70 ms 61.43 ms

App size

Revision Plain With Sentry Diff
62b579c 0 B 0 B 0 B
d501a7e 0 B 0 B 0 B
cf708bd 1.58 MiB 2.11 MiB 539.71 KiB
2195398 0 B 0 B 0 B
cf708bd 1.58 MiB 2.11 MiB 539.71 KiB
e2dce0b 0 B 0 B 0 B
5dee26b 0 B 0 B 0 B
4c04bb8 0 B 0 B 0 B
a1eadfa 0 B 0 B 0 B
5b1a06b 0 B 0 B 0 B

Previous results on branch: feat/support-sqlite-driver

Startup times

Revision Plain With Sentry Diff
703e7ae 350.72 ms 415.76 ms 65.03 ms
f8d2380 309.09 ms 365.52 ms 56.43 ms
2a55b58 365.41 ms 434.64 ms 69.23 ms
4993a1b 303.45 ms 392.65 ms 89.20 ms
9ed1dfc 310.92 ms 361.74 ms 50.82 ms

App size

Revision Plain With Sentry Diff
703e7ae 0 B 0 B 0 B
f8d2380 0 B 0 B 0 B
2a55b58 0 B 0 B 0 B
4993a1b 0 B 0 B 0 B
9ed1dfc 0 B 0 B 0 B

@0xadam-brown 0xadam-brown force-pushed the feat/support-sqlite-driver branch 2 times, most recently from a590992 to 0084312 Compare May 28, 2026 07:37
Introduces support for AndroidX's SQLiteDriver via a new SentrySQLiteDriver wrapper.

SentrySQLiteDriver automatically creates spans for each SQL statement it executes, and its data scheme closely tracks that of SentrySupportSQLiteOpenHelper, which it's designed to replace. (Span duration is an important exception; see the SentrySQLiteStatement KDoc for more details.)

A key motivation for Google's using SQLiteDriver with Room 2.7+ was Kotlin Multiplatform support. We've been careful to keep the SentrySQLiteDriver KMP-compatible as well, should we one day want to lift it into sentry-kotlin-multiplatform.

---

Co-authored-by: Angus Holder <7407345+angusholder@users.noreply.github.com>
@0xadam-brown 0xadam-brown force-pushed the feat/support-sqlite-driver branch from 4c87cb9 to 4c67ea9 Compare May 28, 2026 11:19
Comment thread sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SQLiteSpanManager.kt Outdated
@0xadam-brown 0xadam-brown marked this pull request as ready for review May 28, 2026 11:56
Comment thread sentry-android-sqlite/src/main/java/io/sentry/sqlite/SentrySQLiteDriver.kt Outdated
Method reference `System::nanoTime` compiles to FunctionReferenceImpl, which breaks R8 in the SDK size test app.
Copy link
Copy Markdown
Member

@markushi markushi left a comment

Choose a reason for hiding this comment

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

Looks very promising already, left a few comments. Once we've decided what execution phases spans should cover, I'll have a second look.

Comment thread sentry-android-sqlite/src/main/java/io/sentry/android/sqlite/SQLiteSpanManager.kt Outdated
Comment thread sentry-android-sqlite/src/main/java/io/sentry/sqlite/DbMetadata.kt Outdated
Comment thread sentry-android-sqlite/src/main/java/io/sentry/sqlite/SQLiteSpanRecorder.kt Outdated
Comment thread sentry-android-sqlite/src/main/java/io/sentry/sqlite/SQLiteSpanRecorder.kt Outdated
Copy link
Copy Markdown
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

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

Looks great, nice analysis and research, kudos!

Some more things I'd want to clarify before approving:

  • do we actually need to bump androidx.sqlite version that we compile against in the module to support this new stuff?
  • what's the migration path? do we want to keep them side-by-side in the same module, or would it make sense to introduce sentry-android-sqlite3 (or whatever fits best) to keep them separate? would probably also simplify the future KMP work (if ever)
  • you said you're yet to test it out with SAGP and auto-instrumentation, which is great! I think I would want to also make SAGP instrumentation of the new Driver a part of the success criteria, because this is the main method for our users to get room/sqlite instrumented (obviously it doesn't have to block this PR)

@0xadam-brown
Copy link
Copy Markdown
Member Author

0xadam-brown commented Jun 3, 2026

Thanks for the excellent feedback / comments @romtsn. Answers to your questions + one question about single vs multiple modules on my end...

> do we actually need to bump androidx.sqlite version that we compile against in the module to support this new stuff?

Not for this PR: we're already on androidx.sqlite:sqlite:2.5.2 and SQLiteDriver landed in 2.5.0.

Scratch that^^. I've now bumped to 2.6.2 to pick up the introduction SQLiteDriver.hasConnectionPool() in 2.6.0. (I originally thought I'd give that fix its own PR, but it was trivial so I've included it here for completeness.)

Fyi, I'll wait to merge all the PRs together so we make sure they land in the same release.

> what's the migration path? do we want to keep them side-by-side in the same module, or would it make sense to introduce usentry-android-sqlite3 (or whatever fits best) to keep them separate? would probably also simplify the future KMP work (if ever)

This ended up being an important question (thx!). I propose keeping the driver in the existing sentry-android-sqlite module (see discussion below).

Here are the migration steps I had in mind:

  1. Namespace the driver in a way that lets it (eventually) be lifted into sentry-kotlin-multiplatform + keep its public ABI compatible with KMP common. (Already completed via this PR.)
  2. Prepare for lifting (e.g., update driver to use TimeSource rather than System.nanoTime(); have someone create KMP common versions of ISpan, etc. – this latter work is the real blocker atm).
  3. Lift all of io.sentry.sqlite into a new sentry-kotlin-platform module (eg, sentry-kmp-sqlite) without changing its public ABI.
  4. Remove the driver from sentry-android-sqlite (or a new sentry-sqlite module if we go that route) + update the existing module's build.gradle to pull in the driver so consumers don't have to change their coordinate save for bumping the version:
// build.gradle in sentry-android-sqlite (if the driver lives with the open helper) or 
// sentry-sqlite (if we give the driver its own module)
dependencies {
    api("io.sentry:sentry-kmp-sqlite") 
}

Creating a new module doesn't meaningfully simplify future KMP work because we'll always have to relocate the driver somewhere + redirect from the existing module's build.gradle script. (Eg, if we want to publish the driver in a JAR for non-Android JVM consumers / desktop, we could create a new module then and redirect from sentry-android-sqlite; if we want to go straight for common KMP, we do likewise, just in sentry-kotlin-multiplatform.) See the expandable "migration path to KMP" discussion below for more details.

sentry-compose seems reasonable prior art here, and it doesn't have a separate module for its KMP-compatible code. So my vote would be to keep the driver in sentry-android-sqlite for now to aid open helper -> driver migration, and then break it out if and when the need arises.

cc @markushi

Step-by-step migration path to KMP: Click to expand

Migration path

# Step Path A: Use existing module Path B: Create sentry-sqlite module now
1 Untangle shared helpers (SQLiteSpanHelper, DbMetadata) from the open helper path Must do - trivial Already done
2 Isolate driver + shared code into a standalone, Android-free compilation unit with its own coordinate Must do - this is the module split itself Already done
3 Relocate driver into sentry-kotlin-multiplatform (new module/source set there) Required Required
4 Extend the KMP SDK's common API to cover what the wrapper needs: child span with explicit start/finish timestamps, setData (db.system/db.name/blocked_main_thread/call-stack), span status, trace origin, scope access, integration registration Required Required
5 Rewrite the Sentry-touching code (SQLiteSpanHelper/SQLiteSpanRecorder/registration) against the KMP common API; drop all JVM-only io.sentry.* types (ISpan, IScopes, etc.) Required Required
6 Replace System.nanoTime() with a multiplatform monotonic clock (kotlin.time.TimeSource.Monotonic) Required Required
7 Convert build to kotlin.multiplatform, add targets, set up GMM publishing + .craft/release wiring in the sentry-kmp repo Required Required
8 Redirect the sentry-java artifact toward the new KMP coordinate; migration note for existing Android/JVM users Required Required

User experience

Path A Path B
Add today (typical Android user) Often zero new dep; familiar artifact A new, separate artifact (two during Room 2.7 transition)
Code today io.sentry.sqlite.SentrySQLiteDriver Identical
ProGuard / stack-trace fidelity Automatic (AAR) Manual META-INF/proguard — small risk of silent degradation
Off-Android JVM use Impossible Works
Greenfield driver-only classpath Carries unused open-helper code Cleaner (driver-only)

KMP migration shape for both paths is identical: no code change so long as we can keep the package name in sentry-kotlin-multiplatform; dependency redirect means only version bump needed on existing coordinate.

> you said you're yet to test it out with SAGP and auto-instrumentation, which is great! I think I would want to also make SAGP instrumentation of the new Driver a part of the success criteria, because this is the main method for our users to get room/sqlite instrumented (obviously it doesn't have to block this PR)

Good deal. I'll plan to implement SAGP auto-instrumentation of RoomDatabase.Builder.setDriver() as part of the current project. (I'll also double-check the behavior of our current auto-generated db.sql.room spans, and we can tweak if needed.)

 - Merge SQLiteSpanHelper + SQLiteSpanRecorder into a single SQLiteSpanInstrumentation class.
 - DRY out reference to file name path separators.
Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit d4ab6b7. Configure here.

get() =
try {
delegate.hasConnectionPool
} catch (_: LinkageError) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I see that this is the only reference to a LinkageError in our code base. Do we normally handle these sorts of issues differently?

*** "These sorts of issues" = A compileOnly dependency against an API that's added methods as its versions evolve. In this case, androidx.sqlite 2.6.0 added SQLiteDriver.hasConnectionPool. Because we don't include our own androidx.sqlite dep at runtime, we need to account for users who bring in versions with and without hasConnectionPool.

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.

I see we had a similar one here: #4597 which we just fixed by bumping the version, but apparently we don't account for older versions, so I guess it just breaks there?

Copy link
Copy Markdown
Member

@romtsn romtsn left a comment

Choose a reason for hiding this comment

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

LGTM! great work

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants